diff --git a/.cargo/ci-config.toml b/.cargo/ci-config.toml index 6dbaf4b446..664419837b 100644 --- a/.cargo/ci-config.toml +++ b/.cargo/ci-config.toml @@ -10,3 +10,6 @@ # in one spot, that's going to trigger a rebuild of all of the artifacts. Using ci-config.toml we can define these overrides for CI in one spot and not worry about it. [build] rustflags = ["-D", "warnings"] + +[alias] +xtask = "run --package xtask --" diff --git a/.cargo/config.toml b/.cargo/config.toml index 32fdb271ad..70a2137854 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,6 @@ [build] # v0 mangling scheme provides more detailed backtraces around closures rustflags = ["-C", "symbol-mangling-version=v0"] + +[alias] +xtask = "run --package xtask --" diff --git a/.github/ISSUE_TEMPLATE/0_feature_request.yml b/.github/ISSUE_TEMPLATE/0_feature_request.yml index d8dc7950f6..c5e1fa9237 100644 --- a/.github/ISSUE_TEMPLATE/0_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/0_feature_request.yml @@ -2,23 +2,23 @@ name: Feature Request description: "Tip: open this issue template from within Zed with the `request feature` command palette action" labels: ["admin read", "triage", "enhancement"] body: - - type: checkboxes - attributes: - label: Check for existing issues - description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (πŸ‘) on it. - options: - - label: Completed + - type: checkboxes + attributes: + label: Check for existing issues + description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (πŸ‘) on it. + options: + - label: Completed + required: true + - type: textarea + attributes: + label: Describe the feature + description: A clear and concise description of what you want to happen. + validations: required: true - - type: textarea - attributes: - label: Describe the feature - description: A clear and concise description of what you want to happen. - validations: - required: true - - type: textarea - attributes: - label: | - If applicable, add mockups / screenshots to help present your vision of the feature - description: Drag images into the text input below - validations: - required: false + - type: textarea + attributes: + label: | + If applicable, add mockups / screenshots to help present your vision of the feature + description: Drag images into the text input below + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.yml b/.github/ISSUE_TEMPLATE/1_bug_report.yml new file mode 100644 index 0000000000..9df9a4ac11 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_bug_report.yml @@ -0,0 +1,40 @@ +name: Bug Report +description: | + Use this template for **non-crash-related** bug reports. + Tip: open this issue template from within Zed with the `file bug report` command palette action. +labels: ["admin read", "triage", "defect"] +body: + - type: checkboxes + attributes: + label: Check for existing issues + description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (πŸ‘) on it. + options: + - label: Completed + required: true + - type: textarea + attributes: + label: Describe the bug / provide steps to reproduce it + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. + validations: + required: true + - type: textarea + attributes: + label: If applicable, add mockups / screenshots to help explain present your vision of the feature + description: Drag issues into the text input below + validations: + required: false + - type: textarea + attributes: + label: | + If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue. + If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000. + description: Drag Zed.log into the text input below + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/1_language_support.yml b/.github/ISSUE_TEMPLATE/1_language_support.yml deleted file mode 100644 index d56f7056bc..0000000000 --- a/.github/ISSUE_TEMPLATE/1_language_support.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Language Support -description: Request language support -title: " support" -labels: - [ - "admin read", - "triage", - "enhancement", - "language", - "unsupported language", - "potential extension", - ] -body: - - type: checkboxes - attributes: - label: Check for existing issues - description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (πŸ‘) on it. - options: - - label: Completed - required: true - - type: input - attributes: - label: Language - description: What language do you want support for? - placeholder: HTML - validations: - required: true - - type: input - attributes: - label: Tree Sitter parser link - description: If applicable, provide a link to the appropriate tree sitter parser. Look here first - https://tree-sitter.github.io/tree-sitter/#available-parsers - placeholder: https://github.com/tree-sitter/tree-sitter-html - validations: - required: false - - type: input - attributes: - label: Language server link - description: If applicable, provide a link to the appropriate language server. Look here first - https://microsoft.github.io/language-server-protocol/implementors/servers/ - placeholder: https://github.com/Microsoft/vscode/tree/main/extensions/html-language-features/server - validations: - required: false - - type: textarea - attributes: - label: Misc notes - description: Provide any additional things the team should consider when adding support for this language - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/2_bug_report.yml b/.github/ISSUE_TEMPLATE/2_bug_report.yml deleted file mode 100644 index a1c1b56088..0000000000 --- a/.github/ISSUE_TEMPLATE/2_bug_report.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Bug Report -description: "Tip: open this issue template from within Zed with the `file bug report` command palette action" -labels: ["admin read", "triage", "defect"] -body: - - type: checkboxes - attributes: - label: Check for existing issues - description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (πŸ‘) on it. - options: - - label: Completed - required: true - - type: textarea - attributes: - label: Describe the bug / provide steps to reproduce it - description: A clear and concise description of what the bug is. - validations: - required: true - - type: textarea - id: environment - attributes: - label: Environment - description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. - validations: - required: true - - type: textarea - attributes: - label: If applicable, add mockups / screenshots to help explain present your vision of the feature - description: Drag issues into the text input below - validations: - required: false - - type: textarea - attributes: - label: | - If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue. - If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000. - description: Drag Zed.log into the text input below - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/2_crash_report.yml b/.github/ISSUE_TEMPLATE/2_crash_report.yml new file mode 100644 index 0000000000..0e7f1d4d3a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_crash_report.yml @@ -0,0 +1,39 @@ +name: Crash Report +description: | + Use this template for crash reports. +labels: ["admin read", "triage", "defect", "panic / crash"] +body: + - type: checkboxes + attributes: + label: Check for existing issues + description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (πŸ‘) on it. + options: + - label: Completed + required: true + - type: textarea + attributes: + label: Describe the bug / provide steps to reproduce it + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. + validations: + required: true + - type: textarea + attributes: + label: If applicable, add mockups / screenshots to help explain present your vision of the feature + description: Drag issues into the text input below + validations: + required: false + - type: textarea + attributes: + label: | + If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue. + If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000. + description: Drag Zed.log into the text input below + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 181afd35bc..95d4064e8f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,13 +1,16 @@ contact_links: - - name: Theme Request - url: https://github.com/zed-industries/extensions/issues/new/choose - about: Request a theme in the extensions repository - - name: Top-Ranking Issues - url: https://github.com/zed-industries/zed/issues/5393 - about: See an overview of the most popular Zed issues - - name: Platform Support - url: https://github.com/zed-industries/zed/issues/5391 - about: A quick note on platform support - - name: Positive Feedback - url: https://github.com/zed-industries/zed/discussions/5397 - about: A central location for kind words about Zed + - name: Language Request + url: https://github.com/zed-industries/extensions/issues/new?assignees=&labels=language&projects=&template=1_language_request.yml&title=%3Cname_of_language%3E + about: Request a language in the extensions repository + - name: Theme Request + url: https://github.com/zed-industries/extensions/issues/new?assignees=&labels=theme&projects=&template=0_theme_request.yml&title=%3Cname_of_theme%3E+theme + about: Request a theme in the extensions repository + - name: Top-Ranking Issues + url: https://github.com/zed-industries/zed/issues/5393 + about: See an overview of the most popular Zed issues + - name: Platform Support + url: https://github.com/zed-industries/zed/issues/5391 + about: A quick note on platform support + - name: Positive Feedback + url: https://github.com/zed-industries/zed/discussions/5397 + about: A central location for kind words about Zed diff --git a/.github/cherry-pick-bot.yml b/.github/cherry-pick-bot.yml new file mode 100644 index 0000000000..1f62315d79 --- /dev/null +++ b/.github/cherry-pick-bot.yml @@ -0,0 +1,2 @@ +enabled: true +preservePullRequestTitle: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 03fd339b43..1885fc15eb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,8 +4,8 @@ Release Notes: - Added/Fixed/Improved ... ([#](https://github.com/zed-industries/zed/issues/)). +Optionally, include screenshots / media showcasing your addition that can be included in the release notes. + **or** - N/A - -Optionally, include screenshots / media showcasing your addition that can be included in the release notes. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f07444983..fac8924863 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,9 @@ jobs: - name: Run style checks uses: ./.github/actions/check_style + - name: Check unused dependencies + uses: bnjbvr/cargo-machete@main + - name: Ensure fresh merge shell: bash -euxo pipefail {0} run: | @@ -64,6 +67,8 @@ jobs: fi - uses: bufbuild/buf-setup-action@v1 + with: + version: v1.29.0 - uses: bufbuild/buf-breaking-action@v1 with: input: "crates/rpc/proto/" @@ -81,9 +86,14 @@ jobs: clean: false submodules: "recursive" + - name: Install cargo-component + run: | + if ! which cargo-component > /dev/null; then + cargo install cargo-component + fi + - name: cargo clippy - shell: bash -euxo pipefail {0} - run: script/clippy + run: cargo xtask clippy - name: Run tests uses: ./.github/actions/run_tests @@ -94,7 +104,7 @@ jobs: - name: Build other binaries and features run: cargo build --workspace --bins --all-features; cargo check -p gpui --features "macos-blade" - # todo!(linux): Actually run the tests + # todo(linux): Actually run the tests linux_tests: name: (Linux) Run Clippy and tests runs-on: ubuntu-latest @@ -105,28 +115,43 @@ jobs: clean: false submodules: "recursive" - - name: Restore from cache - uses: actions/cache@v4 + - name: Cache dependencies + uses: swatinem/rust-cache@v2 with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}- + save-if: ${{ github.ref == 'refs/heads/main' }} - name: configure linux shell: bash -euxo pipefail {0} run: script/linux - name: cargo clippy - shell: bash -euxo pipefail {0} - run: script/clippy + run: cargo xtask clippy - name: Build Zed run: cargo build -p zed + + # todo(windows): Actually run the tests + windows_tests: + name: (Windows) Run Clippy and tests + runs-on: windows-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + clean: false + submodules: "recursive" + + - name: Cache dependencies + uses: swatinem/rust-cache@v2 + with: + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: cargo clippy + run: cargo xtask clippy + + - name: Build Zed + run: cargo build -p zed + bundle: name: Bundle macOS app runs-on: diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml index 4d3ad09cbc..220c4ca6f7 100644 --- a/.github/workflows/deploy_collab.yml +++ b/.github/workflows/deploy_collab.yml @@ -28,8 +28,7 @@ jobs: uses: ./.github/actions/check_style - name: Run clippy - shell: bash -euxo pipefail {0} - run: script/clippy + run: cargo xtask clippy tests: name: Run tests @@ -120,6 +119,12 @@ jobs: export ZED_DO_CERTIFICATE_ID=$(doctl compute certificate list --format ID --no-header) export ZED_IMAGE_ID="registry.digitalocean.com/zed/collab:${GITHUB_SHA}" + export ZED_SERVICE_NAME=collab envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f - - kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/collab --watch - echo "deployed collab.template.yml to ${ZED_KUBE_NAMESPACE}" + kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch + echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}" + + export ZED_SERVICE_NAME=api + envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f - + kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch + echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}" diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 1a7a0f704d..6e34e859b5 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -32,8 +32,7 @@ jobs: uses: ./.github/actions/check_style - name: Run clippy - shell: bash -euxo pipefail {0} - run: script/clippy + run: cargo xtask clippy tests: name: Run tests if: github.repository_owner == 'zed-industries' diff --git a/.gitignore b/.gitignore index 9b6df52dd1..584337b840 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ /assets/*licenses.md **/venv .build +*.wasm Packages *.xcodeproj xcuserdata/ diff --git a/.zed/settings.json b/.zed/settings.json index 508c0ba249..4c1c40d3ba 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -1,10 +1,19 @@ { "languages": { + "Markdown": { + "tab_size": 2, + "formatter": "prettier" + }, "TOML": { "formatter": "prettier", "format_on_save": "off" }, "YAML": { + "tab_size": 2, + "formatter": "prettier" + }, + "JSON": { + "tab_size": 2, "formatter": "prettier" } }, diff --git a/Cargo.lock b/Cargo.lock index a5e4b2d4de..3a07d68356 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,7 @@ dependencies = [ "gpui", "language", "project", - "settings", "smallvec", - "theme", "ui", "util", "workspace", @@ -42,6 +40,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if 1.0.0", + "cipher 0.4.4", + "cpufeatures", + "zeroize", +] + [[package]] name = "ahash" version = "0.7.6" @@ -86,7 +96,6 @@ dependencies = [ "gpui", "isahc", "language", - "lazy_static", "log", "matrixmultiply", "ordered-float 2.10.0", @@ -94,8 +103,8 @@ dependencies = [ "parse_duration", "postage", "rand 0.8.5", - "regex", "rusqlite", + "schemars", "serde", "serde_json", "tiktoken-rs", @@ -109,7 +118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7ceabf6fc76511f616ca216b51398a2511f19ba9f71bcbd977999edff1b0d1" dependencies = [ "base64 0.21.4", - "bitflags 2.4.1", + "bitflags 2.4.2", "home", "libc", "log", @@ -160,6 +169,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -223,12 +238,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "any_ascii" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea50b14b7a4b9343f8c627a7a53c52076482bd4bdad0a24fd3ec533ed616cc2c" - [[package]] name = "anyhow" version = "1.0.75" @@ -315,7 +324,7 @@ dependencies = [ "serde", "serde_repr", "url", - "zbus", + "zbus 3.15.1", ] [[package]] @@ -334,7 +343,6 @@ dependencies = [ "ai", "anyhow", "chrono", - "client", "collections", "ctor", "editor", @@ -343,13 +351,11 @@ dependencies = [ "futures 0.3.28", "gpui", "indoc", - "isahc", "language", "log", "menu", "multi_buffer", "ordered-float 2.10.0", - "parking_lot 0.11.2", "project", "rand 0.8.5", "regex", @@ -360,11 +366,12 @@ dependencies = [ "serde_json", "settings", "smol", + "telemetry_events", "theme", "tiktoken-rs", "ui", "util", - "uuid 1.4.1", + "uuid", "workspace", ] @@ -389,6 +396,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-broadcast" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" +dependencies = [ + "event-listener 5.1.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -400,6 +419,19 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-channel" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +dependencies = [ + "concurrent-queue", + "event-listener 5.1.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compat" version = "0.2.1" @@ -409,21 +441,21 @@ dependencies = [ "futures-core", "futures-io", "once_cell", - "pin-project-lite 0.2.13", + "pin-project-lite", "tokio", ] [[package]] name = "async-compression" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5" +checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" dependencies = [ "flate2", "futures-core", "futures-io", "memchr", - "pin-project-lite 0.2.13", + "pin-project-lite", ] [[package]] @@ -460,7 +492,7 @@ checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" dependencies = [ "async-lock 3.3.0", "blocking", - "futures-lite 2.0.0", + "futures-lite 2.2.0", ] [[package]] @@ -469,7 +501,7 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" dependencies = [ - "async-channel", + "async-channel 1.9.0", "async-executor", "async-io 1.13.0", "async-lock 2.8.0", @@ -508,7 +540,7 @@ dependencies = [ "cfg-if 1.0.0", "concurrent-queue", "futures-io", - "futures-lite 2.0.0", + "futures-lite 2.2.0", "parking", "polling 3.3.2", "rustix 0.38.30", @@ -533,8 +565,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ "event-listener 4.0.3", - "event-listener-strategy", - "pin-project-lite 0.2.13", + "event-listener-strategy 0.4.0", + "pin-project-lite", ] [[package]] @@ -569,7 +601,7 @@ checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ "async-io 2.3.1", "blocking", - "futures-lite 2.0.0", + "futures-lite 2.2.0", ] [[package]] @@ -599,6 +631,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "async-process" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451e3cf68011bd56771c79db04a9e333095ab6349f7e47592b788e9b98720cc8" +dependencies = [ + "async-channel 2.2.0", + "async-io 2.3.1", + "async-lock 3.3.0", + "async-signal", + "blocking", + "cfg-if 1.0.0", + "event-listener 5.1.0", + "futures-lite 2.2.0", + "rustix 0.38.30", + "windows-sys 0.52.0", +] + [[package]] name = "async-recursion" version = "0.3.2" @@ -621,16 +671,35 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "async-signal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +dependencies = [ + "async-io 2.3.1", + "async-lock 2.8.0", + "atomic-waker", + "cfg-if 1.0.0", + "futures-core", + "futures-io", + "rustix 0.38.30", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", +] + [[package]] name = "async-std" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" dependencies = [ - "async-channel", + "async-channel 1.9.0", "async-global-executor", "async-io 1.13.0", "async-lock 2.8.0", + "async-process 1.7.0", "crossbeam-utils", "futures-channel", "futures-core", @@ -641,7 +710,7 @@ dependencies = [ "log", "memchr", "once_cell", - "pin-project-lite 0.2.13", + "pin-project-lite", "pin-utils", "slab", "wasm-bindgen-futures", @@ -655,7 +724,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.13", + "pin-project-lite", ] [[package]] @@ -691,9 +760,9 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", @@ -711,7 +780,7 @@ dependencies = [ "futures-io", "futures-util", "log", - "pin-project-lite 0.2.13", + "pin-project-lite", "tungstenite 0.16.0", ] @@ -754,9 +823,7 @@ dependencies = [ "anyhow", "collections", "derive_more", - "futures 0.3.28", "gpui", - "log", "parking_lot 0.11.2", "rodio", "util", @@ -769,12 +836,12 @@ dependencies = [ "anyhow", "client", "db", + "editor", "gpui", "isahc", - "lazy_static", "log", + "markdown_preview", "menu", - "project", "release_channel", "schemars", "serde", @@ -783,7 +850,6 @@ dependencies = [ "settings", "smol", "tempfile", - "theme", "util", "workspace", ] @@ -855,9 +921,9 @@ dependencies = [ "http 0.2.9", "http-body", "percent-encoding", - "pin-project-lite 0.2.13", + "pin-project-lite", "tracing", - "uuid 1.4.1", + "uuid", ] [[package]] @@ -992,7 +1058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "426a5bc369ca7c8d3686439e46edc727f397a47ab3696b13f3ae8c81b3b36132" dependencies = [ "futures-util", - "pin-project-lite 0.2.13", + "pin-project-lite", "tokio", ] @@ -1011,7 +1077,7 @@ dependencies = [ "http 0.2.9", "http-body", "md-5", - "pin-project-lite 0.2.13", + "pin-project-lite", "sha1", "sha2 0.10.7", "tracing", @@ -1044,7 +1110,7 @@ dependencies = [ "http-body", "once_cell", "percent-encoding", - "pin-project-lite 0.2.13", + "pin-project-lite", "pin-utils", "tracing", ] @@ -1086,7 +1152,7 @@ dependencies = [ "hyper", "hyper-rustls", "once_cell", - "pin-project-lite 0.2.13", + "pin-project-lite", "pin-utils", "rustls", "tokio", @@ -1103,7 +1169,7 @@ dependencies = [ "aws-smithy-types", "bytes 1.5.0", "http 0.2.9", - "pin-project-lite 0.2.13", + "pin-project-lite", "tokio", "tracing", "zeroize", @@ -1123,13 +1189,13 @@ dependencies = [ "http-body", "itoa", "num-integer", - "pin-project-lite 0.2.13", + "pin-project-lite", "pin-utils", "ryu", "serde", "time", "tokio", - "tokio-util 0.7.9", + "tokio-util", ] [[package]] @@ -1177,7 +1243,7 @@ dependencies = [ "memchr", "mime", "percent-encoding", - "pin-project-lite 0.2.13", + "pin-project-lite", "serde", "serde_json", "serde_urlencoded", @@ -1186,7 +1252,7 @@ dependencies = [ "tokio", "tokio-tungstenite", "tower", - "tower-http", + "tower-http 0.3.5", "tower-layer", "tower-service", ] @@ -1218,12 +1284,12 @@ dependencies = [ "futures-util", "http 0.2.9", "mime", - "pin-project-lite 0.2.13", + "pin-project-lite", "serde", "serde_json", "tokio", "tower", - "tower-http", + "tower-http 0.3.5", "tower-layer", "tower-service", ] @@ -1243,17 +1309,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "backtrace-on-stack-overflow" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd2d70527f3737a1ad17355e260706c1badebabd1fa06a7a053407380df841b" -dependencies = [ - "backtrace", - "libc", - "nix 0.23.2", -] - [[package]] name = "base16ct" version = "0.1.1" @@ -1348,7 +1403,7 @@ dependencies = [ "rustc-hash", "shlex", "syn 2.0.48", - "which", + "which 4.4.2", ] [[package]] @@ -1374,9 +1429,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" dependencies = [ "serde", ] @@ -1400,7 +1455,7 @@ source = "git+https://github.com/kvark/blade?rev=e9d93a4d41f3946a03ffb76136290d6 dependencies = [ "ash", "ash-window", - "bitflags 2.4.1", + "bitflags 2.4.2", "block", "bytemuck", "codespan-reporting", @@ -1458,18 +1513,28 @@ dependencies = [ ] [[package]] -name = "blocking" -version = "1.3.1" +name = "block-padding" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "async-channel", - "async-lock 2.8.0", + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel 2.2.0", + "async-lock 3.3.0", "async-task", - "atomic-waker", - "fastrand 1.9.0", - "futures-lite 1.13.0", - "log", + "fastrand 2.0.0", + "futures-io", + "futures-lite 2.2.0", + "piper", + "tracing", ] [[package]] @@ -1521,15 +1586,10 @@ dependencies = [ name = "breadcrumbs" version = "0.1.0" dependencies = [ - "collections", "editor", "gpui", - "itertools 0.10.5", - "language", + "itertools 0.11.0", "outline", - "project", - "search", - "settings", "theme", "ui", "workspace", @@ -1642,35 +1702,142 @@ name = "call" version = "0.1.0" dependencies = [ "anyhow", - "async-broadcast 0.4.1", "audio", "client", "collections", "fs", "futures 0.3.28", "gpui", - "image", "language", "live_kit_client", "log", - "media", "postage", "project", "schemars", "serde", "serde_derive", - "serde_json", "settings", - "smallvec", "util", ] +[[package]] +name = "calloop" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +dependencies = [ + "bitflags 2.4.2", + "log", + "polling 3.3.2", + "rustix 0.38.30", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop", + "rustix 0.38.30", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cap-fs-ext" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e341d15ac1029aadce600be764a1a1edafe40e03cde23285bc1d261b3a4866" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes 2.0.3", + "windows-sys 0.52.0", +] + +[[package]] +name = "cap-net-ext" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434168fe6533055f0f4204039abe3ff6d7db338ef46872a5fa39e9d5ad5ab7a9" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 0.38.30", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe16767ed8eee6d3f1f00d6a7576b81c226ab917eb54b96e5f77a5216ef67abb" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 2.0.3", + "ipnet", + "maybe-owned", + "rustix 0.38.30", + "windows-sys 0.52.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20e5695565f0cd7106bc3c7170323597540e772bb73e0be2cd2c662a0f8fa4ca" +dependencies = [ + "ambient-authority", + "rand 0.8.5", +] + +[[package]] +name = "cap-std" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "593db20e4c51f62d3284bae7ee718849c3214f93a3b94ea1899ad85ba119d330" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes 2.0.3", + "rustix 0.38.30", +] + +[[package]] +name = "cap-time-ext" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03261630f291f425430a36f38c847828265bc928f517cdd2004c56f4b02f002b" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 0.38.30", + "winx", +] + [[package]] name = "castaway" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "cbindgen" version = "0.26.0" @@ -1692,9 +1859,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.84" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" +checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" dependencies = [ "libc", ] @@ -1734,34 +1901,18 @@ dependencies = [ "client", "clock", "collections", - "db", - "feature_flags", "futures 0.3.28", "gpui", - "image", "language", - "lazy_static", "log", - "parking_lot 0.11.2", - "postage", "rand 0.8.5", "release_channel", "rpc", - "schemars", - "serde", - "serde_derive", "settings", - "smallvec", - "smol", "sum_tree", - "tempfile", "text", - "thiserror", "time", - "tiny_http", - "url", "util", - "uuid 1.4.1", ] [[package]] @@ -1794,6 +1945,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clang-sys" version = "1.6.1" @@ -1892,14 +2054,55 @@ dependencies = [ "clap 3.2.25", "core-foundation", "core-services", - "dirs 3.0.2", "ipc-channel", "plist", "serde", - "serde_derive", "util", ] +[[package]] +name = "clickhouse" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0875e527e299fc5f4faba42870bf199a39ab0bb2dbba1b8aef0a2151451130f" +dependencies = [ + "bstr", + "bytes 1.5.0", + "clickhouse-derive", + "clickhouse-rs-cityhash-sys", + "futures 0.3.28", + "hyper", + "hyper-tls", + "lz4", + "sealed", + "serde", + "static_assertions", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "clickhouse-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18af5425854858c507eec70f7deb4d5d8cec4216fcb086283a78872387281ea5" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "clickhouse-rs-cityhash-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4baf9d4700a28d6cb600e17ed6ae2b43298a5245f1f76b4eab63027ebfd592b9" +dependencies = [ + "cc", +] + [[package]] name = "client" version = "0.1.0" @@ -1908,12 +2111,11 @@ dependencies = [ "async-recursion 0.3.2", "async-tungstenite", "chrono", + "clock", "collections", - "db", "feature_flags", "futures 0.3.28", "gpui", - "image", "lazy_static", "log", "once_cell", @@ -1924,13 +2126,12 @@ dependencies = [ "rpc", "schemars", "serde", - "serde_derive", "serde_json", "settings", "sha2 0.10.7", "smol", - "sum_tree", "sysinfo", + "telemetry_events", "tempfile", "text", "thiserror", @@ -1938,13 +2139,24 @@ dependencies = [ "tiny_http", "url", "util", - "uuid 1.4.1", +] + +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi 0.3.9", ] [[package]] name = "clock" version = "0.1.0" dependencies = [ + "chrono", + "parking_lot 0.11.2", "smallvec", ] @@ -1967,7 +2179,7 @@ dependencies = [ "block", "cocoa-foundation", "core-foundation", - "core-graphics 0.23.1", + "core-graphics", "foreign-types 0.5.0", "libc", "objc", @@ -2009,11 +2221,10 @@ dependencies = [ "aws-sdk-s3", "axum", "axum-extra", - "base64 0.13.1", "call", "channel", "chrono", - "clap 3.2.25", + "clickhouse", "client", "clock", "collab_ui", @@ -2028,11 +2239,11 @@ dependencies = [ "futures 0.3.28", "git", "gpui", + "hex", "hyper", "indoc", "language", "lazy_static", - "lipsum", "live_kit_client", "live_kit_server", "log", @@ -2050,6 +2261,7 @@ dependencies = [ "release_channel", "reqwest", "rpc", + "rustc-demangle", "scrypt", "sea-orm", "semver", @@ -2057,23 +2269,22 @@ dependencies = [ "serde_derive", "serde_json", "settings", - "sha-1 0.9.8", - "smallvec", + "sha2 0.10.7", "sqlx", + "telemetry_events", "text", "theme", "time", "tokio", - "tokio-tungstenite", "toml 0.8.10", - "tonic", "tower", + "tower-http 0.4.4", "tracing", "tracing-log", "tracing-subscriber", "unindent", "util", - "uuid 1.4.1", + "uuid", "workspace", ] @@ -2090,19 +2301,17 @@ dependencies = [ "collections", "db", "editor", - "feature_flags", + "extensions_ui", "feedback", "futures 0.3.28", "fuzzy", "gpui", "language", "lazy_static", - "log", "menu", "notifications", "parking_lot 0.11.2", "picker", - "postage", "pretty_assertions", "project", "recent_projects", @@ -2115,10 +2324,10 @@ dependencies = [ "settings", "smallvec", "story", - "sys-locale", "theme", "theme_selector", "time", + "time_format", "tree-sitter-markdown", "ui", "util", @@ -2138,9 +2347,7 @@ dependencies = [ name = "color" version = "0.1.0" dependencies = [ - "itertools 0.11.0", "palette", - "story", ] [[package]] @@ -2169,10 +2376,9 @@ dependencies = [ name = "command_palette" version = "0.1.0" dependencies = [ - "anyhow", "client", "collections", - "copilot", + "command_palette_hooks", "ctor", "editor", "env_logger", @@ -2182,8 +2388,8 @@ dependencies = [ "language", "menu", "picker", + "postage", "project", - "release_channel", "serde", "serde_json", "settings", @@ -2194,6 +2400,14 @@ dependencies = [ "zed_actions", ] +[[package]] +name = "command_palette_hooks" +version = "0.1.0" +dependencies = [ + "collections", + "gpui", +] + [[package]] name = "concurrent-queue" version = "2.2.0" @@ -2249,23 +2463,22 @@ version = "0.1.0" dependencies = [ "anyhow", "async-compression", + "async-std", "async-tar", "clock", "collections", + "command_palette_hooks", "fs", "futures 0.3.28", "gpui", "language", - "log", "lsp", "node_runtime", "parking_lot 0.11.2", "rpc", "serde", - "serde_derive", "settings", "smol", - "theme", "util", ] @@ -2277,18 +2490,29 @@ dependencies = [ "copilot", "editor", "fs", - "futures 0.3.28", "gpui", "language", "settings", - "smol", - "theme", "ui", "util", "workspace", "zed_actions", ] +[[package]] +name = "copypasta" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb85422867ca93da58b7f95fb5c0c10f6183ed6e1ef8841568968a896d3a858" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2297,7 +2521,6 @@ checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys 0.8.6", "libc", - "uuid 0.5.1", ] [[package]] @@ -2312,19 +2535,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", - "foreign-types 0.3.2", - "libc", -] - [[package]] name = "core-graphics" version = "0.23.1" @@ -2365,7 +2575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" dependencies = [ "core-foundation", - "core-graphics 0.23.1", + "core-graphics", "foreign-types 0.5.0", "libc", ] @@ -2436,6 +2646,15 @@ dependencies = [ "windows 0.46.0", ] +[[package]] +name = "cpp_demangle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaa953eaad386a53111e47172c2fedba671e5684c8dd601a5f474f4f118710f" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "cpufeatures" version = "0.2.9" @@ -2447,16 +2666,18 @@ dependencies = [ [[package]] name = "cranelift-bforest" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "0.105.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9515fcc42b6cb5137f76b84c1a6f819782d0cf12473d145d3bc5cd67eedc8bc2" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-codegen" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "0.105.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad827c6071bfe6d22de1bc331296a29f9ddc506ff926d8415b435ec6a6efce0" dependencies = [ "bumpalo", "cranelift-bforest", @@ -2475,29 +2696,33 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "0.105.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e6b36237a9ca2ce2fb4cc7741d418a080afa1327402138412ef85d5367bef1" dependencies = [ "cranelift-codegen-shared", ] [[package]] name = "cranelift-codegen-shared" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "0.105.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c36bf4bfb86898a94ccfa773a1f86e8a5346b1983ff72059bdd2db4600325251" [[package]] name = "cranelift-control" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "0.105.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbf36560e7a6bd1409ca91e7b43b2cc7ed8429f343d7605eadf9046e8fac0d0" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "0.105.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a71e11061a75b1184c09bea97c026a88f08b59ade96a7bb1f259d4ea0df2e942" dependencies = [ "serde", "serde_derive", @@ -2505,8 +2730,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "0.105.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af5d4da63143ee3485c7bcedde0a818727d737d1083484a0ceedb8950c89e495" dependencies = [ "cranelift-codegen", "log", @@ -2516,13 +2742,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "0.105.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457a9832b089e26f5eea70dcf49bed8ec6edafed630ce7c83161f24d46ab8085" [[package]] name = "cranelift-native" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "0.105.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b490d579df1ce365e1ea359e24ed86d82289fa785153327c2f6a69a59a731e4" dependencies = [ "cranelift-codegen", "libc", @@ -2531,8 +2759,9 @@ dependencies = [ [[package]] name = "cranelift-wasm" -version = "0.103.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "0.105.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd747ed7f9a461dda9c388415392f6bb95d1a6ef3b7694d17e0817eb74b7798" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -2659,6 +2888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -2761,17 +2991,11 @@ name = "db" version = "0.1.0" dependencies = [ "anyhow", - "async-trait", - "collections", - "env_logger", "gpui", "indoc", "lazy_static", "log", - "parking_lot 0.11.2", "release_channel", - "serde", - "serde_derive", "smol", "sqlez", "sqlez_macros", @@ -2779,6 +3003,15 @@ dependencies = [ "util", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + [[package]] name = "deflate" version = "0.8.6" @@ -2852,22 +3085,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "dhat" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2aaf837aaf456f6706cb46386ba8dffd4013a757e36f4ea05c20dd46b209a3" -dependencies = [ - "backtrace", - "lazy_static", - "mintex", - "parking_lot 0.12.1", - "rustc-hash", - "serde", - "serde_json", - "thousands", -] - [[package]] name = "diagnostics" version = "0.1.0" @@ -2881,14 +3098,11 @@ dependencies = [ "language", "log", "lsp", - "postage", "project", "schemars", "serde", - "serde_derive", "serde_json", "settings", - "smallvec", "theme", "ui", "unindent", @@ -2937,6 +3151,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "3.0.2" @@ -3057,7 +3281,7 @@ dependencies = [ "git", "gpui", "indoc", - "itertools 0.10.5", + "itertools 0.11.0", "language", "lazy_static", "linkify", @@ -3066,25 +3290,20 @@ dependencies = [ "multi_buffer", "ordered-float 2.10.0", "parking_lot 0.11.2", - "postage", "project", "rand 0.8.5", "release_channel", - "rich_text", "rpc", "schemars", "serde", - "serde_derive", "serde_json", "settings", "smallvec", "smol", "snippet", - "sqlez", "sum_tree", "text", "theme", - "tree-sitter", "tree-sitter-html", "tree-sitter-rust", "tree-sitter-typescript", @@ -3139,6 +3358,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enumflags2" version = "0.7.9" @@ -3251,7 +3476,18 @@ checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", "parking", - "pin-project-lite 0.2.13", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ad6fd685ce13acd6d9541a30f6db6567a7a24c9ffd4ba2955d29e3f22c8b27" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", ] [[package]] @@ -3261,7 +3497,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ "event-listener 4.0.3", - "pin-project-lite 0.2.13", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +dependencies = [ + "event-listener 5.1.0", + "pin-project-lite", ] [[package]] @@ -3271,14 +3517,16 @@ dependencies = [ "anyhow", "async-compression", "async-tar", - "client", + "async-trait", "collections", "fs", "futures 0.3.28", "gpui", "language", "log", - "parking_lot 0.11.2", + "lsp", + "node_runtime", + "project", "schemars", "serde", "serde_json", @@ -3286,32 +3534,22 @@ dependencies = [ "theme", "toml 0.8.10", "util", + "wasmparser", + "wasmtime", + "wasmtime-wasi", ] [[package]] name = "extensions_ui" version = "0.1.0" dependencies = [ - "anyhow", - "async-compression", - "async-tar", "client", - "db", "editor", "extension", - "fs", - "futures 0.3.28", - "fuzzy", "gpui", - "log", - "picker", - "project", - "serde", - "serde_json", "settings", "theme", "ui", - "util", "workspace", ] @@ -3364,11 +3602,21 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if 1.0.0", + "rustix 0.38.30", + "windows-sys 0.52.0", +] + [[package]] name = "feature_flags" version = "0.1.0" dependencies = [ - "anyhow", "gpui", ] @@ -3377,7 +3625,7 @@ name = "feedback" version = "0.1.0" dependencies = [ "anyhow", - "bitflags 2.4.1", + "bitflags 2.4.2", "client", "db", "editor", @@ -3386,22 +3634,16 @@ dependencies = [ "human_bytes", "isahc", "language", - "lazy_static", "log", "menu", - "postage", "project", "regex", "release_channel", "serde", "serde_derive", "serde_json", - "settings", - "smallvec", "smol", "sysinfo", - "theme", - "tree-sitter-markdown", "ui", "urlencoding", "util", @@ -3433,11 +3675,8 @@ dependencies = [ "language", "menu", "picker", - "postage", "project", - "serde", "serde_json", - "settings", "text", "theme", "ui", @@ -3517,7 +3756,7 @@ dependencies = [ "bitflags 1.3.2", "byteorder", "core-foundation", - "core-graphics 0.23.1", + "core-graphics", "core-text", "dirs-next", "dwrote", @@ -3650,6 +3889,7 @@ name = "fs" version = "0.1.0" dependencies = [ "anyhow", + "async-tar", "async-trait", "collections", "fsevent", @@ -3661,7 +3901,6 @@ dependencies = [ "log", "notify", "parking_lot 0.11.2", - "regex", "rope", "serde", "serde_derive", @@ -3672,13 +3911,25 @@ dependencies = [ "text", "time", "util", + "windows-sys 0.52.0", +] + +[[package]] +name = "fs-set-times" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033b337d725b97690d86893f9de22b67b80dcc4e9ad815f348254c38119db8fb" +dependencies = [ + "io-lifetimes 2.0.3", + "rustix 0.38.30", + "windows-sys 0.52.0", ] [[package]] name = "fsevent" version = "2.0.2" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "fsevent-sys 3.1.0", "parking_lot 0.11.2", "tempfile", @@ -3810,23 +4061,21 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.13", + "pin-project-lite", "waker-fn", ] [[package]] name = "futures-lite" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1155db57329dca6d018b61e76b1488ce9a2e5e44028cac420a5898f4fcef63" +checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" dependencies = [ "fastrand 2.0.0", "futures-core", "futures-io", - "memchr", "parking", - "pin-project-lite 0.2.13", - "waker-fn", + "pin-project-lite", ] [[package]] @@ -3866,7 +4115,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.13", + "pin-project-lite", "pin-utils", "slab", "tokio-io", @@ -3889,6 +4138,28 @@ dependencies = [ "thread_local", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" +dependencies = [ + "bitflags 2.4.2", + "debugid", + "fxhash", + "serde", + "serde_json", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -3899,6 +4170,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -3948,20 +4229,14 @@ dependencies = [ name = "git" version = "0.1.0" dependencies = [ - "anyhow", - "async-trait", "clock", - "collections", - "futures 0.3.28", "git2", "lazy_static", "log", - "parking_lot 0.11.2", "smol", "sum_tree", "text", "unindent", - "util", ] [[package]] @@ -3985,15 +4260,15 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", "bstr", - "fnv", "log", - "regex", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", ] [[package]] @@ -4027,9 +4302,6 @@ dependencies = [ "editor", "gpui", "menu", - "postage", - "serde", - "settings", "text", "theme", "ui", @@ -4043,7 +4315,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "gpu-alloc-types", ] @@ -4064,7 +4336,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", ] [[package]] @@ -4077,21 +4349,22 @@ dependencies = [ "async-task", "backtrace", "bindgen 0.65.1", - "bitflags 2.4.1", "blade-graphics", "blade-macros", "block", "bytemuck", + "calloop", + "calloop-wayland-source", "cbindgen", "cocoa", "collections", + "copypasta", "core-foundation", - "core-graphics 0.23.1", + "core-graphics", "core-text", "cosmic-text", "ctor", "derive_more", - "dhat", "env_logger", "etagere", "flume", @@ -4100,7 +4373,7 @@ dependencies = [ "futures 0.3.28", "gpui_macros", "image", - "itertools 0.10.5", + "itertools 0.11.0", "lazy_static", "linkme", "log", @@ -4108,13 +4381,13 @@ dependencies = [ "metal", "num_cpus", "objc", + "oo7", "open", - "ordered-float 2.10.0", "parking", "parking_lot 0.11.2", "pathfinder_geometry", - "png", "postage", + "profiling", "rand 0.8.5", "raw-window-handle 0.5.2", "raw-window-handle 0.6.0", @@ -4125,7 +4398,6 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "simplelog", "slotmap", "smallvec", "smol", @@ -4136,11 +4408,13 @@ dependencies = [ "tiny-skia", "usvg", "util", - "uuid 1.4.1", + "uuid", "waker-fn", "wayland-backend", "wayland-client", + "wayland-cursor", "wayland-protocols", + "windows 0.53.0", "xcb", "xkbcommon", ] @@ -4186,7 +4460,7 @@ dependencies = [ "indexmap 1.9.3", "slab", "tokio", - "tokio-util 0.7.9", + "tokio-util", "tracing", ] @@ -4337,11 +4611,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4380,7 +4654,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes 1.5.0", "http 0.2.9", - "pin-project-lite 0.2.13", + "pin-project-lite", ] [[package]] @@ -4429,7 +4703,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite 0.2.13", + "pin-project-lite", "socket2 0.4.9", "tokio", "tower-service", @@ -4453,18 +4727,6 @@ dependencies = [ "tokio-rustls", ] -[[package]] -name = "hyper-timeout" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" -dependencies = [ - "hyper", - "pin-project-lite 0.2.13", - "tokio", - "tokio-io-timeout", -] - [[package]] name = "hyper-tls" version = "0.5.0" @@ -4501,6 +4763,12 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + [[package]] name = "idna" version = "0.4.0" @@ -4513,17 +4781,16 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" dependencies = [ + "crossbeam-deque", "globset", - "lazy_static", "log", "memchr", - "regex", + "regex-automata 0.4.5", "same-file", - "thread_local", "walkdir", "winapi-util", ] @@ -4606,13 +4873,22 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "install_cli" version = "0.1.0" dependencies = [ "anyhow", "gpui", - "log", "smol", "util", ] @@ -4626,6 +4902,16 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "io-extras" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c301e73fb90e8a29e600a9f402d095765f74310d582916a952f618836a1bd1ed" +dependencies = [ + "io-lifetimes 2.0.3", + "windows-sys 0.52.0", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -4637,6 +4923,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "io-lifetimes" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" + [[package]] name = "iovec" version = "0.1.4" @@ -4661,7 +4953,7 @@ dependencies = [ "rand 0.7.3", "serde", "tempfile", - "uuid 1.4.1", + "uuid", "winapi 0.3.9", ] @@ -4696,7 +4988,7 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" dependencies = [ - "async-channel", + "async-channel 1.9.0", "castaway", "crossbeam-utils", "curl", @@ -4741,6 +5033,26 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + [[package]] name = "jni" version = "0.19.0" @@ -4781,7 +5093,6 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "dirs 4.0.0", "editor", "gpui", "log", @@ -4789,7 +5100,6 @@ dependencies = [ "serde", "settings", "shellexpand", - "util", "workspace", ] @@ -4889,9 +5199,7 @@ name = "language" version = "0.1.0" dependencies = [ "anyhow", - "async-broadcast 0.4.1", "async-trait", - "client", "clock", "collections", "ctor", @@ -4914,7 +5222,6 @@ dependencies = [ "schemars", "seahash", "serde", - "serde_derive", "serde_json", "settings", "similar", @@ -4923,7 +5230,6 @@ dependencies = [ "sum_tree", "text", "theme", - "toml 0.8.10", "tree-sitter", "tree-sitter-elixir", "tree-sitter-embedded-template", @@ -4931,7 +5237,6 @@ dependencies = [ "tree-sitter-html", "tree-sitter-json 0.20.0", "tree-sitter-markdown", - "tree-sitter-python", "tree-sitter-ruby", "tree-sitter-rust", "tree-sitter-typescript", @@ -4951,8 +5256,6 @@ dependencies = [ "language", "picker", "project", - "settings", - "theme", "ui", "util", "workspace", @@ -4973,17 +5276,104 @@ dependencies = [ "lsp", "project", "release_channel", - "serde", "serde_json", "settings", "theme", "tree-sitter", "ui", - "unindent", "util", "workspace", ] +[[package]] +name = "languages" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-compression", + "async-tar", + "async-trait", + "collections", + "feature_flags", + "futures 0.3.28", + "gpui", + "language", + "lazy_static", + "log", + "lsp", + "node_runtime", + "parking_lot 0.11.2", + "project", + "regex", + "rope", + "rust-embed", + "schemars", + "serde", + "serde_derive", + "serde_json", + "settings", + "shellexpand", + "smol", + "task", + "text", + "theme", + "toml 0.8.10", + "tree-sitter", + "tree-sitter-astro", + "tree-sitter-bash", + "tree-sitter-c", + "tree-sitter-c-sharp", + "tree-sitter-clojure", + "tree-sitter-cpp", + "tree-sitter-css", + "tree-sitter-dart", + "tree-sitter-dockerfile", + "tree-sitter-elixir", + "tree-sitter-elm", + "tree-sitter-embedded-template", + "tree-sitter-erlang", + "tree-sitter-gitcommit", + "tree-sitter-gleam", + "tree-sitter-glsl", + "tree-sitter-go", + "tree-sitter-gomod", + "tree-sitter-gowork", + "tree-sitter-haskell", + "tree-sitter-hcl", + "tree-sitter-heex", + "tree-sitter-html", + "tree-sitter-json 0.20.0", + "tree-sitter-lua", + "tree-sitter-markdown", + "tree-sitter-nix", + "tree-sitter-nu", + "tree-sitter-ocaml", + "tree-sitter-php", + "tree-sitter-prisma-io", + "tree-sitter-proto", + "tree-sitter-purescript", + "tree-sitter-python", + "tree-sitter-racket", + "tree-sitter-ruby", + "tree-sitter-rust", + "tree-sitter-scheme", + "tree-sitter-svelte", + "tree-sitter-toml", + "tree-sitter-typescript", + "tree-sitter-uiua", + "tree-sitter-vue", + "tree-sitter-yaml", + "tree-sitter-zig", + "unindent", + "util", +] + +[[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + [[package]] name = "lazy_static" version = "1.4.0" @@ -5132,16 +5522,6 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" -[[package]] -name = "lipsum" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8451846f1f337e44486666989fbce40be804da139d5a4477d6b88ece5dc69f4" -dependencies = [ - "rand 0.8.5", - "rand_chacha 0.3.1", -] - [[package]] name = "live_kit_client" version = "0.1.0" @@ -5149,27 +5529,17 @@ dependencies = [ "anyhow", "async-broadcast 0.4.1", "async-trait", - "block", - "byteorder", - "bytes 1.5.0", - "cocoa", "collections", "core-foundation", - "core-graphics 0.22.3", - "foreign-types 0.3.2", "futures 0.3.28", "gpui", - "hmac 0.12.1", - "jwt", "live_kit_server", "log", "media", "nanoid", - "objc", "parking_lot 0.11.2", "postage", "serde", - "serde_derive", "serde_json", "sha2 0.10.7", "simplelog", @@ -5181,7 +5551,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "futures 0.3.28", "hmac 0.12.1", "jwt", "log", @@ -5190,7 +5559,6 @@ dependencies = [ "prost-types 0.8.0", "reqwest", "serde", - "serde_derive", "sha2 0.10.7", ] @@ -5231,10 +5599,8 @@ dependencies = [ "postage", "release_channel", "serde", - "serde_derive", "serde_json", "smol", - "unindent", "util", ] @@ -5250,6 +5616,26 @@ dependencies = [ "url", ] +[[package]] +name = "lz4" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9e2dd86df36ce760a60f6ff6ad526f7ba1f14ba0356f8254fb6905e6494df1" +dependencies = [ + "libc", + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "mac" version = "0.1.1" @@ -5287,19 +5673,13 @@ dependencies = [ name = "markdown_preview" version = "0.1.0" dependencies = [ - "anyhow", "editor", "gpui", "language", - "lazy_static", - "log", - "menu", "pretty_assertions", - "project", "pulldown-cmark", "theme", "ui", - "util", "workspace", ] @@ -5334,6 +5714,12 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "md-5" version = "0.10.5" @@ -5349,8 +5735,6 @@ version = "0.1.0" dependencies = [ "anyhow", "bindgen 0.65.1", - "block", - "bytes 1.5.0", "core-foundation", "foreign-types 0.5.0", "metal", @@ -5391,12 +5775,12 @@ dependencies = [ ] [[package]] -name = "memoffset" -version = "0.6.5" +name = "memmap2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ - "autocfg", + "libc", ] [[package]] @@ -5495,16 +5879,6 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" -[[package]] -name = "mintex" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7c5ba1c3b5a23418d7bbf98c71c3d4946a0125002129231da8d6b723d559cb" -dependencies = [ - "once_cell", - "sys-info", -] - [[package]] name = "mio" version = "0.6.23" @@ -5536,18 +5910,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mio-extras" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" -dependencies = [ - "lazycell", - "log", - "mio 0.6.23", - "slab", -] - [[package]] name = "miow" version = "0.2.2" @@ -5573,46 +5935,20 @@ dependencies = [ name = "multi_buffer" version = "0.1.0" dependencies = [ - "aho-corasick", "anyhow", - "client", "clock", "collections", - "convert_case 0.6.0", - "copilot", - "ctor", - "env_logger", "futures 0.3.28", "git", "gpui", - "indoc", - "itertools 0.10.5", "language", - "lazy_static", "log", - "lsp", - "ordered-float 2.10.0", "parking_lot 0.11.2", - "postage", - "project", - "pulldown-cmark", "rand 0.8.5", - "rich_text", - "schemars", - "serde", - "serde_derive", "settings", - "smallvec", - "smol", - "snippet", "sum_tree", "text", "theme", - "tree-sitter", - "tree-sitter-html", - "tree-sitter-rust", - "tree-sitter-typescript", - "unindent", "util", ] @@ -5629,7 +5965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae585df4b6514cf8842ac0f1ab4992edc975892704835b549cf818dc0191249e" dependencies = [ "bit-set", - "bitflags 2.4.1", + "bitflags 2.4.2", "codespan-reporting", "hexf-parse", "indexmap 2.0.0", @@ -5737,19 +6073,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" -[[package]] -name = "nix" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if 1.0.0", - "libc", - "memoffset 0.6.5", -] - [[package]] name = "nix" version = "0.24.3" @@ -5779,9 +6102,10 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "cfg-if 1.0.0", "libc", + "memoffset 0.9.0", ] [[package]] @@ -5794,9 +6118,7 @@ dependencies = [ "async-trait", "futures 0.3.28", "log", - "parking_lot 0.11.2", "serde", - "serde_derive", "serde_json", "smol", "util", @@ -5819,15 +6141,12 @@ dependencies = [ "anyhow", "channel", "client", - "clock", "collections", "db", - "feature_flags", "gpui", "rpc", "settings", "sum_tree", - "text", "time", "util", ] @@ -5838,7 +6157,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "crossbeam-channel", "filetime", "fsevent-sys 4.1.0", @@ -5851,15 +6170,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "ntapi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "ntapi" version = "0.4.1" @@ -5893,6 +6203,20 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint 0.4.4", + "num-complex 0.4.4", + "num-integer", + "num-iter", + "num-rational 0.4.1", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.2.6" @@ -5945,6 +6269,7 @@ dependencies = [ "num-iter", "num-traits", "rand 0.8.5", + "serde", "smallvec", "zeroize", ] @@ -6023,6 +6348,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint 0.4.4", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -6076,7 +6413,7 @@ dependencies = [ "rmp", "rmpv", "tokio", - "tokio-util 0.7.9", + "tokio-util", ] [[package]] @@ -6089,6 +6426,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc_exception" version = "0.1.2" @@ -6098,6 +6446,15 @@ dependencies = [ "cc", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.32.1" @@ -6139,6 +6496,35 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "oo7" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37558cac1af63a81fd2ff7f3469c02a4da06b163c5671791553b8dac10f07c82" +dependencies = [ + "aes", + "async-fs 2.1.1", + "async-io 2.3.1", + "async-lock 3.3.0", + "blocking", + "cbc", + "cipher 0.4.4", + "digest 0.10.7", + "futures-lite 2.2.0", + "futures-util", + "hkdf", + "hmac 0.12.1", + "num 0.4.1", + "num-bigint-dig 0.8.4", + "pbkdf2 0.12.2", + "rand 0.8.5", + "serde", + "sha2 0.10.7", + "zbus 4.0.1", + "zeroize", + "zvariant 4.0.2", +] + [[package]] name = "opaque-debug" version = "0.3.0" @@ -6162,7 +6548,7 @@ version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "cfg-if 1.0.0", "foreign-types 0.3.2", "libc", @@ -6225,7 +6611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", - "pin-project-lite 0.2.13", + "pin-project-lite", ] [[package]] @@ -6268,10 +6654,8 @@ dependencies = [ "language", "ordered-float 2.10.0", "picker", - "postage", "settings", "smol", - "text", "theme", "ui", "util", @@ -6303,21 +6687,20 @@ dependencies = [ [[package]] name = "palette" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e2f34147767aa758aa649415b50a69eeb46a67f9dc7db8011eeb3d84b351dc" +checksum = "ebfc23a4b76642983d57e4ad00bb4504eb30a8ce3c70f4aee1f725610e36d97a" dependencies = [ "approx", "fast-srgb8", "palette_derive", - "phf", ] [[package]] name = "palette_derive" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7db010ec5ff3d4385e4f133916faacd9dad0f6a09394c92d825b3aed310fa0a" +checksum = "e8890702dbec0bad9116041ae586f84805b13eecd1d8b1df27c29998a9969d6d" dependencies = [ "proc-macro2", "quote", @@ -6340,9 +6723,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" @@ -6399,7 +6782,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7037e5e93e0172a5a96874380bf73bc6ecef022e26fa25f2be26864d6b3ba95d" dependencies = [ "lazy_static", - "num", + "num 0.2.1", "regex", ] @@ -6426,15 +6809,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" -[[package]] -name = "pathfinder_color" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69bdc0d277d559e35e1b374de56df9262a6b71e091ca04a8831a239f8c7f0c62" -dependencies = [ - "pathfinder_simd", -] - [[package]] name = "pathfinder_geometry" version = "0.5.1" @@ -6462,6 +6836,16 @@ dependencies = [ "crypto-mac", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -6504,48 +6888,6 @@ dependencies = [ "indexmap 2.0.0", ] -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.48", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher 0.3.11", -] - [[package]] name = "picker" version = "0.1.0" @@ -6555,12 +6897,8 @@ dependencies = [ "env_logger", "gpui", "menu", - "parking_lot 0.11.2", "serde_json", - "settings", - "theme", "ui", - "util", "workspace", ] @@ -6590,12 +6928,6 @@ dependencies = [ "syn 2.0.48", ] -[[package]] -name = "pin-project-lite" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" - [[package]] name = "pin-project-lite" version = "0.2.13" @@ -6670,28 +7002,6 @@ dependencies = [ "time", ] -[[package]] -name = "plugin" -version = "0.1.0" -dependencies = [ - "bincode", - "plugin_macros", - "serde", - "serde_derive", -] - -[[package]] -name = "plugin_macros" -version = "0.1.0" -dependencies = [ - "bincode", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn 1.0.109", -] - [[package]] name = "png" version = "0.16.8" @@ -6716,7 +7026,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.13", + "pin-project-lite", "windows-sys 0.48.0", ] @@ -6728,7 +7038,7 @@ checksum = "545c980a3880efd47b2e262f6a4bb6daad6555cf3367aa9c4e52895f69537a41" dependencies = [ "cfg-if 1.0.0", "concurrent-queue", - "pin-project-lite 0.2.13", + "pin-project-lite", "rustix 0.38.30", "tracing", "windows-sys 0.52.0", @@ -6768,10 +7078,8 @@ name = "prettier" version = "0.1.0" dependencies = [ "anyhow", - "client", "collections", "fs", - "futures 0.3.28", "gpui", "language", "log", @@ -6779,7 +7087,6 @@ dependencies = [ "node_runtime", "parking_lot 0.11.2", "serde", - "serde_derive", "serde_json", "util", ] @@ -6823,6 +7130,15 @@ dependencies = [ "toml_edit 0.19.15", ] +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -6859,14 +7175,33 @@ dependencies = [ [[package]] name = "procinfo" version = "0.1.0" -source = "git+https://github.com/zed-industries/wezterm?rev=5cd757e5f2eb039ed0c6bb6512223e69d5efc64d#5cd757e5f2eb039ed0c6bb6512223e69d5efc64d" +source = "git+https://github.com/zed-industries/wezterm?rev=0c13436f4fa8b126f46dd4a20106419b41666897#0c13436f4fa8b126f46dd4a20106419b41666897" dependencies = [ "libc", "log", - "ntapi 0.3.7", + "ntapi", "winapi 0.3.9", ] +[[package]] +name = "profiling" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +dependencies = [ + "quote", + "syn 2.0.48", +] + [[package]] name = "project" version = "0.1.0" @@ -6874,26 +7209,19 @@ dependencies = [ "aho-corasick", "anyhow", "async-trait", - "backtrace", "client", "clock", "collections", "copilot", - "ctor", - "db", "env_logger", "fs", - "fsevent", "futures 0.3.28", "fuzzy", - "git", "git2", "globset", "gpui", - "ignore", - "itertools 0.10.5", + "itertools 0.11.0", "language", - "lazy_static", "log", "lsp", "node_runtime", @@ -6901,27 +7229,57 @@ dependencies = [ "postage", "prettier", "pretty_assertions", + "project_core", "rand 0.8.5", "regex", "release_channel", "rpc", - "runnable", - "schemars", "serde", - "serde_derive", "serde_json", "settings", "sha2 0.10.7", "similar", "smol", - "sum_tree", - "tempfile", + "task", "terminal", "text", - "thiserror", - "toml 0.8.10", "unindent", "util", + "which 6.0.0", +] + +[[package]] +name = "project_core" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "clock", + "collections", + "fs", + "futures 0.3.28", + "fuzzy", + "git", + "git2", + "gpui", + "ignore", + "itertools 0.11.0", + "language", + "log", + "lsp", + "parking_lot 0.11.2", + "postage", + "pretty_assertions", + "rand 0.8.5", + "rpc", + "schemars", + "serde", + "serde_json", + "settings", + "smol", + "sum_tree", + "text", + "util", ] [[package]] @@ -6933,11 +7291,9 @@ dependencies = [ "collections", "db", "editor", - "futures 0.3.28", "gpui", "language", "menu", - "postage", "pretty_assertions", "project", "schemars", @@ -6946,7 +7302,6 @@ dependencies = [ "serde_derive", "serde_json", "settings", - "smallvec", "theme", "ui", "unicase", @@ -6967,13 +7322,10 @@ dependencies = [ "lsp", "ordered-float 2.10.0", "picker", - "postage", "project", "release_channel", "serde_json", "settings", - "smol", - "text", "theme", "util", "workspace", @@ -7031,7 +7383,7 @@ dependencies = [ "prost-types 0.9.0", "regex", "tempfile", - "which", + "which 4.4.2", ] [[package]] @@ -7117,11 +7469,11 @@ dependencies = [ [[package]] name = "pulldown-cmark" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" +checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "memchr", "unicase", ] @@ -7268,7 +7620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac4ea493258d54c24cb46aa9345d099e58e2ea3f30dd63667fc54fc892f18e76" dependencies = [ "cocoa", - "core-graphics 0.23.1", + "core-graphics", "objc", "raw-window-handle 0.5.2", ] @@ -7319,17 +7671,16 @@ name = "recent_projects" version = "0.1.0" dependencies = [ "editor", - "futures 0.3.28", "fuzzy", "gpui", "language", + "menu", "ordered-float 2.10.0", "picker", - "postage", - "settings", + "project", + "serde", + "serde_json", "smol", - "text", - "theme", "ui", "util", "workspace", @@ -7378,9 +7729,6 @@ name = "refineable" version = "0.1.0" dependencies = [ "derive_refineable", - "proc-macro2", - "quote", - "syn 1.0.109", ] [[package]] @@ -7398,14 +7746,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.5" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.8", - "regex-syntax 0.7.5", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", ] [[package]] @@ -7422,11 +7770,6 @@ name = "regex-automata" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.7.5", -] [[package]] name = "regex-automata" @@ -7451,12 +7794,6 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" -[[package]] -name = "regex-syntax" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" - [[package]] name = "regex-syntax" version = "0.8.2" @@ -7503,7 +7840,7 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite 0.2.13", + "pin-project-lite", "serde", "serde_json", "serde_urlencoded", @@ -7557,16 +7894,11 @@ dependencies = [ name = "rich_text" version = "0.1.0" dependencies = [ - "anyhow", - "collections", "futures 0.3.28", "gpui", "language", - "lazy_static", + "linkify", "pulldown-cmark", - "smallvec", - "smol", - "sum_tree", "theme", "ui", "util", @@ -7615,7 +7947,7 @@ dependencies = [ "rkyv_derive", "seahash", "tinyvec", - "uuid 1.4.1", + "uuid", ] [[package]] @@ -7694,12 +8026,9 @@ name = "rpc" version = "0.1.0" dependencies = [ "anyhow", - "async-lock 2.8.0", "async-tungstenite", "base64 0.13.1", - "clock", "collections", - "ctor", "env_logger", "futures 0.3.28", "gpui", @@ -7709,12 +8038,8 @@ dependencies = [ "rand 0.8.5", "rsa 0.4.0", "serde", - "serde_derive", "serde_json", - "smol", - "smol-timeout", "strum", - "tempfile", "tracing", "util", "zstd", @@ -7762,55 +8087,13 @@ dependencies = [ "zeroize", ] -[[package]] -name = "runnable" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "futures 0.3.28", - "gpui", - "parking_lot 0.11.2", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "smol", - "util", -] - -[[package]] -name = "runnables_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "db", - "editor", - "fs", - "futures 0.3.28", - "fuzzy", - "gpui", - "log", - "picker", - "project", - "runnable", - "schemars", - "serde", - "serde_json", - "theme", - "ui", - "util", - "workspace", -] - [[package]] name = "rusqlite" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "fallible-iterator 0.2.0", "fallible-streaming-iterator", "hashlink", @@ -7820,9 +8103,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.0.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" +checksum = "a82c0bbc10308ed323529fd3c1dce8badda635aa319a5ff0e6466f33b8101e3f" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -7831,9 +8114,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.0.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29" +checksum = "6227c01b1783cdfee1bcf844eb44594cd16ec71c35305bf1c9fb5aade2735e16" dependencies = [ "proc-macro2", "quote", @@ -7844,9 +8127,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.0.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada" +checksum = "8cb0a25bfbb2d4b4402179c2cf030387d9990857ce08a32592c6238db9fa8665" dependencies = [ "globset", "sha2 0.10.7", @@ -7898,7 +8181,7 @@ checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ "bitflags 1.3.2", "errno", - "io-lifetimes", + "io-lifetimes 1.0.11", "libc", "linux-raw-sys 0.3.8", "windows-sys 0.48.0", @@ -7910,11 +8193,12 @@ version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "errno", "itoa", "libc", "linux-raw-sys 0.4.12", + "once_cell", "windows-sys 0.52.0", ] @@ -8038,7 +8322,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecbd2eb639fd7cab5804a0837fe373cc2172d15437e804c054a9fb885cb923b0" dependencies = [ - "cipher", + "cipher 0.3.0", ] [[package]] @@ -8111,7 +8395,7 @@ dependencies = [ "base64ct", "hmac 0.11.0", "password-hash", - "pbkdf2", + "pbkdf2 0.8.0", "salsa20", "sha2 0.9.9", ] @@ -8164,7 +8448,7 @@ dependencies = [ "time", "tracing", "url", - "uuid 1.4.1", + "uuid", ] [[package]] @@ -8195,7 +8479,7 @@ dependencies = [ "rust_decimal", "serde_json", "time", - "uuid 1.4.1", + "uuid", ] [[package]] @@ -8211,7 +8495,7 @@ dependencies = [ "serde_json", "sqlx", "time", - "uuid 1.4.1", + "uuid", ] [[package]] @@ -8220,25 +8504,34 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sealed" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b5e421024b5e5edfbaa8e60ecf90bda9dbffc602dbb230e6028763f85f0c68c" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "search" version = "0.1.0" dependencies = [ "anyhow", - "bitflags 1.3.2", + "bitflags 2.4.2", "client", "collections", "editor", "futures 0.3.28", "gpui", "language", - "log", "menu", - "postage", "project", "semantic_index", "serde", - "serde_derive", "serde_json", "settings", "smallvec", @@ -8299,19 +8592,15 @@ version = "0.1.0" dependencies = [ "ai", "anyhow", - "async-trait", - "client", "collections", "ctor", "env_logger", "futures 0.3.28", - "globset", "gpui", "language", "lazy_static", "log", "ndarray", - "node_runtime", "ordered-float 2.10.0", "parking_lot 0.11.2", "postage", @@ -8321,7 +8610,6 @@ dependencies = [ "release_channel", "rpc", "rusqlite", - "rust-embed", "schemars", "serde", "serde_json", @@ -8329,7 +8617,6 @@ dependencies = [ "sha1", "smol", "tempfile", - "tiktoken-rs", "tree-sitter", "tree-sitter-cpp", "tree-sitter-elixir", @@ -8459,13 +8746,11 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "feature_flags", "fs", "futures 0.3.28", "gpui", "indoc", "lazy_static", - "postage", "pretty_assertions", "release_channel", "rust-embed", @@ -8475,7 +8760,6 @@ dependencies = [ "serde_json", "serde_json_lenient", "smallvec", - "toml 0.8.10", "tree-sitter", "tree-sitter-json 0.19.0", "unindent", @@ -8567,9 +8851,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" @@ -8660,12 +8944,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "slab" version = "0.4.9" @@ -8696,7 +8974,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" dependencies = [ - "async-channel", + "async-channel 1.9.0", "futures-core", "futures-io", ] @@ -8707,33 +8985,59 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +[[package]] +name = "smithay-client-toolkit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +dependencies = [ + "bitflags 2.4.2", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2 0.9.4", + "rustix 0.38.30", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + [[package]] name = "smol" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" dependencies = [ - "async-channel", + "async-channel 1.9.0", "async-executor", "async-fs 1.6.0", "async-io 1.13.0", "async-lock 2.8.0", "async-net 1.7.0", - "async-process", + "async-process 1.7.0", "blocking", "futures-lite 1.13.0", ] -[[package]] -name = "smol-timeout" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "847d777e2c6c166bad26264479e80a9820f3d364fcb4a0e23cd57bbfa8e94961" -dependencies = [ - "async-io 1.13.0", - "pin-project-lite 0.1.12", -] - [[package]] name = "snippet" version = "0.1.0" @@ -8762,6 +9066,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spdx" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ef1a0fa1e39ac22972c8db23ff89aea700ab96aa87114e1fb55937a631a0c9" +dependencies = [ + "smallvec", +] + [[package]] name = "spin" version = "0.5.2" @@ -8827,7 +9140,7 @@ dependencies = [ "smol", "thread_local", "util", - "uuid 1.4.1", + "uuid", ] [[package]] @@ -8835,8 +9148,6 @@ name = "sqlez_macros" version = "0.1.0" dependencies = [ "lazy_static", - "proc-macro2", - "quote", "sqlez", "sqlformat", "syn 1.0.109", @@ -8910,7 +9221,7 @@ dependencies = [ "tokio-stream", "tracing", "url", - "uuid 1.4.1", + "uuid", "webpki-roots", ] @@ -8962,7 +9273,7 @@ dependencies = [ "atoi", "base64 0.21.4", "bigdecimal", - "bitflags 2.4.1", + "bitflags 2.4.2", "byteorder", "bytes 1.5.0", "chrono", @@ -8996,7 +9307,7 @@ dependencies = [ "thiserror", "time", "tracing", - "uuid 1.4.1", + "uuid", "whoami", ] @@ -9009,7 +9320,7 @@ dependencies = [ "atoi", "base64 0.21.4", "bigdecimal", - "bitflags 2.4.1", + "bitflags 2.4.2", "byteorder", "chrono", "crc", @@ -9041,7 +9352,7 @@ dependencies = [ "thiserror", "time", "tracing", - "uuid 1.4.1", + "uuid", "whoami", ] @@ -9067,7 +9378,7 @@ dependencies = [ "time", "tracing", "url", - "uuid 1.4.1", + "uuid", ] [[package]] @@ -9096,8 +9407,6 @@ name = "storybook" version = "0.1.0" dependencies = [ "anyhow", - "backtrace-on-stack-overflow", - "chrono", "clap 4.4.4", "collab_ui", "ctrlc", @@ -9106,21 +9415,17 @@ dependencies = [ "fuzzy", "gpui", "indoc", - "itertools 0.11.0", "language", "log", "menu", "picker", "rust-embed", - "serde", "settings", "simplelog", - "smallvec", "story", "strum", "theme", "ui", - "util", ] [[package]] @@ -9162,6 +9467,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "subst" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca1318e5d6716d6541696727c88d9b8dfc8cfe6afd6908e186546fd4af7f5b98" +dependencies = [ + "memchr", + "unicode-width", +] + [[package]] name = "subtle" version = "2.5.0" @@ -9271,7 +9586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" dependencies = [ "float-cmp", - "siphasher 0.2.3", + "siphasher", ] [[package]] @@ -9313,16 +9628,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" -[[package]] -name = "sys-info" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "sys-locale" version = "0.3.1" @@ -9341,12 +9646,28 @@ dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.6", "libc", - "ntapi 0.4.1", + "ntapi", "once_cell", "rayon", "winapi 0.3.9", ] +[[package]] +name = "system-interface" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0682e006dd35771e392a6623ac180999a9a854b1d4a6c12fb2e804941c2b1f58" +dependencies = [ + "bitflags 2.4.2", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes 2.0.3", + "rustix 0.38.30", + "windows-sys 0.52.0", + "winx", +] + [[package]] name = "taffy" version = "0.3.11" @@ -9376,6 +9697,51 @@ version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" +[[package]] +name = "task" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "futures 0.3.28", + "gpui", + "schemars", + "serde", + "serde_json_lenient", + "subst", + "util", +] + +[[package]] +name = "tasks_ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "editor", + "fuzzy", + "gpui", + "language", + "menu", + "picker", + "project", + "serde", + "serde_json", + "task", + "tree-sitter-rust", + "tree-sitter-typescript", + "ui", + "util", + "workspace", +] + +[[package]] +name = "telemetry_events" +version = "0.1.0" +dependencies = [ + "serde", + "util", +] + [[package]] name = "tempfile" version = "3.9.0" @@ -9416,29 +9782,23 @@ dependencies = [ "alacritty_terminal", "anyhow", "collections", - "db", "dirs 4.0.0", "futures 0.3.28", "gpui", - "itertools 0.10.5", - "lazy_static", "libc", - "mio-extras", - "ordered-float 2.10.0", "procinfo", "rand 0.8.5", - "runnable", "schemars", "serde", "serde_derive", "serde_json", "settings", - "shellexpand", - "smallvec", "smol", + "task", "theme", "thiserror", "util", + "windows 0.53.0", ] [[package]] @@ -9453,27 +9813,19 @@ dependencies = [ "editor", "futures 0.3.28", "gpui", - "itertools 0.10.5", + "itertools 0.11.0", "language", - "lazy_static", - "libc", - "mio-extras", - "ordered-float 2.10.0", - "procinfo", "project", "rand 0.8.5", - "runnable", "search", "serde", - "serde_derive", "serde_json", "settings", "shellexpand", - "smallvec", "smol", + "task", "terminal", "theme", - "thiserror", "ui", "util", "workspace", @@ -9487,7 +9839,6 @@ dependencies = [ "clock", "collections", "ctor", - "digest 0.9.0", "env_logger", "gpui", "lazy_static", @@ -9520,7 +9871,6 @@ dependencies = [ "futures 0.3.28", "gpui", "indexmap 1.9.3", - "itertools 0.11.0", "palette", "parking_lot 0.11.2", "refineable", @@ -9532,25 +9882,20 @@ dependencies = [ "serde_repr", "settings", "story", - "toml 0.8.10", "util", - "uuid 1.4.1", + "uuid", ] [[package]] name = "theme_importer" version = "0.1.0" dependencies = [ - "any_ascii", "anyhow", "clap 4.4.4", - "convert_case 0.6.0", "gpui", "indexmap 1.9.3", - "indoc", "log", "palette", - "pathfinder_color", "rust-embed", "schemars", "serde", @@ -9559,7 +9904,6 @@ dependencies = [ "simplelog", "strum", "theme", - "uuid 1.4.1", "vscode_theme", ] @@ -9568,17 +9912,13 @@ name = "theme_selector" version = "0.1.0" dependencies = [ "client", - "editor", "feature_flags", "fs", "fuzzy", "gpui", "log", - "parking_lot 0.11.2", "picker", - "postage", "settings", - "smol", "theme", "ui", "util", @@ -9605,12 +9945,6 @@ dependencies = [ "syn 2.0.48", ] -[[package]] -name = "thousands" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" - [[package]] name = "thread_local" version = "1.1.7" @@ -9675,6 +10009,17 @@ dependencies = [ "time-core", ] +[[package]] +name = "time_format" +version = "0.1.0" +dependencies = [ + "anyhow", + "core-foundation", + "core-foundation-sys 0.8.6", + "sys-locale", + "time", +] + [[package]] name = "tiny-skia" version = "0.5.1" @@ -9729,7 +10074,7 @@ dependencies = [ "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.13", + "pin-project-lite", "signal-hook-registry", "socket2 0.5.4", "tokio-macros", @@ -9747,16 +10092,6 @@ dependencies = [ "log", ] -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite 0.2.13", - "tokio", -] - [[package]] name = "tokio-macros" version = "2.1.0" @@ -9795,7 +10130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.13", + "pin-project-lite", "tokio", ] @@ -9811,20 +10146,6 @@ dependencies = [ "tungstenite 0.17.3", ] -[[package]] -name = "tokio-util" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" -dependencies = [ - "bytes 1.5.0", - "futures-core", - "futures-sink", - "log", - "pin-project-lite 0.2.13", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.9" @@ -9835,7 +10156,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.13", + "pin-project-lite", "tokio", "tracing", ] @@ -9881,6 +10202,17 @@ dependencies = [ "winnow 0.5.15", ] +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.0.0", + "toml_datetime", + "winnow 0.5.15", +] + [[package]] name = "toml_edit" version = "0.22.6" @@ -9894,37 +10226,6 @@ dependencies = [ "winnow 0.6.1", ] -[[package]] -name = "tonic" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff08f4649d10a70ffa3522ca559031285d8e421d727ac85c60825761818f5d0a" -dependencies = [ - "async-stream", - "async-trait", - "base64 0.13.1", - "bytes 1.5.0", - "futures-core", - "futures-util", - "h2", - "http 0.2.9", - "http-body", - "hyper", - "hyper-timeout", - "percent-encoding", - "pin-project", - "prost 0.9.0", - "prost-derive 0.9.0", - "tokio", - "tokio-stream", - "tokio-util 0.6.10", - "tower", - "tower-layer", - "tower-service", - "tracing", - "tracing-futures", -] - [[package]] name = "tower" version = "0.4.13" @@ -9933,13 +10234,9 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "indexmap 1.9.3", "pin-project", - "pin-project-lite 0.2.13", - "rand 0.8.5", - "slab", + "pin-project-lite", "tokio", - "tokio-util 0.7.9", "tower-layer", "tower-service", "tracing", @@ -9958,12 +10255,31 @@ dependencies = [ "http 0.2.9", "http-body", "http-range-header", - "pin-project-lite 0.2.13", + "pin-project-lite", "tower", "tower-layer", "tower-service", ] +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.4.2", + "bytes 1.5.0", + "futures-core", + "futures-util", + "http 0.2.9", + "http-body", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -9984,7 +10300,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.13", + "pin-project-lite", "tracing-attributes", "tracing-core", ] @@ -10064,8 +10380,8 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.20.10" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=1d8975319c2d5de1bf710e7e21db25b0eee4bc66#1d8975319c2d5de1bf710e7e21db25b0eee4bc66" +version = "0.20.100" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=e4a23971ec3071a09c1e84816954c98f96e98e52#e4a23971ec3071a09c1e84816954c98f96e98e52" dependencies = [ "cc", "regex", @@ -10137,6 +10453,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-dart" +version = "0.0.1" +source = "git+https://github.com/agent3bood/tree-sitter-dart?rev=48934e3bf757a9b78f17bdfaa3e2b4284656fdc7#48934e3bf757a9b78f17bdfaa3e2b4284656fdc7" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-dockerfile" version = "0.1.0" @@ -10187,7 +10512,7 @@ dependencies = [ [[package]] name = "tree-sitter-gitcommit" version = "0.3.3" -source = "git+https://github.com/gbprod/tree-sitter-gitcommit#e8d9eda4e5ea0b08aa39d48dab0f6553058fbe0f" +source = "git+https://github.com/gbprod/tree-sitter-gitcommit#7c01af8d227b5344f62aade2ff00f19bd0c458ca" dependencies = [ "cc", "tree-sitter", @@ -10241,7 +10566,7 @@ dependencies = [ [[package]] name = "tree-sitter-haskell" version = "0.14.0" -source = "git+https://github.com/tree-sitter/tree-sitter-haskell?rev=cf98de23e4285b8e6bcb57b050ef2326e2cc284b#cf98de23e4285b8e6bcb57b050ef2326e2cc284b" +source = "git+https://github.com/tree-sitter/tree-sitter-haskell?rev=8a99848fc734f9c4ea523b3f2a07df133cbbcec2#8a99848fc734f9c4ea523b3f2a07df133cbbcec2" dependencies = [ "cc", "tree-sitter", @@ -10325,7 +10650,7 @@ dependencies = [ [[package]] name = "tree-sitter-nu" version = "0.0.1" -source = "git+https://github.com/nushell/tree-sitter-nu?rev=26bbaecda0039df4067861ab38ea8ea169f7f5aa#26bbaecda0039df4067861ab38ea8ea169f7f5aa" +source = "git+https://github.com/nushell/tree-sitter-nu?rev=7dd29f9616822e5fc259f5b4ae6c4ded9a71a132#7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" dependencies = [ "cc", "tree-sitter", @@ -10370,8 +10695,8 @@ dependencies = [ [[package]] name = "tree-sitter-purescript" -version = "1.0.0" -source = "git+https://github.com/ivanmoreau/tree-sitter-purescript?rev=a37140f0c7034977b90faa73c94fcb8a5e45ed08#a37140f0c7034977b90faa73c94fcb8a5e45ed08" +version = "0.1.0" +source = "git+https://github.com/postsolar/tree-sitter-purescript?rev=v0.1.0#0554811a512b9cec08b5a83ce9096eb22da18213" dependencies = [ "cc", "tree-sitter", @@ -10428,7 +10753,7 @@ dependencies = [ [[package]] name = "tree-sitter-svelte" version = "0.10.2" -source = "git+https://github.com/Himujjal/tree-sitter-svelte?rev=697bb515471871e85ff799ea57a76298a71a9cca#697bb515471871e85ff799ea57a76298a71a9cca" +source = "git+https://github.com/Himujjal/tree-sitter-svelte?rev=bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd#bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd" dependencies = [ "cc", "tree-sitter", @@ -10454,8 +10779,8 @@ dependencies = [ [[package]] name = "tree-sitter-uiua" -version = "0.3.3" -source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2" +version = "0.10.0" +source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=21dc2db39494585bf29a3f86d5add6e9d11a22ba#21dc2db39494585bf29a3f86d5add6e9d11a22ba" dependencies = [ "cc", "tree-sitter", @@ -10578,13 +10903,10 @@ dependencies = [ name = "ui" version = "0.1.0" dependencies = [ - "anyhow", "chrono", "gpui", "itertools 0.11.0", "menu", - "rand 0.8.5", - "serde", "settings", "smallvec", "story", @@ -10742,7 +11064,7 @@ dependencies = [ "roxmltree 0.14.1", "rustybuzz 0.3.0", "simplecss", - "siphasher 0.2.3", + "siphasher", "svgtypes", "ttf-parser 0.12.3", "unicode-bidi", @@ -10786,15 +11108,10 @@ dependencies = [ "take-until", "tempfile", "tendril", + "unicase", "url", ] -[[package]] -name = "uuid" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22" - [[package]] name = "uuid" version = "1.4.1" @@ -10882,26 +11199,24 @@ dependencies = [ "async-trait", "collections", "command_palette", - "copilot", + "command_palette_hooks", "editor", "futures 0.3.28", "gpui", "indoc", - "itertools 0.10.5", "language", "log", "lsp", "nvim-rs", "parking_lot 0.11.2", - "project", "regex", "release_channel", + "schemars", "search", "serde", "serde_derive", "serde_json", "settings", - "theme", "tokio", "ui", "util", @@ -10930,7 +11245,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "cursor-icon", "log", "serde", @@ -10985,6 +11300,32 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi-common" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "880c1461417b2bf90262591bf8a5f04358fb86dac8a585a49b87024971296763" +dependencies = [ + "anyhow", + "bitflags 2.4.2", + "cap-fs-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "io-extras", + "io-lifetimes 2.0.3", + "log", + "once_cell", + "rustix 0.38.30", + "system-interface", + "thiserror", + "tracing", + "wasmtime", + "wiggle", + "windows-sys 0.52.0", +] + [[package]] name = "wasm-bindgen" version = "0.2.87" @@ -11053,62 +11394,117 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-encoder" -version = "0.38.1" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad2b51884de9c7f4fe2fd1043fccb8dcad4b1e29558146ee57a144d15779f3f" +checksum = "972f97a5d8318f908dded23594188a90bcd09365986b1163e66d70170e5287ae" dependencies = [ "leb128", ] [[package]] -name = "wasmparser" -version = "0.118.1" +name = "wasm-encoder" +version = "0.201.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ee9723b928e735d53000dec9eae7b07a60e490c85ab54abb66659fc61bfcd9" +checksum = "b9c7d2731df60006819b013f64ccc2019691deccf6e11a1804bc850cd6748f1a" dependencies = [ + "leb128", +] + +[[package]] +name = "wasm-metadata" +version = "0.10.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18ebaa7bd0f9e7a5e5dd29b9a998acf21c4abed74265524dd7e85934597bfb10" +dependencies = [ + "anyhow", + "indexmap 2.0.0", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder 0.41.2", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab" +dependencies = [ + "bitflags 2.4.2", "indexmap 2.0.0", "semver", ] [[package]] -name = "wasmtime" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +name = "wasmprinter" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e73986a6b7fdfedb7c5bf9e7eb71135486507c8fbc4c0c42cffcb6532988b7" dependencies = [ "anyhow", + "wasmparser", +] + +[[package]] +name = "wasmtime" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c843b8bc4dd4f3a76173ba93405c71111d570af0d90ea5f6299c705d0c2add2" +dependencies = [ + "addr2line", + "anyhow", + "async-trait", "bincode", "bumpalo", "cfg-if 1.0.0", + "encoding_rs", + "fxprof-processed-profile", + "gimli", "indexmap 2.0.0", + "ittapi", "libc", "log", "object", "once_cell", "paste", + "rayon", + "rustix 0.38.30", "serde", "serde_derive", "serde_json", "target-lexicon", + "wasm-encoder 0.41.2", "wasmparser", + "wasmtime-cache", + "wasmtime-component-macro", + "wasmtime-component-util", "wasmtime-cranelift", "wasmtime-environ", - "wasmtime-jit", + "wasmtime-fiber", + "wasmtime-jit-debug", + "wasmtime-jit-icache-coherence", "wasmtime-runtime", - "windows-sys 0.48.0", + "wasmtime-winch", + "wat", + "windows-sys 0.52.0", ] [[package]] name = "wasmtime-asm-macros" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b9d329c718b3a18412a6a017c912b539baa8fe1210d21b651f6b4dbafed743" dependencies = [ "cfg-if 1.0.0", ] [[package]] name = "wasmtime-c-api-impl" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc93587c24d8e3cb28912eb7abf95f7e350380656faccc46cff04c0821ec58c2" dependencies = [ "anyhow", "log", @@ -11120,17 +11516,60 @@ dependencies = [ [[package]] name = "wasmtime-c-api-macros" -version = "0.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e571a71eba52dfe81ef653a3a336888141f00fc2208a9962722e036fe2a34be" dependencies = [ "proc-macro2", "quote", ] +[[package]] +name = "wasmtime-cache" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb4fc2bbf9c790a57875eba65588fa97acf57a7d784dc86d057e648d9a1ed91" +dependencies = [ + "anyhow", + "base64 0.21.4", + "bincode", + "directories-next", + "log", + "rustix 0.38.30", + "serde", + "serde_derive", + "sha2 0.10.7", + "toml 0.5.11", + "windows-sys 0.52.0", + "zstd", +] + +[[package]] +name = "wasmtime-component-macro" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d55ddfd02898885c39638eae9631cd430c83a368f5996ed0f7bfb181d02157" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser 0.13.2", +] + +[[package]] +name = "wasmtime-component-util" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6d69c430cddc70ec42159506962c66983ce0192ebde4eb125b7aabc49cff88" + [[package]] name = "wasmtime-cranelift" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ca62f519225492bd555d0ec85a2dacb0c10315db3418c8b9aeb3824bf54a24" dependencies = [ "anyhow", "cfg-if 1.0.0", @@ -11153,8 +11592,9 @@ dependencies = [ [[package]] name = "wasmtime-cranelift-shared" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd5f2071f42e61490bf7cb95b9acdbe6a29dd577a398019304a960585f28b844" dependencies = [ "anyhow", "cranelift-codegen", @@ -11168,62 +11608,78 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82bf1a47f384610da19f58b0fd392ca6a3b720974315c08afb0392c0f3951fed" dependencies = [ "anyhow", + "bincode", + "cpp_demangle", "cranelift-entity", "gimli", "indexmap 2.0.0", "log", "object", + "rustc-demangle", "serde", "serde_derive", "target-lexicon", "thiserror", + "wasm-encoder 0.41.2", "wasmparser", + "wasmprinter", + "wasmtime-component-util", "wasmtime-types", ] [[package]] -name = "wasmtime-jit" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "anyhow", - "bincode", - "cfg-if 1.0.0", - "gimli", - "log", - "object", - "rustix 0.38.30", - "serde", - "serde_derive", - "target-lexicon", - "wasmtime-environ", - "wasmtime-jit-icache-coherence", - "wasmtime-runtime", - "windows-sys 0.48.0", -] - -[[package]] -name = "wasmtime-jit-icache-coherence" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "wasmtime-runtime" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +name = "wasmtime-fiber" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e31aecada2831e067ebfe93faa3001cc153d506f8af40bbea58aa1d20fe4820" dependencies = [ "anyhow", "cc", "cfg-if 1.0.0", + "rustix 0.38.30", + "wasmtime-asm-macros", + "wasmtime-versioned-export-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "wasmtime-jit-debug" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833dae95bc7a4f9177bf93f9497419763535b74e37eb8c37be53937d3281e287" +dependencies = [ + "object", + "once_cell", + "rustix 0.38.30", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4121cb29dda08139b2824a734dd095d83ce843f2d613a84eb580b9cfc17ac" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "wasmtime-runtime" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e517f2b996bb3b0e34a82a2bce194f850d9bcfc25c08328ef5fb71b071066b8" +dependencies = [ + "anyhow", + "cc", + "cfg-if 1.0.0", + "encoding_rs", "indexmap 2.0.0", "libc", "log", @@ -11234,18 +11690,21 @@ dependencies = [ "psm", "rustix 0.38.30", "sptr", - "wasm-encoder", + "wasm-encoder 0.41.2", "wasmtime-asm-macros", "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit-debug", "wasmtime-versioned-export-macros", "wasmtime-wmemcheck", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "wasmtime-types" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a327d7a0ef57bd52a507d28b4561a74126c7a8535a2fc6f2025716bc6a52e8" dependencies = [ "cranelift-entity", "serde", @@ -11256,18 +11715,113 @@ dependencies = [ [[package]] name = "wasmtime-versioned-export-macros" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef32eea9fc7035a55159a679d1e89b43ece5ae45d24eed4808e6a92c99a0da4" dependencies = [ "proc-macro2", "quote", "syn 2.0.48", ] +[[package]] +name = "wasmtime-wasi" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d04d2fb2257245aa05ff799ded40520ae4d8cd31b0d14972afac89061f12fe12" +dependencies = [ + "anyhow", + "async-trait", + "bitflags 2.4.2", + "bytes 1.5.0", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures 0.3.28", + "io-extras", + "io-lifetimes 2.0.3", + "log", + "once_cell", + "rustix 0.38.30", + "system-interface", + "thiserror", + "tokio", + "tracing", + "url", + "wasi-common", + "wasmtime", + "wiggle", + "windows-sys 0.52.0", +] + +[[package]] +name = "wasmtime-winch" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3378c0e808a744b5d4df2a9a9d2746a53b151811926731f04fc401707f7d54" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "object", + "target-lexicon", + "wasmparser", + "wasmtime-cranelift-shared", + "wasmtime-environ", + "winch-codegen", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca677c36869e45602617b25a9968ec0d895ad9a0aee3756d9dee1ddd89456f91" +dependencies = [ + "anyhow", + "heck 0.4.1", + "indexmap 2.0.0", + "wit-parser 0.13.2", +] + [[package]] name = "wasmtime-wmemcheck" -version = "16.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime?rev=v16.0.0#6613acd1e4817957a4a7745125ef063b43c273a7" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4cbfb052d66f03603a9b77f18171ea245c7805714caad370a549a6344bf86b" + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "201.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef6e1ef34d7da3e2b374fd2b1a9c0227aff6cad596e1b24df9b58d0f6222faa" +dependencies = [ + "bumpalo", + "leb128", + "memchr", + "unicode-width", + "wasm-encoder 0.201.0", +] + +[[package]] +name = "wat" +version = "1.201.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453d5b37a45b98dee4f4cb68015fc73634d7883bbef1c65e6e9c78d454cf3f32" +dependencies = [ + "wast 201.0.0", +] [[package]] name = "wayland-backend" @@ -11289,24 +11843,59 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "rustix 0.38.30", "wayland-backend", "wayland-scanner", ] +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.4.2", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ce5fa868dd13d11a0d04c5e2e65726d0897be8de247c0c5a65886e283231ba" +dependencies = [ + "rustix 0.38.30", + "wayland-client", + "xcursor", +] + [[package]] name = "wayland-protocols" version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "wayland-backend", "wayland-client", "wayland-scanner", ] +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.4.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + [[package]] name = "wayland-scanner" version = "0.31.1" @@ -11326,6 +11915,7 @@ checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" dependencies = [ "dlib", "log", + "once_cell", "pkg-config", ] @@ -11360,19 +11950,17 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "copilot_ui", "db", "editor", - "fs", "fuzzy", "gpui", "install_cli", - "log", "picker", "project", "schemars", "serde", "settings", - "theme", "theme_selector", "ui", "util", @@ -11392,12 +11980,67 @@ dependencies = [ "rustix 0.38.30", ] +[[package]] +name = "which" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.30", + "windows-sys 0.52.0", +] + [[package]] name = "whoami" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +[[package]] +name = "wiggle" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69812e493f8a43d8551abfaaf9539e1aff0cf56a58cdd276845fc4af035d0cd" +dependencies = [ + "anyhow", + "async-trait", + "bitflags 2.4.2", + "thiserror", + "tracing", + "wasmtime", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0446357a5a7af0172848b6eca7b2aa1ab7d90065cd2ab02b633a322e1a52f636" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro2", + "quote", + "shellexpand", + "syn 2.0.48", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "18.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9498ef53a12cf25dc6de9baef6ccd8b58d159202c412a19f4d72b218393086c5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wiggle-generate", +] + [[package]] name = "winapi" version = "0.2.8" @@ -11441,6 +12084,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8197ed4a2ebf612f0624ddda10de71f8cd2d3a4ecf8ffac0586a264599708d63" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "wasmparser", + "wasmtime-environ", +] + [[package]] name = "windows" version = "0.46.0" @@ -11459,6 +12118,35 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538" +dependencies = [ + "windows-core", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-core" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc5b895a6377f1ab9fa55acedab1fd5ac0db66ad1e6c7f47e28a22e446a5dd" +dependencies = [ + "windows-result", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-result" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd19df78e5168dfb0aedc343d1d1b8d422ab2db6756d2dc3fef75035402a3f64" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -11483,7 +12171,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.4", ] [[package]] @@ -11518,17 +12206,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -11545,9 +12233,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" @@ -11563,9 +12251,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" @@ -11581,9 +12269,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" @@ -11599,9 +12287,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" @@ -11617,9 +12305,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" @@ -11635,9 +12323,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" @@ -11653,9 +12341,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" @@ -11685,6 +12373,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winx" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346" +dependencies = [ + "bitflags 2.4.2", + "windows-sys 0.52.0", +] + [[package]] name = "wio" version = "0.2.2" @@ -11694,6 +12392,119 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "wit-bindgen" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5408d742fcdf418b766f23b2393f0f4d9b10b72b7cd96d9525626943593e8cc0" +dependencies = [ + "bitflags 2.4.2", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7146725463d08ccf9c6c5357a7a6c1fff96185d95d6e84e7c75c92e5b1273c93" +dependencies = [ + "anyhow", + "wit-parser 0.14.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5fefcf93ff2ea03c8fe9b9db2caee3096103c0e3cd62ed54f6f9493aa6b405" +dependencies = [ + "anyhow", + "heck 0.4.1", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce4059a1adc671e4457f457cb638ed2f766a1a462bb7daa3b638c6fb1fda156e" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.48", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be60cd1b2ff7919305301d0c27528d4867bd793afe890ba3837743da9655d91b" +dependencies = [ + "anyhow", + "bitflags 2.4.2", + "indexmap 2.0.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.41.2", + "wasm-metadata", + "wasmparser", + "wit-parser 0.14.0", +] + +[[package]] +name = "wit-parser" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "316b36a9f0005f5aa4b03c39bc3728d045df136f8c13a73b7db4510dec725e08" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.0.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", +] + +[[package]] +name = "wit-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee4ad7310367bf272507c0c8e0c74a80b4ed586b833f7c7ca0b7588f686f11a" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.0.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror", + "wast 35.0.2", +] + [[package]] name = "workspace" version = "0.1.0" @@ -11703,6 +12514,7 @@ dependencies = [ "bincode", "call", "client", + "clock", "collections", "db", "derive_more", @@ -11710,9 +12522,7 @@ dependencies = [ "fs", "futures 0.3.28", "gpui", - "indoc", - "install_cli", - "itertools 0.10.5", + "itertools 0.11.0", "language", "lazy_static", "log", @@ -11720,19 +12530,17 @@ dependencies = [ "parking_lot 0.11.2", "postage", "project", - "runnable", "schemars", "serde", - "serde_derive", "serde_json", "settings", "smallvec", "sqlez", - "terminal", + "task", "theme", "ui", "util", - "uuid 1.4.1", + "uuid", ] [[package]] @@ -11754,6 +12562,33 @@ dependencies = [ "tap", ] +[[package]] +name = "x11-clipboard" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +dependencies = [ + "gethostname", + "rustix 0.38.30", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" + [[package]] name = "xattr" version = "0.2.3" @@ -11775,6 +12610,12 @@ dependencies = [ "quick-xml 0.30.0", ] +[[package]] +name = "xcursor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" + [[package]] name = "xdg-home" version = "1.1.0" @@ -11815,6 +12656,14 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.4.4", +] + [[package]] name = "yansi" version = "0.5.1" @@ -11841,16 +12690,16 @@ dependencies = [ [[package]] name = "zbus" -version = "3.15.0" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c45d06ae3b0f9ba1fb2671268b975557d8f5a84bb5ec6e43964f87e763d8bca8" +checksum = "5acecd3f8422f198b1a2f954bcc812fe89f3fa4281646f3da1da7925db80085d" dependencies = [ "async-broadcast 0.5.1", "async-executor", "async-fs 1.6.0", "async-io 1.13.0", "async-lock 2.8.0", - "async-process", + "async-process 1.7.0", "async-recursion 1.0.5", "async-task", "async-trait", @@ -11875,16 +12724,69 @@ dependencies = [ "uds_windows", "winapi 0.3.9", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 3.15.1", + "zbus_names 2.6.1", + "zvariant 3.15.1", +] + +[[package]] +name = "zbus" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030" +dependencies = [ + "async-broadcast 0.7.0", + "async-executor", + "async-fs 2.1.1", + "async-io 2.3.1", + "async-lock 3.3.0", + "async-process 2.1.0", + "async-recursion 1.0.5", + "async-task", + "async-trait", + "blocking", + "derivative", + "enumflags2", + "event-listener 5.1.0", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.27.1", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros 4.0.1", + "zbus_names 3.0.0", + "zvariant 4.0.2", ] [[package]] name = "zbus_macros" -version = "3.15.0" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a1ba45ed0ad344b85a2bb5a1fe9830aed23d67812ea39a586e7d0136439c7d" +checksum = "2207eb71efebda17221a579ca78b45c4c5f116f074eb745c3a172e688ccf89f5" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_macros" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a3e850ff1e7217a3b7a07eba90d37fe9bb9e89a310f718afcde5885ca9b6d7" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", @@ -11896,28 +12798,34 @@ dependencies = [ [[package]] name = "zbus_names" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" +checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 3.15.1", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant 4.0.2", ] [[package]] name = "zed" -version = "0.124.0" +version = "0.126.0" dependencies = [ "activity_indicator", - "ai", "anyhow", "assets", "assistant", - "async-compression", - "async-recursion 0.3.2", - "async-tar", - "async-trait", "audio", "auto_update", "backtrace", @@ -11927,19 +12835,18 @@ dependencies = [ "chrono", "cli", "client", + "clock", "collab_ui", "collections", "command_palette", "copilot", "copilot_ui", - "ctor", "db", "diagnostics", "editor", "env_logger", "extension", "extensions_ui", - "feature_flags", "feedback", "file_finder", "fs", @@ -11947,9 +12854,6 @@ dependencies = [ "futures 0.3.28", "go_to_line", "gpui", - "ignore", - "image", - "indexmap 1.9.3", "install_cli", "isahc", "itertools 0.11.0", @@ -11957,103 +12861,39 @@ dependencies = [ "language", "language_selector", "language_tools", - "lazy_static", - "libc", + "languages", "log", - "lsp", "markdown_preview", "menu", "mimalloc", "node_runtime", "notifications", - "num_cpus", "outline", "parking_lot 0.11.2", - "postage", + "profiling", "project", "project_panel", "project_symbols", "quick_action_bar", - "rand 0.8.5", "recent_projects", - "regex", "release_channel", "rope", - "rpc", - "rsa 0.4.0", - "runnable", - "runnables_ui", - "rust-embed", - "schemars", "search", "semantic_index", "serde", - "serde_derive", "serde_json", "settings", - "shellexpand", "simplelog", - "smallvec", "smol", - "sum_tree", - "tempfile", + "task", + "tasks_ui", "terminal_view", - "text", "theme", "theme_selector", - "thiserror", - "tiny_http", - "toml 0.8.10", - "tree-sitter", - "tree-sitter-astro", - "tree-sitter-bash", - "tree-sitter-c", - "tree-sitter-c-sharp", - "tree-sitter-clojure", - "tree-sitter-cpp", - "tree-sitter-css", - "tree-sitter-dockerfile", - "tree-sitter-elixir", - "tree-sitter-elm", - "tree-sitter-embedded-template", - "tree-sitter-erlang", - "tree-sitter-gitcommit", - "tree-sitter-gleam", - "tree-sitter-glsl", - "tree-sitter-go", - "tree-sitter-gomod", - "tree-sitter-gowork", - "tree-sitter-haskell", - "tree-sitter-hcl", - "tree-sitter-heex", - "tree-sitter-html", - "tree-sitter-json 0.20.0", - "tree-sitter-lua", - "tree-sitter-markdown", - "tree-sitter-nix", - "tree-sitter-nu", - "tree-sitter-ocaml", - "tree-sitter-php", - "tree-sitter-prisma-io", - "tree-sitter-proto", - "tree-sitter-purescript", - "tree-sitter-python", - "tree-sitter-racket", - "tree-sitter-ruby", "tree-sitter-rust", - "tree-sitter-scheme", - "tree-sitter-svelte", - "tree-sitter-toml", - "tree-sitter-typescript", - "tree-sitter-uiua", - "tree-sitter-vue", - "tree-sitter-yaml", - "tree-sitter-zig", - "unindent", - "url", "urlencoding", "util", - "uuid 1.4.1", + "uuid", "vim", "welcome", "workspace", @@ -12068,6 +12908,20 @@ dependencies = [ "serde", ] +[[package]] +name = "zed_extension_api" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "zed_gleam" +version = "0.0.1" +dependencies = [ + "zed_extension_api", +] + [[package]] name = "zeno" version = "0.2.3" @@ -12146,9 +13000,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "3.15.0" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" +checksum = "c5b4fcf3660d30fc33ae5cd97e2017b23a96e85afd7a1dd014534cd0bf34ba67" dependencies = [ "byteorder", "enumflags2", @@ -12156,14 +13010,27 @@ dependencies = [ "serde", "static_assertions", "url", - "zvariant_derive", + "zvariant_derive 3.15.1", +] + +[[package]] +name = "zvariant" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1b3ca6db667bfada0f1ebfc94b2b1759ba25472ee5373d4551bb892616389a" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive 4.0.2", ] [[package]] name = "zvariant_derive" -version = "3.15.0" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" +checksum = "0277758a8a0afc0e573e80ed5bfd9d9c2b48bd3108ffe09384f9f738c83f4a55" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", @@ -12173,10 +13040,23 @@ dependencies = [ ] [[package]] -name = "zvariant_utils" -version = "1.0.1" +name = "zvariant_derive" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +checksum = "b7a4b236063316163b69039f77ce3117accb41a09567fd24c168e43491e521bc" +dependencies = [ + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index bfb2df80e3..682908e2f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,12 +16,14 @@ members = [ "crates/collab_ui", "crates/collections", "crates/command_palette", + "crates/command_palette_hooks", "crates/copilot", "crates/copilot_ui", "crates/db", "crates/diagnostics", "crates/editor", "crates/extension", + "crates/extension_api", "crates/extensions_ui", "crates/feature_flags", "crates/feedback", @@ -38,6 +40,7 @@ members = [ "crates/language", "crates/language_selector", "crates/language_tools", + "crates/languages", "crates/live_kit_client", "crates/live_kit_server", "crates/lsp", @@ -49,10 +52,9 @@ members = [ "crates/notifications", "crates/outline", "crates/picker", - "crates/plugin", - "crates/plugin_macros", "crates/prettier", "crates/project", + "crates/project_core", "crates/project_panel", "crates/project_symbols", "crates/quick_action_bar", @@ -63,8 +65,8 @@ members = [ "crates/rich_text", "crates/rope", "crates/rpc", - "crates/runnable", - "crates/runnables_ui", + "crates/task", + "crates/tasks_ui", "crates/search", "crates/semantic_index", "crates/settings", @@ -80,6 +82,8 @@ members = [ "crates/theme", "crates/theme_importer", "crates/theme_selector", + "crates/telemetry_events", + "crates/time_format", "crates/ui", "crates/util", "crates/vcs_menu", @@ -88,6 +92,8 @@ members = [ "crates/workspace", "crates/zed", "crates/zed_actions", + "extensions/gleam", + "tooling/xtask", ] default-members = ["crates/zed"] resolver = "2" @@ -110,6 +116,7 @@ collab_ui = { path = "crates/collab_ui" } collections = { path = "crates/collections" } color = { path = "crates/color" } command_palette = { path = "crates/command_palette" } +command_palette_hooks = { path = "crates/command_palette_hooks" } copilot = { path = "crates/copilot" } copilot_ui = { path = "crates/copilot_ui" } db = { path = "crates/db" } @@ -132,6 +139,7 @@ journal = { path = "crates/journal" } language = { path = "crates/language" } language_selector = { path = "crates/language_selector" } language_tools = { path = "crates/language_tools" } +languages = { path = "crates/languages" } live_kit_client = { path = "crates/live_kit_client" } live_kit_server = { path = "crates/live_kit_server" } lsp = { path = "crates/lsp" } @@ -147,6 +155,7 @@ plugin = { path = "crates/plugin" } plugin_macros = { path = "crates/plugin_macros" } prettier = { path = "crates/prettier" } project = { path = "crates/project" } +project_core = { path = "crates/project_core" } project_panel = { path = "crates/project_panel" } project_symbols = { path = "crates/project_symbols" } quick_action_bar = { path = "crates/quick_action_bar" } @@ -155,8 +164,8 @@ release_channel = { path = "crates/release_channel" } rich_text = { path = "crates/rich_text" } rope = { path = "crates/rope" } rpc = { path = "crates/rpc" } -runnable = { path = "crates/runnable" } -runnables_ui = { path = "crates/runnables_ui" } +task = { path = "crates/task" } +tasks_ui = { path = "crates/tasks_ui" } search = { path = "crates/search" } semantic_index = { path = "crates/semantic_index" } settings = { path = "crates/settings" } @@ -172,6 +181,8 @@ text = { path = "crates/text" } theme = { path = "crates/theme" } theme_importer = { path = "crates/theme_importer" } theme_selector = { path = "crates/theme_selector" } +telemetry_events = { path = "crates/telemetry_events" } +time_format = { path = "crates/time_format" } ui = { path = "crates/ui" } util = { path = "crates/util" } vcs_menu = { path = "crates/vcs_menu" } @@ -185,27 +196,38 @@ anyhow = "1.0.57" async-compression = { version = "0.4", features = ["gzip", "futures-io"] } async-tar = "0.4.2" async-trait = "0.1" +bitflags = "2.4.2" blade-graphics = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f3946a03ffb76136290d6ccf7f2b80" } blade-macros = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f3946a03ffb76136290d6ccf7f2b80" } blade-rwh = { package = "raw-window-handle", version = "0.5" } chrono = { version = "0.4", features = ["serde"] } +clap = "4.4" +clickhouse = { version = "0.11.6" } ctor = "0.2.6" +core-foundation = { version = "0.9.3" } +core-foundation-sys = "0.8.6" derive_more = "0.99.17" env_logger = "0.9" futures = "0.3" git2 = { version = "0.15", default-features = false } globset = "0.4" +hex = "0.4.3" +ignore = "0.4.22" indoc = "1" -# We explicitly disable a http2 support in isahc. +# We explicitly disable http2 support in isahc. isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] } +itertools = "0.11.0" lazy_static = "1.4.0" +linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = "2.1.1" +palette = { version = "0.7.5", default-features = false, features = ["std"] } parking_lot = "0.11.1" +profiling = "1" postage = { version = "0.5", features = ["futures-traits"] } pretty_assertions = "1.3.0" prost = "0.8" -pulldown-cmark = { version = "0.9.2", default-features = false } +pulldown-cmark = { version = "0.10.0", default-features = false } rand = "0.8.5" refineable = { path = "./crates/refineable" } regex = "1.5" @@ -218,6 +240,8 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } serde_json_lenient = { version = "0.1", features = ["preserve_order", "raw_value"] } serde_repr = "0.1" +sha2 = "0.10" +shellexpand = "2.1.0" smallvec = { version = "1.6", features = ["union"] } smol = "1.2" strum = { version = "0.25.0", features = ["derive"] } @@ -227,6 +251,7 @@ thiserror = "1.0.29" tiktoken-rs = "0.5.7" time = { version = "0.3", features = ["serde", "serde-well-known", "formatting"] } toml = "0.8" +tower-http = "0.4.4" tree-sitter = { version = "0.20", features = ["wasm"] } tree-sitter-astro = { git = "https://github.com/virchau13/tree-sitter-astro.git", rev = "e924787e12e8a03194f36a113290ac11d6dc10f3" } tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" } @@ -236,6 +261,7 @@ tree-sitter-c-sharp = { git = "https://github.com/tree-sitter/tree-sitter-c-shar tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" } tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } tree-sitter-dockerfile = { git = "https://github.com/camdencheek/tree-sitter-dockerfile", rev = "33e22c33bcdbfc33d42806ee84cfd0b1248cc392" } +tree-sitter-dart = { git = "https://github.com/agent3bood/tree-sitter-dart", rev = "48934e3bf757a9b78f17bdfaa3e2b4284656fdc7" } tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" } tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" } tree-sitter-embedded-template = "0.20.0" @@ -246,41 +272,56 @@ tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" } tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" } tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" } -tree-sitter-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "cf98de23e4285b8e6bcb57b050ef2326e2cc284b" } +tree-sitter-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "8a99848fc734f9c4ea523b3f2a07df133cbbcec2" } tree-sitter-hcl = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0" } +rustc-demangle = "0.1.23" tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" } tree-sitter-html = "0.19.0" tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" } tree-sitter-lua = "0.0.14" tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } -tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa" } +tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" } tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "4abfdc1c7af2c6c77a370aee974627be1c285b3b" } tree-sitter-php = "0.21.1" tree-sitter-prisma-io = { git = "https://github.com/victorhqc/tree-sitter-prisma" } tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" } -tree-sitter-purescript = { git = "https://github.com/ivanmoreau/tree-sitter-purescript", rev = "a37140f0c7034977b90faa73c94fcb8a5e45ed08" } +tree-sitter-purescript = { git = "https://github.com/postsolar/tree-sitter-purescript", rev = "v0.1.0" } tree-sitter-python = "0.20.2" tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" } tree-sitter-ruby = "0.20.0" tree-sitter-rust = "0.20.3" tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9" } -tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "697bb515471871e85ff799ea57a76298a71a9cca" } +tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd" } tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" } tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" } -tree-sitter-uiua = { git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2" } +tree-sitter-uiua = { git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "21dc2db39494585bf29a3f86d5add6e9d11a22ba" } tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42" } tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" } tree-sitter-zig = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" } unindent = "0.1.7" +unicase = "2.6" url = "2.2" uuid = { version = "1.1.2", features = ["v4"] } -wasmtime = "16" +wasmparser = "0.121" +wasmtime = "18.0" +wasmtime-wasi = "18.0" +which = "6.0.0" sys-locale = "0.3.1" +[workspace.dependencies.windows] +version = "0.53.0" +features = [ + "Win32_Graphics_Gdi", + "Win32_UI_WindowsAndMessaging", + "Win32_Security", + "Win32_System_Threading", +] + + + [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "1d8975319c2d5de1bf710e7e21db25b0eee4bc66" } -wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "v16.0.0" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "e4a23971ec3071a09c1e84816954c98f96e98e52" } # Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released. pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "e4fcda0d5259d0acf902aee6de7d2501f2bd6629" } @@ -288,15 +329,20 @@ pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "e4fc split-debuginfo = "unpacked" debug = "limited" -# todo!(linux) - Remove this +# todo(linux) - Remove this [profile.dev.package.blade-graphics] split-debuginfo = "off" debug = "full" -[profile.dev.package.taffy] -opt-level = 3 +[profile.dev.package] +taffy = { opt-level = 3 } +cranelift-codegen = { opt-level = 3 } +wasmtime-cranelift = { opt-level = 3 } [profile.release] debug = "limited" lto = "thin" codegen-units = 1 + +[workspace.metadata.cargo-machete] +ignored = ["bindgen", "cbindgen", "prost_build", "serde"] diff --git a/Procfile b/Procfile index 288842ebd3..34c943daff 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ -collab: RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run --package=collab serve +collab: RUST_LOG=${RUST_LOG:-warn,tower_http=info,collab=info} cargo run --package=collab serve livekit: livekit-server --dev -blob_store: MINIO_ROOT_USER=the-blob-store-access-key MINIO_ROOT_PASSWORD=the-blob-store-secret-key minio server .blob_store +blob_store: ./script/run-local-minio diff --git a/README.md b/README.md index ba3f3d7d66..ba2e2c9598 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ brew install zed ## Developing Zed -- [Building Zed](./docs/src/developing_zed__building_zed.md) +- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md) +- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md) - [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md) ## Contributing diff --git a/assets/icons/file_icons/bun.svg b/assets/icons/file_icons/bun.svg new file mode 100644 index 0000000000..4787085244 --- /dev/null +++ b/assets/icons/file_icons/bun.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/coffeescript.svg b/assets/icons/file_icons/coffeescript.svg new file mode 100644 index 0000000000..984c4649b4 --- /dev/null +++ b/assets/icons/file_icons/coffeescript.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/css.svg b/assets/icons/file_icons/css.svg index 659f856232..3072f2597d 100644 --- a/assets/icons/file_icons/css.svg +++ b/assets/icons/file_icons/css.svg @@ -1,4 +1,6 @@ - - - - + + + + + + diff --git a/assets/icons/file_icons/dart.svg b/assets/icons/file_icons/dart.svg new file mode 100644 index 0000000000..6f3b164cb2 --- /dev/null +++ b/assets/icons/file_icons/dart.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/docker.svg b/assets/icons/file_icons/docker.svg new file mode 100644 index 0000000000..9ca7a1e04e --- /dev/null +++ b/assets/icons/file_icons/docker.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/erlang.svg b/assets/icons/file_icons/erlang.svg index 2dd57910b8..c2fa62ebca 100644 --- a/assets/icons/file_icons/erlang.svg +++ b/assets/icons/file_icons/erlang.svg @@ -1 +1,12 @@ - + + + + + + + + + + + + diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 5a8881ff9f..25aa6c2aa0 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -1,269 +1,341 @@ { - "suffixes": { - "astro": "astro", - "Emakefile": "erlang", - "aac": "audio", - "accdb": "storage", - "app.src": "erlang", - "avi": "video", - "avif": "image", - "bak": "backup", - "bash": "terminal", - "bash_aliases": "terminal", - "bash_logout": "terminal", - "bash_profile": "terminal", - "bashrc": "terminal", - "bmp": "image", - "c": "code", - "cc": "code", - "cjs": "code", - "conf": "settings", - "cpp": "code", - "css": "css", - "csv": "storage", - "cts": "typescript", - "dat": "storage", - "db": "storage", - "dbf": "storage", - "dll": "storage", - "doc": "document", - "docx": "document", - "eex": "elixir", - "elm": "elm", - "erl": "erlang", - "escript": "erlang", - "eslintrc": "eslint", - "eslintrc.js": "eslint", - "eslintrc.json": "eslint", - "ex": "elixir", - "exs": "elixir", - "fish": "terminal", - "flac": "audio", - "fmp": "storage", - "fp7": "storage", - "frm": "storage", - "gdb": "storage", - "gif": "image", - "gitattributes": "vcs", - "gitignore": "vcs", - "gitkeep": "vcs", - "gitmodules": "vcs", - "go": "go", - "h": "code", - "handlebars": "code", - "hbs": "template", - "heex": "elixir", - "heif": "image", - "heic": "image", - "hrl": "erlang", - "hs": "haskell", - "htm": "template", - "html": "template", - "ib": "storage", - "ico": "image", - "ini": "settings", - "j2k": "image", - "java": "code", - "jfif": "image", - "jp2": "image", - "jpeg": "image", - "jpg": "image", - "js": "code", - "json": "storage", - "jsonc": "storage", - "jxl": "image", - "ldf": "storage", - "lock": "lock", - "log": "log", - "lua": "lua", - "m4a": "audio", - "m4v": "video", - "md": "document", - "mdb": "storage", - "mdf": "storage", - "mdx": "document", - "mkv": "video", - "mjs": "code", - "mka": "audio", - "ml": "ocaml", - "mli": "ocaml", - "mov": "video", - "mp3": "audio", - "mp4": "video", - "mts": "typescript", - "myd": "storage", - "myi": "storage", - "odp": "document", - "ods": "document", - "odt": "document", - "ogg": "audio", - "opus": "audio", - "pdb": "storage", - "pdf": "document", - "php": "php", - "png": "image", - "ppt": "document", - "pptx": "document", - "prettierignore": "prettier", - "prettierrc": "prettier", - "prisma": "prisma", - "profile": "terminal", - "ps1": "terminal", - "psd": "image", - "py": "python", - "qoi": "image", - "rb": "ruby", - "rebar.config": "erlang", - "rkt": "code", - "rs": "rust", - "rtf": "document", - "sav": "storage", - "scm": "code", - "sdf": "storage", - "sh": "terminal", - "sqlite": "storage", - "svelte": "template", - "svg": "image", - "swift": "code", - "tiff": "image", - "toml": "toml", - "ts": "typescript", - "tsv": "storage", - "tsx": "code", - "txt": "document", - "vue": "vue", - "wav": "audio", - "webm": "video", - "webp": "image", - "wma": "audio", - "wmv": "video", - "wv": "audio", - "xls": "document", - "xlsx": "document", - "xml": "template", - "xrl": "erlang", - "yaml": "settings", - "yml": "settings", - "yrl": "erlang", - "zlogin": "terminal", - "zsh": "terminal", - "zsh_aliases": "terminal", - "zsh_histfile": "terminal", - "zsh_profile": "terminal", - "zshenv": "terminal", - "zshrc": "terminal" + "stems": { + "Podfile": "ruby", + "Procfile": "heroku", + "Dockerfile": "docker" + }, + "suffixes": { + "astro": "astro", + "Emakefile": "erlang", + "aac": "audio", + "accdb": "storage", + "app.src": "erlang", + "avi": "video", + "avif": "image", + "bak": "backup", + "bash": "terminal", + "bash_aliases": "terminal", + "bash_logout": "terminal", + "bash_profile": "terminal", + "bashrc": "terminal", + "bmp": "image", + "c": "code", + "cc": "code", + "cjs": "code", + "conf": "settings", + "cpp": "code", + "css": "css", + "csv": "storage", + "cts": "typescript", + "coffee": "coffeescript", + "dart": "dart", + "dat": "storage", + "db": "storage", + "dbf": "storage", + "dll": "storage", + "doc": "document", + "docx": "document", + "eex": "elixir", + "elm": "elm", + "erl": "erlang", + "escript": "erlang", + "eslintrc": "eslint", + "eslintrc.js": "eslint", + "eslintrc.json": "eslint", + "ex": "elixir", + "exs": "elixir", + "fish": "terminal", + "flac": "audio", + "fmp": "storage", + "fp7": "storage", + "frm": "storage", + "fs": "fsharp", + "gdb": "storage", + "gif": "image", + "gitattributes": "vcs", + "gitignore": "vcs", + "gitkeep": "vcs", + "gitmodules": "vcs", + "go": "go", + "graphql": "graphql", + "h": "code", + "handlebars": "code", + "hbs": "template", + "heex": "elixir", + "heif": "image", + "heic": "image", + "hrl": "erlang", + "hs": "haskell", + "htm": "template", + "html": "template", + "ib": "storage", + "ico": "image", + "ini": "settings", + "j2k": "image", + "java": "java", + "jfif": "image", + "jp2": "image", + "jpeg": "image", + "jpg": "image", + "js": "code", + "json": "storage", + "jsonc": "storage", + "jxl": "image", + "kt": "kotlin", + "ldf": "storage", + "lock": "lock", + "lockb": "bun", + "log": "log", + "lua": "lua", + "m4a": "audio", + "m4v": "video", + "md": "document", + "mdb": "storage", + "mdf": "storage", + "mdx": "document", + "metadata": "code", + "mkv": "video", + "mjs": "code", + "mka": "audio", + "ml": "ocaml", + "mli": "ocaml", + "mov": "video", + "mp3": "audio", + "mp4": "video", + "mts": "typescript", + "myd": "storage", + "myi": "storage", + "nu": "terminal", + "nim": "nim", + "odp": "document", + "ods": "document", + "odt": "document", + "ogg": "audio", + "opus": "audio", + "otf": "font", + "pdb": "storage", + "pdf": "document", + "php": "php", + "plist": "template", + "png": "image", + "ppt": "document", + "pptx": "document", + "prettierignore": "prettier", + "prettierrc": "prettier", + "prisma": "prisma", + "profile": "terminal", + "ps1": "terminal", + "psd": "image", + "py": "python", + "qoi": "image", + "rb": "ruby", + "rebar.config": "erlang", + "rkt": "code", + "rs": "rust", + "r": "r", + "rtf": "document", + "sav": "storage", + "scm": "code", + "sdf": "storage", + "sh": "terminal", + "sqlite": "storage", + "svelte": "template", + "svg": "image", + "sc": "scala", + "scala": "scala", + "sql": "storage", + "swift": "swift", + "tf": "terraform", + "tfvars": "terraform", + "tiff": "image", + "toml": "toml", + "ts": "typescript", + "tsv": "storage", + "ttf": "font", + "tsx": "code", + "txt": "document", + "tcl": "tcl", + "vue": "vue", + "wav": "audio", + "webm": "video", + "webp": "image", + "wma": "audio", + "wmv": "video", + "wv": "audio", + "xls": "document", + "xlsx": "document", + "xml": "template", + "xrl": "erlang", + "yaml": "settings", + "yml": "settings", + "yrl": "erlang", + "zlogin": "terminal", + "zsh": "terminal", + "zsh_aliases": "terminal", + "zsh_histfile": "terminal", + "zsh_profile": "terminal", + "zshenv": "terminal", + "zshrc": "terminal" + }, + "types": { + "astro": { + "icon": "icons/file_icons/astro.svg" }, - "types": { - "astro": { - "icon": "icons/file_icons/astro.svg" - }, - "audio": { - "icon": "icons/file_icons/audio.svg" - }, - "code": { - "icon": "icons/file_icons/code.svg" - }, - "collapsed_chevron": { - "icon": "icons/file_icons/chevron_right.svg" - }, - "collapsed_folder": { - "icon": "icons/file_icons/folder.svg" - }, - "css": { - "icon": "icons/file_icons/css.svg" - }, - "default": { - "icon": "icons/file_icons/file.svg" - }, - "document": { - "icon": "icons/file_icons/book.svg" - }, - "elixir": { - "icon": "icons/file_icons/elixir.svg" - }, - "elm": { - "icon": "icons/file_icons/elm.svg" - }, - "erlang": { - "icon": "icons/file_icons/erlang.svg" - }, - "eslint": { - "icon": "icons/file_icons/eslint.svg" - }, - "expanded_chevron": { - "icon": "icons/file_icons/chevron_down.svg" - }, - "expanded_folder": { - "icon": "icons/file_icons/folder_open.svg" - }, - "haskell": { - "icon": "icons/file_icons/haskell.svg" - }, - "go": { - "icon": "icons/file_icons/go.svg" - }, - "image": { - "icon": "icons/file_icons/image.svg" - }, - "lock": { - "icon": "icons/file_icons/lock.svg" - }, - "log": { - "icon": "icons/file_icons/info.svg" - }, - "lua": { - "icon": "icons/file_icons/lua.svg" - }, - "ocaml": { - "icon": "icons/file_icons/ocaml.svg" - }, - "phoenix": { - "icon": "icons/file_icons/phoenix.svg" - }, - "php": { - "icon": "icons/file_icons/php.svg" - }, - "prettier": { - "icon": "icons/file_icons/prettier.svg" - }, - "prisma": { - "icon": "icons/file_icons/prisma.svg" - }, - "python": { - "icon": "icons/file_icons/python.svg" - }, - "ruby": { - "icon": "icons/file_icons/ruby.svg" - }, - "rust": { - "icon": "icons/file_icons/rust.svg" - }, - "settings": { - "icon": "icons/file_icons/settings.svg" - }, - "storage": { - "icon": "icons/file_icons/database.svg" - }, - "template": { - "icon": "icons/file_icons/html.svg" - }, - "terminal": { - "icon": "icons/file_icons/terminal.svg" - }, - "toml": { - "icon": "icons/file_icons/toml.svg" - }, - "typescript": { - "icon": "icons/file_icons/typescript.svg" - }, - "vcs": { - "icon": "icons/file_icons/git.svg" - }, - "video": { - "icon": "icons/file_icons/video.svg" - }, - "vue": { - "icon": "icons/file_icons/vue.svg" - } + "audio": { + "icon": "icons/file_icons/audio.svg" + }, + "code": { + "icon": "icons/file_icons/code.svg" + }, + "collapsed_chevron": { + "icon": "icons/file_icons/chevron_right.svg" + }, + "collapsed_folder": { + "icon": "icons/file_icons/folder.svg" + }, + "css": { + "icon": "icons/file_icons/css.svg" + }, + "coffeescript": { + "icon": "icons/file_icons/coffeescript.svg" + }, + "dart": { + "icon": "icons/file_icons/dart.svg" + }, + "default": { + "icon": "icons/file_icons/file.svg" + }, + "docker": { + "icon": "icons/file_icons/docker.svg" + }, + "document": { + "icon": "icons/file_icons/book.svg" + }, + "elixir": { + "icon": "icons/file_icons/elixir.svg" + }, + "elm": { + "icon": "icons/file_icons/elm.svg" + }, + "erlang": { + "icon": "icons/file_icons/erlang.svg" + }, + "eslint": { + "icon": "icons/file_icons/eslint.svg" + }, + "expanded_chevron": { + "icon": "icons/file_icons/chevron_down.svg" + }, + "expanded_folder": { + "icon": "icons/file_icons/folder_open.svg" + }, + "font": { + "icon": "icons/file_icons/font.svg" + }, + "fsharp": { + "icon": "icons/file_icons/fsharp.svg" + }, + "haskell": { + "icon": "icons/file_icons/haskell.svg" + }, + "heroku": { + "icon": "icons/file_icons/heroku.svg" + }, + "go": { + "icon": "icons/file_icons/go.svg" + }, + "graphql": { + "icon": "icons/file_icons/graphql.svg" + }, + "image": { + "icon": "icons/file_icons/image.svg" + }, + "java": { + "icon": "icons/file_icons/java.svg" + }, + "kotlin": { + "icon": "icons/file_icons/kotlin.svg" + }, + "lock": { + "icon": "icons/file_icons/lock.svg" + }, + "bun": { + "icon": "icons/file_icons/bun.svg" + }, + "log": { + "icon": "icons/file_icons/info.svg" + }, + "lua": { + "icon": "icons/file_icons/lua.svg" + }, + "ocaml": { + "icon": "icons/file_icons/ocaml.svg" + }, + "nim": { + "icon": "icons/file_icons/nim.svg" + }, + "phoenix": { + "icon": "icons/file_icons/phoenix.svg" + }, + "php": { + "icon": "icons/file_icons/php.svg" + }, + "prettier": { + "icon": "icons/file_icons/prettier.svg" + }, + "prisma": { + "icon": "icons/file_icons/prisma.svg" + }, + "python": { + "icon": "icons/file_icons/python.svg" + }, + "ruby": { + "icon": "icons/file_icons/ruby.svg" + }, + "rust": { + "icon": "icons/file_icons/rust.svg" + }, + "r": { + "icon": "icons/file_icons/r.svg" + }, + "settings": { + "icon": "icons/file_icons/settings.svg" + }, + "storage": { + "icon": "icons/file_icons/database.svg" + }, + "scala": { + "icon": "icons/file_icons/scala.svg" + }, + "swift": { + "icon": "icons/file_icons/swift.svg" + }, + "template": { + "icon": "icons/file_icons/html.svg" + }, + "terraform": { + "icon": "icons/file_icons/terraform.svg" + }, + "terminal": { + "icon": "icons/file_icons/terminal.svg" + }, + "toml": { + "icon": "icons/file_icons/toml.svg" + }, + "typescript": { + "icon": "icons/file_icons/typescript.svg" + }, + "tcl": { + "icon": "icons/file_icons/tcl.svg" + }, + "vcs": { + "icon": "icons/file_icons/git.svg" + }, + "video": { + "icon": "icons/file_icons/video.svg" + }, + "vue": { + "icon": "icons/file_icons/vue.svg" } + } } diff --git a/assets/icons/file_icons/font.svg b/assets/icons/file_icons/font.svg new file mode 100644 index 0000000000..011f6a6b1d --- /dev/null +++ b/assets/icons/file_icons/font.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/fsharp.svg b/assets/icons/file_icons/fsharp.svg new file mode 100644 index 0000000000..f86febe9db --- /dev/null +++ b/assets/icons/file_icons/fsharp.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/file_icons/graphql.svg b/assets/icons/file_icons/graphql.svg new file mode 100644 index 0000000000..51903a6ffc --- /dev/null +++ b/assets/icons/file_icons/graphql.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/file_icons/haskell.svg b/assets/icons/file_icons/haskell.svg index ff3a687f52..7412d40e94 100644 --- a/assets/icons/file_icons/haskell.svg +++ b/assets/icons/file_icons/haskell.svg @@ -1,13 +1,6 @@ - - - - + + + + + diff --git a/assets/icons/file_icons/heroku.svg b/assets/icons/file_icons/heroku.svg new file mode 100644 index 0000000000..86f8a197a8 --- /dev/null +++ b/assets/icons/file_icons/heroku.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/java.svg b/assets/icons/file_icons/java.svg new file mode 100644 index 0000000000..bfe01dab51 --- /dev/null +++ b/assets/icons/file_icons/java.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/file_icons/kotlin.svg b/assets/icons/file_icons/kotlin.svg new file mode 100644 index 0000000000..468c6cf8c0 --- /dev/null +++ b/assets/icons/file_icons/kotlin.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/nim.svg b/assets/icons/file_icons/nim.svg new file mode 100644 index 0000000000..297d2bc5b3 --- /dev/null +++ b/assets/icons/file_icons/nim.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/php.svg b/assets/icons/file_icons/php.svg index f1b0887f79..63a4090ff4 100644 --- a/assets/icons/file_icons/php.svg +++ b/assets/icons/file_icons/php.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + + + diff --git a/assets/icons/file_icons/r.svg b/assets/icons/file_icons/r.svg new file mode 100644 index 0000000000..f67f0f3206 --- /dev/null +++ b/assets/icons/file_icons/r.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/scala.svg b/assets/icons/file_icons/scala.svg new file mode 100644 index 0000000000..3cf602514f --- /dev/null +++ b/assets/icons/file_icons/scala.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/swift.svg b/assets/icons/file_icons/swift.svg new file mode 100644 index 0000000000..82cf178aa5 --- /dev/null +++ b/assets/icons/file_icons/swift.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/tcl.svg b/assets/icons/file_icons/tcl.svg new file mode 100644 index 0000000000..1c5478b8d7 --- /dev/null +++ b/assets/icons/file_icons/tcl.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/file_icons/terraform.svg b/assets/icons/file_icons/terraform.svg new file mode 100644 index 0000000000..4b2afbb1ba --- /dev/null +++ b/assets/icons/file_icons/terraform.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/keymaps/atom.json b/assets/keymaps/atom.json index c2beb71b56..91844295b1 100644 --- a/assets/keymaps/atom.json +++ b/assets/keymaps/atom.json @@ -55,38 +55,14 @@ "bindings": { "alt-cmd-/": "search::ToggleRegex", "ctrl-0": "project_panel::ToggleFocus", - "cmd-1": [ - "pane::ActivateItem", - 0 - ], - "cmd-2": [ - "pane::ActivateItem", - 1 - ], - "cmd-3": [ - "pane::ActivateItem", - 2 - ], - "cmd-4": [ - "pane::ActivateItem", - 3 - ], - "cmd-5": [ - "pane::ActivateItem", - 4 - ], - "cmd-6": [ - "pane::ActivateItem", - 5 - ], - "cmd-7": [ - "pane::ActivateItem", - 6 - ], - "cmd-8": [ - "pane::ActivateItem", - 7 - ], + "cmd-1": ["pane::ActivateItem", 0], + "cmd-2": ["pane::ActivateItem", 1], + "cmd-3": ["pane::ActivateItem", 2], + "cmd-4": ["pane::ActivateItem", 3], + "cmd-5": ["pane::ActivateItem", 4], + "cmd-6": ["pane::ActivateItem", 5], + "cmd-7": ["pane::ActivateItem", 6], + "cmd-8": ["pane::ActivateItem", 7], "cmd-9": "pane::ActivateLastItem" } }, diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json new file mode 100644 index 0000000000..29e3d19d78 --- /dev/null +++ b/assets/keymaps/default-linux.json @@ -0,0 +1,583 @@ +[ + { + "bindings": { + "up": "menu::SelectPrev", + "pageup": "menu::SelectFirst", + "shift-pageup": "menu::SelectFirst", + "ctrl-p": "menu::SelectPrev", + "down": "menu::SelectNext", + "pagedown": "menu::SelectLast", + "shift-pagedown": "menu::SelectFirst", + "ctrl-n": "menu::SelectNext", + "ctrl-up": "menu::SelectFirst", + "ctrl-down": "menu::SelectLast", + "enter": "menu::Confirm", + "shift-f10": "menu::ShowContextMenu", + "ctrl-enter": "menu::SecondaryConfirm", + "escape": "menu::Cancel", + "ctrl-c": "menu::Cancel", + "shift-enter": "menu::UseSelectedQuery", + "ctrl-shift-w": "workspace::CloseWindow", + "shift-escape": "workspace::ToggleZoom", + "ctrl-o": "workspace::Open", + "ctrl-=": "zed::IncreaseBufferFontSize", + "ctrl-+": "zed::IncreaseBufferFontSize", + "ctrl--": "zed::DecreaseBufferFontSize", + "ctrl-0": "zed::ResetBufferFontSize", + "ctrl-,": "zed::OpenSettings", + "ctrl-q": "zed::Quit", + "ctrl-h": "zed::Hide", + "alt-ctrl-h": "zed::HideOthers", + "ctrl-m": "zed::Minimize", + "f11": "zed::ToggleFullScreen" + } + }, + { + "context": "Editor", + "bindings": { + "escape": "editor::Cancel", + "backspace": "editor::Backspace", + "shift-backspace": "editor::Backspace", + "ctrl-h": "editor::Backspace", + "delete": "editor::Delete", + "ctrl-d": "editor::Delete", + "tab": "editor::Tab", + "shift-tab": "editor::TabPrev", + "ctrl-k": "editor::CutToEndOfLine", + "ctrl-t": "editor::Transpose", + "ctrl-backspace": "editor::DeleteToBeginningOfLine", + "ctrl-delete": "editor::DeleteToEndOfLine", + "alt-backspace": "editor::DeleteToPreviousWordStart", + "alt-delete": "editor::DeleteToNextWordEnd", + "alt-h": "editor::DeleteToPreviousWordStart", + "alt-d": "editor::DeleteToNextWordEnd", + "ctrl-x": "editor::Cut", + "ctrl-c": "editor::Copy", + "ctrl-v": "editor::Paste", + "ctrl-z": "editor::Undo", + "ctrl-shift-z": "editor::Redo", + "ctrl-y": "editor::Redo", + "up": "editor::MoveUp", + "ctrl-up": "editor::MoveToStartOfParagraph", + "pageup": "editor::PageUp", + "shift-pageup": "editor::MovePageUp", + "home": "editor::MoveToBeginningOfLine", + "down": "editor::MoveDown", + "ctrl-down": "editor::MoveToEndOfParagraph", + "pagedown": "editor::PageDown", + "shift-pagedown": "editor::MovePageDown", + "end": "editor::MoveToEndOfLine", + "left": "editor::MoveLeft", + "right": "editor::MoveRight", + "ctrl-p": "editor::MoveUp", + "ctrl-n": "editor::MoveDown", + "ctrl-b": "editor::MoveLeft", + "ctrl-f": "editor::MoveRight", + "ctrl-shift-l": "editor::NextScreen", // todo(linux): What is this + "alt-left": "editor::MoveToPreviousWordStart", + "alt-b": "editor::MoveToPreviousWordStart", + "alt-right": "editor::MoveToNextWordEnd", + "alt-f": "editor::MoveToNextWordEnd", + "ctrl-e": "editor::MoveToEndOfLine", + "ctrl-home": "editor::MoveToBeginning", + "ctrl-=end": "editor::MoveToEnd", + "shift-up": "editor::SelectUp", + "ctrl-shift-p": "editor::SelectUp", + "shift-down": "editor::SelectDown", + "ctrl-shift-n": "editor::SelectDown", + "shift-left": "editor::SelectLeft", + "ctrl-shift-b": "editor::SelectLeft", + "shift-right": "editor::SelectRight", + "ctrl-shift-f": "editor::SelectRight", + "alt-shift-left": "editor::SelectToPreviousWordStart", + "alt-shift-b": "editor::SelectToPreviousWordStart", + "alt-shift-right": "editor::SelectToNextWordEnd", + "alt-shift-f": "editor::SelectToNextWordEnd", + "ctrl-shift-up": "editor::SelectToStartOfParagraph", + "ctrl-shift-down": "editor::SelectToEndOfParagraph", + "ctrl-shift-home": "editor::SelectToBeginning", + "ctrl-shift-end": "editor::SelectToEnd", + "ctrl-a": "editor::SelectAll", + "ctrl-l": "editor::SelectLine", + "ctrl-shift-i": "editor::Format", + "shift-home": [ + "editor::SelectToBeginningOfLine", + { + "stop_at_soft_wraps": true + } + ], + "shift-end": [ + "editor::SelectToEndOfLine", + { + "stop_at_soft_wraps": true + } + ], + "ctrl-shift-e": [ + "editor::SelectToEndOfLine", + { + "stop_at_soft_wraps": true + } + ], + "ctrl-;": "editor::ToggleLineNumbers" + } + }, + { + "context": "Editor && mode == full", + "bindings": { + "enter": "editor::Newline", + "shift-enter": "editor::Newline", + "ctrl-shift-enter": "editor::NewlineAbove", + "ctrl-enter": "editor::NewlineBelow", + "alt-z": "editor::ToggleSoftWrap", + "ctrl-f": [ + "buffer_search::Deploy", + { + "focus": true + } + ], + "ctrl->": "assistant::QuoteSelection" + } + }, + { + "context": "Editor && mode == full && copilot_suggestion", + "bindings": { + "alt-]": "copilot::NextSuggestion", + "alt-[": "copilot::PreviousSuggestion", + "alt-right": "editor::AcceptPartialCopilotSuggestion" + } + }, + { + "context": "Editor && !copilot_suggestion", + "bindings": { + "alt-\\": "copilot::Suggest" + } + }, + { + "context": "Editor && mode == auto_height", + "bindings": { + "ctrl-enter": "editor::Newline", + "shift-enter": "editor::Newline", + "ctrl-shift-enter": "editor::NewlineBelow" + } + }, + { + "context": "AssistantPanel", + "bindings": { + "f3": "search::SelectNextMatch", + "shift-f3": "search::SelectPrevMatch" + } + }, + { + "context": "ConversationEditor > Editor", + "bindings": { + "ctrl-enter": "assistant::Assist", + "ctrl-s": "workspace::Save", + "ctrl->": "assistant::QuoteSelection", + "shift-enter": "assistant::Split", + "ctrl-r": "assistant::CycleMessageRole" + } + }, + { + "context": "BufferSearchBar", + "bindings": { + "escape": "buffer_search::Dismiss", + "tab": "buffer_search::FocusEditor", + "enter": "search::SelectNextMatch", + "shift-enter": "search::SelectPrevMatch", + "alt-enter": "search::SelectAllMatches", + "alt-tab": "search::CycleMode" + } + }, + { + "context": "BufferSearchBar && in_replace", + "bindings": { + "enter": "search::ReplaceNext", + "ctrl-enter": "search::ReplaceAll" + } + }, + { + "context": "BufferSearchBar && !in_replace > Editor", + "bindings": { + "up": "search::PreviousHistoryQuery", + "down": "search::NextHistoryQuery" + } + }, + { + "context": "ProjectSearchBar", + "bindings": { + "escape": "project_search::ToggleFocus", + "alt-tab": "search::CycleMode", + "ctrl-shift-h": "search::ToggleReplace", + "ctrl-alt-g": "search::ActivateRegexMode", + "ctrl-alt-s": "search::ActivateSemanticMode", + "ctrl-alt-x": "search::ActivateTextMode" + } + }, + { + "context": "ProjectSearchBar > Editor", + "bindings": { + "up": "search::PreviousHistoryQuery", + "down": "search::NextHistoryQuery" + } + }, + { + "context": "ProjectSearchBar && in_replace", + "bindings": { + "enter": "search::ReplaceNext", + "ctrl-enter": "search::ReplaceAll" + } + }, + { + "context": "ProjectSearchView", + "bindings": { + "escape": "project_search::ToggleFocus", + "alt-tab": "search::CycleMode", + "ctrl-shift-h": "search::ToggleReplace", + "ctrl-alt-g": "search::ActivateRegexMode", + "ctrl-alt-s": "search::ActivateSemanticMode", + "ctrl-alt-x": "search::ActivateTextMode" + } + }, + { + "context": "Pane", + "bindings": { + "ctrl-{": "pane::ActivatePrevItem", + "ctrl-}": "pane::ActivateNextItem", + "ctrl-alt-left": "pane::ActivatePrevItem", + "ctrl-alt-right": "pane::ActivateNextItem", + "ctrl-w": "pane::CloseActiveItem", + "ctrl-alt-t": "pane::CloseInactiveItems", + "ctrl-alt-shift-w": "workspace::CloseInactiveTabsAndPanes", + "ctrl-k u": "pane::CloseCleanItems", + "ctrl-k ctrl-w": "pane::CloseAllItems", + "ctrl-f": "project_search::ToggleFocus", + "f3": "search::SelectNextMatch", + "shift-f3": "search::SelectPrevMatch", + "ctrl-shift-h": "search::ToggleReplace", + "alt-enter": "search::SelectAllMatches", + "ctrl-alt-c": "search::ToggleCaseSensitive", + "ctrl-alt-w": "search::ToggleWholeWord", + "alt-tab": "search::CycleMode", + "ctrl-alt-f": "project_search::ToggleFilters", + "ctrl-alt-g": "search::ActivateRegexMode", + "ctrl-alt-s": "search::ActivateSemanticMode", + "ctrl-alt-x": "search::ActivateTextMode" + } + }, + // Bindings from VS Code + { + "context": "Editor", + "bindings": { + "ctrl-[": "editor::Outdent", + "ctrl-]": "editor::Indent", + "ctrl-alt-up": "editor::AddSelectionAbove", + "ctrl-alt-down": "editor::AddSelectionBelow", + "ctrl-d": [ + "editor::SelectNext", + { + "replace_newest": false + } + ], + "ctrl-shift-l": "editor::SelectAllMatches", + "ctrl-shift-d": [ + "editor::SelectPrevious", + { + "replace_newest": false + } + ], + "ctrl-k ctrl-d": [ + "editor::SelectNext", + { + "replace_newest": true + } + ], + "ctrl-k ctrl-shift-d": [ + "editor::SelectPrevious", + { + "replace_newest": true + } + ], + "ctrl-k ctrl-i": "editor::Hover", + "ctrl-/": [ + "editor::ToggleComments", + { + "advance_downwards": false + } + ], + "alt-up": "editor::SelectLargerSyntaxNode", + "alt-down": "editor::SelectSmallerSyntaxNode", + "ctrl-u": "editor::UndoSelection", + "ctrl-shift-u": "editor::RedoSelection", + "f8": "editor::GoToDiagnostic", + "shift-f8": "editor::GoToPrevDiagnostic", + "f2": "editor::Rename", + "f12": "editor::GoToDefinition", + "alt-f12": "editor::GoToDefinitionSplit", + "ctrl-f12": "editor::GoToTypeDefinition", + "ctrl-alt-f12": "editor::GoToTypeDefinitionSplit", + "alt-shift-f12": "editor::FindAllReferences", + "ctrl-m": "editor::MoveToEnclosingBracket", + "ctrl-alt-[": "editor::Fold", + "ctrl-alt-]": "editor::UnfoldLines", + "ctrl-space": "editor::ShowCompletions", + "ctrl-.": "editor::ToggleCodeActions", + "ctrl-alt-r": "editor::RevealInFinder", + "ctrl-alt-c": "editor::DisplayCursorNames" + } + }, + { + "context": "Editor && mode == full", + "bindings": { + "ctrl-shift-o": "outline::Toggle", + "ctrl-g": "go_to_line::Toggle" + } + }, + { + "context": "Pane", + "bindings": { + "ctrl-1": ["pane::ActivateItem", 0], + "ctrl-2": ["pane::ActivateItem", 1], + "ctrl-3": ["pane::ActivateItem", 2], + "ctrl-4": ["pane::ActivateItem", 3], + "ctrl-5": ["pane::ActivateItem", 4], + "ctrl-6": ["pane::ActivateItem", 5], + "ctrl-7": ["pane::ActivateItem", 6], + "ctrl-8": ["pane::ActivateItem", 7], + "ctrl-9": ["pane::ActivateItem", 8], + "ctrl-0": "pane::ActivateLastItem", + "ctrl--": "pane::GoBack", + "ctrl-_": "pane::GoForward", + "ctrl-shift-t": "pane::ReopenClosedItem", + "ctrl-shift-f": "project_search::ToggleFocus" + } + }, + { + "context": "Workspace", + "bindings": { + // Change the default action on `menu::Confirm` by setting the parameter + // "alt-cmd-o": [ + // "projects::OpenRecent", + // { + // "create_new_window": true + // } + // ] + "ctrl-alt-o": "projects::OpenRecent", + "ctrl-alt-b": "branches::OpenRecent", + "ctrl-~": "workspace::NewTerminal", + "ctrl-s": "workspace::Save", + "ctrl-k s": "workspace::SaveWithoutFormat", + "ctrl-shift-s": "workspace::SaveAs", + "ctrl-n": "workspace::NewFile", + "ctrl-shift-n": "workspace::NewWindow", + "ctrl-`": "terminal_panel::ToggleFocus", + "ctrl-1": ["workspace::ActivatePane", 0], + "ctrl-2": ["workspace::ActivatePane", 1], + "ctrl-3": ["workspace::ActivatePane", 2], + "ctrl-4": ["workspace::ActivatePane", 3], + "ctrl-5": ["workspace::ActivatePane", 4], + "ctrl-6": ["workspace::ActivatePane", 5], + "ctrl-7": ["workspace::ActivatePane", 6], + "ctrl-8": ["workspace::ActivatePane", 7], + "ctrl-9": ["workspace::ActivatePane", 8], + "ctrl-b": "workspace::ToggleLeftDock", + "ctrl-r": "workspace::ToggleRightDock", + "ctrl-j": "workspace::ToggleBottomDock", + "ctrl-alt-y": "workspace::CloseAllDocks", + "ctrl-shift-f": "pane::DeploySearch", + "ctrl-k ctrl-t": "theme_selector::Toggle", + "ctrl-k ctrl-s": "zed::OpenKeymap", + "ctrl-t": "project_symbols::Toggle", + "ctrl-p": "file_finder::Toggle", + "ctrl-shift-p": "command_palette::Toggle", + "ctrl-shift-m": "diagnostics::Deploy", + "ctrl-shift-e": "project_panel::ToggleFocus", + "ctrl-?": "assistant::ToggleFocus", + "ctrl-alt-s": "workspace::SaveAll", + "ctrl-k m": "language_selector::Toggle", + "escape": "workspace::Unfollow", + "ctrl-k ctrl-left": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-k ctrl-right": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-k ctrl-up": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-k ctrl-down": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-k shift-left": ["workspace::SwapPaneInDirection", "Left"], + "ctrl-k shift-right": ["workspace::SwapPaneInDirection", "Right"], + "ctrl-k shift-up": ["workspace::SwapPaneInDirection", "Up"], + "ctrl-k shift-down": ["workspace::SwapPaneInDirection", "Down"], + "alt-t": "task::Rerun", + "alt-shift-t": "task::Spawn" + } + }, + // Bindings from Sublime Text + // todo(linux) make sure these match linux bindings or remove above comment? + { + "context": "Editor", + "bindings": { + "ctrl-shift-k": "editor::DeleteLine", + "ctrl-shift-d": "editor::DuplicateLine", + "ctrl-j": "editor::JoinLines", + "ctrl-alt-up": "editor::MoveLineUp", + "ctrl-alt-down": "editor::MoveLineDown", + "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", + "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart", + "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd", + "ctrl-alt-d": "editor::DeleteToNextSubwordEnd", + "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", + "ctrl-alt-b": "editor::MoveToPreviousSubwordStart", + "ctrl-alt-right": "editor::MoveToNextSubwordEnd", + "ctrl-alt-f": "editor::MoveToNextSubwordEnd", + "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart", + "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart", + "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd", + "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd" + } + }, + // Bindings from Atom + // todo(linux) make sure these match linux bindings or remove above comment? + { + "context": "Pane", + "bindings": { + "ctrl-k up": "pane::SplitUp", + "ctrl-k down": "pane::SplitDown", + "ctrl-k left": "pane::SplitLeft", + "ctrl-k right": "pane::SplitRight" + } + }, + // Bindings that should be unified with bindings for more general actions + { + "context": "Editor && renaming", + "bindings": { + "enter": "editor::ConfirmRename" + } + }, + { + "context": "Editor && showing_completions", + "bindings": { + "enter": "editor::ConfirmCompletion", + "tab": "editor::ConfirmCompletion" + } + }, + { + "context": "Editor && showing_code_actions", + "bindings": { + "enter": "editor::ConfirmCodeAction" + } + }, + { + "context": "Editor && (showing_code_actions || showing_completions)", + "bindings": { + "up": "editor::ContextMenuPrev", + "ctrl-p": "editor::ContextMenuPrev", + "down": "editor::ContextMenuNext", + "ctrl-n": "editor::ContextMenuNext", + "pageup": "editor::ContextMenuFirst", + "pagedown": "editor::ContextMenuLast" + } + }, + // Custom bindings + { + "bindings": { + "ctrl-alt-shift-f": "workspace::FollowNextCollaborator", + // TODO: Move this to a dock open action + "ctrl-alt-c": "collab_panel::ToggleFocus", + "ctrl-alt-i": "zed::DebugElements", + "ctrl-:": "editor::ToggleInlayHints" + } + }, + { + "context": "Editor && mode == full", + "bindings": { + "alt-enter": "editor::OpenExcerpts", + "ctrl-k enter": "editor::OpenExcerptsSplit", + "ctrl-f8": "editor::GoToHunk", + "ctrl-shift-f8": "editor::GoToPrevHunk", + "ctrl-enter": "assistant::InlineAssist" + } + }, + { + "context": "ProjectSearchBar && !in_replace", + "bindings": { + "ctrl-enter": "project_search::SearchInNew" + } + }, + { + "context": "ProjectPanel", + "bindings": { + "left": "project_panel::CollapseSelectedEntry", + "right": "project_panel::ExpandSelectedEntry", + "ctrl-n": "project_panel::NewFile", + "ctrl-alt-n": "project_panel::NewDirectory", + "ctrl-x": "project_panel::Cut", + "ctrl-c": "project_panel::Copy", + "ctrl-v": "project_panel::Paste", + "ctrl-alt-c": "project_panel::CopyPath", + "ctrl-alt-shift-c": "project_panel::CopyRelativePath", + "f2": "project_panel::Rename", + "enter": "project_panel::Rename", + "backspace": "project_panel::Delete", + "ctrl-alt-r": "project_panel::RevealInFinder", + "alt-shift-f": "project_panel::NewSearchInDirectory" + } + }, + { + "context": "ProjectPanel && not_editing", + "bindings": { + "space": "project_panel::Open" + } + }, + { + "context": "CollabPanel && not_editing", + "bindings": { + "ctrl-backspace": "collab_panel::Remove", + "space": "menu::Confirm" + } + }, + { + "context": "(CollabPanel && editing) > Editor", + "bindings": { + "space": "collab_panel::InsertSpace" + } + }, + { + "context": "ChannelModal", + "bindings": { + "tab": "channel_modal::ToggleMode" + } + }, + { + "context": "ChannelModal > Picker > Editor", + "bindings": { + "tab": "channel_modal::ToggleMode" + } + }, + { + "context": "ChatPanel > MessageEditor", + "bindings": { + "escape": "chat_panel::CloseReplyPreview" + } + }, + { + "context": "Terminal", + "bindings": { + "ctrl-alt-space": "terminal::ShowCharacterPalette", + "ctrl-shift-c": "terminal::Copy", + "ctrl-shift-v": "terminal::Paste", + "ctrl-k": "terminal::Clear", + // Some nice conveniences + "ctrl-backspace": ["terminal::SendText", "\u0015"], + "ctrl-right": ["terminal::SendText", "\u0005"], + "ctrl-left": ["terminal::SendText", "\u0001"], + // Terminal.app compatibility + "alt-left": ["terminal::SendText", "\u001bb"], + "alt-right": ["terminal::SendText", "\u001bf"], + // There are conflicting bindings for these keys in the global context. + // these bindings override them, remove at your own risk: + "up": ["terminal::SendKeystroke", "up"], + "pageup": ["terminal::SendKeystroke", "pageup"], + "down": ["terminal::SendKeystroke", "down"], + "pagedown": ["terminal::SendKeystroke", "pagedown"], + "escape": ["terminal::SendKeystroke", "escape"], + "enter": ["terminal::SendKeystroke", "enter"], + "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"] + } + } +] diff --git a/assets/keymaps/default.json b/assets/keymaps/default-macos.json similarity index 95% rename from assets/keymaps/default.json rename to assets/keymaps/default-macos.json index 6e2b96b338..02d05c0409 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default-macos.json @@ -17,6 +17,7 @@ "cmd-enter": "menu::SecondaryConfirm", "escape": "menu::Cancel", "ctrl-c": "menu::Cancel", + "shift-enter": "menu::UseSelectedQuery", "cmd-shift-w": "workspace::CloseWindow", "shift-escape": "workspace::ToggleZoom", "cmd-o": "workspace::Open", @@ -48,6 +49,7 @@ "cmd-backspace": "editor::DeleteToBeginningOfLine", "cmd-delete": "editor::DeleteToEndOfLine", "alt-backspace": "editor::DeleteToPreviousWordStart", + "ctrl-w": "editor::DeleteToPreviousWordStart", "alt-delete": "editor::DeleteToNextWordEnd", "alt-h": "editor::DeleteToPreviousWordStart", "alt-d": "editor::DeleteToNextWordEnd", @@ -150,7 +152,8 @@ "center_cursor": true } ], - "ctrl-cmd-space": "editor::ShowCharacterPalette" + "ctrl-cmd-space": "editor::ShowCharacterPalette", + "cmd-;": "editor::ToggleLineNumbers" } }, { @@ -173,10 +176,21 @@ "focus": false } ], - "alt-\\": "copilot::Suggest", + "cmd->": "assistant::QuoteSelection" + } + }, + { + "context": "Editor && mode == full && copilot_suggestion", + "bindings": { "alt-]": "copilot::NextSuggestion", "alt-[": "copilot::PreviousSuggestion", - "cmd->": "assistant::QuoteSelection" + "alt-right": "editor::AcceptPartialCopilotSuggestion" + } + }, + { + "context": "Editor && !copilot_suggestion", + "bindings": { + "alt-\\": "copilot::Suggest" } }, { @@ -383,10 +397,18 @@ { "context": "Workspace", "bindings": { + // Change the default action on `menu::Confirm` by setting the parameter + // "alt-cmd-o": [ + // "projects::OpenRecent", + // { + // "create_new_window": true + // } + // ] "alt-cmd-o": "projects::OpenRecent", "alt-cmd-b": "branches::OpenRecent", "ctrl-~": "workspace::NewTerminal", "cmd-s": "workspace::Save", + "cmd-k s": "workspace::SaveWithoutFormat", "cmd-shift-s": "workspace::SaveAs", "cmd-n": "workspace::NewFile", "cmd-shift-n": "workspace::NewWindow", @@ -405,8 +427,8 @@ "cmd-j": "workspace::ToggleBottomDock", "alt-cmd-y": "workspace::CloseAllDocks", "cmd-shift-f": "pane::DeploySearch", - "cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-s": "zed::OpenKeymap", + "cmd-k cmd-t": "theme_selector::Toggle", "cmd-t": "project_symbols::Toggle", "cmd-p": "file_finder::Toggle", "cmd-shift-p": "command_palette::Toggle", @@ -423,7 +445,9 @@ "cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"], "cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"], "cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"], - "cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"] + "cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"], + "alt-t": "task::Rerun", + "alt-shift-t": "task::Spawn" } }, // Bindings from Sublime Text @@ -504,6 +528,7 @@ "context": "Editor && mode == full", "bindings": { "alt-enter": "editor::OpenExcerpts", + "cmd-k enter": "editor::OpenExcerptsSplit", "cmd-f8": "editor::GoToHunk", "cmd-shift-f8": "editor::GoToPrevHunk", "ctrl-enter": "assistant::InlineAssist" @@ -529,7 +554,8 @@ "alt-cmd-shift-c": "project_panel::CopyRelativePath", "f2": "project_panel::Rename", "enter": "project_panel::Rename", - "backspace": "project_panel::Delete", + "delete": "project_panel::Delete", + "cmd-backspace": "project_panel::Delete", "alt-cmd-r": "project_panel::RevealInFinder", "alt-shift-f": "project_panel::NewSearchInDirectory" } diff --git a/assets/keymaps/storybook.json b/assets/keymaps/storybook.json new file mode 100644 index 0000000000..658a9d67bd --- /dev/null +++ b/assets/keymaps/storybook.json @@ -0,0 +1,23 @@ +[ + // Standard macOS bindings + { + "bindings": { + "up": "menu::SelectPrev", + "pageup": "menu::SelectFirst", + "shift-pageup": "menu::SelectFirst", + "ctrl-p": "menu::SelectPrev", + "down": "menu::SelectNext", + "pagedown": "menu::SelectLast", + "shift-pagedown": "menu::SelectFirst", + "ctrl-n": "menu::SelectNext", + "cmd-up": "menu::SelectFirst", + "cmd-down": "menu::SelectLast", + "enter": "menu::Confirm", + "ctrl-enter": "menu::ShowContextMenu", + "cmd-enter": "menu::SecondaryConfirm", + "escape": "menu::Cancel", + "ctrl-c": "menu::Cancel", + "cmd-q": "storybook::Quit" + } + } +] diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 2af71b607f..c68f209917 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -218,6 +218,7 @@ // z commands "z t": "editor::ScrollCursorTop", "z z": "editor::ScrollCursorCenter", + "z .": ["workspace::SendKeystrokes", "z z ^"], "z b": "editor::ScrollCursorBottom", "z c": "editor::Fold", "z o": "editor::UnfoldLines", @@ -288,6 +289,13 @@ "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", "ctrl-w n": ["workspace::NewFileInDirection", "Up"], "ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"], + + "ctrl-w d": "editor::GoToDefinitionSplit", + "ctrl-w g d": "editor::GoToDefinitionSplit", + "ctrl-w shift-d": "editor::GoToTypeDefinitionSplit", + "ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit", + "ctrl-w space": "editor::OpenExcerptsSplit", + "ctrl-w g space": "editor::OpenExcerptsSplit", "-": "pane::RevealInProjectPanel" } }, @@ -342,8 +350,8 @@ "r": ["vim::PushOperator", "Replace"], "s": "vim::Substitute", "shift-s": "vim::SubstituteLine", - "> >": "editor::Indent", - "< <": "editor::Outdent", + "> >": "vim::Indent", + "< <": "vim::Outdent", "ctrl-pagedown": "pane::ActivateNextItem", "ctrl-pageup": "pane::ActivatePrevItem" } @@ -383,7 +391,9 @@ "ignorePunctuation": true } ], + "t": "vim::Tag", "s": "vim::Sentence", + "p": "vim::Paragraph", "'": "vim::Quotes", "`": "vim::BackQuotes", "\"": "vim::DoubleQuotes", @@ -397,7 +407,8 @@ "}": "vim::CurlyBrackets", "shift-b": "vim::CurlyBrackets", "<": "vim::AngleBrackets", - ">": "vim::AngleBrackets" + ">": "vim::AngleBrackets", + "a": "vim::Argument" } }, { @@ -458,8 +469,8 @@ "ctrl-c": ["vim::SwitchMode", "Normal"], "escape": ["vim::SwitchMode", "Normal"], "ctrl-[": ["vim::SwitchMode", "Normal"], - ">": "editor::Indent", - "<": "editor::Outdent", + ">": "vim::Indent", + "<": "vim::Outdent", "i": [ "vim::PushOperator", { @@ -490,7 +501,9 @@ "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific "ctrl-x ctrl-z": "editor::Cancel", "ctrl-w": "editor::DeleteToPreviousWordStart", - "ctrl-u": "editor::DeleteToBeginningOfLine" + "ctrl-u": "editor::DeleteToBeginningOfLine", + "ctrl-t": "vim::Indent", + "ctrl-d": "vim::Outdent" } }, { diff --git a/assets/settings/default.json b/assets/settings/default.json index 8d12c54fde..29d07eea0f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -140,6 +140,14 @@ // Whether to show diagnostic indicators in the scrollbar. "diagnostics": true }, + "gutter": { + // Whether to show line numbers in the gutter. + "line_numbers": true, + // Whether to show code action buttons in the gutter. + "code_actions": true, + // Whether to show fold buttons in the gutter. + "folds": true + }, // The number of lines to keep above/below the cursor when scrolling. "vertical_scroll_margin": 3, "relative_line_numbers": false, @@ -161,7 +169,13 @@ "show_type_hints": true, "show_parameter_hints": true, // Corresponds to null/None LSP hint type value. - "show_other_hints": true + "show_other_hints": true, + // Time to wait after editing the buffer, before requesting the hints, + // set to 0 to disable debouncing. + "edit_debounce_ms": 700, + // Time to wait after scrolling the buffer, before requesting the hints, + // set to 0 to disable debouncing. + "scroll_debounce_ms": 50 }, "project_panel": { // Default width of the project panel. @@ -214,15 +228,29 @@ "default_width": 640, // Default height when the assistant is docked to the bottom. "default_height": 320, + // Deprecated: Please use `provider.api_url` instead. // The default OpenAI API endpoint to use when starting new conversations. "openai_api_url": "https://api.openai.com/v1", + // Deprecated: Please use `provider.default_model` instead. // The default OpenAI model to use when starting new conversations. This // setting can take three values: // // 1. "gpt-3.5-turbo-0613"" // 2. "gpt-4-0613"" // 3. "gpt-4-1106-preview" - "default_open_ai_model": "gpt-4-1106-preview" + "default_open_ai_model": "gpt-4-1106-preview", + "provider": { + "type": "openai", + // The default OpenAI API endpoint to use when starting new conversations. + "api_url": "https://api.openai.com/v1", + // The default OpenAI model to use when starting new conversations. This + // setting can take three values: + // + // 1. "gpt-3.5-turbo-0613"" + // 2. "gpt-4-0613"" + // 3. "gpt-4-1106-preview" + "default_model": "gpt-4-1106-preview" + } }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, @@ -444,6 +472,10 @@ // Can also be 'csh', 'fish', and `nushell` "activate_script": "default" } + }, + "toolbar": { + // Whether to display the terminal title in its toolbar. + "title": true } // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. @@ -451,7 +483,10 @@ // Set the terminal's font family. If this option is not included, // the terminal will default to matching the buffer's font family. // "font_family": "Zed Mono", - // --- + // Sets the maximum number of lines in the terminal's scrollback buffer. + // Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling. + // Existing terminals will not pick up this change until they are recreated. + // "max_scroll_history_lines": 10000, }, // Difference settings for semantic_index "semantic_index": { @@ -491,6 +526,9 @@ "Elixir": { "tab_size": 2 }, + "Gleam": { + "tab_size": 2 + }, "Go": { "tab_size": 4, "hard_tabs": true, @@ -499,6 +537,7 @@ } }, "Markdown": { + "tab_size": 2, "soft_wrap": "preferred_line_length" }, "JavaScript": { @@ -540,14 +579,19 @@ "lsp": { // Specify the LSP name as a key here. // "rust-analyzer": { - // //These initialization options are merged into Zed's defaults + // // These initialization options are merged into Zed's defaults // "initialization_options": { - // "checkOnSave": { - // "command": "clippy" + // "check": { + // "command": "clippy" // rust-analyzer.check.command (default: "check") // } // } // } }, + // Vim settings + "vim": { + "use_system_clipboard": "always", + "use_multiline_find": false + }, // The server to connect to. If the environment variable // ZED_SERVER_URL is set, it will override this setting. "server_url": "https://zed.dev", diff --git a/assets/settings/initial_runnables.json b/assets/settings/initial_runnables.json deleted file mode 100644 index 93bfd66b73..0000000000 --- a/assets/settings/initial_runnables.json +++ /dev/null @@ -1,19 +0,0 @@ -// Static runnables configuration. -// -// Example: -// { -// "label": "human-readable label for UI", -// "command": "bash", -// // rest of the parameters are optional -// "args": ["-c", "for i in {1..10}; do echo \"Second $i\"; sleep 1; done"], -// // Env overrides for the command, will be appended to the terminal's environment from the settings. -// "env": {"foo": "bar"}, -// // Current working directory to spawn the command into, defaults to current project root. -// "cwd": "/path/to/working/directory", -// // Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`. -// "use_new_terminal": false, -// // Whether to allow multiple instances of the same runnable to be run, or rather wait for the existing ones to finish, defaults to `false`. -// "allow_concurrent_runs": false, -// }, -// -{} diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json new file mode 100644 index 0000000000..0f4d8f98b3 --- /dev/null +++ b/assets/settings/initial_tasks.json @@ -0,0 +1,19 @@ +// Static tasks configuration. +// +// Example: +[ + { + "label": "Example task", + "command": "bash", + // rest of the parameters are optional + "args": ["-c", "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done"], + // Env overrides for the command, will be appended to the terminal's environment from the settings. + "env": { "foo": "bar" }, + // Current working directory to spawn the command into, defaults to current project root. + //"cwd": "/path/to/working/directory", + // Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`. + "use_new_terminal": false, + // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`. + "allow_concurrent_runs": false + } +] diff --git a/assets/themes/andromeda/andromeda.json b/assets/themes/andromeda/andromeda.json index a9144907cd..9eabd85cee 100644 --- a/assets/themes/andromeda/andromeda.json +++ b/assets/themes/andromeda/andromeda.json @@ -46,7 +46,7 @@ "panel.background": "#21242bff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#f7f7f84c", + "scrollbar.thumb.background": "#f7f7f84c", "scrollbar.thumb.hover_background": "#252931ff", "scrollbar.thumb.border": "#252931ff", "scrollbar.track.background": "#00000000", diff --git a/assets/themes/atelier/atelier.json b/assets/themes/atelier/atelier.json index 7f8bdfbaf3..55195bbf80 100644 --- a/assets/themes/atelier/atelier.json +++ b/assets/themes/atelier/atelier.json @@ -46,7 +46,7 @@ "panel.background": "#221f26ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#efecf44c", + "scrollbar.thumb.background": "#efecf44c", "scrollbar.thumb.hover_background": "#332f38ff", "scrollbar.thumb.border": "#332f38ff", "scrollbar.track.background": "#00000000", @@ -430,7 +430,7 @@ "panel.background": "#e6e3ebff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#19171c4c", + "scrollbar.thumb.background": "#19171c4c", "scrollbar.thumb.hover_background": "#cbc8d1ff", "scrollbar.thumb.border": "#cbc8d1ff", "scrollbar.track.background": "#00000000", @@ -814,7 +814,7 @@ "panel.background": "#262622ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#fefbec4c", + "scrollbar.thumb.background": "#fefbec4c", "scrollbar.thumb.hover_background": "#3b3933ff", "scrollbar.thumb.border": "#3b3933ff", "scrollbar.track.background": "#00000000", @@ -1198,7 +1198,7 @@ "panel.background": "#eeebd7ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#20201d4c", + "scrollbar.thumb.background": "#20201d4c", "scrollbar.thumb.hover_background": "#d7d3beff", "scrollbar.thumb.border": "#d7d3beff", "scrollbar.track.background": "#00000000", @@ -1582,7 +1582,7 @@ "panel.background": "#2c2b23ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#f4f3ec4c", + "scrollbar.thumb.background": "#f4f3ec4c", "scrollbar.thumb.hover_background": "#3c3b31ff", "scrollbar.thumb.border": "#3c3b31ff", "scrollbar.track.background": "#00000000", @@ -1966,7 +1966,7 @@ "panel.background": "#ebeae3ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#22221b4c", + "scrollbar.thumb.background": "#22221b4c", "scrollbar.thumb.hover_background": "#d0cfc5ff", "scrollbar.thumb.border": "#d0cfc5ff", "scrollbar.track.background": "#00000000", @@ -2350,7 +2350,7 @@ "panel.background": "#27211eff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#f0eeed4c", + "scrollbar.thumb.background": "#f0eeed4c", "scrollbar.thumb.hover_background": "#3b3431ff", "scrollbar.thumb.border": "#3b3431ff", "scrollbar.track.background": "#00000000", @@ -2734,7 +2734,7 @@ "panel.background": "#e9e6e4ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#1b19184c", + "scrollbar.thumb.background": "#1b19184c", "scrollbar.thumb.hover_background": "#d6d1cfff", "scrollbar.thumb.border": "#d6d1cfff", "scrollbar.track.background": "#00000000", @@ -3118,7 +3118,7 @@ "panel.background": "#252025ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#f7f3f74c", + "scrollbar.thumb.background": "#f7f3f74c", "scrollbar.thumb.hover_background": "#393239ff", "scrollbar.thumb.border": "#393239ff", "scrollbar.track.background": "#00000000", @@ -3502,7 +3502,7 @@ "panel.background": "#e0d5e0ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#1b181b4c", + "scrollbar.thumb.background": "#1b181b4c", "scrollbar.thumb.hover_background": "#ccbdccff", "scrollbar.thumb.border": "#ccbdccff", "scrollbar.track.background": "#00000000", @@ -3886,7 +3886,7 @@ "panel.background": "#1c2529ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#ebf8ff4c", + "scrollbar.thumb.background": "#ebf8ff4c", "scrollbar.thumb.hover_background": "#2c3b42ff", "scrollbar.thumb.border": "#2c3b42ff", "scrollbar.track.background": "#00000000", @@ -4270,7 +4270,7 @@ "panel.background": "#cdeaf9ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#161b1d4c", + "scrollbar.thumb.background": "#161b1d4c", "scrollbar.thumb.hover_background": "#b0d3e5ff", "scrollbar.thumb.border": "#b0d3e5ff", "scrollbar.track.background": "#00000000", @@ -4654,7 +4654,7 @@ "panel.background": "#252020ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#f4ecec4c", + "scrollbar.thumb.background": "#f4ecec4c", "scrollbar.thumb.hover_background": "#352f2fff", "scrollbar.thumb.border": "#352f2fff", "scrollbar.track.background": "#00000000", @@ -5038,7 +5038,7 @@ "panel.background": "#ebe3e3ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#1b18184c", + "scrollbar.thumb.background": "#1b18184c", "scrollbar.thumb.hover_background": "#cfc7c7ff", "scrollbar.thumb.border": "#cfc7c7ff", "scrollbar.track.background": "#00000000", @@ -5422,7 +5422,7 @@ "panel.background": "#1f2621ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#ecf4ee4c", + "scrollbar.thumb.background": "#ecf4ee4c", "scrollbar.thumb.hover_background": "#2f3832ff", "scrollbar.thumb.border": "#2f3832ff", "scrollbar.track.background": "#00000000", @@ -5806,7 +5806,7 @@ "panel.background": "#e3ebe6ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#171c194c", + "scrollbar.thumb.background": "#171c194c", "scrollbar.thumb.hover_background": "#c8d1cbff", "scrollbar.thumb.border": "#c8d1cbff", "scrollbar.track.background": "#00000000", @@ -6190,7 +6190,7 @@ "panel.background": "#1f231fff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#f3faf34c", + "scrollbar.thumb.background": "#f3faf34c", "scrollbar.thumb.hover_background": "#333b33ff", "scrollbar.thumb.border": "#333b33ff", "scrollbar.track.background": "#00000000", @@ -6574,7 +6574,7 @@ "panel.background": "#daeedaff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#1315134c", + "scrollbar.thumb.background": "#1315134c", "scrollbar.thumb.hover_background": "#bed7beff", "scrollbar.thumb.border": "#bed7beff", "scrollbar.track.background": "#00000000", @@ -6958,7 +6958,7 @@ "panel.background": "#262f51ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#f5f7ff4c", + "scrollbar.thumb.background": "#f5f7ff4c", "scrollbar.thumb.hover_background": "#363f62ff", "scrollbar.thumb.border": "#363f62ff", "scrollbar.track.background": "#00000000", @@ -7342,7 +7342,7 @@ "panel.background": "#e5e8f5ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#2026464c", + "scrollbar.thumb.background": "#2026464c", "scrollbar.thumb.hover_background": "#ccd0e1ff", "scrollbar.thumb.border": "#ccd0e1ff", "scrollbar.track.background": "#00000000", diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json index 40b4262204..87640ffeb9 100644 --- a/assets/themes/ayu/ayu.json +++ b/assets/themes/ayu/ayu.json @@ -46,7 +46,7 @@ "panel.background": "#1f2127ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#bfbdb64c", + "scrollbar.thumb.background": "#bfbdb64c", "scrollbar.thumb.hover_background": "#2d2f34ff", "scrollbar.thumb.border": "#2d2f34ff", "scrollbar.track.background": "#00000000", @@ -415,7 +415,7 @@ "panel.background": "#ececedff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#5c61664c", + "scrollbar.thumb.background": "#5c61664c", "scrollbar.thumb.hover_background": "#dfe0e1ff", "scrollbar.thumb.border": "#dfe0e1ff", "scrollbar.track.background": "#00000000", @@ -784,7 +784,7 @@ "panel.background": "#353944ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#cccac24c", + "scrollbar.thumb.background": "#cccac24c", "scrollbar.thumb.hover_background": "#43464fff", "scrollbar.thumb.border": "#43464fff", "scrollbar.track.background": "#00000000", diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json index eddcae6ff8..8ba68c74db 100644 --- a/assets/themes/gruvbox/gruvbox.json +++ b/assets/themes/gruvbox/gruvbox.json @@ -46,7 +46,7 @@ "panel.background": "#3a3735ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#fbf1c74c", + "scrollbar.thumb.background": "#fbf1c74c", "scrollbar.thumb.hover_background": "#494340ff", "scrollbar.thumb.border": "#494340ff", "scrollbar.track.background": "#00000000", @@ -420,7 +420,7 @@ "panel.background": "#393634ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#fbf1c74c", + "scrollbar.thumb.background": "#fbf1c74c", "scrollbar.thumb.hover_background": "#494340ff", "scrollbar.thumb.border": "#494340ff", "scrollbar.track.background": "#00000000", @@ -482,7 +482,7 @@ "hidden": "#998b78ff", "hidden.background": "#4c4642ff", "hidden.border": "#544c48ff", - "hint": "#8c957dff", + "hint": "#6a695bff", "hint.background": "#1e2321ff", "hint.border": "#303a36ff", "ignored": "#c5b597ff", @@ -794,7 +794,7 @@ "panel.background": "#3b3735ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#fbf1c74c", + "scrollbar.thumb.background": "#fbf1c74c", "scrollbar.thumb.hover_background": "#494340ff", "scrollbar.thumb.border": "#494340ff", "scrollbar.track.background": "#00000000", @@ -1168,7 +1168,7 @@ "panel.background": "#ecddb4ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#2828284c", + "scrollbar.thumb.background": "#2828284c", "scrollbar.thumb.hover_background": "#ddcca7ff", "scrollbar.thumb.border": "#ddcca7ff", "scrollbar.track.background": "#00000000", @@ -1542,7 +1542,7 @@ "panel.background": "#ecddb5ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#2828284c", + "scrollbar.thumb.background": "#2828284c", "scrollbar.thumb.hover_background": "#ddcca7ff", "scrollbar.thumb.border": "#ddcca7ff", "scrollbar.track.background": "#00000000", @@ -1916,7 +1916,7 @@ "panel.background": "#ecdcb3ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#2828284c", + "scrollbar.thumb.background": "#2828284c", "scrollbar.thumb.hover_background": "#ddcca7ff", "scrollbar.thumb.border": "#ddcca7ff", "scrollbar.track.background": "#00000000", diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json index b8953f95f4..cdf9bb05fe 100644 --- a/assets/themes/one/one.json +++ b/assets/themes/one/one.json @@ -46,7 +46,7 @@ "panel.background": "#2f343eff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#c8ccd44c", + "scrollbar.thumb.background": "#c8ccd44c", "scrollbar.thumb.hover_background": "#363c46ff", "scrollbar.thumb.border": "#363c46ff", "scrollbar.track.background": "#00000000", @@ -420,7 +420,7 @@ "panel.background": "#ebebecff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#383a414c", + "scrollbar.thumb.background": "#383a414c", "scrollbar.thumb.hover_background": "#dfdfe0ff", "scrollbar.thumb.border": "#dfdfe0ff", "scrollbar.track.background": "#00000000", diff --git a/assets/themes/rose_pine/rose_pine.json b/assets/themes/rose_pine/rose_pine.json index 69b89f8005..e74d1c7dcf 100644 --- a/assets/themes/rose_pine/rose_pine.json +++ b/assets/themes/rose_pine/rose_pine.json @@ -46,7 +46,7 @@ "panel.background": "#1c1b2aff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#e0def44c", + "scrollbar.thumb.background": "#e0def44c", "scrollbar.thumb.hover_background": "#232132ff", "scrollbar.thumb.border": "#232132ff", "scrollbar.track.background": "#00000000", @@ -425,7 +425,7 @@ "panel.background": "#fef9f2ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#5752794c", + "scrollbar.thumb.background": "#5752794c", "scrollbar.thumb.hover_background": "#e5e0dfff", "scrollbar.thumb.border": "#e5e0dfff", "scrollbar.track.background": "#00000000", @@ -804,7 +804,7 @@ "panel.background": "#28253cff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#e0def44c", + "scrollbar.thumb.background": "#e0def44c", "scrollbar.thumb.hover_background": "#322f48ff", "scrollbar.thumb.border": "#322f48ff", "scrollbar.track.background": "#00000000", diff --git a/assets/themes/sandcastle/sandcastle.json b/assets/themes/sandcastle/sandcastle.json index 6c42fee3c6..f56b1b8b4b 100644 --- a/assets/themes/sandcastle/sandcastle.json +++ b/assets/themes/sandcastle/sandcastle.json @@ -46,7 +46,7 @@ "panel.background": "#2b3038ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#fdf4c14c", + "scrollbar.thumb.background": "#fdf4c14c", "scrollbar.thumb.hover_background": "#313741ff", "scrollbar.thumb.border": "#313741ff", "scrollbar.track.background": "#00000000", diff --git a/assets/themes/solarized/solarized.json b/assets/themes/solarized/solarized.json index 217dddf4f7..c6e274860f 100644 --- a/assets/themes/solarized/solarized.json +++ b/assets/themes/solarized/solarized.json @@ -46,7 +46,7 @@ "panel.background": "#04313bff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#fdf6e34c", + "scrollbar.thumb.background": "#fdf6e34c", "scrollbar.thumb.hover_background": "#053541ff", "scrollbar.thumb.border": "#053541ff", "scrollbar.track.background": "#00000000", @@ -415,7 +415,7 @@ "panel.background": "#f3eddaff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#002a354c", + "scrollbar.thumb.background": "#002a354c", "scrollbar.thumb.hover_background": "#dcdacbff", "scrollbar.thumb.border": "#dcdacbff", "scrollbar.track.background": "#00000000", diff --git a/assets/themes/summercamp/summercamp.json b/assets/themes/summercamp/summercamp.json index 187d1fd23f..f46fb37744 100644 --- a/assets/themes/summercamp/summercamp.json +++ b/assets/themes/summercamp/summercamp.json @@ -46,7 +46,7 @@ "panel.background": "#231f16ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar_thumb.background": "#f8f5de4c", + "scrollbar.thumb.background": "#f8f5de4c", "scrollbar.thumb.hover_background": "#29251bff", "scrollbar.thumb.border": "#29251bff", "scrollbar.track.background": "#00000000", diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index 325c9be6e0..1513377e7d 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -17,9 +17,7 @@ futures.workspace = true gpui.workspace = true language.workspace = true project.workspace = true -settings.workspace = true smallvec.workspace = true -theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index ac63592d6b..7c4c5b1913 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -6,7 +6,7 @@ use gpui::{ ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _, }; -use language::{LanguageRegistry, LanguageServerBinaryStatus}; +use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName}; use project::{LanguageServerProgress, Project}; use smallvec::SmallVec; use std::{cmp::Reverse, fmt::Write, sync::Arc}; @@ -30,7 +30,7 @@ pub struct ActivityIndicator { } struct LspStatus { - name: Arc, + name: LanguageServerName, status: LanguageServerBinaryStatus, } @@ -58,13 +58,10 @@ impl ActivityIndicator { let this = cx.new_view(|cx: &mut ViewContext| { let mut status_events = languages.language_server_binary_statuses(); cx.spawn(|this, mut cx| async move { - while let Some((language, event)) = status_events.next().await { + while let Some((name, status)) = status_events.next().await { this.update(&mut cx, |this, cx| { - this.statuses.retain(|s| s.name != language.name()); - this.statuses.push(LspStatus { - name: language.name(), - status: event, - }); + this.statuses.retain(|s| s.name != name); + this.statuses.push(LspStatus { name, status }); cx.notify(); })?; } @@ -97,7 +94,7 @@ impl ActivityIndicator { cx, ); }); - workspace.add_item( + workspace.add_item_to_active_pane( Box::new( cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), ), @@ -114,7 +111,7 @@ impl ActivityIndicator { self.statuses.retain(|status| { if let LanguageServerBinaryStatus::Failed { error } = &status.status { cx.emit(Event::ShowError { - lsp_name: status.name.clone(), + lsp_name: status.name.0.clone(), error: error.clone(), }); false @@ -202,11 +199,12 @@ impl ActivityIndicator { let mut checking_for_update = SmallVec::<[_; 3]>::new(); let mut failed = SmallVec::<[_; 3]>::new(); for status in &self.statuses { - let name = status.name.clone(); match status.status { - LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name), - LanguageServerBinaryStatus::Downloading => downloading.push(name), - LanguageServerBinaryStatus::Failed { .. } => failed.push(name), + LanguageServerBinaryStatus::CheckingForUpdate => { + checking_for_update.push(status.name.0.as_ref()) + } + LanguageServerBinaryStatus::Downloading => downloading.push(status.name.0.as_ref()), + LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.0.as_ref()), LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {} } } @@ -214,34 +212,28 @@ impl ActivityIndicator { if !downloading.is_empty() { return Content { icon: Some(DOWNLOAD_ICON), - message: format!( - "Downloading {} language server{}...", - downloading.join(", "), - if downloading.len() > 1 { "s" } else { "" } - ), + message: format!("Downloading {}...", downloading.join(", "),), on_click: None, }; - } else if !checking_for_update.is_empty() { + } + + if !checking_for_update.is_empty() { return Content { icon: Some(DOWNLOAD_ICON), message: format!( - "Checking for updates to {} language server{}...", + "Checking for updates to {}...", checking_for_update.join(", "), - if checking_for_update.len() > 1 { - "s" - } else { - "" - } ), on_click: None, }; - } else if !failed.is_empty() { + } + + if !failed.is_empty() { return Content { icon: Some(WARNING_ICON), message: format!( - "Failed to download {} language server{}. Click to show error.", + "Failed to download {}. Click to show error.", failed.join(", "), - if failed.len() > 1 { "s" } else { "" } ), on_click: Some(Arc::new(|this, cx| { this.show_error_message(&Default::default(), cx) diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index de21b2b501..726c7329dc 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -20,7 +20,6 @@ futures.workspace = true gpui.workspace = true isahc.workspace = true language.workspace = true -lazy_static.workspace = true log.workspace = true matrixmultiply = "0.3.7" ordered-float.workspace = true @@ -28,8 +27,8 @@ parking_lot.workspace = true parse_duration = "2.1.1" postage.workspace = true rand.workspace = true -regex.workspace = true rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } +schemars.workspace = true serde.workspace = true serde_json.workspace = true tiktoken-rs.workspace = true diff --git a/crates/ai/src/embedding.rs b/crates/ai/src/embedding.rs index 6768b7ce7b..49611e002a 100644 --- a/crates/ai/src/embedding.rs +++ b/crates/ai/src/embedding.rs @@ -19,11 +19,9 @@ pub struct Embedding(pub Vec); impl FromSql for Embedding { fn column_result(value: ValueRef) -> FromSqlResult { let bytes = value.as_blob()?; - let embedding: Result, Box> = bincode::deserialize(bytes); - if embedding.is_err() { - return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); - } - Ok(Embedding(embedding.unwrap())) + let embedding = + bincode::deserialize(bytes).map_err(|err| rusqlite::types::FromSqlError::Other(err))?; + Ok(Embedding(embedding)) } } @@ -112,7 +110,7 @@ mod tests { } fn round_to_decimals(n: OrderedFloat, decimal_places: i32) -> f32 { - let factor = (10.0 as f32).powi(decimal_places); + let factor = 10.0_f32.powi(decimal_places); (n * factor).round() / factor } diff --git a/crates/ai/src/prompts/base.rs b/crates/ai/src/prompts/base.rs index 5e624f23ac..529f775ae8 100644 --- a/crates/ai/src/prompts/base.rs +++ b/crates/ai/src/prompts/base.rs @@ -30,7 +30,7 @@ impl PromptArguments { if self .language_name .as_ref() - .and_then(|name| Some(!["Markdown", "Plain Text"].contains(&name.as_str()))) + .map(|name| !["Markdown", "Plain Text"].contains(&name.as_str())) .unwrap_or(true) { PromptFileType::Code @@ -51,8 +51,10 @@ pub trait PromptTemplate { #[repr(i8)] #[derive(PartialEq, Eq, Ord)] pub enum PromptPriority { - Mandatory, // Ignores truncation - Ordered { order: usize }, // Truncates based on priority + /// Ignores truncation. + Mandatory, + /// Truncates based on priority. + Ordered { order: usize }, } impl PartialOrd for PromptPriority { @@ -86,7 +88,6 @@ impl PromptChain { let mut sorted_indices = (0..self.templates.len()).collect::>(); sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0)); - // If Truncate let mut tokens_outstanding = if truncate { Some(self.args.model.capacity()? - self.args.reserved_tokens) } else { diff --git a/crates/ai/src/prompts/repository_context.rs b/crates/ai/src/prompts/repository_context.rs index 89869c53a0..b31a3f63c2 100644 --- a/crates/ai/src/prompts/repository_context.rs +++ b/crates/ai/src/prompts/repository_context.rs @@ -24,11 +24,9 @@ impl PromptCodeSnippet { let language_name = buffer .language() - .and_then(|language| Some(language.name().to_string().to_lowercase())); + .map(|language| language.name().to_string().to_lowercase()); - let file_path = buffer - .file() - .and_then(|file| Some(file.path().to_path_buf())); + let file_path = buffer.file().map(|file| file.path().to_path_buf()); (content, language_name, file_path) })?; @@ -46,7 +44,7 @@ impl ToString for PromptCodeSnippet { let path = self .path .as_ref() - .and_then(|path| Some(path.to_string_lossy().to_string())) + .map(|path| path.to_string_lossy().to_string()) .unwrap_or("".to_string()); let language_name = self.language_name.clone().unwrap_or("".to_string()); let content = self.content.clone(); @@ -67,7 +65,7 @@ impl PromptTemplate for RepositoryContext { let template = "You are working inside a large repository, here are a few code snippets that may be useful."; let mut prompt = String::new(); - let mut remaining_tokens = max_token_length.clone(); + let mut remaining_tokens = max_token_length; let separator_token_length = args.model.count_tokens("\n")?; for snippet in &args.snippets { let mut snippet_prompt = template.to_string(); diff --git a/crates/ai/src/providers/mod.rs b/crates/ai/src/providers.rs similarity index 100% rename from crates/ai/src/providers/mod.rs rename to crates/ai/src/providers.rs diff --git a/crates/ai/src/providers/open_ai.rs b/crates/ai/src/providers/open_ai.rs index 9de21b8a60..8aff4877a8 100644 --- a/crates/ai/src/providers/open_ai.rs +++ b/crates/ai/src/providers/open_ai.rs @@ -6,4 +6,4 @@ pub use completion::*; pub use embedding::*; pub use model::OpenAiLanguageModel; -pub const OPEN_AI_API_URL: &'static str = "https://api.openai.com/v1"; +pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1"; diff --git a/crates/ai/src/providers/open_ai/completion.rs b/crates/ai/src/providers/open_ai/completion.rs index f3c7ebbdbc..04cc358894 100644 --- a/crates/ai/src/providers/open_ai/completion.rs +++ b/crates/ai/src/providers/open_ai/completion.rs @@ -1,3 +1,10 @@ +use std::{ + env, + fmt::{self, Display}, + io, + sync::Arc, +}; + use anyhow::{anyhow, Result}; use futures::{ future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt, @@ -6,23 +13,17 @@ use futures::{ use gpui::{AppContext, BackgroundExecutor}; use isahc::{http::StatusCode, Request, RequestExt}; use parking_lot::RwLock; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::{ - env, - fmt::{self, Display}, - io, - sync::Arc, -}; use util::ResultExt; +use crate::providers::open_ai::{OpenAiLanguageModel, OPEN_AI_API_URL}; use crate::{ auth::{CredentialProvider, ProviderCredential}, completion::{CompletionProvider, CompletionRequest}, models::LanguageModel, }; -use crate::providers::open_ai::{OpenAiLanguageModel, OPEN_AI_API_URL}; - #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Role { @@ -102,8 +103,9 @@ pub struct OpenAiResponseStreamEvent { pub usage: Option, } -pub async fn stream_completion( +async fn stream_completion( api_url: String, + kind: OpenAiCompletionProviderKind, credential: ProviderCredential, executor: BackgroundExecutor, request: Box, @@ -117,10 +119,11 @@ pub async fn stream_completion( let (tx, rx) = futures::channel::mpsc::unbounded::>(); + let (auth_header_name, auth_header_value) = kind.auth_header(api_key); let json_data = request.data()?; - let mut response = Request::post(format!("{api_url}/chat/completions")) + let mut response = Request::post(kind.completions_endpoint_url(&api_url)) .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) + .header(auth_header_name, auth_header_value) .body(json_data)? .send_async() .await?; @@ -194,22 +197,109 @@ pub async fn stream_completion( } } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)] +pub enum AzureOpenAiApiVersion { + /// Retiring April 2, 2024. + #[serde(rename = "2023-03-15-preview")] + V2023_03_15Preview, + #[serde(rename = "2023-05-15")] + V2023_05_15, + /// Retiring April 2, 2024. + #[serde(rename = "2023-06-01-preview")] + V2023_06_01Preview, + /// Retiring April 2, 2024. + #[serde(rename = "2023-07-01-preview")] + V2023_07_01Preview, + /// Retiring April 2, 2024. + #[serde(rename = "2023-08-01-preview")] + V2023_08_01Preview, + /// Retiring April 2, 2024. + #[serde(rename = "2023-09-01-preview")] + V2023_09_01Preview, + #[serde(rename = "2023-12-01-preview")] + V2023_12_01Preview, + #[serde(rename = "2024-02-15-preview")] + V2024_02_15Preview, +} + +impl fmt::Display for AzureOpenAiApiVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::V2023_03_15Preview => "2023-03-15-preview", + Self::V2023_05_15 => "2023-05-15", + Self::V2023_06_01Preview => "2023-06-01-preview", + Self::V2023_07_01Preview => "2023-07-01-preview", + Self::V2023_08_01Preview => "2023-08-01-preview", + Self::V2023_09_01Preview => "2023-09-01-preview", + Self::V2023_12_01Preview => "2023-12-01-preview", + Self::V2024_02_15Preview => "2024-02-15-preview", + } + ) + } +} + +#[derive(Clone)] +pub enum OpenAiCompletionProviderKind { + OpenAi, + AzureOpenAi { + deployment_id: String, + api_version: AzureOpenAiApiVersion, + }, +} + +impl OpenAiCompletionProviderKind { + /// Returns the chat completion endpoint URL for this [`OpenAiCompletionProviderKind`]. + fn completions_endpoint_url(&self, api_url: &str) -> String { + match self { + Self::OpenAi => { + // https://platform.openai.com/docs/api-reference/chat/create + format!("{api_url}/chat/completions") + } + Self::AzureOpenAi { + deployment_id, + api_version, + } => { + // https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions + format!("{api_url}/openai/deployments/{deployment_id}/chat/completions?api-version={api_version}") + } + } + } + + /// Returns the authentication header for this [`OpenAiCompletionProviderKind`]. + fn auth_header(&self, api_key: String) -> (&'static str, String) { + match self { + Self::OpenAi => ("Authorization", format!("Bearer {api_key}")), + Self::AzureOpenAi { .. } => ("Api-Key", api_key), + } + } +} + #[derive(Clone)] pub struct OpenAiCompletionProvider { api_url: String, + kind: OpenAiCompletionProviderKind, model: OpenAiLanguageModel, credential: Arc>, executor: BackgroundExecutor, } impl OpenAiCompletionProvider { - pub async fn new(api_url: String, model_name: String, executor: BackgroundExecutor) -> Self { + pub async fn new( + api_url: String, + kind: OpenAiCompletionProviderKind, + model_name: String, + executor: BackgroundExecutor, + ) -> Self { let model = executor .spawn(async move { OpenAiLanguageModel::load(&model_name) }) .await; let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials)); Self { api_url, + kind, model, credential, executor, @@ -297,6 +387,7 @@ impl CompletionProvider for OpenAiCompletionProvider { let model: Box = Box::new(self.model.clone()); model } + fn complete( &self, prompt: Box, @@ -307,7 +398,8 @@ impl CompletionProvider for OpenAiCompletionProvider { // At some point in the future we should rectify this. let credential = self.credential.read().clone(); let api_url = self.api_url.clone(); - let request = stream_completion(api_url, credential, self.executor.clone(), prompt); + let kind = self.kind.clone(); + let request = stream_completion(api_url, kind, credential, self.executor.clone(), prompt); async move { let response = request.await?; let stream = response @@ -322,6 +414,7 @@ impl CompletionProvider for OpenAiCompletionProvider { } .boxed() } + fn box_clone(&self) -> Box { Box::new((*self).clone()) } diff --git a/crates/ai/src/providers/open_ai/embedding.rs b/crates/ai/src/providers/open_ai/embedding.rs index 588861a972..ddff082359 100644 --- a/crates/ai/src/providers/open_ai/embedding.rs +++ b/crates/ai/src/providers/open_ai/embedding.rs @@ -8,7 +8,6 @@ use gpui::BackgroundExecutor; use isahc::http::StatusCode; use isahc::prelude::Configurable; use isahc::{AsyncBody, Response}; -use lazy_static::lazy_static; use parking_lot::{Mutex, RwLock}; use parse_duration::parse; use postage::watch; @@ -16,7 +15,7 @@ use serde::{Deserialize, Serialize}; use serde_json; use std::env; use std::ops::Add; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use std::time::{Duration, Instant}; use tiktoken_rs::{cl100k_base, CoreBPE}; use util::http::{HttpClient, Request}; @@ -29,8 +28,9 @@ use crate::providers::open_ai::OpenAiLanguageModel; use crate::providers::open_ai::OPEN_AI_API_URL; -lazy_static! { - pub(crate) static ref OPEN_AI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap(); +pub(crate) fn open_ai_bpe_tokenizer() -> &'static CoreBPE { + static OPEN_AI_BPE_TOKENIZER: OnceLock = OnceLock::new(); + OPEN_AI_BPE_TOKENIZER.get_or_init(|| cl100k_base().unwrap()) } #[derive(Clone)] diff --git a/crates/ai/src/providers/open_ai/model.rs b/crates/ai/src/providers/open_ai/model.rs index 21ea0334bd..f2f75977e4 100644 --- a/crates/ai/src/providers/open_ai/model.rs +++ b/crates/ai/src/providers/open_ai/model.rs @@ -3,7 +3,7 @@ use tiktoken_rs::CoreBPE; use crate::models::{LanguageModel, TruncationDirection}; -use super::OPEN_AI_BPE_TOKENIZER; +use super::open_ai_bpe_tokenizer; #[derive(Clone)] pub struct OpenAiLanguageModel { @@ -13,8 +13,8 @@ pub struct OpenAiLanguageModel { impl OpenAiLanguageModel { pub fn load(model_name: &str) -> Self { - let bpe = - tiktoken_rs::get_bpe_from_model(model_name).unwrap_or(OPEN_AI_BPE_TOKENIZER.to_owned()); + let bpe = tiktoken_rs::get_bpe_from_model(model_name) + .unwrap_or(open_ai_bpe_tokenizer().to_owned()); OpenAiLanguageModel { name: model_name.to_string(), bpe: Some(bpe), diff --git a/crates/ai/src/test.rs b/crates/ai/src/test.rs index 89edc71b0b..f10ca4f5fa 100644 --- a/crates/ai/src/test.rs +++ b/crates/ai/src/test.rs @@ -54,6 +54,7 @@ impl LanguageModel for FakeLanguageModel { } } +#[derive(Default)] pub struct FakeEmbeddingProvider { pub embedding_count: AtomicUsize, } @@ -66,14 +67,6 @@ impl Clone for FakeEmbeddingProvider { } } -impl Default for FakeEmbeddingProvider { - fn default() -> Self { - FakeEmbeddingProvider { - embedding_count: AtomicUsize::default(), - } - } -} - impl FakeEmbeddingProvider { pub fn embedding_count(&self) -> usize { self.embedding_count.load(atomic::Ordering::SeqCst) diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 97e1a13765..3f24babef6 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -13,20 +13,17 @@ doctest = false ai.workspace = true anyhow.workspace = true chrono.workspace = true -client.workspace = true collections.workspace = true editor.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true indoc.workspace = true -isahc.workspace = true language.workspace = true log.workspace = true menu.workspace = true multi_buffer.workspace = true ordered-float.workspace = true -parking_lot.workspace = true project.workspace = true regex.workspace = true schemars.workspace = true @@ -36,6 +33,7 @@ serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true +telemetry_events.workspace = true theme.workspace = true tiktoken-rs.workspace = true ui.workspace = true diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 4e861c9d3e..97f0bca083 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -7,15 +7,16 @@ use crate::{ SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, ToggleRetrieveContext, }; use ai::prompts::repository_context::PromptCodeSnippet; -use ai::providers::open_ai::OPEN_AI_API_URL; use ai::{ auth::ProviderCredential, completion::{CompletionProvider, CompletionRequest}, - providers::open_ai::{OpenAiCompletionProvider, OpenAiRequest, RequestMessage}, + providers::open_ai::{ + OpenAiCompletionProvider, OpenAiCompletionProviderKind, OpenAiRequest, RequestMessage, + OPEN_AI_API_URL, + }, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; -use client::telemetry::AssistantKind; use collections::{hash_map, HashMap, HashSet, VecDeque}; use editor::{ actions::{MoveDown, MoveUp}, @@ -52,6 +53,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use telemetry_events::AssistantKind; use theme::ThemeSettings; use ui::{ prelude::*, @@ -122,18 +124,18 @@ impl AssistantPanel { .await .log_err() .unwrap_or_default(); - let (api_url, model_name) = cx - .update(|cx| { - let settings = AssistantSettings::get_global(cx); - ( - settings.openai_api_url.clone(), - settings.default_open_ai_model.full_name().to_string(), - ) - }) - .log_err() - .unwrap(); + let (provider_kind, api_url, model_name) = cx.update(|cx| { + let settings = AssistantSettings::get_global(cx); + anyhow::Ok(( + settings.provider_kind()?, + settings.provider_api_url()?, + settings.provider_model_name()?, + )) + })??; + let completion_provider = OpenAiCompletionProvider::new( api_url, + provider_kind, model_name, cx.background_executor().clone(), ) @@ -365,7 +367,7 @@ impl AssistantPanel { move |cx: &mut BlockContext| { measurements.set(BlockMeasurements { anchor_x: cx.anchor_x, - gutter_width: cx.gutter_width, + gutter_width: cx.gutter_dimensions.width, }); inline_assistant.clone().into_any_element() } @@ -693,24 +695,29 @@ impl AssistantPanel { Task::ready(Ok(Vec::new())) }; - let mut model = AssistantSettings::get_global(cx) - .default_open_ai_model - .clone(); - let model_name = model.full_name(); + let Some(mut model_name) = AssistantSettings::get_global(cx) + .provider_model_name() + .log_err() + else { + return; + }; - let prompt = cx.background_executor().spawn(async move { - let snippets = snippets.await?; + let prompt = cx.background_executor().spawn({ + let model_name = model_name.clone(); + async move { + let snippets = snippets.await?; - let language_name = language_name.as_deref(); - generate_content_prompt( - user_prompt, - language_name, - buffer, - range, - snippets, - model_name, - project_name, - ) + let language_name = language_name.as_deref(); + generate_content_prompt( + user_prompt, + language_name, + buffer, + range, + snippets, + &model_name, + project_name, + ) + } }); let mut messages = Vec::new(); @@ -722,7 +729,7 @@ impl AssistantPanel { .messages(cx) .map(|message| message.to_open_ai_message(buffer)), ); - model = conversation.model.clone(); + model_name = conversation.model.full_name().to_string(); } cx.spawn(|_, mut cx| async move { @@ -735,7 +742,7 @@ impl AssistantPanel { }); let request = Box::new(OpenAiRequest { - model: model.full_name().into(), + model: model_name, messages, stream: true, stop: vec!["|END|>".to_string()], @@ -774,7 +781,7 @@ impl AssistantPanel { } else { editor.highlight_background::( background_ranges, - |theme| theme.editor_active_line_background, // todo!("use the appropriate color") + |theme| theme.editor_active_line_background, // todo("use the appropriate color") cx, ); } @@ -972,7 +979,7 @@ impl AssistantPanel { font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, - line_height: relative(1.3).into(), + line_height: relative(1.3), background_color: None, underline: None, strikethrough: None, @@ -1454,8 +1461,14 @@ impl Conversation { }); let settings = AssistantSettings::get_global(cx); - let model = settings.default_open_ai_model.clone(); - let api_url = settings.openai_api_url.clone(); + let model = settings + .provider_model() + .log_err() + .unwrap_or(OpenAiModel::FourTurbo); + let api_url = settings + .provider_api_url() + .log_err() + .unwrap_or_else(|| OPEN_AI_API_URL.to_string()); let mut this = Self { id: Some(Uuid::new_v4().to_string()), @@ -1470,7 +1483,7 @@ impl Conversation { max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), pending_token_count: Task::ready(None), api_url: Some(api_url), - model: model.clone(), + model, _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], pending_save: Task::ready(Ok(())), path: None, @@ -1514,7 +1527,7 @@ impl Conversation { .as_ref() .map(|summary| summary.text.clone()) .unwrap_or_default(), - model: self.model.clone(), + model: self.model, api_url: self.api_url.clone(), } } @@ -1536,6 +1549,7 @@ impl Conversation { api_url .clone() .unwrap_or_else(|| OPEN_AI_API_URL.to_string()), + OpenAiCompletionProviderKind::OpenAi, model.full_name().into(), cx.background_executor().clone(), ) @@ -1619,26 +1633,23 @@ impl Conversation { fn count_remaining_tokens(&mut self, cx: &mut ModelContext) { let messages = self .messages(cx) - .into_iter() - .filter_map(|message| { - Some(tiktoken_rs::ChatCompletionRequestMessage { - role: match message.role { - Role::User => "user".into(), - Role::Assistant => "assistant".into(), - Role::System => "system".into(), - }, - content: Some( - self.buffer - .read(cx) - .text_for_range(message.offset_range) - .collect(), - ), - name: None, - function_call: None, - }) + .map(|message| tiktoken_rs::ChatCompletionRequestMessage { + role: match message.role { + Role::User => "user".into(), + Role::Assistant => "assistant".into(), + Role::System => "system".into(), + }, + content: Some( + self.buffer + .read(cx) + .text_for_range(message.offset_range) + .collect(), + ), + name: None, + function_call: None, }) .collect::>(); - let model = self.model.clone(); + let model = self.model; self.pending_token_count = cx.spawn(|this, mut cx| { async move { cx.background_executor() @@ -2821,6 +2832,7 @@ impl FocusableView for InlineAssistant { } impl InlineAssistant { + #[allow(clippy::too_many_arguments)] fn new( id: usize, measurements: Rc>, @@ -3186,7 +3198,7 @@ impl InlineAssistant { font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, - line_height: relative(1.3).into(), + line_height: relative(1.3), background_color: None, underline: None, strikethrough: None, @@ -3654,9 +3666,9 @@ fn report_assistant_event( let client = workspace.read(cx).project().read(cx).client(); let telemetry = client.telemetry(); - let model = AssistantSettings::get_global(cx) - .default_open_ai_model - .clone(); + let Ok(model_name) = AssistantSettings::get_global(cx).provider_model_name() else { + return; + }; - telemetry.report_assistant_event(conversation_id, assistant_kind, model.full_name()) + telemetry.report_assistant_event(conversation_id, assistant_kind, &model_name) } diff --git a/crates/assistant/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs index 16c2d452c2..007e994389 100644 --- a/crates/assistant/src/assistant_settings.rs +++ b/crates/assistant/src/assistant_settings.rs @@ -1,10 +1,14 @@ -use anyhow; +use ai::providers::open_ai::{ + AzureOpenAiApiVersion, OpenAiCompletionProviderKind, OPEN_AI_API_URL, +}; +use anyhow::anyhow; use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] pub enum OpenAiModel { #[serde(rename = "gpt-3.5-turbo-0613")] ThreePointFiveTurbo, @@ -17,25 +21,25 @@ pub enum OpenAiModel { impl OpenAiModel { pub fn full_name(&self) -> &'static str { match self { - OpenAiModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613", - OpenAiModel::Four => "gpt-4-0613", - OpenAiModel::FourTurbo => "gpt-4-1106-preview", + Self::ThreePointFiveTurbo => "gpt-3.5-turbo-0613", + Self::Four => "gpt-4-0613", + Self::FourTurbo => "gpt-4-1106-preview", } } pub fn short_name(&self) -> &'static str { match self { - OpenAiModel::ThreePointFiveTurbo => "gpt-3.5-turbo", - OpenAiModel::Four => "gpt-4", - OpenAiModel::FourTurbo => "gpt-4-turbo", + Self::ThreePointFiveTurbo => "gpt-3.5-turbo", + Self::Four => "gpt-4", + Self::FourTurbo => "gpt-4-turbo", } } pub fn cycle(&self) -> Self { match self { - OpenAiModel::ThreePointFiveTurbo => OpenAiModel::Four, - OpenAiModel::Four => OpenAiModel::FourTurbo, - OpenAiModel::FourTurbo => OpenAiModel::ThreePointFiveTurbo, + Self::ThreePointFiveTurbo => Self::Four, + Self::Four => Self::FourTurbo, + Self::FourTurbo => Self::ThreePointFiveTurbo, } } } @@ -48,14 +52,113 @@ pub enum AssistantDockPosition { Bottom, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Deserialize)] pub struct AssistantSettings { + /// Whether to show the assistant panel button in the status bar. pub button: bool, + /// Where to dock the assistant. pub dock: AssistantDockPosition, + /// Default width in pixels when the assistant is docked to the left or right. pub default_width: Pixels, + /// Default height in pixels when the assistant is docked to the bottom. pub default_height: Pixels, + /// The default OpenAI model to use when starting new conversations. + #[deprecated = "Please use `provider.default_model` instead."] pub default_open_ai_model: OpenAiModel, + /// OpenAI API base URL to use when starting new conversations. + #[deprecated = "Please use `provider.api_url` instead."] pub openai_api_url: String, + /// The settings for the AI provider. + pub provider: AiProviderSettings, +} + +impl AssistantSettings { + pub fn provider_kind(&self) -> anyhow::Result { + match &self.provider { + AiProviderSettings::OpenAi(_) => Ok(OpenAiCompletionProviderKind::OpenAi), + AiProviderSettings::AzureOpenAi(settings) => { + let deployment_id = settings + .deployment_id + .clone() + .ok_or_else(|| anyhow!("no Azure OpenAI deployment ID"))?; + let api_version = settings + .api_version + .ok_or_else(|| anyhow!("no Azure OpenAI API version"))?; + + Ok(OpenAiCompletionProviderKind::AzureOpenAi { + deployment_id, + api_version, + }) + } + } + } + + pub fn provider_api_url(&self) -> anyhow::Result { + match &self.provider { + AiProviderSettings::OpenAi(settings) => Ok(settings + .api_url + .clone() + .unwrap_or_else(|| OPEN_AI_API_URL.to_string())), + AiProviderSettings::AzureOpenAi(settings) => settings + .api_url + .clone() + .ok_or_else(|| anyhow!("no Azure OpenAI API URL")), + } + } + + pub fn provider_model(&self) -> anyhow::Result { + match &self.provider { + AiProviderSettings::OpenAi(settings) => { + Ok(settings.default_model.unwrap_or(OpenAiModel::FourTurbo)) + } + AiProviderSettings::AzureOpenAi(settings) => { + let deployment_id = settings + .deployment_id + .as_deref() + .ok_or_else(|| anyhow!("no Azure OpenAI deployment ID"))?; + + match deployment_id { + // https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#gpt-4-and-gpt-4-turbo-preview + "gpt-4" | "gpt-4-32k" => Ok(OpenAiModel::Four), + // https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#gpt-35 + "gpt-35-turbo" | "gpt-35-turbo-16k" | "gpt-35-turbo-instruct" => { + Ok(OpenAiModel::ThreePointFiveTurbo) + } + _ => Err(anyhow!( + "no matching OpenAI model found for deployment ID: '{deployment_id}'" + )), + } + } + } + } + + pub fn provider_model_name(&self) -> anyhow::Result { + match &self.provider { + AiProviderSettings::OpenAi(settings) => Ok(settings + .default_model + .unwrap_or(OpenAiModel::FourTurbo) + .full_name() + .to_string()), + AiProviderSettings::AzureOpenAi(settings) => settings + .deployment_id + .clone() + .ok_or_else(|| anyhow!("no Azure OpenAI deployment ID")), + } + } +} + +impl Settings for AssistantSettings { + const KEY: Option<&'static str> = Some("assistant"); + + type FileContent = AssistantSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } } /// Assistant panel settings @@ -77,26 +180,88 @@ pub struct AssistantSettingsContent { /// /// Default: 320 pub default_height: Option, + /// Deprecated: Please use `provider.default_model` instead. /// The default OpenAI model to use when starting new conversations. /// /// Default: gpt-4-1106-preview + #[deprecated = "Please use `provider.default_model` instead."] pub default_open_ai_model: Option, + /// Deprecated: Please use `provider.api_url` instead. /// OpenAI API base URL to use when starting new conversations. /// /// Default: https://api.openai.com/v1 + #[deprecated = "Please use `provider.api_url` instead."] pub openai_api_url: Option, + /// The settings for the AI provider. + #[serde(default)] + pub provider: AiProviderSettingsContent, } -impl Settings for AssistantSettings { - const KEY: Option<&'static str> = Some("assistant"); +#[derive(Debug, Clone, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AiProviderSettings { + /// The settings for the OpenAI provider. + #[serde(rename = "openai")] + OpenAi(OpenAiProviderSettings), + /// The settings for the Azure OpenAI provider. + #[serde(rename = "azure_openai")] + AzureOpenAi(AzureOpenAiProviderSettings), +} - type FileContent = AssistantSettingsContent; +/// The settings for the AI provider used by the Zed Assistant. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AiProviderSettingsContent { + /// The settings for the OpenAI provider. + #[serde(rename = "openai")] + OpenAi(OpenAiProviderSettingsContent), + /// The settings for the Azure OpenAI provider. + #[serde(rename = "azure_openai")] + AzureOpenAi(AzureOpenAiProviderSettingsContent), +} - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &mut gpui::AppContext, - ) -> anyhow::Result { - Self::load_via_json_merge(default_value, user_values) +impl Default for AiProviderSettingsContent { + fn default() -> Self { + Self::OpenAi(OpenAiProviderSettingsContent::default()) } } + +#[derive(Debug, Clone, Deserialize)] +pub struct OpenAiProviderSettings { + /// The OpenAI API base URL to use when starting new conversations. + pub api_url: Option, + /// The default OpenAI model to use when starting new conversations. + pub default_model: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +pub struct OpenAiProviderSettingsContent { + /// The OpenAI API base URL to use when starting new conversations. + /// + /// Default: https://api.openai.com/v1 + pub api_url: Option, + /// The default OpenAI model to use when starting new conversations. + /// + /// Default: gpt-4-1106-preview + pub default_model: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AzureOpenAiProviderSettings { + /// The Azure OpenAI API base URL to use when starting new conversations. + pub api_url: Option, + /// The Azure OpenAI API version. + pub api_version: Option, + /// The Azure OpenAI API deployment ID. + pub deployment_id: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +pub struct AzureOpenAiProviderSettingsContent { + /// The Azure OpenAI API base URL to use when starting new conversations. + pub api_url: Option, + /// The Azure OpenAI API version. + pub api_version: Option, + /// The Azure OpenAI deployment ID. + pub deployment_id: Option, +} diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index c1a663d7ef..04d08a3315 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -297,7 +297,7 @@ fn strip_invalid_spans_from_codeblock( } else if buffer.starts_with("<|") || buffer.starts_with("<|S") || buffer.starts_with("<|S|") - || buffer.ends_with("|") + || buffer.ends_with('|') || buffer.ends_with("|E") || buffer.ends_with("|E|") { @@ -335,7 +335,7 @@ fn strip_invalid_spans_from_codeblock( .strip_suffix("|E|>") .or_else(|| text.strip_suffix("E|>")) .or_else(|| text.strip_prefix("|>")) - .or_else(|| text.strip_prefix(">")) + .or_else(|| text.strip_prefix('>')) .unwrap_or(&text) .to_string(); }; diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 58ab15d910..d66df11f9f 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -13,9 +13,7 @@ doctest = false anyhow.workspace = true collections.workspace = true derive_more.workspace = true -futures.workspace = true gpui.workspace = true -log.workspace = true parking_lot.workspace = true rodio = { version = "0.17.1", default-features = false, features = ["wav"] } util.workspace = true diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 9341fbe369..8135d5b795 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -13,12 +13,12 @@ doctest = false anyhow.workspace = true client.workspace = true db.workspace = true +editor.workspace = true gpui.workspace = true isahc.workspace = true -lazy_static.workspace = true log.workspace = true +markdown_preview.workspace = true menu.workspace = true -project.workspace = true release_channel.workspace = true schemars.workspace = true serde.workspace = true @@ -27,6 +27,5 @@ serde_json.workspace = true settings.workspace = true smol.workspace = true tempfile.workspace = true -theme.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index cb08871c6d..f364304d59 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -4,12 +4,14 @@ use anyhow::{anyhow, Context, Result}; use client::{Client, TelemetrySettings, ZED_APP_PATH}; use db::kvp::KEY_VALUE_STORE; use db::RELEASE_CHANNEL; +use editor::{Editor, MultiBuffer}; use gpui::{ actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext, - SemanticVersion, Task, ViewContext, VisualContext, WindowContext, + SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext, }; use isahc::AsyncBody; +use markdown_preview::markdown_preview_view::MarkdownPreviewView; use schemars::JsonSchema; use serde::Deserialize; use serde_derive::Serialize; @@ -18,7 +20,7 @@ use smol::io::AsyncReadExt; use settings::{Settings, SettingsStore}; use smol::{fs::File, process::Command}; -use release_channel::{AppCommitSha, ReleaseChannel}; +use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use std::{ env::consts::{ARCH, OS}, ffi::OsString, @@ -26,13 +28,24 @@ use std::{ time::Duration, }; use update_notification::UpdateNotification; -use util::http::{HttpClient, ZedHttpClient}; +use util::{ + http::{HttpClient, HttpClientWithUrl}, + ResultExt, +}; use workspace::Workspace; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); -actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]); +actions!( + auto_update, + [ + Check, + DismissErrorMessage, + ViewReleaseNotes, + ViewReleaseNotesLocally + ] +); #[derive(Serialize)] struct UpdateRequestBody { @@ -54,7 +67,7 @@ pub enum AutoUpdateStatus { pub struct AutoUpdater { status: AutoUpdateStatus, current_version: SemanticVersion, - http_client: Arc, + http_client: Arc, pending_poll: Option>>, } @@ -96,7 +109,13 @@ struct GlobalAutoUpdate(Option>); impl Global for GlobalAutoUpdate {} -pub fn init(http_client: Arc, cx: &mut AppContext) { +#[derive(Deserialize)] +struct ReleaseNotesBody { + title: String, + release_notes: String, +} + +pub fn init(http_client: Arc, cx: &mut AppContext) { AutoUpdateSetting::register(cx); cx.observe_new_views(|workspace: &mut Workspace, _cx| { @@ -105,6 +124,10 @@ pub fn init(http_client: Arc, cx: &mut AppContext) { workspace.register_action(|_, action, cx| { view_release_notes(action, cx); }); + + workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| { + view_release_notes_locally(workspace, cx); + }); }) .detach(); @@ -158,13 +181,78 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<( let current_version = auto_updater.current_version; let url = &auto_updater .http_client - .zed_url(&format!("/releases/{release_channel}/{current_version}")); + .build_url(&format!("/releases/{release_channel}/{current_version}")); cx.open_url(&url); } None } +fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext) { + let release_channel = ReleaseChannel::global(cx); + let version = AppVersion::global(cx).to_string(); + + let client = client::Client::global(cx).http_client(); + let url = client.build_url(&format!( + "/api/release_notes/{}/{}", + release_channel.dev_name(), + version + )); + + let markdown = workspace + .app_state() + .languages + .language_for_name("Markdown"); + + workspace + .with_local_workspace(cx, move |_, cx| { + cx.spawn(|workspace, mut cx| async move { + let markdown = markdown.await.log_err(); + let response = client.get(&url, Default::default(), true).await; + let Some(mut response) = response.log_err() else { + return; + }; + + let mut body = Vec::new(); + response.body_mut().read_to_end(&mut body).await.ok(); + + let body: serde_json::Result = + serde_json::from_slice(body.as_slice()); + + if let Ok(body) = body { + workspace + .update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project + .update(cx, |project, cx| project.create_buffer("", markdown, cx)) + .expect("creating buffers on a local workspace always succeeds"); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, body.release_notes)], None, cx) + }); + + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + + let tab_description = SharedString::from(body.title.to_string()); + let editor = cx + .new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx)); + let workspace_handle = workspace.weak_handle(); + let view: View = MarkdownPreviewView::new( + editor, + workspace_handle, + Some(tab_description), + cx, + ); + workspace.add_item_to_active_pane(Box::new(view.clone()), cx); + cx.notify(); + }) + .log_err(); + } + }) + .detach(); + }) + .detach(); +} + pub fn notify_of_any_new_update(cx: &mut ViewContext) -> Option<()> { let updater = AutoUpdater::get(cx)?; let version = updater.read(cx).current_version; @@ -195,7 +283,7 @@ impl AutoUpdater { cx.default_global::().0.clone() } - fn new(current_version: SemanticVersion, http_client: Arc) -> Self { + fn new(current_version: SemanticVersion, http_client: Arc) -> Self { Self { status: AutoUpdateStatus::Idle, current_version, @@ -249,14 +337,13 @@ impl AutoUpdater { (this.http_client.clone(), this.current_version) })?; - let mut url_string = client.zed_url(&format!( + let mut url_string = client.build_url(&format!( "/api/releases/latest?asset=Zed.dmg&os={}&arch={}", OS, ARCH )); cx.update(|cx| { if let Some(param) = ReleaseChannel::try_global(cx) - .map(|release_channel| release_channel.release_query_param()) - .flatten() + .and_then(|release_channel| release_channel.release_query_param()) { url_string += "&"; url_string += param; diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index 1dbee71806..66028c2401 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -44,7 +44,7 @@ impl Render for UpdateNotification { crate::view_release_notes(&Default::default(), cx); this.dismiss(&menu::Cancel, cx) })), - ); + ) } } diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index 57724663ca..24f3a70fd3 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -10,15 +10,10 @@ path = "src/breadcrumbs.rs" doctest = false [dependencies] -collections.workspace = true editor.workspace = true gpui.workspace = true -itertools = "0.10" -language.workspace = true +itertools.workspace = true outline.workspace = true -project.workspace = true -search.workspace = true -settings.workspace = true theme.workspace = true ui.workspace = true workspace.workspace = true diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 74726a57a3..a0acf544b5 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -21,26 +21,21 @@ test-support = [ [dependencies] anyhow.workspace = true -async-broadcast = "0.4" audio.workspace = true client.workspace = true collections.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true -image = "0.23" language.workspace = true live_kit_client.workspace = true log.workspace = true -media.workspace = true postage.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true serde_derive.workspace = true -serde_json.workspace = true settings.workspace = true -smallvec.workspace = true util.workspace = true [dev-dependencies] diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 11fc549084..f0a0a22fb3 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -5,7 +5,7 @@ pub mod room; use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; -use client::{proto, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; +use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ @@ -107,7 +107,7 @@ impl ActiveCall { } } - pub fn channel_id(&self, cx: &AppContext) -> Option { + pub fn channel_id(&self, cx: &AppContext) -> Option { self.room()?.read(cx).channel_id() } @@ -302,7 +302,7 @@ impl ActiveCall { return Task::ready(Ok(())); } - let room_id = call.room_id.clone(); + let room_id = call.room_id; let client = self.client.clone(); let user_store = self.user_store.clone(); let join = self @@ -336,7 +336,7 @@ impl ActiveCall { pub fn join_channel( &mut self, - channel_id: u64, + channel_id: ChannelId, cx: &mut ModelContext, ) -> Task>>> { if let Some(room) = self.room().cloned() { @@ -487,7 +487,7 @@ impl ActiveCall { pub fn report_call_event_for_room( operation: &'static str, room_id: u64, - channel_id: Option, + channel_id: Option, client: &Arc, ) { let telemetry = client.telemetry(); @@ -497,7 +497,7 @@ pub fn report_call_event_for_room( pub fn report_call_event_for_channel( operation: &'static str, - channel_id: u64, + channel_id: ChannelId, client: &Arc, cx: &AppContext, ) { diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index cd8af385ed..5599c15b6d 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -6,7 +6,7 @@ use anyhow::{anyhow, Result}; use audio::{Audio, Sound}; use client::{ proto::{self, PeerId}, - Client, ParticipantIndex, TypedEnvelope, User, UserStore, + ChannelId, Client, ParticipantIndex, TypedEnvelope, User, UserStore, }; use collections::{BTreeMap, HashMap, HashSet}; use fs::Fs; @@ -27,7 +27,7 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { RoomJoined { - channel_id: Option, + channel_id: Option, }, ParticipantLocationChanged { participant_id: proto::PeerId, @@ -53,13 +53,13 @@ pub enum Event { project_id: u64, }, Left { - channel_id: Option, + channel_id: Option, }, } pub struct Room { id: u64, - channel_id: Option, + channel_id: Option, live_kit: Option, status: RoomStatus, shared_projects: HashSet>, @@ -84,7 +84,7 @@ pub struct Room { impl EventEmitter for Room {} impl Room { - pub fn channel_id(&self) -> Option { + pub fn channel_id(&self) -> Option { self.channel_id } @@ -106,7 +106,7 @@ impl Room { fn new( id: u64, - channel_id: Option, + channel_id: Option, live_kit_connection_info: Option, client: Arc, user_store: Model, @@ -156,7 +156,7 @@ impl Room { cx.spawn(|this, mut cx| async move { connect.await?; this.update(&mut cx, |this, cx| { - if !this.read_only() { + if this.can_use_microphone() { if let Some(live_kit) = &this.live_kit { if !live_kit.muted_by_user && !live_kit.deafened { return this.share_microphone(cx); @@ -273,13 +273,17 @@ impl Room { } pub(crate) async fn join_channel( - channel_id: u64, + channel_id: ChannelId, client: Arc, user_store: Model, cx: AsyncAppContext, ) -> Result> { Self::from_join_response( - client.request(proto::JoinChannel { channel_id }).await?, + client + .request(proto::JoinChannel { + channel_id: channel_id.0, + }) + .await?, client, user_store, cx, @@ -337,7 +341,7 @@ impl Room { let room = cx.new_model(|cx| { Self::new( room_proto.id, - response.channel_id, + response.channel_id.map(ChannelId), response.live_kit_connection_info, client, user_store, @@ -1178,19 +1182,10 @@ impl Room { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); - let role = self.local_participant.role; cx.emit(Event::RemoteProjectJoined { project_id: id }); cx.spawn(move |this, mut cx| async move { - let project = Project::remote( - id, - client, - user_store, - language_registry, - fs, - role, - cx.clone(), - ) - .await?; + let project = + Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?; this.update(&mut cx, |this, cx| { this.joined_projects.retain(|project| { @@ -1322,11 +1317,6 @@ impl Room { }) } - pub fn read_only(&self) -> bool { - !(self.local_participant().role == proto::ChannelRole::Member - || self.local_participant().role == proto::ChannelRole::Admin) - } - pub fn is_speaking(&self) -> bool { self.live_kit .as_ref() @@ -1337,6 +1327,22 @@ impl Room { self.live_kit.as_ref().map(|live_kit| live_kit.deafened) } + pub fn can_use_microphone(&self) -> bool { + use proto::ChannelRole::*; + match self.local_participant.role { + Admin | Member | Talker => true, + Guest | Banned => false, + } + } + + pub fn can_share_projects(&self) -> bool { + use proto::ChannelRole::*; + match self.local_participant.role { + Admin | Member => true, + Guest | Banned | Talker => false, + } + } + #[track_caller] pub fn share_microphone(&mut self, cx: &mut ModelContext) -> Task> { if self.status.is_offline() { diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index f922e8c6d0..ccd690059f 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -17,34 +17,18 @@ anyhow.workspace = true client.workspace = true clock.workspace = true collections.workspace = true -db.workspace = true -feature_flags.workspace = true futures.workspace = true gpui.workspace = true -image = "0.23" language.workspace = true -lazy_static.workspace = true log.workspace = true -parking_lot.workspace = true -postage.workspace = true rand.workspace = true release_channel.workspace = true rpc.workspace = true -schemars.workspace = true -serde.workspace = true -serde_derive.workspace = true settings.workspace = true -smallvec.workspace = true -smol.workspace = true sum_tree.workspace = true -tempfile.workspace = true text.workspace = true -thiserror.workspace = true time.workspace = true -tiny_http = "0.8" -url.workspace = true util.workspace = true -uuid.workspace = true [dev-dependencies] collections = { workspace = true, features = ["test-support"] } diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index f38ae4078a..aee92d0f6c 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -11,7 +11,7 @@ pub use channel_chat::{ mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, MessageParams, }; -pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore}; +pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore}; #[cfg(test)] mod channel_store_tests; diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index dc63c55d15..c2115a7cab 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,6 +1,6 @@ -use crate::{Channel, ChannelId, ChannelStore}; +use crate::{Channel, ChannelStore}; use anyhow::Result; -use client::{Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE}; +use client::{ChannelId, Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE}; use collections::HashMap; use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task}; use language::proto::serialize_version; @@ -51,7 +51,7 @@ impl ChannelBuffer { ) -> Result> { let response = client .request(proto::JoinChannelBuffer { - channel_id: channel.id, + channel_id: channel.id.0, }) .await?; let buffer_id = BufferId::new(response.buffer_id)?; @@ -68,7 +68,7 @@ impl ChannelBuffer { })?; buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))??; - let subscription = client.subscribe_to_entity(channel.id)?; + let subscription = client.subscribe_to_entity(channel.id.0)?; anyhow::Ok(cx.new_model(|cx| { cx.subscribe(&buffer, Self::on_buffer_update).detach(); @@ -97,7 +97,7 @@ impl ChannelBuffer { } self.client .send(proto::LeaveChannelBuffer { - channel_id: self.channel_id, + channel_id: self.channel_id.0, }) .log_err(); } @@ -126,7 +126,7 @@ impl ChannelBuffer { for (_, old_collaborator) in &self.collaborators { if !new_collaborators.contains_key(&old_collaborator.peer_id) { self.buffer.update(cx, |buffer, cx| { - buffer.remove_peer(old_collaborator.replica_id as u16, cx) + buffer.remove_peer(old_collaborator.replica_id, cx) }); } } @@ -191,7 +191,7 @@ impl ChannelBuffer { let operation = language::proto::serialize_operation(operation); self.client .send(proto::UpdateChannelBuffer { - channel_id: self.channel_id, + channel_id: self.channel_id.0, operations: vec![operation], }) .log_err(); diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index e6ed013ade..bc26a8477b 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -1,9 +1,9 @@ -use crate::{Channel, ChannelId, ChannelStore}; +use crate::{Channel, ChannelStore}; use anyhow::{anyhow, Result}; use client::{ proto, user::{User, UserStore}, - Client, Subscription, TypedEnvelope, UserId, + ChannelId, Client, Subscription, TypedEnvelope, UserId, }; use collections::HashSet; use futures::lock::Mutex; @@ -104,10 +104,12 @@ impl ChannelChat { mut cx: AsyncAppContext, ) -> Result> { let channel_id = channel.id; - let subscription = client.subscribe_to_entity(channel_id).unwrap(); + let subscription = client.subscribe_to_entity(channel_id.0).unwrap(); let response = client - .request(proto::JoinChannelChat { channel_id }) + .request(proto::JoinChannelChat { + channel_id: channel_id.0, + }) .await?; let handle = cx.new_model(|cx| { @@ -143,7 +145,7 @@ impl ChannelChat { fn release(&mut self, _: &mut AppContext) { self.rpc .send(proto::LeaveChannelChat { - channel_id: self.channel_id, + channel_id: self.channel_id.0, }) .log_err(); } @@ -200,7 +202,7 @@ impl ChannelChat { Ok(cx.spawn(move |this, mut cx| async move { let outgoing_message_guard = outgoing_messages_lock.lock().await; let request = rpc.request(proto::SendChannelMessage { - channel_id, + channel_id: channel_id.0, body: message.text, nonce: Some(nonce.into()), mentions: mentions_to_proto(&message.mentions), @@ -220,7 +222,7 @@ impl ChannelChat { pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext) -> Task> { let response = self.rpc.request(proto::RemoveChannelMessage { - channel_id: self.channel_id, + channel_id: self.channel_id.0, message_id: id, }); cx.spawn(move |this, mut cx| async move { @@ -245,7 +247,7 @@ impl ChannelChat { async move { let response = rpc .request(proto::GetChannelMessages { - channel_id, + channel_id: channel_id.0, before_message_id, }) .await?; @@ -323,7 +325,7 @@ impl ChannelChat { { self.rpc .send(proto::AckChannelMessage { - channel_id: self.channel_id, + channel_id: self.channel_id.0, message_id: latest_message_id, }) .ok(); @@ -401,7 +403,11 @@ impl ChannelChat { let channel_id = self.channel_id; cx.spawn(move |this, mut cx| { async move { - let response = rpc.request(proto::JoinChannelChat { channel_id }).await?; + let response = rpc + .request(proto::JoinChannelChat { + channel_id: channel_id.0, + }) + .await?; Self::handle_loaded_messages( this.clone(), user_store.clone(), @@ -418,7 +424,7 @@ impl ChannelChat { for pending_message in pending_messages { let request = rpc.request(proto::SendChannelMessage { - channel_id, + channel_id: channel_id.0, body: pending_message.body, mentions: mentions_to_proto(&pending_message.mentions), nonce: Some(pending_message.nonce.into()), @@ -461,7 +467,7 @@ impl ChannelChat { if self.acknowledged_message_ids.insert(id) { self.rpc .send(proto::AckChannelMessage { - channel_id: self.channel_id, + channel_id: self.channel_id.0, message_id: id, }) .ok(); @@ -675,7 +681,7 @@ pub fn mentions_to_proto(mentions: &[(Range, UserId)]) -> Vec, user_store: Model, cx: &mut AppContext) { let channel_store = cx.new_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); cx.set_global(GlobalChannelStore(channel_store)); } -pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); - -pub type ChannelId = u64; - #[derive(Debug, Clone, Default)] struct NotesVersion { epoch: u64, version: clock::Global, } +#[derive(Debug, Clone)] +pub struct HostedProject { + id: HostedProjectId, + channel_id: ChannelId, + name: SharedString, + _visibility: proto::ChannelVisibility, +} + +impl From for HostedProject { + fn from(project: proto::HostedProject) -> Self { + Self { + id: HostedProjectId(project.id), + channel_id: ChannelId(project.channel_id), + _visibility: project.visibility(), + name: project.name.into(), + } + } +} + pub struct ChannelStore { pub channel_index: ChannelIndex, channel_invitations: Vec>, channel_participants: HashMap>>, channel_states: HashMap, + hosted_projects: HashMap, outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, @@ -58,7 +78,7 @@ pub struct Channel { pub id: ChannelId, pub name: SharedString, pub visibility: proto::ChannelVisibility, - pub parent_path: Vec, + pub parent_path: Vec, } #[derive(Default)] @@ -68,19 +88,21 @@ pub struct ChannelState { observed_chat_message: Option, observed_notes_versions: Option, role: Option, + projects: HashSet, } impl Channel { - pub fn link(&self) -> String { - RELEASE_CHANNEL.link_prefix().to_owned() - + "channel/" - + &Self::slug(&self.name) - + "-" - + &self.id.to_string() + pub fn link(&self, cx: &AppContext) -> String { + format!( + "{}/channel/{}-{}", + ClientSettings::get_global(cx).server_url, + Self::slug(&self.name), + self.id + ) } - pub fn notes_link(&self, heading: Option) -> String { - self.link() + pub fn notes_link(&self, heading: Option, cx: &AppContext) -> String { + self.link(cx) + "/notes" + &heading .map(|h| format!("#{}", Self::slug(&h))) @@ -92,10 +114,7 @@ impl Channel { } pub fn root_id(&self) -> ChannelId { - self.parent_path - .first() - .map(|id| *id as ChannelId) - .unwrap_or(self.id) + self.parent_path.first().copied().unwrap_or(self.id) } pub fn slug(str: &str) -> String { @@ -120,7 +139,8 @@ impl ChannelMembership { proto::ChannelRole::Admin => 0, proto::ChannelRole::Member => 1, proto::ChannelRole::Banned => 2, - proto::ChannelRole::Guest => 3, + proto::ChannelRole::Talker => 3, + proto::ChannelRole::Guest => 4, }, kind_order: match self.kind { proto::channel_member::Kind::Member => 0, @@ -198,6 +218,7 @@ impl ChannelStore { channel_invitations: Vec::default(), channel_index: ChannelIndex::default(), channel_participants: Default::default(), + hosted_projects: Default::default(), outgoing_invites: Default::default(), opened_buffers: Default::default(), opened_chats: Default::default(), @@ -284,6 +305,19 @@ impl ChannelStore { self.channel_index.by_id().get(&channel_id) } + pub fn projects_for_id(&self, channel_id: ChannelId) -> Vec<(SharedString, HostedProjectId)> { + let mut projects: Vec<(SharedString, HostedProjectId)> = self + .channel_states + .get(&channel_id) + .map(|state| state.projects.clone()) + .unwrap_or_default() + .into_iter() + .flat_map(|id| Some((self.hosted_projects.get(&id)?.name.clone(), id))) + .collect(); + projects.sort(); + projects + } + pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool { if let Some(buffer) = self.opened_buffers.get(&channel_id) { if let OpenedModelHandle::Open(buffer) = buffer { @@ -348,6 +382,21 @@ impl ChannelStore { .is_some_and(|state| state.has_new_messages()) } + pub fn last_acknowledge_message_id(&self, channel_id: ChannelId) -> Option { + self.channel_states.get(&channel_id).and_then(|state| { + if let Some(last_message_id) = state.latest_chat_message { + if state + .last_acknowledged_message_id() + .is_some_and(|id| id < last_message_id) + { + return state.last_acknowledged_message_id(); + } + } + + None + }) + } + pub fn acknowledge_message_id( &mut self, channel_id: ChannelId, @@ -543,16 +592,19 @@ impl ChannelStore { cx: &mut ModelContext, ) -> Task> { let client = self.client.clone(); - let name = name.trim_start_matches("#").to_owned(); + let name = name.trim_start_matches('#').to_owned(); cx.spawn(move |this, mut cx| async move { let response = client - .request(proto::CreateChannel { name, parent_id }) + .request(proto::CreateChannel { + name, + parent_id: parent_id.map(|cid| cid.0), + }) .await?; let channel = response .channel .ok_or_else(|| anyhow!("missing channel in response"))?; - let channel_id = channel.id; + let channel_id = ChannelId(channel.id); this.update(&mut cx, |this, cx| { let task = this.update_channels( @@ -584,7 +636,10 @@ impl ChannelStore { let client = self.client.clone(); cx.spawn(move |_, _| async move { let _ = client - .request(proto::MoveChannel { channel_id, to }) + .request(proto::MoveChannel { + channel_id: channel_id.0, + to: to.0, + }) .await?; Ok(()) @@ -601,7 +656,7 @@ impl ChannelStore { cx.spawn(move |_, _| async move { let _ = client .request(proto::SetChannelVisibility { - channel_id, + channel_id: channel_id.0, visibility: visibility.into(), }) .await?; @@ -626,7 +681,7 @@ impl ChannelStore { cx.spawn(move |this, mut cx| async move { let result = client .request(proto::InviteChannelMember { - channel_id, + channel_id: channel_id.0, user_id, role: role.into(), }) @@ -658,7 +713,7 @@ impl ChannelStore { cx.spawn(move |this, mut cx| async move { let result = client .request(proto::RemoveChannelMember { - channel_id, + channel_id: channel_id.0, user_id, }) .await; @@ -688,7 +743,7 @@ impl ChannelStore { cx.spawn(move |this, mut cx| async move { let result = client .request(proto::SetChannelMemberRole { - channel_id, + channel_id: channel_id.0, user_id, role: role.into(), }) @@ -714,7 +769,10 @@ impl ChannelStore { let name = new_name.to_string(); cx.spawn(move |this, mut cx| async move { let channel = client - .request(proto::RenameChannel { channel_id, name }) + .request(proto::RenameChannel { + channel_id: channel_id.0, + name, + }) .await? .channel .ok_or_else(|| anyhow!("missing channel in response"))?; @@ -747,7 +805,10 @@ impl ChannelStore { let client = self.client.clone(); cx.background_executor().spawn(async move { client - .request(proto::RespondToChannelInvite { channel_id, accept }) + .request(proto::RespondToChannelInvite { + channel_id: channel_id.0, + accept, + }) .await?; Ok(()) }) @@ -762,7 +823,9 @@ impl ChannelStore { let user_store = self.user_store.downgrade(); cx.spawn(move |_, mut cx| async move { let response = client - .request(proto::GetChannelMembers { channel_id }) + .request(proto::GetChannelMembers { + channel_id: channel_id.0, + }) .await?; let user_ids = response.members.iter().map(|m| m.user_id).collect(); @@ -776,12 +839,10 @@ impl ChannelStore { Ok(users .into_iter() .zip(response.members) - .filter_map(|(user, member)| { - Some(ChannelMembership { - user, - role: member.role(), - kind: member.kind(), - }) + .map(|(user, member)| ChannelMembership { + user, + role: member.role(), + kind: member.kind(), }) .collect()) }) @@ -790,7 +851,11 @@ impl ChannelStore { pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future> { let client = self.client.clone(); async move { - client.request(proto::DeleteChannel { channel_id }).await?; + client + .request(proto::DeleteChannel { + channel_id: channel_id.0, + }) + .await?; Ok(()) } } @@ -827,19 +892,23 @@ impl ChannelStore { for buffer_version in message.payload.observed_channel_buffer_version { let version = language::proto::deserialize_version(&buffer_version.version); this.acknowledge_notes_version( - buffer_version.channel_id, + ChannelId(buffer_version.channel_id), buffer_version.epoch, &version, cx, ); } for message_id in message.payload.observed_channel_message_id { - this.acknowledge_message_id(message_id.channel_id, message_id.message_id, cx); + this.acknowledge_message_id( + ChannelId(message_id.channel_id), + message_id.message_id, + cx, + ); } for membership in message.payload.channel_memberships { if let Some(role) = ChannelRole::from_i32(membership.role) { this.channel_states - .entry(membership.channel_id) + .entry(ChannelId(membership.channel_id)) .or_insert_with(|| ChannelState::default()) .set_role(role) } @@ -872,7 +941,7 @@ impl ChannelStore { let channel_buffer = buffer.read(cx); let buffer = channel_buffer.buffer().read(cx); buffer_versions.push(proto::ChannelBufferVersion { - channel_id: channel_buffer.channel_id, + channel_id: channel_buffer.channel_id.0, epoch: channel_buffer.epoch(), version: language::proto::serialize_version(&buffer.version()), }); @@ -903,7 +972,7 @@ impl ChannelStore { if let Some(remote_buffer) = response .buffers .iter_mut() - .find(|buffer| buffer.channel_id == channel_id) + .find(|buffer| buffer.channel_id == channel_id.0) { let channel_id = channel_buffer.channel_id; let remote_version = @@ -939,7 +1008,7 @@ impl ChannelStore { { client .send(proto::UpdateChannelBuffer { - channel_id, + channel_id: channel_id.0, operations: chunk, }) .ok(); @@ -994,12 +1063,12 @@ impl ChannelStore { ) -> Option>> { if !payload.remove_channel_invitations.is_empty() { self.channel_invitations - .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); + .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id.0)); } for channel in payload.channel_invitations { match self .channel_invitations - .binary_search_by_key(&channel.id, |c| c.id) + .binary_search_by_key(&channel.id, |c| c.id.0) { Ok(ix) => { Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into() @@ -1007,10 +1076,14 @@ impl ChannelStore { Err(ix) => self.channel_invitations.insert( ix, Arc::new(Channel { - id: channel.id, + id: ChannelId(channel.id), visibility: channel.visibility(), name: channel.name.into(), - parent_path: channel.parent_path, + parent_path: channel + .parent_path + .into_iter() + .map(|cid| ChannelId(cid)) + .collect(), }), ), } @@ -1019,20 +1092,27 @@ impl ChannelStore { let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty() || !payload.latest_channel_message_ids.is_empty() - || !payload.latest_channel_buffer_versions.is_empty(); + || !payload.latest_channel_buffer_versions.is_empty() + || !payload.hosted_projects.is_empty() + || !payload.deleted_hosted_projects.is_empty(); if channels_changed { if !payload.delete_channels.is_empty() { - self.channel_index.delete_channels(&payload.delete_channels); + let delete_channels: Vec = payload + .delete_channels + .into_iter() + .map(|cid| ChannelId(cid)) + .collect(); + self.channel_index.delete_channels(&delete_channels); self.channel_participants - .retain(|channel_id, _| !&payload.delete_channels.contains(channel_id)); + .retain(|channel_id, _| !delete_channels.contains(&channel_id)); - for channel_id in &payload.delete_channels { + for channel_id in &delete_channels { let channel_id = *channel_id; if payload .channels .iter() - .any(|channel| channel.id == channel_id) + .any(|channel| channel.id == channel_id.0) { continue; } @@ -1048,7 +1128,7 @@ impl ChannelStore { let mut index = self.channel_index.bulk_insert(); for channel in payload.channels { - let id = channel.id; + let id = ChannelId(channel.id); let channel_changed = index.insert(channel); if channel_changed { @@ -1063,17 +1143,45 @@ impl ChannelStore { for latest_buffer_version in payload.latest_channel_buffer_versions { let version = language::proto::deserialize_version(&latest_buffer_version.version); self.channel_states - .entry(latest_buffer_version.channel_id) + .entry(ChannelId(latest_buffer_version.channel_id)) .or_default() .update_latest_notes_version(latest_buffer_version.epoch, &version) } for latest_channel_message in payload.latest_channel_message_ids { self.channel_states - .entry(latest_channel_message.channel_id) + .entry(ChannelId(latest_channel_message.channel_id)) .or_default() .update_latest_message_id(latest_channel_message.message_id); } + + for hosted_project in payload.hosted_projects { + let hosted_project: HostedProject = hosted_project.into(); + if let Some(old_project) = self + .hosted_projects + .insert(hosted_project.id, hosted_project.clone()) + { + self.channel_states + .entry(old_project.channel_id) + .or_default() + .remove_hosted_project(old_project.id); + } + self.channel_states + .entry(hosted_project.channel_id) + .or_default() + .add_hosted_project(hosted_project.id); + } + + for hosted_project_id in payload.deleted_hosted_projects { + let hosted_project_id = HostedProjectId(hosted_project_id); + + if let Some(old_project) = self.hosted_projects.remove(&hosted_project_id) { + self.channel_states + .entry(old_project.channel_id) + .or_default() + .remove_hosted_project(old_project.id); + } + } } cx.notify(); @@ -1113,7 +1221,7 @@ impl ChannelStore { participants.sort_by_key(|u| u.id); this.channel_participants - .insert(entry.channel_id, participants); + .insert(ChannelId(entry.channel_id), participants); } cx.notify(); @@ -1152,6 +1260,10 @@ impl ChannelState { }) } + fn last_acknowledged_message_id(&self) -> Option { + self.observed_chat_message + } + fn acknowledge_message_id(&mut self, message_id: u64) { let observed = self.observed_chat_message.get_or_insert(message_id); *observed = (*observed).max(message_id); @@ -1187,4 +1299,12 @@ impl ChannelState { version: version.clone(), }); } + + fn add_hosted_project(&mut self, project_id: HostedProjectId) { + self.projects.insert(project_id); + } + + fn remove_hosted_project(&mut self, project_id: HostedProjectId) { + self.projects.remove(&project_id); + } } diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index e70b3e4c46..02a8cd333b 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -1,4 +1,5 @@ -use crate::{Channel, ChannelId}; +use crate::Channel; +use client::ChannelId; use collections::BTreeMap; use rpc::proto; use std::sync::Arc; @@ -50,27 +51,32 @@ pub struct ChannelPathsInsertGuard<'a> { impl<'a> ChannelPathsInsertGuard<'a> { pub fn insert(&mut self, channel_proto: proto::Channel) -> bool { let mut ret = false; - if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { + let parent_path = channel_proto + .parent_path + .iter() + .map(|cid| ChannelId(*cid)) + .collect(); + if let Some(existing_channel) = self.channels_by_id.get_mut(&ChannelId(channel_proto.id)) { let existing_channel = Arc::make_mut(existing_channel); ret = existing_channel.visibility != channel_proto.visibility() || existing_channel.name != channel_proto.name - || existing_channel.parent_path != channel_proto.parent_path; + || existing_channel.parent_path != parent_path; existing_channel.visibility = channel_proto.visibility(); existing_channel.name = channel_proto.name.into(); - existing_channel.parent_path = channel_proto.parent_path.into(); + existing_channel.parent_path = parent_path; } else { self.channels_by_id.insert( - channel_proto.id, + ChannelId(channel_proto.id), Arc::new(Channel { - id: channel_proto.id, + id: ChannelId(channel_proto.id), visibility: channel_proto.visibility(), name: channel_proto.name.into(), - parent_path: channel_proto.parent_path, + parent_path, }), ); - self.insert_root(channel_proto.id); + self.insert_root(ChannelId(channel_proto.id)); } ret } @@ -91,17 +97,20 @@ impl<'a> Drop for ChannelPathsInsertGuard<'a> { } } -fn channel_path_sorting_key<'a>( +fn channel_path_sorting_key( id: ChannelId, - channels_by_id: &'a BTreeMap>, -) -> impl Iterator { + channels_by_id: &BTreeMap>, +) -> impl Iterator { let (parent_path, name) = channels_by_id .get(&id) .map_or((&[] as &[_], None), |channel| { - (channel.parent_path.as_slice(), Some(channel.name.as_ref())) + ( + channel.parent_path.as_slice(), + Some((channel.name.as_ref(), channel.id)), + ) }); parent_path .iter() - .filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref())) + .filter_map(|id| Some((channels_by_id.get(id)?.name.as_ref(), *id))) .chain(name) } diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index c668b72022..fd76bcc301 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -2,6 +2,7 @@ use crate::channel_chat::ChannelChatEvent; use super::*; use client::{test::FakeServer, Client, UserStore}; +use clock::FakeSystemClock; use gpui::{AppContext, Context, Model, TestAppContext}; use rpc::proto::{self}; use settings::SettingsStore; @@ -337,8 +338,9 @@ fn init_test(cx: &mut AppContext) -> Model { release_channel::init("0.0.0", cx); client::init_settings(cx); + let clock = Arc::new(FakeSystemClock::default()); let http = FakeHttpClient::with_404_response(); - let client = Client::new(http.clone(), cx); + let client = Client::new(clock, http.clone(), cx); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); client::init(&client, cx); diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 6156520034..60285628e8 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -15,14 +15,13 @@ path = "src/main.rs" [dependencies] anyhow.workspace = true +# TODO: Use workspace version of `clap`. clap = { version = "3.1", features = ["derive"] } -dirs = "3.0" ipc-channel = "0.16" serde.workspace = true -serde_derive.workspace = true util.workspace = true [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = "0.9" +core-foundation.workspace = true core-services = "0.2" plist = "1.3" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 47a824a04b..e6ec1e559d 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -156,6 +156,39 @@ mod linux { } } +// todo("windows") +#[cfg(target_os = "windows")] +mod windows { + use std::path::Path; + + use cli::{CliRequest, CliResponse}; + use ipc_channel::ipc::{IpcReceiver, IpcSender}; + + use crate::{Bundle, InfoPlist}; + + impl Bundle { + pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result { + unimplemented!() + } + + pub fn plist(&self) -> &InfoPlist { + unimplemented!() + } + + pub fn path(&self) -> &Path { + unimplemented!() + } + + pub fn launch(&self) -> anyhow::Result<(IpcSender, IpcReceiver)> { + unimplemented!() + } + + pub fn zed_version_string(&self) -> String { + unimplemented!() + } + } +} + #[cfg(target_os = "macos")] mod mac_os { use anyhow::Context; diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 9770ee9b8e..4dc8c38ef4 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -10,12 +10,12 @@ path = "src/client.rs" doctest = false [features] -test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] +test-support = ["clock/test-support", "collections/test-support", "gpui/test-support", "rpc/test-support"] [dependencies] -chrono = { version = "0.4", features = ["serde"] } +chrono = { workspace = true, features = ["serde"] } +clock.workspace = true collections.workspace = true -db.workspace = true gpui.workspace = true util.workspace = true release_channel.workspace = true @@ -23,13 +23,11 @@ rpc.workspace = true text.workspace = true settings.workspace = true feature_flags.workspace = true -sum_tree.workspace = true anyhow.workspace = true async-recursion = "0.3" async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] } futures.workspace = true -image = "0.23" lazy_static.workspace = true log.workspace = true once_cell = "1.19.0" @@ -38,19 +36,19 @@ postage.workspace = true rand.workspace = true schemars.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true -sha2 = "0.10" +sha2.workspace = true smol.workspace = true sysinfo.workspace = true +telemetry_events.workspace = true tempfile.workspace = true thiserror.workspace = true time.workspace = true tiny_http = "0.8" -uuid.workspace = true url.workspace = true [dev-dependencies] +clock = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } rpc = { workspace = true, features = ["test-support"] } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index e454d3ddaf..754a47baa4 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -10,6 +10,7 @@ use async_tungstenite::tungstenite::{ error::Error as WebsocketError, http::{Request, StatusCode}, }; +use clock::SystemClock; use collections::HashMap; use futures::{ channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, @@ -26,7 +27,7 @@ use release_channel::{AppVersion, ReleaseChannel}; use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use serde_json; + use settings::{Settings, SettingsStore}; use std::{ any::TypeId, @@ -41,11 +42,11 @@ use std::{ use telemetry::Telemetry; use thiserror::Error; use url::Url; -use util::http::{HttpClient, ZedHttpClient}; +use util::http::{HttpClient, HttpClientWithUrl}; use util::{ResultExt, TryFutureExt}; pub use rpc::*; -pub use telemetry::Event; +pub use telemetry_events::Event; pub use user::*; lazy_static! { @@ -60,7 +61,7 @@ lazy_static! { pub static ref ZED_APP_PATH: Option = std::env::var("ZED_APP_PATH").ok().map(PathBuf::from); pub static ref ZED_ALWAYS_ACTIVE: bool = - std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| e.len() > 0); + std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty()); } pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100); @@ -152,7 +153,7 @@ impl Global for GlobalClient {} pub struct Client { id: AtomicU64, peer: Arc, - http: Arc, + http: Arc, telemetry: Arc, state: RwLock, @@ -421,11 +422,15 @@ impl settings::Settings for TelemetrySettings { } impl Client { - pub fn new(http: Arc, cx: &mut AppContext) -> Arc { - let client = Arc::new(Self { + pub fn new( + clock: Arc, + http: Arc, + cx: &mut AppContext, + ) -> Arc { + Arc::new(Self { id: AtomicU64::new(0), peer: Peer::new(0), - telemetry: Telemetry::new(http.clone(), cx), + telemetry: Telemetry::new(clock, http.clone(), cx), http, state: Default::default(), @@ -433,16 +438,14 @@ impl Client { authenticate: Default::default(), #[cfg(any(test, feature = "test-support"))] establish_connection: Default::default(), - }); - - client + }) } pub fn id(&self) -> u64 { self.id.load(std::sync::atomic::Ordering::SeqCst) } - pub fn http_client(&self) -> Arc { + pub fn http_client(&self) -> Arc { self.http.clone() } @@ -568,17 +571,18 @@ impl Client { let mut state = self.state.write(); if state.entities_by_type_and_remote_id.contains_key(&id) { return Err(anyhow!("already subscribed to entity")); - } else { - state - .entities_by_type_and_remote_id - .insert(id, WeakSubscriber::Pending(Default::default())); - Ok(PendingEntitySubscription { - client: self.clone(), - remote_id, - consumed: false, - _entity_type: PhantomData, - }) } + + state + .entities_by_type_and_remote_id + .insert(id, WeakSubscriber::Pending(Default::default())); + + Ok(PendingEntitySubscription { + client: self.clone(), + remote_id, + consumed: false, + _entity_type: PhantomData, + }) } #[track_caller] @@ -921,7 +925,7 @@ impl Client { move |cx| async move { match handle_io.await { Ok(()) => { - if this.status().borrow().clone() + if *this.status().borrow() == (Status::Connected { connection_id, peer_id, @@ -965,14 +969,14 @@ impl Client { } async fn get_rpc_url( - http: Arc, + http: Arc, release_channel: Option, ) -> Result { if let Some(url) = &*ZED_RPC_URL { return Url::parse(url).context("invalid rpc url"); } - let mut url = http.zed_url("/rpc"); + let mut url = http.build_url("/rpc"); if let Some(preview_param) = release_channel.and_then(|channel| channel.release_query_param()) { @@ -1105,7 +1109,7 @@ impl Client { // Open the Zed sign-in page in the user's browser, with query parameters that indicate // that the user is signing in from a Zed app running on the same device. - let mut url = http.zed_url(&format!( + let mut url = http.build_url(&format!( "/native_app_signin?native_app_port={}&native_app_public_key={}", port, public_key_string )); @@ -1140,7 +1144,7 @@ impl Client { } let post_auth_url = - http.zed_url("/native_app_signin_succeeded"); + http.build_url("/native_app_signin_succeeded"); req.respond( tiny_http::Response::empty(302).with_header( tiny_http::Header::from_bytes( @@ -1182,7 +1186,7 @@ impl Client { } async fn authenticate_as_admin( - http: Arc, + http: Arc, login: String, mut api_token: String, ) -> Result { @@ -1330,7 +1334,7 @@ impl Client { pending.push(message); return; } - Some(weak_subscriber @ _) => match weak_subscriber { + Some(weak_subscriber) => match weak_subscriber { WeakSubscriber::Entity { handle } => { subscriber = handle.upgrade(); } @@ -1433,21 +1437,29 @@ async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> { .await } -const WORKTREE_URL_PREFIX: &str = "zed://worktrees/"; +/// prefix for the zed:// url scheme +pub static ZED_URL_SCHEME: &str = "zed"; -pub fn encode_worktree_url(id: u64, access_token: &str) -> String { - format!("{}{}/{}", WORKTREE_URL_PREFIX, id, access_token) -} - -pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> { - let path = url.trim().strip_prefix(WORKTREE_URL_PREFIX)?; - let mut parts = path.split('/'); - let id = parts.next()?.parse::().ok()?; - let access_token = parts.next()?; - if access_token.is_empty() { - return None; +/// Parses the given link into a Zed link. +/// +/// Returns a [`Some`] containing the unprefixed link if the link is a Zed link. +/// Returns [`None`] otherwise. +pub fn parse_zed_link<'a>(link: &'a str, cx: &AppContext) -> Option<&'a str> { + let server_url = &ClientSettings::get_global(cx).server_url; + if let Some(stripped) = link + .strip_prefix(server_url) + .and_then(|result| result.strip_prefix('/')) + { + return Some(stripped); } - Some((id, access_token.to_string())) + if let Some(stripped) = link + .strip_prefix(ZED_URL_SCHEME) + .and_then(|result| result.strip_prefix("://")) + { + return Some(stripped); + } + + None } #[cfg(test)] @@ -1455,6 +1467,7 @@ mod tests { use super::*; use crate::test::FakeServer; + use clock::FakeSystemClock; use gpui::{BackgroundExecutor, Context, TestAppContext}; use parking_lot::Mutex; use settings::SettingsStore; @@ -1465,7 +1478,13 @@ mod tests { async fn test_reconnection(cx: &mut TestAppContext) { init_test(cx); let user_id = 5; - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client = cx.update(|cx| { + Client::new( + Arc::new(FakeSystemClock::default()), + FakeHttpClient::with_404_response(), + cx, + ) + }); let server = FakeServer::for_client(user_id, &client, cx).await; let mut status = client.status(); assert!(matches!( @@ -1500,7 +1519,13 @@ mod tests { async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); let user_id = 5; - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client = cx.update(|cx| { + Client::new( + Arc::new(FakeSystemClock::default()), + FakeHttpClient::with_404_response(), + cx, + ) + }); let mut status = client.status(); // Time out when client tries to connect. @@ -1573,7 +1598,13 @@ mod tests { init_test(cx); let auth_count = Arc::new(Mutex::new(0)); let dropped_auth_count = Arc::new(Mutex::new(0)); - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client = cx.update(|cx| { + Client::new( + Arc::new(FakeSystemClock::default()), + FakeHttpClient::with_404_response(), + cx, + ) + }); client.override_authenticate({ let auth_count = auth_count.clone(); let dropped_auth_count = dropped_auth_count.clone(); @@ -1606,22 +1637,17 @@ mod tests { assert_eq!(*dropped_auth_count.lock(), 1); } - #[test] - fn test_encode_and_decode_worktree_url() { - let url = encode_worktree_url(5, "deadbeef"); - assert_eq!(decode_worktree_url(&url), Some((5, "deadbeef".to_string()))); - assert_eq!( - decode_worktree_url(&format!("\n {}\t", url)), - Some((5, "deadbeef".to_string())) - ); - assert_eq!(decode_worktree_url("not://the-right-format"), None); - } - #[gpui::test] async fn test_subscribing_to_entity(cx: &mut TestAppContext) { init_test(cx); let user_id = 5; - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client = cx.update(|cx| { + Client::new( + Arc::new(FakeSystemClock::default()), + FakeHttpClient::with_404_response(), + cx, + ) + }); let server = FakeServer::for_client(user_id, &client, cx).await; let (done_tx1, mut done_rx1) = smol::channel::unbounded(); @@ -1675,7 +1701,13 @@ mod tests { async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) { init_test(cx); let user_id = 5; - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client = cx.update(|cx| { + Client::new( + Arc::new(FakeSystemClock::default()), + FakeHttpClient::with_404_response(), + cx, + ) + }); let server = FakeServer::for_client(user_id, &client, cx).await; let model = cx.new_model(|_| TestModel::default()); @@ -1704,7 +1736,13 @@ mod tests { async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) { init_test(cx); let user_id = 5; - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client = cx.update(|cx| { + Client::new( + Arc::new(FakeSystemClock::default()), + FakeHttpClient::with_404_response(), + cx, + ) + }); let server = FakeServer::for_client(user_id, &client, cx).await; let model = cx.new_model(|_| TestModel::default()); diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 9bdf038b26..475da1658c 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,13 +1,13 @@ mod event_coalescer; -use crate::TelemetrySettings; +use crate::{ChannelId, TelemetrySettings}; use chrono::{DateTime, Utc}; +use clock::SystemClock; use futures::Future; use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task}; use once_cell::sync::Lazy; use parking_lot::Mutex; use release_channel::ReleaseChannel; -use serde::Serialize; use settings::{Settings, SettingsStore}; use sha2::{Digest, Sha256}; use std::io::Write; @@ -15,8 +15,12 @@ use std::{env, mem, path::PathBuf, sync::Arc, time::Duration}; use sysinfo::{ CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt, }; +use telemetry_events::{ + ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CopilotEvent, CpuEvent, + EditEvent, EditorEvent, Event, EventRequestBody, EventWrapper, MemoryEvent, SettingEvent, +}; use tempfile::NamedTempFile; -use util::http::{self, HttpClient, Method, ZedHttpClient}; +use util::http::{self, HttpClient, HttpClientWithUrl, Method}; #[cfg(not(debug_assertions))] use util::ResultExt; use util::TryFutureExt; @@ -24,7 +28,8 @@ use util::TryFutureExt; use self::event_coalescer::EventCoalescer; pub struct Telemetry { - http_client: Arc, + clock: Arc, + http_client: Arc, executor: BackgroundExecutor, state: Arc>, } @@ -33,7 +38,7 @@ struct TelemetryState { settings: TelemetrySettings, metrics_id: Option>, // Per logged-in user installation_id: Option>, // Per app installation (different for dev, nightly, preview, and stable) - session_id: Option>, // Per app launch + session_id: Option, // Per app launch release_channel: Option<&'static str>, app_metadata: AppMetadata, architecture: &'static str, @@ -46,93 +51,6 @@ struct TelemetryState { max_queue_size: usize, } -#[derive(Serialize, Debug)] -struct EventRequestBody { - installation_id: Option>, - session_id: Option>, - is_staff: Option, - app_version: Option, - os_name: &'static str, - os_version: Option, - architecture: &'static str, - release_channel: Option<&'static str>, - events: Vec, -} - -#[derive(Serialize, Debug)] -struct EventWrapper { - signed_in: bool, - #[serde(flatten)] - event: Event, -} - -#[derive(Clone, Debug, PartialEq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum AssistantKind { - Panel, - Inline, -} - -#[derive(Clone, Debug, PartialEq, Serialize)] -#[serde(tag = "type")] -pub enum Event { - Editor { - operation: &'static str, - file_extension: Option, - vim_mode: bool, - copilot_enabled: bool, - copilot_enabled_for_language: bool, - milliseconds_since_first_event: i64, - }, - Copilot { - suggestion_id: Option, - suggestion_accepted: bool, - file_extension: Option, - milliseconds_since_first_event: i64, - }, - Call { - operation: &'static str, - room_id: Option, - channel_id: Option, - milliseconds_since_first_event: i64, - }, - Assistant { - conversation_id: Option, - kind: AssistantKind, - model: &'static str, - milliseconds_since_first_event: i64, - }, - Cpu { - usage_as_percentage: f32, - core_count: u32, - milliseconds_since_first_event: i64, - }, - Memory { - memory_in_bytes: u64, - virtual_memory_in_bytes: u64, - milliseconds_since_first_event: i64, - }, - App { - operation: String, - milliseconds_since_first_event: i64, - }, - Setting { - setting: &'static str, - value: String, - milliseconds_since_first_event: i64, - }, - Edit { - duration: i64, - environment: &'static str, - milliseconds_since_first_event: i64, - }, - Action { - source: &'static str, - action: String, - milliseconds_since_first_event: i64, - }, -} - #[cfg(debug_assertions)] const MAX_QUEUE_LEN: usize = 5; @@ -144,7 +62,6 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1); #[cfg(not(debug_assertions))] const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5); - static ZED_CLIENT_CHECKSUM_SEED: Lazy>> = Lazy::new(|| { option_env!("ZED_CLIENT_CHECKSUM_SEED") .map(|s| s.as_bytes().into()) @@ -156,14 +73,18 @@ static ZED_CLIENT_CHECKSUM_SEED: Lazy>> = Lazy::new(|| { }); impl Telemetry { - pub fn new(client: Arc, cx: &mut AppContext) -> Arc { + pub fn new( + clock: Arc, + client: Arc, + cx: &mut AppContext, + ) -> Arc { let release_channel = ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name()); TelemetrySettings::register(cx); let state = Arc::new(Mutex::new(TelemetryState { - settings: TelemetrySettings::get_global(cx).clone(), + settings: *TelemetrySettings::get_global(cx), app_metadata: cx.app_metadata(), architecture: env::consts::ARCH, release_channel, @@ -175,7 +96,7 @@ impl Telemetry { log_file: None, is_staff: None, first_event_date_time: None, - event_coalescer: EventCoalescer::new(), + event_coalescer: EventCoalescer::new(clock.clone()), max_queue_size: MAX_QUEUE_LEN, })); @@ -198,13 +119,14 @@ impl Telemetry { move |cx| { let mut state = state.lock(); - state.settings = TelemetrySettings::get_global(cx).clone(); + state.settings = *TelemetrySettings::get_global(cx); } }) .detach(); // TODO: Replace all hardware stuff with nested SystemSpecs json let this = Arc::new(Self { + clock, http_client: client, executor: cx.background_executor().clone(), state, @@ -246,7 +168,7 @@ impl Telemetry { ) { let mut state = self.state.lock(); state.installation_id = installation_id.map(|id| id.into()); - state.session_id = Some(session_id.into()); + state.session_id = Some(session_id); drop(state); let this = self.clone(); @@ -311,14 +233,13 @@ impl Telemetry { copilot_enabled: bool, copilot_enabled_for_language: bool, ) { - let event = Event::Editor { + let event = Event::Editor(EditorEvent { file_extension, vim_mode, - operation, + operation: operation.into(), copilot_enabled, copilot_enabled_for_language, - milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), - }; + }); self.report_event(event) } @@ -329,12 +250,11 @@ impl Telemetry { suggestion_accepted: bool, file_extension: Option, ) { - let event = Event::Copilot { + let event = Event::Copilot(CopilotEvent { suggestion_id, suggestion_accepted, file_extension, - milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), - }; + }); self.report_event(event) } @@ -343,14 +263,13 @@ impl Telemetry { self: &Arc, conversation_id: Option, kind: AssistantKind, - model: &'static str, + model: &str, ) { - let event = Event::Assistant { + let event = Event::Assistant(AssistantEvent { conversation_id, kind, - model, - milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), - }; + model: model.to_string(), + }); self.report_event(event) } @@ -359,24 +278,22 @@ impl Telemetry { self: &Arc, operation: &'static str, room_id: Option, - channel_id: Option, + channel_id: Option, ) { - let event = Event::Call { - operation, + let event = Event::Call(CallEvent { + operation: operation.to_string(), room_id, - channel_id, - milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), - }; + channel_id: channel_id.map(|cid| cid.0), + }); self.report_event(event) } pub fn report_cpu_event(self: &Arc, usage_as_percentage: f32, core_count: u32) { - let event = Event::Cpu { + let event = Event::Cpu(CpuEvent { usage_as_percentage, core_count, - milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), - }; + }); self.report_event(event) } @@ -386,28 +303,16 @@ impl Telemetry { memory_in_bytes: u64, virtual_memory_in_bytes: u64, ) { - let event = Event::Memory { + let event = Event::Memory(MemoryEvent { memory_in_bytes, virtual_memory_in_bytes, - milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), - }; + }); self.report_event(event) } - pub fn report_app_event(self: &Arc, operation: String) { - self.report_app_event_with_date_time(operation, Utc::now()); - } - - fn report_app_event_with_date_time( - self: &Arc, - operation: String, - date_time: DateTime, - ) -> Event { - let event = Event::App { - operation, - milliseconds_since_first_event: self.milliseconds_since_first_event(date_time), - }; + pub fn report_app_event(self: &Arc, operation: String) -> Event { + let event = Event::App(AppEvent { operation }); self.report_event(event.clone()); @@ -415,11 +320,10 @@ impl Telemetry { } pub fn report_setting_event(self: &Arc, setting: &'static str, value: String) { - let event = Event::Setting { - setting, + let event = Event::Setting(SettingEvent { + setting: setting.to_string(), value, - milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), - }; + }); self.report_event(event) } @@ -430,40 +334,24 @@ impl Telemetry { drop(state); if let Some((start, end, environment)) = period_data { - let event = Event::Edit { + let event = Event::Edit(EditEvent { duration: end.timestamp_millis() - start.timestamp_millis(), - environment, - milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), - }; + environment: environment.to_string(), + }); self.report_event(event); } } pub fn report_action_event(self: &Arc, source: &'static str, action: String) { - let event = Event::Action { - source, + let event = Event::Action(ActionEvent { + source: source.to_string(), action, - milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()), - }; + }); self.report_event(event) } - fn milliseconds_since_first_event(self: &Arc, date_time: DateTime) -> i64 { - let mut state = self.state.lock(); - - match state.first_event_date_time { - Some(first_event_date_time) => { - date_time.timestamp_millis() - first_event_date_time.timestamp_millis() - } - None => { - state.first_event_date_time = Some(date_time); - 0 - } - } - } - fn report_event(self: &Arc, event: Event) { let mut state = self.state.lock(); @@ -480,14 +368,28 @@ impl Telemetry { })); } - let signed_in = state.metrics_id.is_some(); - state.events_queue.push(EventWrapper { signed_in, event }); + let date_time = self.clock.utc_now(); - if state.installation_id.is_some() { - if state.events_queue.len() >= state.max_queue_size { - drop(state); - self.flush_events(); + let milliseconds_since_first_event = match state.first_event_date_time { + Some(first_event_date_time) => { + date_time.timestamp_millis() - first_event_date_time.timestamp_millis() } + None => { + state.first_event_date_time = Some(date_time); + 0 + } + }; + + let signed_in = state.metrics_id.is_some(); + state.events_queue.push(EventWrapper { + signed_in, + milliseconds_since_first_event, + event, + }); + + if state.installation_id.is_some() && state.events_queue.len() >= state.max_queue_size { + drop(state); + self.flush_events(); } } @@ -529,28 +431,29 @@ impl Telemetry { json_bytes.clear(); serde_json::to_writer(&mut json_bytes, event)?; file.write_all(&json_bytes)?; - file.write(b"\n")?; + file.write_all(b"\n")?; } } { let state = this.state.lock(); let request_body = EventRequestBody { - installation_id: state.installation_id.clone(), + installation_id: state.installation_id.as_deref().map(Into::into), session_id: state.session_id.clone(), - is_staff: state.is_staff.clone(), + is_staff: state.is_staff, app_version: state .app_metadata .app_version - .map(|version| version.to_string()), - os_name: state.app_metadata.os_name, + .unwrap_or_default() + .to_string(), + os_name: state.app_metadata.os_name.to_string(), os_version: state .app_metadata .os_version .map(|version| version.to_string()), - architecture: state.architecture, + architecture: state.architecture.to_string(), - release_channel: state.release_channel, + release_channel: state.release_channel.map(Into::into), events, }; json_bytes.clear(); @@ -569,7 +472,7 @@ impl Telemetry { let request = http::Request::builder() .method(Method::POST) - .uri(&this.http_client.zed_url("/api/events")) + .uri(this.http_client.build_zed_api_url("/telemetry/events")) .header("Content-Type", "text/plain") .header("x-zed-checksum", checksum) .body(json_bytes.into()); @@ -590,35 +493,37 @@ impl Telemetry { mod tests { use super::*; use chrono::TimeZone; + use clock::FakeSystemClock; use gpui::TestAppContext; use util::http::FakeHttpClient; #[gpui::test] fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) { init_test(cx); + let clock = Arc::new(FakeSystemClock::new( + Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(), + )); let http = FakeHttpClient::with_200_response(); let installation_id = Some("installation_id".to_string()); let session_id = "session_id".to_string(); cx.update(|cx| { - let telemetry = Telemetry::new(http, cx); + let telemetry = Telemetry::new(clock.clone(), http, cx); telemetry.state.lock().max_queue_size = 4; telemetry.start(installation_id, session_id, cx); assert!(is_empty_state(&telemetry)); - let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(); + let first_date_time = clock.utc_now(); let operation = "test".to_string(); - let event = - telemetry.report_app_event_with_date_time(operation.clone(), first_date_time); + let event = telemetry.report_app_event(operation.clone()); assert_eq!( event, - Event::App { + Event::App(AppEvent { operation: operation.clone(), - milliseconds_since_first_event: 0 - } + }) ); assert_eq!(telemetry.state.lock().events_queue.len(), 1); assert!(telemetry.state.lock().flush_events_task.is_some()); @@ -627,15 +532,14 @@ mod tests { Some(first_date_time) ); - let mut date_time = first_date_time + chrono::Duration::milliseconds(100); + clock.advance(chrono::Duration::milliseconds(100)); - let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time); + let event = telemetry.report_app_event(operation.clone()); assert_eq!( event, - Event::App { + Event::App(AppEvent { operation: operation.clone(), - milliseconds_since_first_event: 100 - } + }) ); assert_eq!(telemetry.state.lock().events_queue.len(), 2); assert!(telemetry.state.lock().flush_events_task.is_some()); @@ -644,15 +548,14 @@ mod tests { Some(first_date_time) ); - date_time += chrono::Duration::milliseconds(100); + clock.advance(chrono::Duration::milliseconds(100)); - let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time); + let event = telemetry.report_app_event(operation.clone()); assert_eq!( event, - Event::App { + Event::App(AppEvent { operation: operation.clone(), - milliseconds_since_first_event: 200 - } + }) ); assert_eq!(telemetry.state.lock().events_queue.len(), 3); assert!(telemetry.state.lock().flush_events_task.is_some()); @@ -661,16 +564,15 @@ mod tests { Some(first_date_time) ); - date_time += chrono::Duration::milliseconds(100); + clock.advance(chrono::Duration::milliseconds(100)); // Adding a 4th event should cause a flush - let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time); + let event = telemetry.report_app_event(operation.clone()); assert_eq!( event, - Event::App { + Event::App(AppEvent { operation: operation.clone(), - milliseconds_since_first_event: 300 - } + }) ); assert!(is_empty_state(&telemetry)); @@ -680,28 +582,29 @@ mod tests { #[gpui::test] async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); + let clock = Arc::new(FakeSystemClock::new( + Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(), + )); let http = FakeHttpClient::with_200_response(); let installation_id = Some("installation_id".to_string()); let session_id = "session_id".to_string(); cx.update(|cx| { - let telemetry = Telemetry::new(http, cx); + let telemetry = Telemetry::new(clock.clone(), http, cx); telemetry.state.lock().max_queue_size = 4; telemetry.start(installation_id, session_id, cx); assert!(is_empty_state(&telemetry)); - let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(); + let first_date_time = clock.utc_now(); let operation = "test".to_string(); - let event = - telemetry.report_app_event_with_date_time(operation.clone(), first_date_time); + let event = telemetry.report_app_event(operation.clone()); assert_eq!( event, - Event::App { + Event::App(AppEvent { operation: operation.clone(), - milliseconds_since_first_event: 0 - } + }) ); assert_eq!(telemetry.state.lock().events_queue.len(), 1); assert!(telemetry.state.lock().flush_events_task.is_some()); diff --git a/crates/client/src/telemetry/event_coalescer.rs b/crates/client/src/telemetry/event_coalescer.rs index f0efeb38e6..33bcf492f6 100644 --- a/crates/client/src/telemetry/event_coalescer.rs +++ b/crates/client/src/telemetry/event_coalescer.rs @@ -1,6 +1,9 @@ -use chrono::{DateTime, Duration, Utc}; +use std::sync::Arc; use std::time; +use chrono::{DateTime, Duration, Utc}; +use clock::SystemClock; + const COALESCE_TIMEOUT: time::Duration = time::Duration::from_secs(20); const SIMULATED_DURATION_FOR_SINGLE_EVENT: time::Duration = time::Duration::from_millis(1); @@ -12,30 +15,20 @@ struct PeriodData { } pub struct EventCoalescer { + clock: Arc, state: Option, } impl EventCoalescer { - pub fn new() -> Self { - Self { state: None } + pub fn new(clock: Arc) -> Self { + Self { clock, state: None } } pub fn log_event( &mut self, environment: &'static str, ) -> Option<(DateTime, DateTime, &'static str)> { - self.log_event_with_time(Utc::now(), environment) - } - - // pub fn close_current_period(&mut self) -> Option<(DateTime, DateTime)> { - // self.environment.map(|env| self.log_event(env)).flatten() - // } - - fn log_event_with_time( - &mut self, - log_time: DateTime, - environment: &'static str, - ) -> Option<(DateTime, DateTime, &'static str)> { + let log_time = self.clock.utc_now(); let coalesce_timeout = Duration::from_std(COALESCE_TIMEOUT).unwrap(); let Some(state) = &mut self.state else { @@ -78,18 +71,22 @@ impl EventCoalescer { #[cfg(test)] mod tests { use chrono::TimeZone; + use clock::FakeSystemClock; use super::*; #[test] fn test_same_context_exceeding_timeout() { + let clock = Arc::new(FakeSystemClock::new( + Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), + )); let environment_1 = "environment_1"; - let mut event_coalescer = EventCoalescer::new(); + let mut event_coalescer = EventCoalescer::new(clock.clone()); assert_eq!(event_coalescer.state, None); - let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(); - let period_data = event_coalescer.log_event_with_time(period_start, environment_1); + let period_start = clock.utc_now(); + let period_data = event_coalescer.log_event(environment_1); assert_eq!(period_data, None); assert_eq!( @@ -102,12 +99,12 @@ mod tests { ); let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); - let mut period_end = period_start; // Ensure that many calls within the timeout don't start a new period for _ in 0..100 { - period_end += within_timeout_adjustment; - let period_data = event_coalescer.log_event_with_time(period_end, environment_1); + clock.advance(within_timeout_adjustment); + let period_data = event_coalescer.log_event(environment_1); + let period_end = clock.utc_now(); assert_eq!(period_data, None); assert_eq!( @@ -120,10 +117,12 @@ mod tests { ); } + let period_end = clock.utc_now(); let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap(); // Logging an event exceeding the timeout should start a new period - let new_period_start = period_end + exceed_timeout_adjustment; - let period_data = event_coalescer.log_event_with_time(new_period_start, environment_1); + clock.advance(exceed_timeout_adjustment); + let new_period_start = clock.utc_now(); + let period_data = event_coalescer.log_event(environment_1); assert_eq!(period_data, Some((period_start, period_end, environment_1))); assert_eq!( @@ -138,13 +137,16 @@ mod tests { #[test] fn test_different_environment_under_timeout() { + let clock = Arc::new(FakeSystemClock::new( + Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), + )); let environment_1 = "environment_1"; - let mut event_coalescer = EventCoalescer::new(); + let mut event_coalescer = EventCoalescer::new(clock.clone()); assert_eq!(event_coalescer.state, None); - let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(); - let period_data = event_coalescer.log_event_with_time(period_start, environment_1); + let period_start = clock.utc_now(); + let period_data = event_coalescer.log_event(environment_1); assert_eq!(period_data, None); assert_eq!( @@ -157,8 +159,9 @@ mod tests { ); let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); - let period_end = period_start + within_timeout_adjustment; - let period_data = event_coalescer.log_event_with_time(period_end, environment_1); + clock.advance(within_timeout_adjustment); + let period_end = clock.utc_now(); + let period_data = event_coalescer.log_event(environment_1); assert_eq!(period_data, None); assert_eq!( @@ -170,10 +173,12 @@ mod tests { }) ); + clock.advance(within_timeout_adjustment); + // Logging an event within the timeout but with a different environment should start a new period - let period_end = period_end + within_timeout_adjustment; + let period_end = clock.utc_now(); let environment_2 = "environment_2"; - let period_data = event_coalescer.log_event_with_time(period_end, environment_2); + let period_data = event_coalescer.log_event(environment_2); assert_eq!(period_data, Some((period_start, period_end, environment_1))); assert_eq!( @@ -188,13 +193,16 @@ mod tests { #[test] fn test_switching_environment_while_within_timeout() { + let clock = Arc::new(FakeSystemClock::new( + Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), + )); let environment_1 = "environment_1"; - let mut event_coalescer = EventCoalescer::new(); + let mut event_coalescer = EventCoalescer::new(clock.clone()); assert_eq!(event_coalescer.state, None); - let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(); - let period_data = event_coalescer.log_event_with_time(period_start, environment_1); + let period_start = clock.utc_now(); + let period_data = event_coalescer.log_event(environment_1); assert_eq!(period_data, None); assert_eq!( @@ -207,9 +215,10 @@ mod tests { ); let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); - let period_end = period_start + within_timeout_adjustment; + clock.advance(within_timeout_adjustment); + let period_end = clock.utc_now(); let environment_2 = "environment_2"; - let period_data = event_coalescer.log_event_with_time(period_end, environment_2); + let period_data = event_coalescer.log_event(environment_2); assert_eq!(period_data, Some((period_start, period_end, environment_1))); assert_eq!( @@ -221,22 +230,26 @@ mod tests { }) ); } - // // 0 20 40 60 - // // |-------------------|-------------------|-------------------|------------------- - // // |--------|----------env change - // // |------------------- - // // |period_start |period_end - // // |new_period_start + + // 0 20 40 60 + // |-------------------|-------------------|-------------------|------------------- + // |--------|----------env change + // |------------------- + // |period_start |period_end + // |new_period_start #[test] fn test_switching_environment_while_exceeding_timeout() { + let clock = Arc::new(FakeSystemClock::new( + Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), + )); let environment_1 = "environment_1"; - let mut event_coalescer = EventCoalescer::new(); + let mut event_coalescer = EventCoalescer::new(clock.clone()); assert_eq!(event_coalescer.state, None); - let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(); - let period_data = event_coalescer.log_event_with_time(period_start, environment_1); + let period_start = clock.utc_now(); + let period_data = event_coalescer.log_event(environment_1); assert_eq!(period_data, None); assert_eq!( @@ -249,9 +262,10 @@ mod tests { ); let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap(); - let period_end = period_start + exceed_timeout_adjustment; + clock.advance(exceed_timeout_adjustment); + let period_end = clock.utc_now(); let environment_2 = "environment_2"; - let period_data = event_coalescer.log_event_with_time(period_end, environment_2); + let period_data = event_coalescer.log_event(environment_2); assert_eq!( period_data, @@ -270,6 +284,7 @@ mod tests { }) ); } + // 0 20 40 60 // |-------------------|-------------------|-------------------|------------------- // |--------|----------------------------------------env change diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 0263d9a950..0c10e50ac5 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -15,6 +15,18 @@ use util::TryFutureExt as _; pub type UserId = u64; +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub struct ChannelId(pub u64); + +impl std::fmt::Display for ChannelId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub struct HostedProjectId(pub u64); + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ParticipantIndex(pub u32); @@ -644,7 +656,7 @@ impl UserStore { let users = response .users .into_iter() - .map(|user| User::new(user)) + .map(User::new) .collect::>(); this.update(&mut cx, |this, _| { diff --git a/crates/clock/Cargo.toml b/crates/clock/Cargo.toml index c81a6fda67..adef338cc7 100644 --- a/crates/clock/Cargo.toml +++ b/crates/clock/Cargo.toml @@ -9,5 +9,10 @@ license = "GPL-3.0-or-later" path = "src/clock.rs" doctest = false +[features] +test-support = ["dep:parking_lot"] + [dependencies] +chrono.workspace = true +parking_lot = { workspace = true, optional = true } smallvec.workspace = true diff --git a/crates/clock/src/clock.rs b/crates/clock/src/clock.rs index a781cf2d44..7a1377981a 100644 --- a/crates/clock/src/clock.rs +++ b/crates/clock/src/clock.rs @@ -1,13 +1,17 @@ +mod system_clock; + use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, fmt, iter, }; -/// A unique identifier for each distributed node +pub use system_clock::*; + +/// A unique identifier for each distributed node. pub type ReplicaId = u16; -/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp), +/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp). pub type Seq = u32; /// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp), @@ -18,7 +22,7 @@ pub struct Lamport { pub value: Seq, } -/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock) +/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock). #[derive(Clone, Default, Hash, Eq, PartialEq)] pub struct Global(SmallVec<[u32; 8]>); diff --git a/crates/clock/src/system_clock.rs b/crates/clock/src/system_clock.rs new file mode 100644 index 0000000000..a462ffc35b --- /dev/null +++ b/crates/clock/src/system_clock.rs @@ -0,0 +1,59 @@ +use chrono::{DateTime, Utc}; + +pub trait SystemClock: Send + Sync { + /// Returns the current date and time in UTC. + fn utc_now(&self) -> DateTime; +} + +pub struct RealSystemClock; + +impl SystemClock for RealSystemClock { + fn utc_now(&self) -> DateTime { + Utc::now() + } +} + +#[cfg(any(test, feature = "test-support"))] +pub struct FakeSystemClockState { + now: DateTime, +} + +#[cfg(any(test, feature = "test-support"))] +pub struct FakeSystemClock { + // Use an unfair lock to ensure tests are deterministic. + state: parking_lot::Mutex, +} + +#[cfg(any(test, feature = "test-support"))] +impl Default for FakeSystemClock { + fn default() -> Self { + Self::new(Utc::now()) + } +} + +#[cfg(any(test, feature = "test-support"))] +impl FakeSystemClock { + pub fn new(now: DateTime) -> Self { + let state = FakeSystemClockState { now }; + + Self { + state: parking_lot::Mutex::new(state), + } + } + + pub fn set_now(&self, now: DateTime) { + self.state.lock().now = now; + } + + /// Advances the [`FakeSystemClock`] by the specified [`Duration`](chrono::Duration). + pub fn advance(&self, duration: chrono::Duration) { + self.state.lock().now += duration; + } +} + +#[cfg(any(test, feature = "test-support"))] +impl SystemClock for FakeSystemClock { + fn utc_now(&self) -> DateTime { + self.state.lock().now + } +} diff --git a/crates/collab/.env.toml b/crates/collab/.env.toml index 7340a71cd9..b244e83eb2 100644 --- a/crates/collab/.env.toml +++ b/crates/collab/.env.toml @@ -12,6 +12,14 @@ BLOB_STORE_SECRET_KEY = "the-blob-store-secret-key" BLOB_STORE_BUCKET = "the-extensions-bucket" BLOB_STORE_URL = "http://127.0.0.1:9000" BLOB_STORE_REGION = "the-region" +ZED_CLIENT_CHECKSUM_SEED = "development-checksum-seed" + +# CLICKHOUSE_URL = "" +# CLICKHOUSE_USER = "default" +# CLICKHOUSE_PASSWORD = "" +# CLICKHOUSE_DATABASE = "default" + +# SLACK_PANICS_WEBHOOK = "" # RUST_LOG=info # LOG_JSON=true diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 8beb83518b..7cd499513a 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -12,7 +12,6 @@ name = "collab" [[bin]] name = "seed" -required-features = ["seed-support"] [dependencies] anyhow.workspace = true @@ -21,17 +20,15 @@ aws-config = { version = "1.1.5" } aws-sdk-s3 = { version = "1.15.0" } axum = { version = "0.5", features = ["json", "headers", "ws"] } axum-extra = { version = "0.3", features = ["erased-json"] } -base64 = "0.13" chrono.workspace = true -clap = { version = "3.1", features = ["derive"], optional = true } clock.workspace = true +clickhouse.workspace = true collections.workspace = true dashmap = "5.4" envy = "0.4.2" futures.workspace = true +hex.workspace = true hyper = "0.14" -lazy_static.workspace = true -lipsum = { version = "0.8", optional = true } live_kit_server.workspace = true log.workspace = true nanoid = "0.4" @@ -39,7 +36,7 @@ parking_lot.workspace = true prometheus = "0.13" prost.workspace = true rand.workspace = true -reqwest = { version = "0.11", features = ["json"], optional = true } +reqwest = { version = "0.11", features = ["json"] } rpc.workspace = true scrypt = "0.7" sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] } @@ -47,16 +44,16 @@ semver.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true -sha-1 = "0.9" -smallvec.workspace = true +sha2.workspace = true sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] } +rustc-demangle.workspace = true +telemetry_events.workspace = true text.workspace = true time.workspace = true tokio = { version = "1", features = ["full"] } -tokio-tungstenite = "0.17" toml.workspace = true -tonic = "0.6" tower = "0.4" +tower-http = { workspace = true, features = ["trace"] } tracing = "0.1.34" tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } @@ -98,6 +95,3 @@ theme.workspace = true unindent.workspace = true util.workspace = true workspace = { workspace = true, features = ["test-support"] } - -[features] -seed-support = ["clap", "lipsum", "reqwest"] diff --git a/crates/collab/k8s/collab.template.yml b/crates/collab/k8s/collab.template.yml index 9ff7cee9e1..4915c6c97c 100644 --- a/crates/collab/k8s/collab.template.yml +++ b/crates/collab/k8s/collab.template.yml @@ -9,7 +9,7 @@ kind: Service apiVersion: v1 metadata: namespace: ${ZED_KUBE_NAMESPACE} - name: collab + name: ${ZED_SERVICE_NAME} annotations: service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443" service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID} @@ -17,7 +17,7 @@ metadata: spec: type: LoadBalancer selector: - app: collab + app: ${ZED_SERVICE_NAME} ports: - name: web protocol: TCP @@ -29,17 +29,17 @@ apiVersion: apps/v1 kind: Deployment metadata: namespace: ${ZED_KUBE_NAMESPACE} - name: collab + name: ${ZED_SERVICE_NAME} spec: replicas: 1 selector: matchLabels: - app: collab + app: ${ZED_SERVICE_NAME} template: metadata: labels: - app: collab + app: ${ZED_SERVICE_NAME} annotations: ad.datadoghq.com/collab.check_names: | ["openmetrics"] @@ -55,10 +55,11 @@ spec: ] spec: containers: - - name: collab + - name: ${ZED_SERVICE_NAME} image: "${ZED_IMAGE_ID}" args: - serve + - ${ZED_SERVICE_NAME} ports: - containerPort: 8080 protocol: TCP @@ -90,6 +91,11 @@ spec: secretKeyRef: name: api key: token + - name: ZED_CLIENT_CHECKSUM_SEED + valueFrom: + secretKeyRef: + name: zed-client + key: checksum-seed - name: LIVE_KIT_SERVER valueFrom: secretKeyRef: @@ -130,6 +136,31 @@ spec: secretKeyRef: name: blob-store key: bucket + - name: CLICKHOUSE_URL + valueFrom: + secretKeyRef: + name: clickhouse + key: url + - name: CLICKHOUSE_USER + valueFrom: + secretKeyRef: + name: clickhouse + key: user + - name: CLICKHOUSE_PASSWORD + valueFrom: + secretKeyRef: + name: clickhouse + key: password + - name: CLICKHOUSE_DATABASE + valueFrom: + secretKeyRef: + name: clickhouse + key: database + - name: SLACK_PANICS_WEBHOOK + valueFrom: + secretKeyRef: + name: slack + key: panics_webhook - name: INVITE_LINK_PREFIX value: ${INVITE_LINK_PREFIX} - name: RUST_BACKTRACE diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index fef10f987e..b7b427a6b7 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -46,10 +46,11 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); CREATE TABLE "projects" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL, - "host_user_id" INTEGER REFERENCES users (id) NOT NULL, + "host_user_id" INTEGER REFERENCES users (id), "host_connection_id" INTEGER, "host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE, - "unregistered" BOOLEAN NOT NULL DEFAULT FALSE + "unregistered" BOOLEAN NOT NULL DEFAULT FALSE, + "hosted_project_id" INTEGER REFERENCES hosted_projects (id) ); CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id"); CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id"); @@ -375,3 +376,13 @@ CREATE TABLE extension_versions ( CREATE UNIQUE INDEX "index_extensions_external_id" ON "extensions" ("external_id"); CREATE INDEX "index_extensions_total_download_count" ON "extensions" ("total_download_count"); + +CREATE TABLE hosted_projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channel_id INTEGER NOT NULL REFERENCES channels(id), + name TEXT NOT NULL, + visibility TEXT NOT NULL, + deleted_at TIMESTAMP NULL +); +CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id); +CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL); diff --git a/crates/collab/migrations/20240226163408_hosted_projects.sql b/crates/collab/migrations/20240226163408_hosted_projects.sql new file mode 100644 index 0000000000..c6ade7161c --- /dev/null +++ b/crates/collab/migrations/20240226163408_hosted_projects.sql @@ -0,0 +1,11 @@ +-- Add migration script here + +CREATE TABLE hosted_projects ( + id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + channel_id INT NOT NULL REFERENCES channels(id), + name TEXT NOT NULL, + visibility TEXT NOT NULL, + deleted_at TIMESTAMP NULL +); +CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id); +CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL); diff --git a/crates/collab/migrations/20240226164505_unique_channel_names.sql b/crates/collab/migrations/20240226164505_unique_channel_names.sql new file mode 100644 index 0000000000..c9d9f0a1cb --- /dev/null +++ b/crates/collab/migrations/20240226164505_unique_channel_names.sql @@ -0,0 +1,3 @@ +-- Add migration script here + +CREATE UNIQUE INDEX uix_channels_parent_path_name ON channels(parent_path, name) WHERE (parent_path IS NOT NULL AND parent_path != ''); diff --git a/crates/collab/migrations/20240227215556_hosted_projects_in_projects.sql b/crates/collab/migrations/20240227215556_hosted_projects_in_projects.sql new file mode 100644 index 0000000000..69905d12f6 --- /dev/null +++ b/crates/collab/migrations/20240227215556_hosted_projects_in_projects.sql @@ -0,0 +1,3 @@ +-- Add migration script here +ALTER TABLE projects ALTER COLUMN host_user_id DROP NOT NULL; +ALTER TABLE projects ADD COLUMN hosted_project_id INTEGER REFERENCES hosted_projects(id) UNIQUE NULL; diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 44d6fc3eb5..d227211fc1 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -1,4 +1,7 @@ -mod extensions; +pub mod events; +pub mod extensions; +pub mod ips_file; +pub mod slack; use crate::{ auth, @@ -20,19 +23,16 @@ use chrono::SecondsFormat; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tower::ServiceBuilder; -use tracing::instrument; pub use extensions::fetch_extensions_from_blob_store_periodically; -pub fn routes(rpc_server: Arc, state: Arc) -> Router { +pub fn routes(rpc_server: Option>, state: Arc) -> Router { Router::new() .route("/user", get(get_authenticated_user)) .route("/users/:id/access_tokens", post(create_access_token)) - .route("/panic", post(trace_panic)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) .route("/contributors", get(get_contributors).post(add_contributor)) .route("/contributor", get(check_is_contributor)) - .merge(extensions::router()) .layer( ServiceBuilder::new() .layer(Extension(state)) @@ -120,23 +120,13 @@ struct CreateUserResponse { metrics_id: String, } -#[derive(Debug, Deserialize)] -struct Panic { - version: String, - release_channel: String, - backtrace_hash: String, - text: String, -} - -#[instrument(skip(panic))] -async fn trace_panic(panic: Json) -> Result<()> { - tracing::error!(version = %panic.version, release_channel = %panic.release_channel, backtrace_hash = %panic.backtrace_hash, text = %panic.text, "panic report"); - Ok(()) -} - async fn get_rpc_server_snapshot( - Extension(rpc_server): Extension>, + Extension(rpc_server): Extension>>, ) -> Result { + let Some(rpc_server) = rpc_server else { + return Err(Error::Internal(anyhow!("rpc server is not available"))); + }; + Ok(ErasedJson::pretty(rpc_server.snapshot().await)) } @@ -189,14 +179,13 @@ async fn add_contributor( Json(params): Json, Extension(app): Extension>, ) -> Result<()> { - Ok(app - .db + app.db .add_contributor( ¶ms.github_login, params.github_user_id, params.github_email.as_deref(), ) - .await?) + .await } #[derive(Deserialize)] diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs new file mode 100644 index 0000000000..40700a859b --- /dev/null +++ b/crates/collab/src/api/events.rs @@ -0,0 +1,961 @@ +use std::sync::{Arc, OnceLock}; + +use anyhow::{anyhow, Context}; +use aws_sdk_s3::primitives::ByteStream; +use axum::{ + body::Bytes, headers::Header, http::HeaderName, routing::post, Extension, Router, TypedHeader, +}; +use hyper::{HeaderMap, StatusCode}; +use serde::{Serialize, Serializer}; +use sha2::{Digest, Sha256}; +use telemetry_events::{ + ActionEvent, AppEvent, AssistantEvent, CallEvent, CopilotEvent, CpuEvent, EditEvent, + EditorEvent, Event, EventRequestBody, EventWrapper, MemoryEvent, SettingEvent, +}; +use util::SemanticVersion; + +use crate::{api::slack, AppState, Error, Result}; + +use super::ips_file::IpsFile; + +pub fn router() -> Router { + Router::new() + .route("/telemetry/events", post(post_events)) + .route("/telemetry/crashes", post(post_crash)) +} + +pub struct ZedChecksumHeader(Vec); + +impl Header for ZedChecksumHeader { + fn name() -> &'static HeaderName { + static ZED_CHECKSUM_HEADER: OnceLock = OnceLock::new(); + ZED_CHECKSUM_HEADER.get_or_init(|| HeaderName::from_static("x-zed-checksum")) + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + let checksum = values + .next() + .ok_or_else(axum::headers::Error::invalid)? + .to_str() + .map_err(|_| axum::headers::Error::invalid())?; + + let bytes = hex::decode(checksum).map_err(|_| axum::headers::Error::invalid())?; + Ok(Self(bytes)) + } + + fn encode>(&self, _values: &mut E) { + unimplemented!() + } +} + +pub struct CloudflareIpCountryHeader(String); + +impl Header for CloudflareIpCountryHeader { + fn name() -> &'static HeaderName { + static CLOUDFLARE_IP_COUNTRY_HEADER: OnceLock = OnceLock::new(); + CLOUDFLARE_IP_COUNTRY_HEADER.get_or_init(|| HeaderName::from_static("cf-ipcountry")) + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + let country_code = values + .next() + .ok_or_else(axum::headers::Error::invalid)? + .to_str() + .map_err(|_| axum::headers::Error::invalid())?; + + Ok(Self(country_code.to_string())) + } + + fn encode>(&self, _values: &mut E) { + unimplemented!() + } +} + +pub async fn post_crash( + Extension(app): Extension>, + body: Bytes, + headers: HeaderMap, +) -> Result<()> { + static CRASH_REPORTS_BUCKET: &str = "zed-crash-reports"; + + let report = IpsFile::parse(&body)?; + let version_threshold = SemanticVersion::new(0, 123, 0); + + let bundle_id = &report.header.bundle_id; + let app_version = &report.app_version(); + + if bundle_id == "dev.zed.Zed-Dev" { + log::error!("Crash uploads from {} are ignored.", bundle_id); + return Ok(()); + } + + if app_version.is_none() || app_version.unwrap() < version_threshold { + log::error!( + "Crash uploads from {} are ignored.", + report.header.app_version + ); + return Ok(()); + } + let app_version = app_version.unwrap(); + + if let Some(blob_store_client) = app.blob_store_client.as_ref() { + let response = blob_store_client + .head_object() + .bucket(CRASH_REPORTS_BUCKET) + .key(report.header.incident_id.clone() + ".ips") + .send() + .await; + + if response.is_ok() { + log::info!("We've already uploaded this crash"); + return Ok(()); + } + + blob_store_client + .put_object() + .bucket(CRASH_REPORTS_BUCKET) + .key(report.header.incident_id.clone() + ".ips") + .acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead) + .body(ByteStream::from(body.to_vec())) + .send() + .await + .map_err(|e| log::error!("Failed to upload crash: {}", e)) + .ok(); + } + + let recent_panic_on: Option = headers + .get("x-zed-panicked-on") + .and_then(|h| h.to_str().ok()) + .and_then(|s| s.parse().ok()); + let mut recent_panic = None; + + if let Some(recent_panic_on) = recent_panic_on { + let crashed_at = match report.timestamp() { + Ok(t) => Some(t), + Err(e) => { + log::error!("Can't parse {}: {}", report.header.timestamp, e); + None + } + }; + if crashed_at.is_some_and(|t| (t.timestamp_millis() - recent_panic_on).abs() <= 30000) { + recent_panic = headers.get("x-zed-panic").and_then(|h| h.to_str().ok()); + } + } + + let description = report.description(recent_panic); + let summary = report.backtrace_summary(); + + tracing::error!( + service = "client", + version = %report.header.app_version, + os_version = %report.header.os_version, + bundle_id = %report.header.bundle_id, + incident_id = %report.header.incident_id, + description = %description, + backtrace = %summary, + "crash report"); + + if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() { + let payload = slack::WebhookBody::new(|w| { + w.add_section(|s| s.text(slack::Text::markdown(description))) + .add_section(|s| { + s.add_field(slack::Text::markdown(format!( + "*Version:*\n{} ({})", + bundle_id, app_version + ))) + .add_field({ + let hostname = app.config.blob_store_url.clone().unwrap_or_default(); + let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| { + hostname.strip_prefix("http://").unwrap_or_default() + }); + + slack::Text::markdown(format!( + "*Incident:*\n", + CRASH_REPORTS_BUCKET, + hostname, + report.header.incident_id, + report + .header + .incident_id + .chars() + .take(8) + .collect::(), + )) + }) + }) + .add_rich_text(|r| r.add_preformatted(|p| p.add_text(summary))) + }); + let payload_json = serde_json::to_string(&payload).map_err(|err| { + log::error!("Failed to serialize payload to JSON: {err}"); + Error::Internal(anyhow!(err)) + })?; + + reqwest::Client::new() + .post(slack_panics_webhook) + .header("Content-Type", "application/json") + .body(payload_json) + .send() + .await + .map_err(|err| { + log::error!("Failed to send payload to Slack: {err}"); + Error::Internal(anyhow!(err)) + })?; + } + + Ok(()) +} + +pub async fn post_events( + Extension(app): Extension>, + TypedHeader(ZedChecksumHeader(checksum)): TypedHeader, + country_code_header: Option>, + body: Bytes, +) -> Result<()> { + let Some(clickhouse_client) = app.clickhouse_client.clone() else { + Err(Error::Http( + StatusCode::NOT_IMPLEMENTED, + "not supported".into(), + ))? + }; + + let Some(checksum_seed) = app.config.zed_client_checksum_seed.as_ref() else { + return Err(Error::Http( + StatusCode::INTERNAL_SERVER_ERROR, + "events not enabled".into(), + ))?; + }; + + let mut summer = Sha256::new(); + summer.update(checksum_seed); + summer.update(&body); + summer.update(checksum_seed); + + if &checksum != &summer.finalize()[..] { + return Err(Error::Http( + StatusCode::BAD_REQUEST, + "invalid checksum".into(), + ))?; + } + + let request_body: telemetry_events::EventRequestBody = + serde_json::from_slice(&body).map_err(|err| { + log::error!("can't parse event json: {err}"); + Error::Internal(anyhow!(err)) + })?; + + let mut to_upload = ToUpload::default(); + let Some(last_event) = request_body.events.last() else { + return Err(Error::Http(StatusCode::BAD_REQUEST, "no events".into()))?; + }; + let country_code = country_code_header.map(|h| h.0 .0); + + let first_event_at = chrono::Utc::now() + - chrono::Duration::milliseconds(last_event.milliseconds_since_first_event); + + for wrapper in &request_body.events { + match &wrapper.event { + Event::Editor(event) => to_upload.editor_events.push(EditorEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + country_code.clone(), + )), + Event::Copilot(event) => to_upload.copilot_events.push(CopilotEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + country_code.clone(), + )), + Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::Assistant(event) => { + to_upload + .assistant_events + .push(AssistantEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )) + } + Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::App(event) => to_upload.app_events.push(AppEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::Setting(event) => to_upload.setting_events.push(SettingEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::Action(event) => to_upload.action_events.push(ActionEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + } + } + + to_upload + .upload(&clickhouse_client) + .await + .map_err(|err| Error::Internal(anyhow!(err)))?; + + Ok(()) +} + +#[derive(Default)] +struct ToUpload { + editor_events: Vec, + copilot_events: Vec, + assistant_events: Vec, + call_events: Vec, + cpu_events: Vec, + memory_events: Vec, + app_events: Vec, + setting_events: Vec, + edit_events: Vec, + action_events: Vec, +} + +impl ToUpload { + pub async fn upload(&self, clickhouse_client: &clickhouse::Client) -> anyhow::Result<()> { + const EDITOR_EVENTS_TABLE: &str = "editor_events"; + Self::upload_to_table(EDITOR_EVENTS_TABLE, &self.editor_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table '{EDITOR_EVENTS_TABLE}'"))?; + + const COPILOT_EVENTS_TABLE: &str = "copilot_events"; + Self::upload_to_table( + COPILOT_EVENTS_TABLE, + &self.copilot_events, + clickhouse_client, + ) + .await + .with_context(|| format!("failed to upload to table '{COPILOT_EVENTS_TABLE}'"))?; + + const ASSISTANT_EVENTS_TABLE: &str = "assistant_events"; + Self::upload_to_table( + ASSISTANT_EVENTS_TABLE, + &self.assistant_events, + clickhouse_client, + ) + .await + .with_context(|| format!("failed to upload to table '{ASSISTANT_EVENTS_TABLE}'"))?; + + const CALL_EVENTS_TABLE: &str = "call_events"; + Self::upload_to_table(CALL_EVENTS_TABLE, &self.call_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table '{CALL_EVENTS_TABLE}'"))?; + + const CPU_EVENTS_TABLE: &str = "cpu_events"; + Self::upload_to_table(CPU_EVENTS_TABLE, &self.cpu_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table '{CPU_EVENTS_TABLE}'"))?; + + const MEMORY_EVENTS_TABLE: &str = "memory_events"; + Self::upload_to_table(MEMORY_EVENTS_TABLE, &self.memory_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table '{MEMORY_EVENTS_TABLE}'"))?; + + const APP_EVENTS_TABLE: &str = "app_events"; + Self::upload_to_table(APP_EVENTS_TABLE, &self.app_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table '{APP_EVENTS_TABLE}'"))?; + + const SETTING_EVENTS_TABLE: &str = "setting_events"; + Self::upload_to_table( + SETTING_EVENTS_TABLE, + &self.setting_events, + clickhouse_client, + ) + .await + .with_context(|| format!("failed to upload to table '{SETTING_EVENTS_TABLE}'"))?; + + const EDIT_EVENTS_TABLE: &str = "edit_events"; + Self::upload_to_table(EDIT_EVENTS_TABLE, &self.edit_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table '{EDIT_EVENTS_TABLE}'"))?; + + const ACTION_EVENTS_TABLE: &str = "action_events"; + Self::upload_to_table(ACTION_EVENTS_TABLE, &self.action_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table '{ACTION_EVENTS_TABLE}'"))?; + + Ok(()) + } + + async fn upload_to_table( + table: &str, + rows: &[T], + clickhouse_client: &clickhouse::Client, + ) -> anyhow::Result<()> { + if !rows.is_empty() { + let mut insert = clickhouse_client.insert(table)?; + + for event in rows { + insert.write(event).await?; + } + + insert.end().await?; + } + + Ok(()) + } +} + +pub fn serialize_country_code(country_code: &str, serializer: S) -> Result +where + S: Serializer, +{ + if country_code.len() != 2 { + use serde::ser::Error; + return Err(S::Error::custom( + "country_code must be exactly 2 characters", + )); + } + + let country_code = country_code.as_bytes(); + + serializer.serialize_u16(((country_code[0] as u16) << 8) + country_code[1] as u16) +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct EditorEventRow { + pub installation_id: String, + pub operation: String, + pub app_version: String, + pub file_extension: String, + pub os_name: String, + pub os_version: String, + pub release_channel: String, + pub signed_in: bool, + pub vim_mode: bool, + #[serde(serialize_with = "serialize_country_code")] + pub country_code: String, + pub region_code: String, + pub city: String, + pub time: i64, + pub copilot_enabled: bool, + pub copilot_enabled_for_language: bool, + pub historical_event: bool, + pub architecture: String, + pub is_staff: Option, + pub session_id: Option, + pub major: Option, + pub minor: Option, + pub patch: Option, +} + +impl EditorEventRow { + fn from_event( + event: EditorEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + country_code: Option, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + os_name: body.os_name.clone(), + os_version: body.os_version.clone().unwrap_or_default(), + architecture: body.architecture.clone(), + installation_id: body.installation_id.clone().unwrap_or_default(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + operation: event.operation, + file_extension: event.file_extension.unwrap_or_default(), + signed_in: wrapper.signed_in, + vim_mode: event.vim_mode, + copilot_enabled: event.copilot_enabled, + copilot_enabled_for_language: event.copilot_enabled_for_language, + country_code: country_code.unwrap_or("XX".to_string()), + region_code: "".to_string(), + city: "".to_string(), + historical_event: false, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct CopilotEventRow { + pub installation_id: String, + pub suggestion_id: String, + pub suggestion_accepted: bool, + pub app_version: String, + pub file_extension: String, + pub os_name: String, + pub os_version: String, + pub release_channel: String, + pub signed_in: bool, + #[serde(serialize_with = "serialize_country_code")] + pub country_code: String, + pub region_code: String, + pub city: String, + pub time: i64, + pub is_staff: Option, + pub session_id: Option, + pub major: Option, + pub minor: Option, + pub patch: Option, +} + +impl CopilotEventRow { + fn from_event( + event: CopilotEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + country_code: Option, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + os_name: body.os_name.clone(), + os_version: body.os_version.clone().unwrap_or_default(), + installation_id: body.installation_id.clone().unwrap_or_default(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + file_extension: event.file_extension.unwrap_or_default(), + signed_in: wrapper.signed_in, + country_code: country_code.unwrap_or("XX".to_string()), + region_code: "".to_string(), + city: "".to_string(), + suggestion_id: event.suggestion_id.unwrap_or_default(), + suggestion_accepted: event.suggestion_accepted, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct CallEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: String, + session_id: Option, + is_staff: Option, + time: i64, + + // CallEventRow + operation: String, + room_id: Option, + channel_id: Option, +} + +impl CallEventRow { + fn from_event( + event: CallEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone().unwrap_or_default(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + operation: event.operation, + room_id: event.room_id, + channel_id: event.channel_id, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct AssistantEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + session_id: Option, + is_staff: Option, + time: i64, + + // AssistantEventRow + conversation_id: String, + kind: String, + model: String, +} + +impl AssistantEventRow { + fn from_event( + event: AssistantEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + conversation_id: event.conversation_id.unwrap_or_default(), + kind: event.kind.to_string(), + model: event.model, + } + } +} + +#[derive(Debug, clickhouse::Row, Serialize)] +pub struct CpuEventRow { + pub installation_id: Option, + pub is_staff: Option, + pub usage_as_percentage: f32, + pub core_count: u32, + pub app_version: String, + pub release_channel: String, + pub time: i64, + pub session_id: Option, + // pub normalized_cpu_usage: f64, MATERIALIZED + pub major: Option, + pub minor: Option, + pub patch: Option, +} + +impl CpuEventRow { + fn from_event( + event: CpuEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + usage_as_percentage: event.usage_as_percentage, + core_count: event.core_count, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct MemoryEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + session_id: Option, + is_staff: Option, + time: i64, + + // MemoryEventRow + memory_in_bytes: u64, + virtual_memory_in_bytes: u64, +} + +impl MemoryEventRow { + fn from_event( + event: MemoryEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + memory_in_bytes: event.memory_in_bytes, + virtual_memory_in_bytes: event.virtual_memory_in_bytes, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct AppEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + session_id: Option, + is_staff: Option, + time: i64, + + // AppEventRow + operation: String, +} + +impl AppEventRow { + fn from_event( + event: AppEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + operation: event.operation, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct SettingEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + session_id: Option, + is_staff: Option, + time: i64, + // SettingEventRow + setting: String, + value: String, +} + +impl SettingEventRow { + fn from_event( + event: SettingEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + setting: event.setting, + value: event.value, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct EditEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + // Note: This column name has a typo in the ClickHouse table. + #[serde(rename = "sesssion_id")] + session_id: Option, + is_staff: Option, + time: i64, + + // EditEventRow + period_start: i64, + period_end: i64, + environment: String, +} + +impl EditEventRow { + fn from_event( + event: EditEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + let period_start = time - chrono::Duration::milliseconds(event.duration); + let period_end = time; + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + period_start: period_start.timestamp_millis(), + period_end: period_end.timestamp_millis(), + environment: event.environment, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct ActionEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + // Note: This column name has a typo in the ClickHouse table. + #[serde(rename = "sesssion_id")] + session_id: Option, + is_staff: Option, + time: i64, + // ActionEventRow + source: String, + action: String, +} + +impl ActionEventRow { + fn from_event( + event: ActionEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + source: event.source, + action: event.action, + } + } +} diff --git a/crates/collab/src/api/extensions.rs b/crates/collab/src/api/extensions.rs index 5cf211f972..c276e219d1 100644 --- a/crates/collab/src/api/extensions.rs +++ b/crates/collab/src/api/extensions.rs @@ -56,7 +56,7 @@ async fn get_extensions( Extension(app): Extension>, Query(params): Query, ) -> Result> { - let extensions = app.db.get_extensions(params.filter.as_deref(), 30).await?; + let extensions = app.db.get_extensions(params.filter.as_deref(), 500).await?; Ok(Json(GetExtensionsResponse { data: extensions })) } @@ -147,9 +147,7 @@ async fn fetch_extensions_from_blob_store( .send() .await?; - let objects = list - .contents - .ok_or_else(|| anyhow!("missing bucket contents"))?; + let objects = list.contents.unwrap_or_default(); let mut published_versions = HashMap::<&str, Vec<&str>>::default(); for object in &objects { diff --git a/crates/collab/src/api/ips_file.rs b/crates/collab/src/api/ips_file.rs new file mode 100644 index 0000000000..cbbf042abf --- /dev/null +++ b/crates/collab/src/api/ips_file.rs @@ -0,0 +1,352 @@ +use collections::HashMap; + +use serde_derive::Deserialize; +use serde_derive::Serialize; +use serde_json::Value; +use util::SemanticVersion; + +#[derive(Debug)] +pub struct IpsFile { + pub header: Header, + pub body: Body, +} + +impl IpsFile { + pub fn parse(bytes: &[u8]) -> anyhow::Result { + let mut split = bytes.splitn(2, |&b| b == b'\n'); + let header_bytes = split + .next() + .ok_or_else(|| anyhow::anyhow!("No header found"))?; + let header: Header = serde_json::from_slice(header_bytes) + .map_err(|e| anyhow::anyhow!("Failed to parse header: {}", e))?; + + let body_bytes = split + .next() + .ok_or_else(|| anyhow::anyhow!("No body found"))?; + + let body: Body = serde_json::from_slice(body_bytes) + .map_err(|e| anyhow::anyhow!("Failed to parse body: {}", e))?; + Ok(IpsFile { header, body }) + } + + pub fn faulting_thread(&self) -> Option<&Thread> { + self.body.threads.get(self.body.faulting_thread? as usize) + } + + pub fn app_version(&self) -> Option { + self.header.app_version.parse().ok() + } + + pub fn timestamp(&self) -> anyhow::Result> { + chrono::DateTime::parse_from_str(&self.header.timestamp, "%Y-%m-%d %H:%M:%S%.f %#z") + .map_err(|e| anyhow::anyhow!(e)) + } + + pub fn description(&self, panic: Option<&str>) -> String { + let mut desc = if self.body.termination.indicator == "Abort trap: 6" { + match panic { + Some(panic_message) => format!("Panic `{}`", panic_message), + None => "Crash `Abort trap: 6` (possible panic)".into(), + } + } else if let Some(msg) = &self.body.exception.message { + format!("Exception `{}`", msg) + } else { + format!("Crash `{}`", self.body.termination.indicator) + }; + if let Some(thread) = self.faulting_thread() { + if let Some(queue) = thread.queue.as_ref() { + desc += &format!( + " on thread {} ({})", + self.body.faulting_thread.unwrap_or_default(), + queue + ); + } else { + desc += &format!( + " on thread {} ({})", + self.body.faulting_thread.unwrap_or_default(), + thread.name.clone().unwrap_or_default() + ); + } + } + desc + } + + pub fn backtrace_summary(&self) -> String { + if let Some(thread) = self.faulting_thread() { + let mut frames = thread + .frames + .iter() + .filter_map(|frame| { + if let Some(name) = &frame.symbol { + if self.is_ignorable_frame(name) { + return None; + } + Some(format!("{:#}", rustc_demangle::demangle(name))) + } else if let Some(image) = self.body.used_images.get(frame.image_index) { + Some(image.name.clone().unwrap_or("".into())) + } else { + Some("".into()) + } + }) + .collect::>(); + + let total = frames.len(); + if total > 21 { + frames = frames.into_iter().take(20).collect(); + frames.push(format!(" and {} more...", total - 20)) + } + frames.join("\n") + } else { + "".into() + } + } + + fn is_ignorable_frame(&self, symbol: &String) -> bool { + [ + "pthread_kill", + "panic", + "backtrace", + "rust_begin_unwind", + "abort", + ] + .iter() + .any(|s| symbol.contains(s)) + } +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct Header { + pub app_name: String, + pub timestamp: String, + pub app_version: String, + pub slice_uuid: String, + pub build_version: String, + pub platform: i64, + #[serde(rename = "bundleID", default)] + pub bundle_id: String, + pub share_with_app_devs: i64, + pub is_first_party: i64, + pub bug_type: String, + pub os_version: String, + pub roots_installed: i64, + pub name: String, + pub incident_id: String, +} +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct Body { + pub uptime: i64, + pub proc_role: String, + pub version: i64, + #[serde(rename = "userID")] + pub user_id: i64, + pub deploy_version: i64, + pub model_code: String, + #[serde(rename = "coalitionID")] + pub coalition_id: i64, + pub os_version: OsVersion, + pub capture_time: String, + pub code_signing_monitor: i64, + pub incident: String, + pub pid: i64, + pub translated: bool, + pub cpu_type: String, + #[serde(rename = "roots_installed")] + pub roots_installed: i64, + #[serde(rename = "bug_type")] + pub bug_type: String, + pub proc_launch: String, + pub proc_start_abs_time: i64, + pub proc_exit_abs_time: i64, + pub proc_name: String, + pub proc_path: String, + pub bundle_info: BundleInfo, + pub store_info: StoreInfo, + pub parent_proc: String, + pub parent_pid: i64, + pub coalition_name: String, + pub crash_reporter_key: String, + #[serde(rename = "codeSigningID")] + pub code_signing_id: String, + #[serde(rename = "codeSigningTeamID")] + pub code_signing_team_id: String, + pub code_signing_flags: i64, + pub code_signing_validation_category: i64, + pub code_signing_trust_level: i64, + pub instruction_byte_stream: InstructionByteStream, + pub sip: String, + pub exception: Exception, + pub termination: Termination, + pub asi: Asi, + pub ext_mods: ExtMods, + pub faulting_thread: Option, + pub threads: Vec, + pub used_images: Vec, + pub shared_cache: SharedCache, + pub vm_summary: String, + pub legacy_info: LegacyInfo, + pub log_writing_signature: String, + pub trial_info: TrialInfo, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct OsVersion { + pub train: String, + pub build: String, + pub release_type: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct BundleInfo { + #[serde(rename = "CFBundleShortVersionString")] + pub cfbundle_short_version_string: String, + #[serde(rename = "CFBundleVersion")] + pub cfbundle_version: String, + #[serde(rename = "CFBundleIdentifier")] + pub cfbundle_identifier: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct StoreInfo { + pub device_identifier_for_vendor: String, + pub third_party: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct InstructionByteStream { + #[serde(rename = "beforePC")] + pub before_pc: String, + #[serde(rename = "atPC")] + pub at_pc: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct Exception { + pub codes: String, + pub raw_codes: Vec, + #[serde(rename = "type")] + pub type_field: String, + pub subtype: Option, + pub signal: String, + pub port: Option, + pub guard_id: Option, + pub message: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct Termination { + pub flags: i64, + pub code: i64, + pub namespace: String, + pub indicator: String, + pub by_proc: String, + pub by_pid: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct Asi { + #[serde(rename = "libsystem_c.dylib")] + pub libsystem_c_dylib: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct ExtMods { + pub caller: ExtMod, + pub system: ExtMod, + pub targeted: ExtMod, + pub warnings: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct ExtMod { + #[serde(rename = "thread_create")] + pub thread_create: i64, + #[serde(rename = "thread_set_state")] + pub thread_set_state: i64, + #[serde(rename = "task_for_pid")] + pub task_for_pid: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct Thread { + pub thread_state: HashMap, + pub id: i64, + pub triggered: Option, + pub name: Option, + pub queue: Option, + pub frames: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct Frame { + pub image_offset: i64, + pub symbol: Option, + pub symbol_location: Option, + pub image_index: usize, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct UsedImage { + pub source: String, + pub arch: Option, + pub base: i64, + #[serde(rename = "CFBundleShortVersionString")] + pub cfbundle_short_version_string: Option, + #[serde(rename = "CFBundleIdentifier")] + pub cfbundle_identifier: Option, + pub size: i64, + pub uuid: String, + pub path: Option, + pub name: Option, + #[serde(rename = "CFBundleVersion")] + pub cfbundle_version: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct SharedCache { + pub base: i64, + pub size: i64, + pub uuid: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct LegacyInfo { + pub thread_triggered: ThreadTriggered, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct ThreadTriggered { + pub name: String, + pub queue: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct TrialInfo { + pub rollouts: Vec, + pub experiments: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct Rollout { + pub rollout_id: String, + pub factor_pack_ids: HashMap, + pub deployment_id: i64, +} diff --git a/crates/collab/src/api/slack.rs b/crates/collab/src/api/slack.rs new file mode 100644 index 0000000000..2f4234b165 --- /dev/null +++ b/crates/collab/src/api/slack.rs @@ -0,0 +1,144 @@ +use serde::{Deserialize, Serialize}; + +/// https://api.slack.com/reference/messaging/payload +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct WebhookBody { + text: String, + #[serde(skip_serializing_if = "Vec::is_empty")] + blocks: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + thread_ts: Option, + #[serde(skip_serializing_if = "Option::is_none")] + mrkdwn: Option, +} + +impl WebhookBody { + pub fn new(f: impl FnOnce(Self) -> Self) -> Self { + f(Self::default()) + } + + pub fn add_section(mut self, build: impl FnOnce(Section) -> Section) -> Self { + self.blocks.push(Block::Section(build(Section::default()))); + self + } + + pub fn add_rich_text(mut self, build: impl FnOnce(RichText) -> RichText) -> Self { + self.blocks + .push(Block::RichText(build(RichText::default()))); + self + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +/// https://api.slack.com/reference/block-kit/blocks +pub enum Block { + #[serde(rename = "section")] + Section(Section), + #[serde(rename = "rich_text")] + RichText(RichText), + // .... etc. +} + +/// https://api.slack.com/reference/block-kit/blocks#section +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct Section { + #[serde(skip_serializing_if = "Option::is_none")] + text: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + fields: Vec, + // fields, accessories... +} + +impl Section { + pub fn text(mut self, text: Text) -> Self { + self.text = Some(text); + self + } + + pub fn add_field(mut self, field: Text) -> Self { + self.fields.push(field); + self + } +} + +/// https://api.slack.com/reference/block-kit/composition-objects#text +#[derive(Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Text { + #[serde(rename = "plain_text")] + PlainText { text: String, emoji: bool }, + #[serde(rename = "mrkdwn")] + Markdown { text: String, verbatim: bool }, +} + +impl Text { + pub fn plain(s: String) -> Self { + Self::PlainText { + text: s, + emoji: true, + } + } + + pub fn markdown(s: String) -> Self { + Self::Markdown { + text: s, + verbatim: false, + } + } +} + +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct RichText { + elements: Vec, +} + +impl RichText { + pub fn new(f: impl FnOnce(Self) -> Self) -> Self { + f(Self::default()) + } + + pub fn add_preformatted( + mut self, + build: impl FnOnce(RichTextPreformatted) -> RichTextPreformatted, + ) -> Self { + self.elements.push(RichTextObject::Preformatted(build( + RichTextPreformatted::default(), + ))); + self + } +} + +/// https://api.slack.com/reference/block-kit/blocks#rich_text +#[derive(Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum RichTextObject { + #[serde(rename = "rich_text_preformatted")] + Preformatted(RichTextPreformatted), + // etc. +} + +/// https://api.slack.com/reference/block-kit/blocks#rich_text_preformatted +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct RichTextPreformatted { + #[serde(skip_serializing_if = "Vec::is_empty")] + elements: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + border: Option, +} + +impl RichTextPreformatted { + pub fn add_text(mut self, text: String) -> Self { + self.elements.push(RichTextElement::Text { text }); + self + } +} + +/// https://api.slack.com/reference/block-kit/blocks#element-types +#[derive(Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum RichTextElement { + #[serde(rename = "text")] + Text { text: String }, + // etc. +} diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index dc0374df6a..59141e252e 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -8,7 +8,6 @@ use axum::{ middleware::Next, response::IntoResponse, }; -use lazy_static::lazy_static; use prometheus::{exponential_buckets, register_histogram, Histogram}; use rand::thread_rng; use scrypt::{ @@ -16,17 +15,9 @@ use scrypt::{ Scrypt, }; use serde::{Deserialize, Serialize}; +use std::sync::OnceLock; use std::{sync::Arc, time::Instant}; -lazy_static! { - static ref METRIC_ACCESS_TOKEN_HASHING_TIME: Histogram = register_histogram!( - "access_token_hashing_time", - "time spent hashing access tokens", - exponential_buckets(10.0, 2.0, 10).unwrap(), - ) - .unwrap(); -} - #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Impersonator(pub Option); @@ -182,6 +173,16 @@ pub async fn verify_access_token( user_id: UserId, db: &Arc, ) -> Result { + static METRIC_ACCESS_TOKEN_HASHING_TIME: OnceLock = OnceLock::new(); + let metric_access_token_hashing_time = METRIC_ACCESS_TOKEN_HASHING_TIME.get_or_init(|| { + register_histogram!( + "access_token_hashing_time", + "time spent hashing access tokens", + exponential_buckets(10.0, 2.0, 10).unwrap(), + ) + .unwrap() + }); + let token: AccessTokenJson = serde_json::from_str(&token)?; let db_token = db.get_access_token(token.id).await?; @@ -197,7 +198,7 @@ pub async fn verify_access_token( .is_ok(); let duration = t0.elapsed(); log::info!("hashed access token in {:?}", duration); - METRIC_ACCESS_TOKEN_HASHING_TIME.observe(duration.as_millis() as f64); + metric_access_token_hashing_time.observe(duration.as_millis() as f64); Ok(VerifyAccessTokenResult { is_valid, impersonator_id: if db_token.impersonated_user_id.is_some() { diff --git a/crates/collab/src/bin/seed.rs b/crates/collab/src/bin/seed.rs index 632116a363..8a998d0bf3 100644 --- a/crates/collab/src/bin/seed.rs +++ b/crates/collab/src/bin/seed.rs @@ -88,9 +88,9 @@ async fn fetch_github(client: &reqwest::Client, url: &str) .header("user-agent", "zed") .send() .await - .expect(&format!("failed to fetch '{}'", url)); + .unwrap_or_else(|_| panic!("failed to fetch '{}'", url)); response .json() .await - .expect(&format!("failed to deserialize github user from '{}'", url)) + .unwrap_or_else(|_| panic!("failed to deserialize github user from '{}'", url)) } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 7db1faea9f..caf8a83d50 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -359,7 +359,7 @@ impl Database { const SLEEPS: [f32; 10] = [10., 20., 40., 80., 160., 320., 640., 1280., 2560., 5120.]; if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() { let base_delay = SLEEPS[prev_attempt_count]; - let randomized_delay = base_delay as f32 * self.rng.lock().await.gen_range(0.5..=2.0); + let randomized_delay = base_delay * self.rng.lock().await.gen_range(0.5..=2.0); log::info!( "retrying transaction after serialization error. delay: {} ms.", randomized_delay @@ -375,7 +375,7 @@ impl Database { } fn is_serialization_error(error: &Error) -> bool { - const SERIALIZATION_FAILURE_CODE: &'static str = "40001"; + const SERIALIZATION_FAILURE_CODE: &str = "40001"; match error { Error::Database( DbErr::Exec(sea_orm::RuntimeErr::SqlxError(error)) @@ -587,6 +587,7 @@ pub struct ChannelsForUser { pub channels: Vec, pub channel_memberships: Vec, pub channel_participants: HashMap>, + pub hosted_projects: Vec, pub observed_buffer_versions: Vec, pub observed_channel_messages: Vec, @@ -669,6 +670,8 @@ pub struct RefreshedChannelBuffer { } pub struct Project { + pub id: ProjectId, + pub role: ChannelRole, pub collaborators: Vec, pub worktrees: BTreeMap, pub language_servers: Vec, @@ -694,7 +697,7 @@ impl ProjectCollaborator { #[derive(Debug)] pub struct LeftProject { pub id: ProjectId, - pub host_user_id: UserId, + pub host_user_id: Option, pub host_connection_id: Option, pub connection_ids: Vec, } diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 44a5db6a75..d552f646a0 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -88,6 +88,7 @@ id_type!(FlagId); id_type!(ExtensionId); id_type!(NotificationId); id_type!(NotificationKindId); +id_type!(HostedProjectId); /// ChannelRole gives you permissions for both channels and calls. #[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] @@ -100,8 +101,12 @@ pub enum ChannelRole { #[sea_orm(string_value = "member")] #[default] Member, + /// Talker can read, but not write. + /// They can use microphones and the channel chat + #[sea_orm(string_value = "talker")] + Talker, /// Guest can read, but not write. - /// (thought they can use the channel chat) + /// They can not use microphones but can use the chat. #[sea_orm(string_value = "guest")] Guest, /// Banned may not read. @@ -114,8 +119,9 @@ impl ChannelRole { pub fn should_override(&self, other: Self) -> bool { use ChannelRole::*; match self { - Admin => matches!(other, Member | Banned | Guest), - Member => matches!(other, Banned | Guest), + Admin => matches!(other, Member | Banned | Talker | Guest), + Member => matches!(other, Banned | Talker | Guest), + Talker => matches!(other, Guest), Banned => matches!(other, Guest), Guest => false, } @@ -134,7 +140,7 @@ impl ChannelRole { use ChannelRole::*; match self { Admin | Member => true, - Guest => visibility == ChannelVisibility::Public, + Guest | Talker => visibility == ChannelVisibility::Public, Banned => false, } } @@ -144,7 +150,7 @@ impl ChannelRole { use ChannelRole::*; match self { Admin | Member => true, - Guest | Banned => false, + Guest | Talker | Banned => false, } } @@ -152,16 +158,16 @@ impl ChannelRole { pub fn can_only_see_public_descendants(&self) -> bool { use ChannelRole::*; match self { - Guest => true, + Guest | Talker => true, Admin | Member | Banned => false, } } /// True if the role can share screen/microphone/projects into rooms. - pub fn can_publish_to_rooms(&self) -> bool { + pub fn can_use_microphone(&self) -> bool { use ChannelRole::*; match self { - Admin | Member => true, + Admin | Member | Talker => true, Guest | Banned => false, } } @@ -171,7 +177,7 @@ impl ChannelRole { use ChannelRole::*; match self { Admin | Member => true, - Guest | Banned => false, + Talker | Guest | Banned => false, } } @@ -179,7 +185,7 @@ impl ChannelRole { pub fn can_read_projects(&self) -> bool { use ChannelRole::*; match self { - Admin | Member | Guest => true, + Admin | Member | Guest | Talker => true, Banned => false, } } @@ -188,7 +194,7 @@ impl ChannelRole { use ChannelRole::*; match self { Admin | Member => true, - Banned | Guest => false, + Banned | Guest | Talker => false, } } } @@ -198,6 +204,7 @@ impl From for ChannelRole { match value { proto::ChannelRole::Admin => ChannelRole::Admin, proto::ChannelRole::Member => ChannelRole::Member, + proto::ChannelRole::Talker => ChannelRole::Talker, proto::ChannelRole::Guest => ChannelRole::Guest, proto::ChannelRole::Banned => ChannelRole::Banned, } @@ -209,6 +216,7 @@ impl Into for ChannelRole { match self { ChannelRole::Admin => proto::ChannelRole::Admin, ChannelRole::Member => proto::ChannelRole::Member, + ChannelRole::Talker => proto::ChannelRole::Talker, ChannelRole::Guest => proto::ChannelRole::Guest, ChannelRole::Banned => proto::ChannelRole::Banned, } diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 7d9043f595..0326cf4374 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -6,6 +6,7 @@ pub mod channels; pub mod contacts; pub mod contributors; pub mod extensions; +pub mod hosted_projects; pub mod messages; pub mod notifications; pub mod projects; diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index e814ea42a4..4ebca6cebb 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -18,7 +18,7 @@ impl Database { connection: ConnectionId, ) -> Result { self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; + let channel = self.get_channel_internal(channel_id, &tx).await?; self.check_user_is_channel_participant(&channel, user_id, &tx) .await?; @@ -134,10 +134,10 @@ impl Database { let mut results = Vec::new(); for client_buffer in buffers { let channel = self - .get_channel_internal(ChannelId::from_proto(client_buffer.channel_id), &*tx) + .get_channel_internal(ChannelId::from_proto(client_buffer.channel_id), &tx) .await?; if self - .check_user_is_channel_participant(&channel, user_id, &*tx) + .check_user_is_channel_participant(&channel, user_id, &tx) .await .is_err() { @@ -145,7 +145,7 @@ impl Database { continue; } - let buffer = self.get_channel_buffer(channel.id, &*tx).await?; + let buffer = self.get_channel_buffer(channel.id, &tx).await?; let mut collaborators = channel_buffer_collaborator::Entity::find() .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel.id)) .all(&*tx) @@ -161,11 +161,9 @@ impl Database { // Find the collaborator record for this user's previous lost // connection. Update it with the new connection id. - let server_id = ServerId(connection_id.owner_id as i32); - let Some(self_collaborator) = collaborators.iter_mut().find(|c| { - c.user_id == user_id - && (c.connection_lost || c.connection_server_id != server_id) - }) else { + let Some(self_collaborator) = + collaborators.iter_mut().find(|c| c.user_id == user_id) + else { log::info!("can't rejoin buffer, no previous collaborator found"); continue; }; @@ -182,7 +180,7 @@ impl Database { let client_version = version_from_wire(&client_buffer.version); let serialization_version = self - .get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &*tx) + .get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &tx) .await?; let mut rows = buffer_operation::Entity::find() @@ -285,7 +283,7 @@ impl Database { connection: ConnectionId, ) -> Result { self.transaction(|tx| async move { - self.leave_channel_buffer_internal(channel_id, connection, &*tx) + self.leave_channel_buffer_internal(channel_id, connection, &tx) .await }) .await @@ -339,7 +337,7 @@ impl Database { let mut result = Vec::new(); for channel_id in channel_ids { let left_channel_buffer = self - .leave_channel_buffer_internal(channel_id, connection, &*tx) + .leave_channel_buffer_internal(channel_id, connection, &tx) .await?; result.push(left_channel_buffer); } @@ -408,7 +406,7 @@ impl Database { channel_id: ChannelId, ) -> Result> { self.transaction(|tx| async move { - self.get_channel_buffer_collaborators_internal(channel_id, &*tx) + self.get_channel_buffer_collaborators_internal(channel_id, &tx) .await }) .await @@ -449,7 +447,7 @@ impl Database { Vec, )> { self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; + let channel = self.get_channel_internal(channel_id, &tx).await?; let mut requires_write_permission = false; for op in operations.iter() { @@ -459,10 +457,10 @@ impl Database { } } if requires_write_permission { - self.check_user_is_channel_member(&channel, user, &*tx) + self.check_user_is_channel_member(&channel, user, &tx) .await?; } else { - self.check_user_is_channel_participant(&channel, user, &*tx) + self.check_user_is_channel_participant(&channel, user, &tx) .await?; } @@ -473,7 +471,7 @@ impl Database { .ok_or_else(|| anyhow!("no such buffer"))?; let serialization_version = self - .get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &*tx) + .get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &tx) .await?; let operations = operations @@ -502,13 +500,13 @@ impl Database { buffer.epoch, *max_operation.replica_id.as_ref(), *max_operation.lamport_timestamp.as_ref(), - &*tx, + &tx, ) .await?; - channel_members = self.get_channel_participants(&channel, &*tx).await?; + channel_members = self.get_channel_participants(&channel, &tx).await?; let collaborators = self - .get_channel_buffer_collaborators_internal(channel_id, &*tx) + .get_channel_buffer_collaborators_internal(channel_id, &tx) .await?; channel_members.retain(|member| !collaborators.contains(member)); @@ -739,7 +737,7 @@ impl Database { epoch, component.replica_id as i32, component.timestamp as i32, - &*tx, + &tx, ) .await?; Ok(()) diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 1023b1b7c3..376a44b6eb 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -53,8 +53,8 @@ impl Database { let mut membership = None; if let Some(parent_channel_id) = parent_channel_id { - let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?; - self.check_user_is_channel_admin(&parent_channel, admin_id, &*tx) + let parent_channel = self.get_channel_internal(parent_channel_id, &tx).await?; + self.check_user_is_channel_admin(&parent_channel, admin_id, &tx) .await?; parent = Some(parent_channel); } @@ -105,14 +105,14 @@ impl Database { connection: ConnectionId, ) -> Result<(JoinRoom, Option, ChannelRole)> { self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - let mut role = self.channel_role_for_user(&channel, user_id, &*tx).await?; + let channel = self.get_channel_internal(channel_id, &tx).await?; + let mut role = self.channel_role_for_user(&channel, user_id, &tx).await?; let mut accept_invite_result = None; if role.is_none() { if let Some(invitation) = self - .pending_invite_for_channel(&channel, user_id, &*tx) + .pending_invite_for_channel(&channel, user_id, &tx) .await? { // note, this may be a parent channel @@ -125,12 +125,12 @@ impl Database { .await?; accept_invite_result = Some( - self.calculate_membership_updated(&channel, user_id, &*tx) + self.calculate_membership_updated(&channel, user_id, &tx) .await?, ); debug_assert!( - self.channel_role_for_user(&channel, user_id, &*tx).await? == role + self.channel_role_for_user(&channel, user_id, &tx).await? == role ); } else if channel.visibility == ChannelVisibility::Public { role = Some(ChannelRole::Guest); @@ -145,12 +145,12 @@ impl Database { .await?; accept_invite_result = Some( - self.calculate_membership_updated(&channel, user_id, &*tx) + self.calculate_membership_updated(&channel, user_id, &tx) .await?, ); debug_assert!( - self.channel_role_for_user(&channel, user_id, &*tx).await? == role + self.channel_role_for_user(&channel, user_id, &tx).await? == role ); } } @@ -162,10 +162,10 @@ impl Database { let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); let room_id = self - .get_or_create_channel_room(channel_id, &live_kit_room, &*tx) + .get_or_create_channel_room(channel_id, &live_kit_room, &tx) .await?; - self.join_channel_room_internal(room_id, user_id, connection, role, &*tx) + self.join_channel_room_internal(room_id, user_id, connection, role, &tx) .await .map(|jr| (jr, accept_invite_result, role)) }) @@ -180,13 +180,13 @@ impl Database { admin_id: UserId, ) -> Result<(Channel, Vec)> { self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, admin_id, &*tx) + let channel = self.get_channel_internal(channel_id, &tx).await?; + self.check_user_is_channel_admin(&channel, admin_id, &tx) .await?; if visibility == ChannelVisibility::Public { if let Some(parent_id) = channel.parent_id() { - let parent = self.get_channel_internal(parent_id, &*tx).await?; + let parent = self.get_channel_internal(parent_id, &tx).await?; if parent.visibility != ChannelVisibility::Public { Err(ErrorCode::BadPublicNesting @@ -196,7 +196,7 @@ impl Database { } } else if visibility == ChannelVisibility::Members { if self - .get_channel_descendants_excluding_self([&channel], &*tx) + .get_channel_descendants_excluding_self([&channel], &tx) .await? .into_iter() .any(|channel| channel.visibility == ChannelVisibility::Public) @@ -228,7 +228,7 @@ impl Database { requires_zed_cla: bool, ) -> Result<()> { self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; + let channel = self.get_channel_internal(channel_id, &tx).await?; let mut model = channel.into_active_model(); model.requires_zed_cla = ActiveValue::Set(requires_zed_cla); model.update(&*tx).await?; @@ -244,8 +244,8 @@ impl Database { user_id: UserId, ) -> Result<(Vec, Vec)> { self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &tx).await?; + self.check_user_is_channel_admin(&channel, user_id, &tx) .await?; let members_to_notify: Vec = channel_member::Entity::find() @@ -258,7 +258,7 @@ impl Database { .await?; let channels_to_remove = self - .get_channel_descendants_excluding_self([&channel], &*tx) + .get_channel_descendants_excluding_self([&channel], &tx) .await? .into_iter() .map(|channel| channel.id) @@ -284,8 +284,8 @@ impl Database { role: ChannelRole, ) -> Result { self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, inviter_id, &*tx) + let channel = self.get_channel_internal(channel_id, &tx).await?; + self.check_user_is_channel_admin(&channel, inviter_id, &tx) .await?; if !channel.is_root() { Err(ErrorCode::NotARootChannel.anyhow())? @@ -312,7 +312,7 @@ impl Database { inviter_id: inviter_id.to_proto(), }, true, - &*tx, + &tx, ) .await? .into_iter() @@ -344,8 +344,8 @@ impl Database { self.transaction(move |tx| async move { let new_name = Self::sanitize_channel_name(new_name)?.to_string(); - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, admin_id, &*tx) + let channel = self.get_channel_internal(channel_id, &tx).await?; + self.check_user_is_channel_admin(&channel, admin_id, &tx) .await?; let mut model = channel.into_active_model(); @@ -370,7 +370,7 @@ impl Database { accept: bool, ) -> Result { self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; + let channel = self.get_channel_internal(channel_id, &tx).await?; let membership_update = if accept { let rows_affected = channel_member::Entity::update_many() @@ -393,7 +393,7 @@ impl Database { } Some( - self.calculate_membership_updated(&channel, user_id, &*tx) + self.calculate_membership_updated(&channel, user_id, &tx) .await?, ) } else { @@ -425,7 +425,7 @@ impl Database { inviter_id: Default::default(), }, accept, - &*tx, + &tx, ) .await? .into_iter() @@ -466,10 +466,10 @@ impl Database { admin_id: UserId, ) -> Result { self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; + let channel = self.get_channel_internal(channel_id, &tx).await?; if member_id != admin_id { - self.check_user_is_channel_admin(&channel, admin_id, &*tx) + self.check_user_is_channel_admin(&channel, admin_id, &tx) .await?; } @@ -488,7 +488,7 @@ impl Database { Ok(RemoveChannelMemberResult { membership_update: self - .calculate_membership_updated(&channel, member_id, &*tx) + .calculate_membership_updated(&channel, member_id, &tx) .await?, notification_id: self .remove_notification( @@ -498,7 +498,7 @@ impl Database { channel_name: Default::default(), inviter_id: Default::default(), }, - &*tx, + &tx, ) .await?, }) @@ -529,10 +529,7 @@ impl Database { .all(&*tx) .await?; - let channels = channels - .into_iter() - .filter_map(|channel| Some(Channel::from_model(channel))) - .collect(); + let channels = channels.into_iter().map(Channel::from_model).collect(); Ok(channels) }) @@ -652,9 +649,14 @@ impl Database { .observed_channel_messages(&channel_ids, user_id, &*tx) .await?; + let hosted_projects = self + .get_hosted_projects(&channel_ids, &roles_by_channel_id, &*tx) + .await?; + Ok(ChannelsForUser { channel_memberships, channels, + hosted_projects, channel_participants, latest_buffer_versions, latest_channel_messages, @@ -672,8 +674,8 @@ impl Database { role: ChannelRole, ) -> Result { self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, admin_id, &*tx) + let channel = self.get_channel_internal(channel_id, &tx).await?; + self.check_user_is_channel_admin(&channel, admin_id, &tx) .await?; let membership = channel_member::Entity::find() @@ -695,7 +697,7 @@ impl Database { if updated.accepted { Ok(SetMemberRoleResult::MembershipUpdated( - self.calculate_membership_updated(&channel, for_user, &*tx) + self.calculate_membership_updated(&channel, for_user, &tx) .await?, )) } else { @@ -715,13 +717,13 @@ impl Database { ) -> Result> { let (role, members) = self .transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; + let channel = self.get_channel_internal(channel_id, &tx).await?; let role = self - .check_user_is_channel_participant(&channel, user_id, &*tx) + .check_user_is_channel_participant(&channel, user_id, &tx) .await?; Ok(( role, - self.get_channel_participant_details_internal(&channel, &*tx) + self.get_channel_participant_details_internal(&channel, &tx) .await?, )) }) @@ -795,6 +797,7 @@ impl Database { match role { Some(ChannelRole::Admin) => Ok(role.unwrap()), Some(ChannelRole::Member) + | Some(ChannelRole::Talker) | Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!( @@ -813,7 +816,10 @@ impl Database { let channel_role = self.channel_role_for_user(channel, user_id, tx).await?; match channel_role { Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()), - Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!( + Some(ChannelRole::Banned) + | Some(ChannelRole::Guest) + | Some(ChannelRole::Talker) + | None => Err(anyhow!( "user is not a channel member or channel does not exist" ))?, } @@ -828,9 +834,10 @@ impl Database { ) -> Result { let role = self.channel_role_for_user(channel, user_id, tx).await?; match role { - Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => { - Ok(role.unwrap()) - } + Some(ChannelRole::Admin) + | Some(ChannelRole::Member) + | Some(ChannelRole::Guest) + | Some(ChannelRole::Talker) => Ok(role.unwrap()), Some(ChannelRole::Banned) | None => Err(anyhow!( "user is not a channel participant or channel does not exist" ))?, @@ -908,8 +915,8 @@ impl Database { /// Returns the channel with the given ID. pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result { self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_participant(&channel, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &tx) .await?; Ok(Channel::from_model(channel)) @@ -964,10 +971,10 @@ impl Database { admin_id: UserId, ) -> Result<(Vec, Vec)> { self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, admin_id, &*tx) + let channel = self.get_channel_internal(channel_id, &tx).await?; + self.check_user_is_channel_admin(&channel, admin_id, &tx) .await?; - let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?; + let new_parent = self.get_channel_internal(new_parent_id, &tx).await?; if new_parent.root_id() != channel.root_id() { Err(anyhow!(ErrorCode::WrongMoveTarget))?; diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index c66c33b80d..89bb07f3d9 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -177,7 +177,7 @@ impl Database { sender_id: sender_id.to_proto(), }, true, - &*tx, + &tx, ) .await? .into_iter() @@ -227,7 +227,7 @@ impl Database { rpc::Notification::ContactRequest { sender_id: requester_id.to_proto(), }, - &*tx, + &tx, ) .await?; } @@ -335,7 +335,7 @@ impl Database { sender_id: requester_id.to_proto(), }, accept, - &*tx, + &tx, ) .await?, ); @@ -348,7 +348,7 @@ impl Database { responder_id: responder_id.to_proto(), }, true, - &*tx, + &tx, ) .await?, ); diff --git a/crates/collab/src/db/queries/contributors.rs b/crates/collab/src/db/queries/contributors.rs index 0972779ce9..49194d6861 100644 --- a/crates/collab/src/db/queries/contributors.rs +++ b/crates/collab/src/db/queries/contributors.rs @@ -72,7 +72,7 @@ impl Database { github_login, github_user_id, github_email, - &*tx, + &tx, ) .await?; diff --git a/crates/collab/src/db/queries/hosted_projects.rs b/crates/collab/src/db/queries/hosted_projects.rs new file mode 100644 index 0000000000..394f1055c6 --- /dev/null +++ b/crates/collab/src/db/queries/hosted_projects.rs @@ -0,0 +1,82 @@ +use rpc::{proto, ErrorCode}; + +use super::*; + +impl Database { + pub async fn get_hosted_projects( + &self, + channel_ids: &Vec, + roles: &HashMap, + tx: &DatabaseTransaction, + ) -> Result> { + Ok(hosted_project::Entity::find() + .filter(hosted_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0))) + .all(&*tx) + .await? + .into_iter() + .flat_map(|project| { + if project.deleted_at.is_some() { + return None; + } + match project.visibility { + ChannelVisibility::Public => {} + ChannelVisibility::Members => { + let is_visible = roles + .get(&project.channel_id) + .map(|role| role.can_see_all_descendants()) + .unwrap_or(false); + if !is_visible { + return None; + } + } + }; + Some(proto::HostedProject { + id: project.id.to_proto(), + channel_id: project.channel_id.to_proto(), + name: project.name.clone(), + visibility: project.visibility.into(), + }) + }) + .collect()) + } + + pub async fn get_hosted_project( + &self, + hosted_project_id: HostedProjectId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result<(hosted_project::Model, ChannelRole)> { + let project = hosted_project::Entity::find_by_id(hosted_project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?; + let channel = channel::Entity::find_by_id(project.channel_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!(ErrorCode::NoSuchChannel))?; + + let role = match project.visibility { + ChannelVisibility::Public => { + self.check_user_is_channel_participant(&channel, user_id, tx) + .await? + } + ChannelVisibility::Members => { + self.check_user_is_channel_member(&channel, user_id, tx) + .await? + } + }; + + Ok((project, role)) + } + + pub async fn is_hosted_project(&self, project_id: ProjectId) -> Result { + self.transaction(|tx| async move { + Ok(project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .map(|project| project.hosted_project_id.is_some()) + .ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?) + }) + .await + } +} diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index d63b4cf1c5..83796d4512 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -12,8 +12,8 @@ impl Database { user_id: UserId, ) -> Result<()> { self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_participant(&channel, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &tx) .await?; channel_chat_participant::ActiveModel { id: ActiveValue::NotSet, @@ -87,8 +87,8 @@ impl Database { before_message_id: Option, ) -> Result> { self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_participant(&channel, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &tx) .await?; let mut condition = @@ -105,7 +105,7 @@ impl Database { .all(&*tx) .await?; - self.load_channel_messages(rows, &*tx).await + self.load_channel_messages(rows, &tx).await }) .await } @@ -127,16 +127,16 @@ impl Database { for row in &rows { channels.insert( row.channel_id, - self.get_channel_internal(row.channel_id, &*tx).await?, + self.get_channel_internal(row.channel_id, &tx).await?, ); } for (_, channel) in channels { - self.check_user_is_channel_participant(&channel, user_id, &*tx) + self.check_user_is_channel_participant(&channel, user_id, &tx) .await?; } - let messages = self.load_channel_messages(rows, &*tx).await?; + let messages = self.load_channel_messages(rows, &tx).await?; Ok(messages) }) .await @@ -200,6 +200,7 @@ impl Database { } /// Creates a new channel message. + #[allow(clippy::too_many_arguments)] pub async fn create_channel_message( &self, channel_id: ChannelId, @@ -211,8 +212,8 @@ impl Database { reply_to_message_id: Option, ) -> Result { self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_participant(&channel, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &tx) .await?; let mut rows = channel_chat_participant::Entity::find() @@ -302,13 +303,13 @@ impl Database { channel_id: channel_id.to_proto(), }, false, - &*tx, + &tx, ) .await?, ); } - self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx) + self.observe_channel_message_internal(channel_id, user_id, message_id, &tx) .await?; } _ => { @@ -321,7 +322,7 @@ impl Database { } } - let mut channel_members = self.get_channel_participants(&channel, &*tx).await?; + let mut channel_members = self.get_channel_participants(&channel, &tx).await?; channel_members.retain(|member| !participant_user_ids.contains(member)); Ok(CreatedChannelMessage { @@ -341,7 +342,7 @@ impl Database { message_id: MessageId, ) -> Result { self.transaction(|tx| async move { - self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx) + self.observe_channel_message_internal(channel_id, user_id, message_id, &tx) .await?; let mut batch = NotificationBatch::default(); batch.extend( @@ -352,7 +353,7 @@ impl Database { sender_id: Default::default(), channel_id: Default::default(), }, - &*tx, + &tx, ) .await?, ); @@ -500,9 +501,9 @@ impl Database { .await?; if result.rows_affected == 0 { - let channel = self.get_channel_internal(channel_id, &*tx).await?; + let channel = self.get_channel_internal(channel_id, &tx).await?; if self - .check_user_is_channel_admin(&channel, user_id, &*tx) + .check_user_is_channel_admin(&channel, user_id, &tx) .await .is_ok() { diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 3fdb94b343..190f854bfa 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -51,19 +51,20 @@ impl Database { if !participant .role .unwrap_or(ChannelRole::Member) - .can_publish_to_rooms() + .can_edit_projects() { return Err(anyhow!("guests cannot share projects"))?; } let project = project::ActiveModel { - room_id: ActiveValue::set(participant.room_id), - host_user_id: ActiveValue::set(participant.user_id), + room_id: ActiveValue::set(Some(participant.room_id)), + host_user_id: ActiveValue::set(Some(participant.user_id)), host_connection_id: ActiveValue::set(Some(connection.id as i32)), host_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, ))), - ..Default::default() + id: ActiveValue::NotSet, + hosted_project_id: ActiveValue::Set(None), } .insert(&*tx) .await?; @@ -153,8 +154,12 @@ impl Database { self.update_project_worktrees(project.id, worktrees, &tx) .await?; + let room_id = project + .room_id + .ok_or_else(|| anyhow!("project not in a room"))?; + let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?; - let room = self.get_room(project.room_id, &tx).await?; + let room = self.get_room(room_id, &tx).await?; Ok((room, guest_connection_ids)) }) .await @@ -382,7 +387,6 @@ impl Database { language_server_id: ActiveValue::set(summary.language_server_id as i64), error_count: ActiveValue::set(summary.error_count as i32), warning_count: ActiveValue::set(summary.warning_count as i32), - ..Default::default() }) .on_conflict( OnConflict::columns([ @@ -434,7 +438,6 @@ impl Database { project_id: ActiveValue::set(project_id), id: ActiveValue::set(server.id as i64), name: ActiveValue::set(server.name.clone()), - ..Default::default() }) .on_conflict( OnConflict::columns([ @@ -506,8 +509,30 @@ impl Database { .await } - /// Adds the given connection to the specified project. - pub async fn join_project( + /// Adds the given connection to the specified hosted project + pub async fn join_hosted_project( + &self, + id: HostedProjectId, + user_id: UserId, + connection: ConnectionId, + ) -> Result<(Project, ReplicaId)> { + self.transaction(|tx| async move { + let (hosted_project, role) = self.get_hosted_project(id, user_id, &tx).await?; + let project = project::Entity::find() + .filter(project::Column::HostedProjectId.eq(hosted_project.id)) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("hosted project is no longer shared"))?; + + self.join_project_internal(project, user_id, connection, role, &tx) + .await + }) + .await + } + + /// Adds the given connection to the specified project + /// in the current room. + pub async fn join_project_in_room( &self, project_id: ProjectId, connection: ConnectionId, @@ -534,180 +559,240 @@ impl Database { .one(&*tx) .await? .ok_or_else(|| anyhow!("no such project"))?; - if project.room_id != participant.room_id { + if project.room_id != Some(participant.room_id) { return Err(anyhow!("no such project"))?; } + self.join_project_internal( + project, + participant.user_id, + connection, + participant.role.unwrap_or(ChannelRole::Member), + &tx, + ) + .await + }) + .await + } - let mut collaborators = project + async fn join_project_internal( + &self, + project: project::Model, + user_id: UserId, + connection: ConnectionId, + role: ChannelRole, + tx: &DatabaseTransaction, + ) -> Result<(Project, ReplicaId)> { + let mut collaborators = project + .find_related(project_collaborator::Entity) + .all(&*tx) + .await?; + let replica_ids = collaborators + .iter() + .map(|c| c.replica_id) + .collect::>(); + let mut replica_id = ReplicaId(1); + while replica_ids.contains(&replica_id) { + replica_id.0 += 1; + } + let new_collaborator = project_collaborator::ActiveModel { + project_id: ActiveValue::set(project.id), + connection_id: ActiveValue::set(connection.id as i32), + connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), + user_id: ActiveValue::set(user_id), + replica_id: ActiveValue::set(replica_id), + is_host: ActiveValue::set(false), + ..Default::default() + } + .insert(&*tx) + .await?; + collaborators.push(new_collaborator); + + let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; + let mut worktrees = db_worktrees + .into_iter() + .map(|db_worktree| { + ( + db_worktree.id as u64, + Worktree { + id: db_worktree.id as u64, + abs_path: db_worktree.abs_path, + root_name: db_worktree.root_name, + visible: db_worktree.visible, + entries: Default::default(), + repository_entries: Default::default(), + diagnostic_summaries: Default::default(), + settings_files: Default::default(), + scan_id: db_worktree.scan_id as u64, + completed_scan_id: db_worktree.completed_scan_id as u64, + }, + ) + }) + .collect::>(); + + // Populate worktree entries. + { + let mut db_entries = worktree_entry::Entity::find() + .filter( + Condition::all() + .add(worktree_entry::Column::ProjectId.eq(project.id)) + .add(worktree_entry::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + while let Some(db_entry) = db_entries.next().await { + let db_entry = db_entry?; + if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) { + worktree.entries.push(proto::Entry { + id: db_entry.id as u64, + is_dir: db_entry.is_dir, + path: db_entry.path, + inode: db_entry.inode as u64, + mtime: Some(proto::Timestamp { + seconds: db_entry.mtime_seconds as u64, + nanos: db_entry.mtime_nanos as u32, + }), + is_symlink: db_entry.is_symlink, + is_ignored: db_entry.is_ignored, + is_external: db_entry.is_external, + git_status: db_entry.git_status.map(|status| status as i32), + }); + } + } + } + + // Populate repository entries. + { + let mut db_repository_entries = worktree_repository::Entity::find() + .filter( + Condition::all() + .add(worktree_repository::Column::ProjectId.eq(project.id)) + .add(worktree_repository::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + while let Some(db_repository_entry) = db_repository_entries.next().await { + let db_repository_entry = db_repository_entry?; + if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) + { + worktree.repository_entries.insert( + db_repository_entry.work_directory_id as u64, + proto::RepositoryEntry { + work_directory_id: db_repository_entry.work_directory_id as u64, + branch: db_repository_entry.branch, + }, + ); + } + } + } + + // Populate worktree diagnostic summaries. + { + let mut db_summaries = worktree_diagnostic_summary::Entity::find() + .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project.id)) + .stream(&*tx) + .await?; + while let Some(db_summary) = db_summaries.next().await { + let db_summary = db_summary?; + if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) { + worktree + .diagnostic_summaries + .push(proto::DiagnosticSummary { + path: db_summary.path, + language_server_id: db_summary.language_server_id as u64, + error_count: db_summary.error_count as u32, + warning_count: db_summary.warning_count as u32, + }); + } + } + } + + // Populate worktree settings files + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project.id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = worktrees.get_mut(&(db_settings_file.worktree_id as u64)) { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + + // Populate language servers. + let language_servers = project + .find_related(language_server::Entity) + .all(&*tx) + .await?; + + let project = Project { + id: project.id, + role, + collaborators: collaborators + .into_iter() + .map(|collaborator| ProjectCollaborator { + connection_id: collaborator.connection(), + user_id: collaborator.user_id, + replica_id: collaborator.replica_id, + is_host: collaborator.is_host, + }) + .collect(), + worktrees, + language_servers: language_servers + .into_iter() + .map(|language_server| proto::LanguageServer { + id: language_server.id as u64, + name: language_server.name, + }) + .collect(), + }; + Ok((project, replica_id as ReplicaId)) + } + + pub async fn leave_hosted_project( + &self, + project_id: ProjectId, + connection: ConnectionId, + ) -> Result { + self.transaction(|tx| async move { + let result = project_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add(project_collaborator::Column::ProjectId.eq(project_id)) + .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32)) + .add( + project_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + return Err(anyhow!("not in the project"))?; + } + + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + let collaborators = project .find_related(project_collaborator::Entity) .all(&*tx) .await?; - let replica_ids = collaborators - .iter() - .map(|c| c.replica_id) - .collect::>(); - let mut replica_id = ReplicaId(1); - while replica_ids.contains(&replica_id) { - replica_id.0 += 1; - } - let new_collaborator = project_collaborator::ActiveModel { - project_id: ActiveValue::set(project_id), - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - user_id: ActiveValue::set(participant.user_id), - replica_id: ActiveValue::set(replica_id), - is_host: ActiveValue::set(false), - ..Default::default() - } - .insert(&*tx) - .await?; - collaborators.push(new_collaborator); - - let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; - let mut worktrees = db_worktrees + let connection_ids = collaborators .into_iter() - .map(|db_worktree| { - ( - db_worktree.id as u64, - Worktree { - id: db_worktree.id as u64, - abs_path: db_worktree.abs_path, - root_name: db_worktree.root_name, - visible: db_worktree.visible, - entries: Default::default(), - repository_entries: Default::default(), - diagnostic_summaries: Default::default(), - settings_files: Default::default(), - scan_id: db_worktree.scan_id as u64, - completed_scan_id: db_worktree.completed_scan_id as u64, - }, - ) - }) - .collect::>(); - - // Populate worktree entries. - { - let mut db_entries = worktree_entry::Entity::find() - .filter( - Condition::all() - .add(worktree_entry::Column::ProjectId.eq(project_id)) - .add(worktree_entry::Column::IsDeleted.eq(false)), - ) - .stream(&*tx) - .await?; - while let Some(db_entry) = db_entries.next().await { - let db_entry = db_entry?; - if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) { - worktree.entries.push(proto::Entry { - id: db_entry.id as u64, - is_dir: db_entry.is_dir, - path: db_entry.path, - inode: db_entry.inode as u64, - mtime: Some(proto::Timestamp { - seconds: db_entry.mtime_seconds as u64, - nanos: db_entry.mtime_nanos as u32, - }), - is_symlink: db_entry.is_symlink, - is_ignored: db_entry.is_ignored, - is_external: db_entry.is_external, - git_status: db_entry.git_status.map(|status| status as i32), - }); - } - } - } - - // Populate repository entries. - { - let mut db_repository_entries = worktree_repository::Entity::find() - .filter( - Condition::all() - .add(worktree_repository::Column::ProjectId.eq(project_id)) - .add(worktree_repository::Column::IsDeleted.eq(false)), - ) - .stream(&*tx) - .await?; - while let Some(db_repository_entry) = db_repository_entries.next().await { - let db_repository_entry = db_repository_entry?; - if let Some(worktree) = - worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) - { - worktree.repository_entries.insert( - db_repository_entry.work_directory_id as u64, - proto::RepositoryEntry { - work_directory_id: db_repository_entry.work_directory_id as u64, - branch: db_repository_entry.branch, - }, - ); - } - } - } - - // Populate worktree diagnostic summaries. - { - let mut db_summaries = worktree_diagnostic_summary::Entity::find() - .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_summary) = db_summaries.next().await { - let db_summary = db_summary?; - if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) { - worktree - .diagnostic_summaries - .push(proto::DiagnosticSummary { - path: db_summary.path, - language_server_id: db_summary.language_server_id as u64, - error_count: db_summary.error_count as u32, - warning_count: db_summary.warning_count as u32, - }); - } - } - } - - // Populate worktree settings files - { - let mut db_settings_files = worktree_settings_file::Entity::find() - .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_settings_file) = db_settings_files.next().await { - let db_settings_file = db_settings_file?; - if let Some(worktree) = - worktrees.get_mut(&(db_settings_file.worktree_id as u64)) - { - worktree.settings_files.push(WorktreeSettingsFile { - path: db_settings_file.path, - content: db_settings_file.content, - }); - } - } - } - - // Populate language servers. - let language_servers = project - .find_related(language_server::Entity) - .all(&*tx) - .await?; - - let project = Project { - collaborators: collaborators - .into_iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect(), - worktrees, - language_servers: language_servers - .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - }) - .collect(), - }; - Ok((project, replica_id as ReplicaId)) + .map(|collaborator| collaborator.connection()) + .collect(); + Ok(LeftProject { + id: project.id, + connection_ids, + host_user_id: None, + host_connection_id: None, + }) }) .await } @@ -774,7 +859,7 @@ impl Database { .exec(&*tx) .await?; - let room = self.get_room(project.room_id, &tx).await?; + let room = self.get_room(room_id, &tx).await?; let left_project = LeftProject { id: project_id, host_user_id: project.host_user_id, @@ -998,7 +1083,9 @@ impl Database { .one(&*tx) .await? .ok_or_else(|| anyhow!("project {} not found", project_id))?; - Ok(project.room_id) + Ok(project + .room_id + .ok_or_else(|| anyhow!("project not in room"))?) }) .await } @@ -1061,7 +1148,7 @@ impl Database { .insert(&*tx) .await?; - let room = self.get_room(room_id, &*tx).await?; + let room = self.get_room(room_id, &tx).await?; Ok(room) }) .await @@ -1095,7 +1182,7 @@ impl Database { .exec(&*tx) .await?; - let room = self.get_room(room_id, &*tx).await?; + let room = self.get_room(room_id, &tx).await?; Ok(room) }) .await diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 275e75ff11..13707d6f47 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -169,7 +169,7 @@ impl Database { let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) { ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member, - ChannelRole::Guest => ChannelRole::Guest, + ChannelRole::Guest | ChannelRole::Talker => ChannelRole::Guest, ChannelRole::Banned => return Err(anyhow!("banned users cannot invite").into()), }; @@ -321,7 +321,7 @@ impl Database { } let participant_index = self - .get_next_participant_index_internal(room_id, &*tx) + .get_next_participant_index_internal(room_id, &tx) .await?; let result = room_participant::Entity::update_many() @@ -468,15 +468,7 @@ impl Database { Condition::all() .add(room_participant::Column::RoomId.eq(room_id)) .add(room_participant::Column::UserId.eq(user_id)) - .add(room_participant::Column::AnsweringConnectionId.is_not_null()) - .add( - Condition::any() - .add(room_participant::Column::AnsweringConnectionLost.eq(true)) - .add( - room_participant::Column::AnsweringConnectionServerId - .ne(connection.owner_id as i32), - ), - ), + .add(room_participant::Column::AnsweringConnectionId.is_not_null()), ) .set(room_participant::ActiveModel { answering_connection_id: ActiveValue::set(Some(connection.id as i32)), @@ -499,7 +491,7 @@ impl Database { .one(&*tx) .await? .ok_or_else(|| anyhow!("project does not exist"))?; - if project.host_user_id != user_id { + if project.host_user_id != Some(user_id) { return Err(anyhow!("no such project"))?; } @@ -859,7 +851,7 @@ impl Database { } if collaborator.is_host { - left_project.host_user_id = collaborator.user_id; + left_project.host_user_id = Some(collaborator.user_id); left_project.host_connection_id = Some(collaborator_connection_id); } } @@ -1018,7 +1010,7 @@ impl Database { .ok_or_else(|| anyhow!("only admins can set participant role"))?; if role.requires_cla() { - self.check_user_has_signed_cla(user_id, room_id, &*tx) + self.check_user_has_signed_cla(user_id, room_id, &tx) .await?; } @@ -1029,7 +1021,7 @@ impl Database { .add(room_participant::Column::UserId.eq(user_id)), ) .set(room_participant::ActiveModel { - role: ActiveValue::set(Some(ChannelRole::from(role))), + role: ActiveValue::set(Some(role)), ..Default::default() }) .exec(&*tx) @@ -1038,7 +1030,7 @@ impl Database { if result.rows_affected != 1 { Err(anyhow!("could not update room participant role"))?; } - Ok(self.get_room(room_id, &tx).await?) + self.get_room(room_id, &tx).await }) .await } @@ -1084,10 +1076,9 @@ impl Database { pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> { self.transaction(|tx| async move { - self.room_connection_lost(connection, &*tx).await?; - self.channel_buffer_connection_lost(connection, &*tx) - .await?; - self.channel_chat_connection_lost(connection, &*tx).await?; + self.room_connection_lost(connection, &tx).await?; + self.channel_buffer_connection_lost(connection, &tx).await?; + self.channel_chat_connection_lost(connection, &tx).await?; Ok(()) }) .await diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index f0768a3a9c..e60ff63385 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -80,7 +80,7 @@ impl Database { github_login, github_user_id, github_email, - &*tx, + &tx, ) .await }) diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index 72d9835032..468e7390ab 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -14,6 +14,7 @@ pub mod extension; pub mod extension_version; pub mod feature_flag; pub mod follower; +pub mod hosted_project; pub mod language_server; pub mod notification; pub mod notification_kind; diff --git a/crates/collab/src/db/tables/hosted_project.rs b/crates/collab/src/db/tables/hosted_project.rs new file mode 100644 index 0000000000..265acd80ff --- /dev/null +++ b/crates/collab/src/db/tables/hosted_project.rs @@ -0,0 +1,18 @@ +use crate::db::{ChannelId, ChannelVisibility, HostedProjectId}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "hosted_projects")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: HostedProjectId, + pub channel_id: ChannelId, + pub name: String, + pub visibility: ChannelVisibility, + pub deleted_at: Option, +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} diff --git a/crates/collab/src/db/tables/project.rs b/crates/collab/src/db/tables/project.rs index 8c26836046..550f8415d7 100644 --- a/crates/collab/src/db/tables/project.rs +++ b/crates/collab/src/db/tables/project.rs @@ -1,4 +1,4 @@ -use crate::db::{ProjectId, Result, RoomId, ServerId, UserId}; +use crate::db::{HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId}; use anyhow::anyhow; use rpc::ConnectionId; use sea_orm::entity::prelude::*; @@ -8,10 +8,11 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: ProjectId, - pub room_id: RoomId, - pub host_user_id: UserId, + pub room_id: Option, + pub host_user_id: Option, pub host_connection_id: Option, pub host_connection_server_id: Option, + pub hosted_project_id: Option, } impl Model { diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 7790b951b2..35da659e54 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -23,7 +23,7 @@ pub struct TestDb { impl TestDb { pub fn sqlite(background: BackgroundExecutor) -> Self { - let url = format!("sqlite::memory:"); + let url = "sqlite::memory:"; let runtime = tokio::runtime::Builder::new_current_thread() .enable_io() .enable_time() @@ -109,13 +109,13 @@ macro_rules! test_both_dbs { ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => { #[gpui::test] async fn $postgres_test_name(cx: &mut gpui::TestAppContext) { - let test_db = crate::db::TestDb::postgres(cx.executor().clone()); + let test_db = $crate::db::TestDb::postgres(cx.executor().clone()); $test_name(test_db.db()).await; } #[gpui::test] async fn $sqlite_test_name(cx: &mut gpui::TestAppContext) { - let test_db = crate::db::TestDb::sqlite(cx.executor().clone()); + let test_db = $crate::db::TestDb::sqlite(cx.executor().clone()); $test_name(test_db.db()).await; } }; @@ -168,7 +168,7 @@ async fn new_test_user(db: &Arc, email: &str) -> UserId { email, false, NewUserParams { - github_login: email[0..email.find("@").unwrap()].to_string(), + github_login: email[0..email.find('@').unwrap()].to_string(), github_user_id: GITHUB_USER_ID.fetch_add(1, SeqCst), }, ) diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index c1015330bb..8f9492d648 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -68,11 +68,12 @@ async fn test_channel_buffers(db: &Arc) { .unwrap(); let mut buffer_a = Buffer::new(0, text::BufferId::new(1).unwrap(), "".to_string()); - let mut operations = Vec::new(); - operations.push(buffer_a.edit([(0..0, "hello world")])); - operations.push(buffer_a.edit([(5..5, ", cruel")])); - operations.push(buffer_a.edit([(0..5, "goodbye")])); - operations.push(buffer_a.undo().unwrap().1); + let operations = vec![ + buffer_a.edit([(0..0, "hello world")]), + buffer_a.edit([(5..5, ", cruel")]), + buffer_a.edit([(0..5, "goodbye")]), + buffer_a.undo().unwrap().1, + ]; assert_eq!(buffer_a.text(), "hello, cruel world"); let operations = operations @@ -222,7 +223,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { .unwrap(); buffers.push( - db.transaction(|tx| async move { db.get_channel_buffer(channel, &*tx).await }) + db.transaction(|tx| async move { db.get_channel_buffer(channel, &tx).await }) .await .unwrap(), ); @@ -238,7 +239,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { .transaction(|tx| { let buffers = &buffers; async move { - db.get_latest_operations_for_buffers([buffers[0].id, buffers[2].id], &*tx) + db.get_latest_operations_for_buffers([buffers[0].id, buffers[2].id], &tx) .await } }) @@ -302,7 +303,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { .transaction(|tx| { let buffers = &buffers; async move { - db.get_latest_operations_for_buffers([buffers[1].id, buffers[2].id], &*tx) + db.get_latest_operations_for_buffers([buffers[1].id, buffers[2].id], &tx) .await } }) @@ -320,7 +321,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { .transaction(|tx| { let buffers = &buffers; async move { - db.get_latest_operations_for_buffers([buffers[0].id, buffers[1].id], &*tx) + db.get_latest_operations_for_buffers([buffers[0].id, buffers[1].id], &tx) .await } }) @@ -342,7 +343,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { hash.insert(buffers[1].id, buffers[1].channel_id); hash.insert(buffers[2].id, buffers[2].channel_id); - async move { db.latest_channel_buffer_changes(&hash, &*tx).await } + async move { db.latest_channel_buffer_changes(&hash, &tx).await } }) .await .unwrap(); diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 7f74bf15aa..54be002c41 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -42,8 +42,8 @@ async fn test_channels(db: &Arc) { let mut members = db .transaction(|tx| async move { - let channel = db.get_channel_internal(replace_id, &*tx).await?; - Ok(db.get_channel_participants(&channel, &*tx).await?) + let channel = db.get_channel_internal(replace_id, &tx).await?; + db.get_channel_participants(&channel, &tx).await }) .await .unwrap(); @@ -464,9 +464,9 @@ async fn test_user_is_channel_participant(db: &Arc) { db.transaction(|tx| async move { db.check_user_is_channel_participant( - &db.get_channel_internal(public_channel_id, &*tx).await?, + &db.get_channel_internal(public_channel_id, &tx).await?, admin, - &*tx, + &tx, ) .await }) @@ -474,9 +474,9 @@ async fn test_user_is_channel_participant(db: &Arc) { .unwrap(); db.transaction(|tx| async move { db.check_user_is_channel_participant( - &db.get_channel_internal(public_channel_id, &*tx).await?, + &db.get_channel_internal(public_channel_id, &tx).await?, member, - &*tx, + &tx, ) .await }) @@ -517,9 +517,9 @@ async fn test_user_is_channel_participant(db: &Arc) { db.transaction(|tx| async move { db.check_user_is_channel_participant( - &db.get_channel_internal(public_channel_id, &*tx).await?, + &db.get_channel_internal(public_channel_id, &tx).await?, guest, - &*tx, + &tx, ) .await }) @@ -547,11 +547,11 @@ async fn test_user_is_channel_participant(db: &Arc) { assert!(db .transaction(|tx| async move { db.check_user_is_channel_participant( - &db.get_channel_internal(public_channel_id, &*tx) + &db.get_channel_internal(public_channel_id, &tx) .await .unwrap(), guest, - &*tx, + &tx, ) .await }) @@ -629,9 +629,9 @@ async fn test_user_is_channel_participant(db: &Arc) { db.transaction(|tx| async move { db.check_user_is_channel_participant( - &db.get_channel_internal(zed_channel, &*tx).await.unwrap(), + &db.get_channel_internal(zed_channel, &tx).await.unwrap(), guest, - &*tx, + &tx, ) .await }) @@ -640,11 +640,11 @@ async fn test_user_is_channel_participant(db: &Arc) { assert!(db .transaction(|tx| async move { db.check_user_is_channel_participant( - &db.get_channel_internal(internal_channel_id, &*tx) + &db.get_channel_internal(internal_channel_id, &tx) .await .unwrap(), guest, - &*tx, + &tx, ) .await }) @@ -653,11 +653,11 @@ async fn test_user_is_channel_participant(db: &Arc) { db.transaction(|tx| async move { db.check_user_is_channel_participant( - &db.get_channel_internal(public_channel_id, &*tx) + &db.get_channel_internal(public_channel_id, &tx) .await .unwrap(), guest, - &*tx, + &tx, ) .await }) diff --git a/crates/collab/src/db/tests/contributor_tests.rs b/crates/collab/src/db/tests/contributor_tests.rs index c826f0083a..a51817574a 100644 --- a/crates/collab/src/db/tests/contributor_tests.rs +++ b/crates/collab/src/db/tests/contributor_tests.rs @@ -10,16 +10,15 @@ test_both_dbs!( async fn test_contributors(db: &Arc) { db.create_user( - &format!("user1@example.com"), + "user1@example.com", false, NewUserParams { - github_login: format!("user1"), + github_login: "user1".to_string(), github_user_id: 1, }, ) .await - .unwrap() - .user_id; + .unwrap(); assert_eq!(db.get_contributors().await.unwrap(), Vec::::new()); diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 3149c8a8b6..2edaaa4314 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -87,8 +87,7 @@ async fn test_get_or_create_user_by_github_account(db: &Arc) { }, ) .await - .unwrap() - .user_id; + .unwrap(); let user_id2 = db .create_user( "user2@example.com", @@ -495,7 +494,7 @@ async fn test_project_count(db: &Arc) { let user1 = db .create_user( - &format!("admin@example.com"), + "admin@example.com", true, NewUserParams { github_login: "admin".into(), @@ -506,7 +505,7 @@ async fn test_project_count(db: &Arc) { .unwrap(); let user2 = db .create_user( - &format!("user@example.com"), + "user@example.com", false, NewUserParams { github_login: "user".into(), diff --git a/crates/collab/src/db/tests/feature_flag_tests.rs b/crates/collab/src/db/tests/feature_flag_tests.rs index 0286a6308e..5269d5354f 100644 --- a/crates/collab/src/db/tests/feature_flag_tests.rs +++ b/crates/collab/src/db/tests/feature_flag_tests.rs @@ -13,10 +13,10 @@ test_both_dbs!( async fn test_get_user_flags(db: &Arc) { let user_1 = db .create_user( - &format!("user1@example.com"), + "user1@example.com", false, NewUserParams { - github_login: format!("user1"), + github_login: "user1".to_string(), github_user_id: 1, }, ) @@ -26,10 +26,10 @@ async fn test_get_user_flags(db: &Arc) { let user_2 = db .create_user( - &format!("user2@example.com"), + "user2@example.com", false, NewUserParams { - github_login: format!("user2"), + github_login: "user2".to_string(), github_user_id: 2, }, ) @@ -37,8 +37,8 @@ async fn test_get_user_flags(db: &Arc) { .unwrap() .user_id; - const CHANNELS_ALPHA: &'static str = "channels-alpha"; - const NEW_SEARCH: &'static str = "new-search"; + const CHANNELS_ALPHA: &str = "channels-alpha"; + const NEW_SEARCH: &str = "new-search"; let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap(); let search_flag = db.create_user_flag(NEW_SEARCH).await.unwrap(); diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index c785e3cb73..e20473d3bd 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -297,7 +297,7 @@ async fn test_unseen_channel_messages(db: &Arc) { // Check that observer has new messages let latest_messages = db .transaction(|tx| async move { - db.latest_channel_messages(&[channel_1, channel_2], &*tx) + db.latest_channel_messages(&[channel_1, channel_2], &tx) .await }) .await diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 195ed7b11d..0be28c1358 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -58,11 +58,24 @@ impl From for Error { impl IntoResponse for Error { fn into_response(self) -> axum::response::Response { match self { - Error::Http(code, message) => (code, message).into_response(), + Error::Http(code, message) => { + log::error!("HTTP error {}: {}", code, &message); + (code, message).into_response() + } Error::Database(error) => { + log::error!( + "HTTP error {}: {:?}", + StatusCode::INTERNAL_SERVER_ERROR, + &error + ); (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response() } Error::Internal(error) => { + log::error!( + "HTTP error {}: {:?}", + StatusCode::INTERNAL_SERVER_ERROR, + &error + ); (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response() } } @@ -97,6 +110,10 @@ pub struct Config { pub database_url: String, pub database_max_connections: u32, pub api_token: String, + pub clickhouse_url: Option, + pub clickhouse_user: Option, + pub clickhouse_password: Option, + pub clickhouse_database: Option, pub invite_link_prefix: String, pub live_kit_server: Option, pub live_kit_key: Option, @@ -109,6 +126,8 @@ pub struct Config { pub blob_store_secret_key: Option, pub blob_store_bucket: Option, pub zed_environment: Arc, + pub zed_client_checksum_seed: Option, + pub slack_panics_webhook: Option, } impl Config { @@ -127,6 +146,7 @@ pub struct AppState { pub db: Arc, pub live_kit_client: Option>, pub blob_store_client: Option, + pub clickhouse_client: Option, pub config: Config, } @@ -156,6 +176,10 @@ impl AppState { db: Arc::new(db), live_kit_client, blob_store_client: build_blob_store_client(&config).await.log_err(), + clickhouse_client: config + .clickhouse_url + .as_ref() + .and_then(|_| build_clickhouse_client(&config).log_err()), config, }; Ok(Arc::new(this)) @@ -196,3 +220,31 @@ async fn build_blob_store_client(config: &Config) -> anyhow::Result anyhow::Result { + Ok(clickhouse::Client::default() + .with_url( + config + .clickhouse_url + .as_ref() + .ok_or_else(|| anyhow!("missing clickhouse_url"))?, + ) + .with_user( + config + .clickhouse_user + .as_ref() + .ok_or_else(|| anyhow!("missing clickhouse_user"))?, + ) + .with_password( + config + .clickhouse_password + .as_ref() + .ok_or_else(|| anyhow!("missing clickhouse_password"))?, + ) + .with_database( + config + .clickhouse_database + .as_ref() + .ok_or_else(|| anyhow!("missing clickhouse_database"))?, + )) +} diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index b80e8961df..add6bc47f8 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -1,22 +1,26 @@ use anyhow::anyhow; -use axum::{routing::get, Extension, Router}; +use axum::{extract::MatchedPath, routing::get, Extension, Router}; use collab::{ api::fetch_extensions_from_blob_store_periodically, db, env, executor::Executor, AppState, Config, MigrateConfig, Result, }; use db::Database; +use hyper::Request; use std::{ env::args, net::{SocketAddr, TcpListener}, path::Path, sync::Arc, }; +#[cfg(unix)] use tokio::signal::unix::SignalKind; +use tower_http::trace::{self, TraceLayer}; +use tracing::Level; use tracing_log::LogTracer; use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer}; use util::ResultExt; -const VERSION: &'static str = env!("CARGO_PKG_VERSION"); +const VERSION: &str = env!("CARGO_PKG_VERSION"); const REVISION: Option<&'static str> = option_env!("GITHUB_SHA"); #[tokio::main] @@ -28,7 +32,8 @@ async fn main() -> Result<()> { ); } - match args().skip(1).next().as_deref() { + let mut args = args().skip(1); + match args.next().as_deref() { Some("version") => { println!("collab v{} ({})", VERSION, REVISION.unwrap_or("unknown")); } @@ -36,6 +41,17 @@ async fn main() -> Result<()> { run_migrations().await?; } Some("serve") => { + let (is_api, is_collab) = if let Some(next) = args.next() { + (next == "api", next == "collab") + } else { + (true, true) + }; + if !is_api && !is_collab { + Err(anyhow!( + "usage: collab " + ))?; + } + let config = envy::from_env::().expect("error loading config"); init_tracing(&config); @@ -46,24 +62,55 @@ async fn main() -> Result<()> { let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port)) .expect("failed to bind TCP listener"); - let epoch = state - .db - .create_server(&state.config.zed_environment) - .await?; - let rpc_server = collab::rpc::Server::new(epoch, state.clone(), Executor::Production); - rpc_server.start().await?; + let rpc_server = if is_collab { + let epoch = state + .db + .create_server(&state.config.zed_environment) + .await?; + let rpc_server = + collab::rpc::Server::new(epoch, state.clone(), Executor::Production); + rpc_server.start().await?; - fetch_extensions_from_blob_store_periodically(state.clone(), Executor::Production); + Some(rpc_server) + } else { + None + }; - let app = collab::api::routes(rpc_server.clone(), state.clone()) - .merge(collab::rpc::routes(rpc_server.clone())) + if is_api { + fetch_extensions_from_blob_store_periodically(state.clone(), Executor::Production); + } + + let mut app = collab::api::routes(rpc_server.clone(), state.clone()); + if let Some(rpc_server) = rpc_server.clone() { + app = app.merge(collab::rpc::routes(rpc_server)) + } + app = app .merge( Router::new() .route("/", get(handle_root)) .route("/healthz", get(handle_liveness_probe)) + .merge(collab::api::extensions::router()) + .merge(collab::api::events::router()) .layer(Extension(state.clone())), + ) + .layer( + TraceLayer::new_for_http() + .make_span_with(|request: &Request<_>| { + let matched_path = request + .extensions() + .get::() + .map(MatchedPath::as_str); + + tracing::info_span!( + "http_request", + method = ?request.method(), + matched_path, + ) + }) + .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), ); + #[cfg(unix)] axum::Server::from_tcp(listener)? .serve(app.into_make_service_with_connect_info::()) .with_graceful_shutdown(async move { @@ -76,12 +123,21 @@ async fn main() -> Result<()> { futures::pin_mut!(sigterm, sigint); futures::future::select(sigterm, sigint).await; tracing::info!("Received interrupt signal"); - rpc_server.teardown(); + + if let Some(rpc_server) = rpc_server { + rpc_server.teardown(); + } }) .await?; + + // todo("windows") + #[cfg(windows)] + unimplemented!(); } _ => { - Err(anyhow!("usage: collab "))?; + Err(anyhow!( + "usage: collab " + ))?; } } Ok(()) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 086465965b..4fda8b9959 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -4,8 +4,9 @@ use crate::{ auth::{self, Impersonator}, db::{ self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database, - InviteMemberResult, MembershipUpdated, MessageId, NotificationId, ProjectId, - RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, User, UserId, + HostedProjectId, InviteMemberResult, MembershipUpdated, MessageId, NotificationId, Project, + ProjectId, RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId, ServerId, + User, UserId, }, executor::Executor, AppState, Error, Result, @@ -28,14 +29,13 @@ use axum::{ Extension, Router, TypedHeader, }; use collections::{HashMap, HashSet}; -pub use connection_pool::ConnectionPool; +pub use connection_pool::{ConnectionPool, ZedVersion}; use futures::{ channel::oneshot, future::{self, BoxFuture}, stream::FuturesUnordered, FutureExt, SinkExt, StreamExt, TryStreamExt, }; -use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ @@ -56,7 +56,7 @@ use std::{ rc::Rc, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, - Arc, + Arc, OnceLock, }, time::{Duration, Instant}, }; @@ -73,16 +73,6 @@ const MESSAGE_COUNT_PER_PAGE: usize = 100; const MAX_MESSAGE_LEN: usize = 1024; const NOTIFICATION_COUNT_PER_PAGE: usize = 50; -lazy_static! { - static ref METRIC_CONNECTIONS: IntGauge = - register_int_gauge!("connections", "number of connections").unwrap(); - static ref METRIC_SHARED_PROJECTS: IntGauge = register_int_gauge!( - "shared_projects", - "number of open projects with one or more guests" - ) - .unwrap(); -} - type MessageHandler = Box, Session) -> BoxFuture<'static, ()>>; @@ -158,7 +148,7 @@ pub struct Server { app_state: Arc, executor: Executor, handlers: HashMap, - teardown: watch::Sender<()>, + teardown: watch::Sender, } pub(crate) struct ConnectionPoolGuard<'a> { @@ -191,7 +181,7 @@ impl Server { executor, connection_pool: Default::default(), handlers: Default::default(), - teardown: watch::channel(()).0, + teardown: watch::channel(false).0, }; server @@ -208,6 +198,7 @@ impl Server { .add_request_handler(share_project) .add_message_handler(unshare_project) .add_request_handler(join_project) + .add_request_handler(join_hosted_project) .add_message_handler(leave_project) .add_request_handler(update_project) .add_request_handler(update_worktree) @@ -364,7 +355,7 @@ impl Server { &refreshed_room.room, &refreshed_room.channel_members, &peer, - &*pool.lock(), + &pool.lock(), ); } contacts_to_update @@ -447,7 +438,7 @@ impl Server { pub fn teardown(&self) { self.peer.teardown(); self.connection_pool.lock().reset(); - let _ = self.teardown.send(()); + let _ = self.teardown.send(true); } #[cfg(test)] @@ -455,6 +446,7 @@ impl Server { self.teardown(); *self.id.lock() = id; self.peer.reset(id.0 as u32); + let _ = self.teardown.send(false); } #[cfg(test)] @@ -553,11 +545,13 @@ impl Server { }) } + #[allow(clippy::too_many_arguments)] pub fn handle_connection( self: &Arc, connection: Connection, address: String, user: User, + zed_version: ZedVersion, impersonator: Option, mut send_connection_id: Option>, executor: Executor, @@ -571,6 +565,9 @@ impl Server { } let mut teardown = self.teardown.subscribe(); async move { + if *teardown.borrow() { + return Err(anyhow!("server is tearing down"))?; + } let (connection_id, handle_io, mut incoming_rx) = this .peer .add_connection(connection, { @@ -599,7 +596,7 @@ impl Server { { let mut pool = this.connection_pool.lock(); - pool.add_connection(connection_id, user_id, user.admin); + pool.add_connection(connection_id, user_id, user.admin, zed_version); this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; this.peer.send(connection_id, build_update_user_channels(&channels_for_user))?; this.peer.send(connection_id, build_channels_update( @@ -759,13 +756,13 @@ impl<'a> Deref for ConnectionPoolGuard<'a> { type Target = ConnectionPool; fn deref(&self) -> &Self::Target { - &*self.guard + &self.guard } } impl<'a> DerefMut for ConnectionPoolGuard<'a> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut *self.guard + &mut self.guard } } @@ -792,16 +789,12 @@ fn broadcast( } } -lazy_static! { - static ref ZED_PROTOCOL_VERSION: HeaderName = HeaderName::from_static("x-zed-protocol-version"); - static ref ZED_APP_VERSION: HeaderName = HeaderName::from_static("x-zed-app-version"); -} - pub struct ProtocolVersion(u32); impl Header for ProtocolVersion { fn name() -> &'static HeaderName { - &ZED_PROTOCOL_VERSION + static ZED_PROTOCOL_VERSION: OnceLock = OnceLock::new(); + ZED_PROTOCOL_VERSION.get_or_init(|| HeaderName::from_static("x-zed-protocol-version")) } fn decode<'i, I>(values: &mut I) -> Result @@ -827,7 +820,8 @@ impl Header for ProtocolVersion { pub struct AppVersionHeader(SemanticVersion); impl Header for AppVersionHeader { fn name() -> &'static HeaderName { - &ZED_APP_VERSION + static ZED_APP_VERSION: OnceLock = OnceLock::new(); + ZED_APP_VERSION.get_or_init(|| HeaderName::from_static("x-zed-app-version")) } fn decode<'i, I>(values: &mut I) -> Result @@ -879,17 +873,20 @@ pub async fn handle_websocket_request( .into_response(); } - // the first version of zed that sent this header was 0.121.x - if let Some(version) = app_version_header.map(|header| header.0 .0) { - // 0.123.0 was a nightly version with incompatible collab changes - // that were reverted. - if version == "0.123.0".parse().unwrap() { - return ( - StatusCode::UPGRADE_REQUIRED, - "client must be upgraded".to_string(), - ) - .into_response(); - } + let Some(version) = app_version_header.map(|header| ZedVersion(header.0 .0)) else { + return ( + StatusCode::UPGRADE_REQUIRED, + "no version header found".to_string(), + ) + .into_response(); + }; + + if !version.is_supported() { + return ( + StatusCode::UPGRADE_REQUIRED, + "client must be upgraded".to_string(), + ) + .into_response(); } let socket_address = socket_address.to_string(); @@ -906,6 +903,7 @@ pub async fn handle_websocket_request( connection, socket_address, user, + version, impersonator.0, None, Executor::Production, @@ -917,17 +915,29 @@ pub async fn handle_websocket_request( } pub async fn handle_metrics(Extension(server): Extension>) -> Result { + static CONNECTIONS_METRIC: OnceLock = OnceLock::new(); + let connections_metric = CONNECTIONS_METRIC + .get_or_init(|| register_int_gauge!("connections", "number of connections").unwrap()); + let connections = server .connection_pool .lock() .connections() .filter(|connection| !connection.admin) .count(); + connections_metric.set(connections as _); - METRIC_CONNECTIONS.set(connections as _); + static SHARED_PROJECTS_METRIC: OnceLock = OnceLock::new(); + let shared_projects_metric = SHARED_PROJECTS_METRIC.get_or_init(|| { + register_int_gauge!( + "shared_projects", + "number of open projects with one or more guests" + ) + .unwrap() + }); let shared_projects = server.app_state.db.project_count_excluding_admins().await?; - METRIC_SHARED_PROJECTS.set(shared_projects as _); + shared_projects_metric.set(shared_projects as _); let encoder = prometheus::TextEncoder::new(); let metric_families = prometheus::gather(); @@ -940,7 +950,7 @@ pub async fn handle_metrics(Extension(server): Extension>) -> Result #[instrument(err, skip(executor))] async fn connection_lost( session: Session, - mut teardown: watch::Receiver<()>, + mut teardown: watch::Receiver, executor: Executor, ) -> Result<()> { session.peer.disconnect(session.connection_id); @@ -1311,6 +1321,22 @@ async fn set_room_participant_role( response: Response, session: Session, ) -> Result<()> { + let user_id = UserId::from_proto(request.user_id); + let role = ChannelRole::from(request.role()); + + if role == ChannelRole::Talker { + let pool = session.connection_pool().await; + + for connection in pool.user_connections(user_id) { + if !connection.zed_version.supports_talker_role() { + Err(anyhow!( + "This user is on zed {} which does not support unmute", + connection.zed_version + ))?; + } + } + } + let (live_kit_room, can_publish) = { let room = session .db() @@ -1318,13 +1344,13 @@ async fn set_room_participant_role( .set_room_participant_role( session.user_id, RoomId::from_proto(request.room_id), - UserId::from_proto(request.user_id), - ChannelRole::from(request.role()), + user_id, + role, ) .await?; let live_kit_room = room.live_kit_room.clone(); - let can_publish = ChannelRole::from(request.role()).can_publish_to_rooms(); + let can_publish = ChannelRole::from(request.role()).can_use_microphone(); room_updated(&room, &session.peer); (live_kit_room, can_publish) }; @@ -1560,22 +1586,46 @@ async fn join_project( session: Session, ) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); - let guest_user_id = session.user_id; tracing::info!(%project_id, "join project"); let (project, replica_id) = &mut *session .db() .await - .join_project(project_id, session.connection_id) + .join_project_in_room(project_id, session.connection_id) .await?; + join_project_internal(response, session, project, replica_id) +} + +trait JoinProjectInternalResponse { + fn send(self, result: proto::JoinProjectResponse) -> Result<()>; +} +impl JoinProjectInternalResponse for Response { + fn send(self, result: proto::JoinProjectResponse) -> Result<()> { + Response::::send(self, result) + } +} +impl JoinProjectInternalResponse for Response { + fn send(self, result: proto::JoinProjectResponse) -> Result<()> { + Response::::send(self, result) + } +} + +fn join_project_internal( + response: impl JoinProjectInternalResponse, + session: Session, + project: &mut Project, + replica_id: &ReplicaId, +) -> Result<()> { let collaborators = project .collaborators .iter() .filter(|collaborator| collaborator.connection_id != session.connection_id) .map(|collaborator| collaborator.to_proto()) .collect::>(); + let project_id = project.id; + let guest_user_id = session.user_id; let worktrees = project .worktrees @@ -1607,10 +1657,12 @@ async fn join_project( // First, we send the metadata associated with each worktree. response.send(proto::JoinProjectResponse { + project_id: project.id.0 as u64, worktrees: worktrees.clone(), replica_id: replica_id.0 as u32, collaborators: collaborators.clone(), language_servers: project.language_servers.clone(), + role: project.role.into(), // todo })?; for (worktree_id, worktree) in mem::take(&mut project.worktrees) { @@ -1683,15 +1735,17 @@ async fn join_project( async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> { let sender_id = session.connection_id; let project_id = ProjectId::from_proto(request.project_id); + let db = session.db().await; + if db.is_hosted_project(project_id).await? { + let project = db.leave_hosted_project(project_id, sender_id).await?; + project_left(&project, &session); + return Ok(()); + } - let (room, project) = &*session - .db() - .await - .leave_project(project_id, sender_id) - .await?; + let (room, project) = &*db.leave_project(project_id, sender_id).await?; tracing::info!( %project_id, - host_user_id = %project.host_user_id, + host_user_id = ?project.host_user_id, host_connection_id = ?project.host_connection_id, "leave project" ); @@ -1702,6 +1756,24 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result Ok(()) } +async fn join_hosted_project( + request: proto::JoinHostedProject, + response: Response, + session: Session, +) -> Result<()> { + let (mut project, replica_id) = session + .db() + .await + .join_hosted_project( + HostedProjectId(request.id as i32), + session.user_id, + session.connection_id, + ) + .await?; + + join_project_internal(response, session, &mut project, &replica_id) +} + /// Updates other participants with changes to the project async fn update_project( request: proto::UpdateProject, @@ -2088,21 +2160,16 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) -> }; // For now, don't send view update messages back to that view's current leader. - let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant { + let peer_id_to_omit = request.variant.as_ref().and_then(|variant| match variant { proto::update_followers::Variant::UpdateView(payload) => payload.leader_id, _ => None, }); - for follower_peer_id in request.follower_ids.iter().copied() { - let follower_connection_id = follower_peer_id.into(); - if Some(follower_peer_id) != connection_id_to_omit - && connection_ids.contains(&follower_connection_id) - { - session.peer.forward_send( - session.connection_id, - follower_connection_id, - request.clone(), - )?; + for connection_id in connection_ids.iter().cloned() { + if Some(connection_id.into()) != peer_id_to_omit && connection_id != session.connection_id { + session + .peer + .forward_send(session.connection_id, connection_id, request.clone())?; } } Ok(()) @@ -2207,7 +2274,7 @@ async fn request_contact( session.peer.send(connection_id, update.clone())?; } - send_notifications(&*connection_pool, &session.peer, notifications); + send_notifications(&connection_pool, &session.peer, notifications); response.send(proto::Ack {})?; Ok(()) @@ -2264,7 +2331,7 @@ async fn respond_to_contact_request( session.peer.send(connection_id, update.clone())?; } - send_notifications(&*pool, &session.peer, notifications); + send_notifications(&pool, &session.peer, notifications); } response.send(proto::Ack {})?; @@ -2432,7 +2499,7 @@ async fn invite_channel_member( session.peer.send(connection_id, update.clone())?; } - send_notifications(&*connection_pool, &session.peer, notifications); + send_notifications(&connection_pool, &session.peer, notifications); response.send(proto::Ack {})?; Ok(()) @@ -2694,7 +2761,7 @@ async fn respond_to_channel_invite( } }; - send_notifications(&*connection_pool, &session.peer, notifications); + send_notifications(&connection_pool, &session.peer, notifications); response.send(proto::Ack {})?; @@ -2864,7 +2931,7 @@ async fn update_channel_buffer( .flat_map(|user_id| pool.user_connection_ids(*user_id)), |peer_id| { session.peer.send( - peer_id.into(), + peer_id, proto::UpdateChannels { latest_channel_buffer_versions: vec![proto::ChannelBufferVersion { channel_id: channel_id.to_proto(), @@ -2949,8 +3016,8 @@ fn channel_buffer_updated( message: &T, peer: &Peer, ) { - broadcast(Some(sender_id), collaborators.into_iter(), |peer_id| { - peer.send(peer_id.into(), message.clone()) + broadcast(Some(sender_id), collaborators, |peer_id| { + peer.send(peer_id, message.clone()) }); } @@ -3055,7 +3122,7 @@ async fn send_channel_message( .flat_map(|user_id| pool.user_connection_ids(*user_id)), |peer_id| { session.peer.send( - peer_id.into(), + peer_id, proto::UpdateChannels { latest_channel_message_ids: vec![proto::ChannelMessageId { channel_id: channel_id.to_proto(), @@ -3351,7 +3418,6 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh .collect(), observed_channel_buffer_version: channels.observed_buffer_versions.clone(), observed_channel_message_id: channels.observed_channel_messages.clone(), - ..Default::default() } } @@ -3380,6 +3446,9 @@ fn build_channels_update( for channel in channel_invites { update.channel_invitations.push(channel.to_proto()); } + for project in channels.hosted_projects { + update.hosted_projects.push(project); + } update } @@ -3425,7 +3494,7 @@ fn room_updated(room: &proto::Room, peer: &Peer) { .filter_map(|participant| Some(participant.peer_id?.into())), |peer_id| { peer.send( - peer_id.into(), + peer_id, proto::RoomUpdated { room: Some(room.clone()), }, @@ -3454,7 +3523,7 @@ fn channel_updated( .flat_map(|user_id| pool.user_connection_ids(*user_id)), |peer_id| { peer.send( - peer_id.into(), + peer_id, proto::UpdateChannels { channel_participants: vec![proto::ChannelParticipants { channel_id: channel_id.to_proto(), @@ -3603,7 +3672,7 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> { fn project_left(project: &db::LeftProject, session: &Session) { for connection_id in &project.connection_ids { - if project.host_user_id == session.user_id { + if project.host_user_id == Some(session.user_id) { session .peer .send( diff --git a/crates/collab/src/rpc/connection_pool.rs b/crates/collab/src/rpc/connection_pool.rs index 30c4e144ed..2d28290373 100644 --- a/crates/collab/src/rpc/connection_pool.rs +++ b/crates/collab/src/rpc/connection_pool.rs @@ -4,6 +4,7 @@ use collections::{BTreeMap, HashSet}; use rpc::ConnectionId; use serde::Serialize; use tracing::instrument; +use util::SemanticVersion; #[derive(Default, Serialize)] pub struct ConnectionPool { @@ -16,10 +17,30 @@ struct ConnectedUser { connection_ids: HashSet, } +#[derive(Debug, Serialize)] +pub struct ZedVersion(pub SemanticVersion); +use std::fmt; + +impl fmt::Display for ZedVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl ZedVersion { + pub fn is_supported(&self) -> bool { + self.0 != SemanticVersion::new(0, 123, 0) + } + pub fn supports_talker_role(&self) -> bool { + self.0 >= SemanticVersion::new(0, 125, 0) + } +} + #[derive(Serialize)] pub struct Connection { pub user_id: UserId, pub admin: bool, + pub zed_version: ZedVersion, } impl ConnectionPool { @@ -29,9 +50,21 @@ impl ConnectionPool { } #[instrument(skip(self))] - pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) { - self.connections - .insert(connection_id, Connection { user_id, admin }); + pub fn add_connection( + &mut self, + connection_id: ConnectionId, + user_id: UserId, + admin: bool, + zed_version: ZedVersion, + ) { + self.connections.insert( + connection_id, + Connection { + user_id, + admin, + zed_version, + }, + ); let connected_user = self.connected_users.entry(user_id).or_default(); connected_user.connection_ids.insert(connection_id); } @@ -57,12 +90,23 @@ impl ConnectionPool { self.connections.values() } + pub fn user_connections(&self, user_id: UserId) -> impl Iterator + '_ { + self.connected_users + .get(&user_id) + .into_iter() + .flat_map(|state| { + state + .connection_ids + .iter() + .flat_map(|cid| self.connections.get(cid)) + }) + } + pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator + '_ { self.connected_users .get(&user_id) .into_iter() - .map(|state| &state.connection_ids) - .flatten() + .flat_map(|state| &state.connection_ids) .copied() } diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index aca9329d5a..6185b8d582 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -1,4 +1,7 @@ +use std::sync::Arc; + use call::Room; +use client::ChannelId; use gpui::{Model, TestAppContext}; mod channel_buffer_tests; @@ -14,6 +17,7 @@ mod random_project_collaboration_tests; mod randomized_test_helpers; mod test_server; +use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher}; pub use randomized_test_helpers::{ run_randomized_test, save_randomized_test_plan, RandomizedTest, TestError, UserTestPlan, }; @@ -43,6 +47,20 @@ fn room_participants(room: &Model, cx: &mut TestAppContext) -> RoomPartici }) } -fn channel_id(room: &Model, cx: &mut TestAppContext) -> Option { +fn channel_id(room: &Model, cx: &mut TestAppContext) -> Option { cx.read(|cx| room.read(cx).channel_id()) } + +fn rust_lang() -> Arc { + Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )) +} diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index bb1f493f0c..24828e42ef 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -104,7 +104,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test }); assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx))); - assert!(room_b.read_with(cx_b, |room, _| room.read_only())); + assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone())); assert!(room_b .update(cx_b, |room, cx| room.share_microphone(cx)) .await @@ -130,7 +130,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx))); // B sees themselves as muted, and can unmute. - assert!(room_b.read_with(cx_b, |room, _| !room.read_only())); + assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone())); room_b.read_with(cx_b, |room, _| assert!(room.is_muted())); room_b.update(cx_b, |room, cx| room.toggle_mute(cx)); cx_a.run_until_parked(); @@ -183,7 +183,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes server .app_state .db - .set_channel_requires_zed_cla(ChannelId::from_proto(parent_channel_id), true) + .set_channel_requires_zed_cla(ChannelId::from_proto(parent_channel_id.0), true) .await .unwrap(); @@ -223,7 +223,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes let room_b = cx_b .read(ActiveCall::global) .update(cx_b, |call, _| call.room().unwrap().clone()); - assert!(room_b.read_with(cx_b, |room, _| room.read_only())); + assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone())); // A tries to grant write access to B, but cannot because B has not // yet signed the zed CLA. @@ -240,7 +240,26 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes .await .unwrap_err(); cx_a.run_until_parked(); - assert!(room_b.read_with(cx_b, |room, _| room.read_only())); + assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects())); + assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone())); + + // A tries to grant write access to B, but cannot because B has not + // yet signed the zed CLA. + active_call_a + .update(cx_a, |call, cx| { + call.room().unwrap().update(cx, |room, cx| { + room.set_participant_role( + client_b.user_id().unwrap(), + proto::ChannelRole::Talker, + cx, + ) + }) + }) + .await + .unwrap(); + cx_a.run_until_parked(); + assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects())); + assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone())); // User B signs the zed CLA. server @@ -264,5 +283,6 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes .await .unwrap(); cx_a.run_until_parked(); - assert!(room_b.read_with(cx_b, |room, _| !room.read_only())); + assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects())); + assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone())); } diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs index 081ac8d19a..18462a0e24 100644 --- a/crates/collab/src/tests/channel_message_tests.rs +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -100,13 +100,13 @@ async fn test_basic_channel_messages( Notification::ChannelMessageMention { message_id, sender_id: client_a.id(), - channel_id, + channel_id: channel_id.0, } ); assert_eq!( store.notification_at(1).unwrap().notification, Notification::ChannelInvitation { - channel_id, + channel_id: channel_id.0, channel_name: "the-channel".to_string(), inviter_id: client_a.id() } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index eda7377c77..14b0a87485 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -4,8 +4,8 @@ use crate::{ tests::{room_participants, RoomParticipants, TestServer}, }; use call::ActiveCall; -use channel::{ChannelId, ChannelMembership, ChannelStore}; -use client::User; +use channel::{ChannelMembership, ChannelStore}; +use client::{ChannelId, User}; use futures::future::try_join_all; use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext}; use rpc::{ @@ -281,7 +281,7 @@ async fn test_core_channels( .app_state .db .rename_channel( - db::ChannelId::from_proto(channel_a_id), + db::ChannelId::from_proto(channel_a_id.0), UserId::from_proto(client_a.id()), "channel-a-renamed", ) @@ -1431,7 +1431,7 @@ fn assert_channels( .ordered_channels() .map(|(depth, channel)| ExpectedChannel { depth, - name: channel.name.clone().into(), + name: channel.name.clone(), id: channel.id, }) .collect::>() @@ -1444,7 +1444,7 @@ fn assert_channels( fn assert_channels_list_shape( channel_store: &Model, cx: &TestAppContext, - expected_channels: &[(u64, usize)], + expected_channels: &[(ChannelId, usize)], ) { let actual = cx.read(|cx| { channel_store.read_with(cx, |store, _| { diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 458f347efb..50a798b5a6 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -1,11 +1,7 @@ -use std::{ - path::Path, - sync::{ - atomic::{self, AtomicBool, AtomicUsize}, - Arc, - }, +use crate::{ + rpc::RECONNECT_TIMEOUT, + tests::{rust_lang, TestServer}, }; - use call::ActiveCall; use editor::{ actions::{ @@ -19,16 +15,21 @@ use gpui::{TestAppContext, VisualContext, VisualTestContext}; use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, InlayHintSettings}, - tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, + FakeLspAdapter, }; use rpc::RECEIVE_TIMEOUT; use serde_json::json; use settings::SettingsStore; +use std::{ + path::Path, + sync::{ + atomic::{self, AtomicBool, AtomicUsize}, + Arc, + }, +}; use text::Point; use workspace::Workspace; -use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; - #[gpui::test(iterations = 10)] async fn test_host_disconnect( cx_a: &mut TestAppContext, @@ -265,20 +266,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu .await; let active_call_a = cx_a.read(ActiveCall::global); - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { capabilities: lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string()]), @@ -288,9 +279,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu ..Default::default() }, ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); + }, + ); client_a .fs() @@ -455,19 +445,10 @@ async fn test_collaborating_with_code_actions( cx_b.update(editor::init); // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a + .language_registry() + .register_fake_lsp_adapter("Rust", FakeLspAdapter::default()); client_a .fs() @@ -671,19 +652,10 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T cx_b.update(editor::init); // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { capabilities: lsp::ServerCapabilities { rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { prepare_provider: Some(true), @@ -692,9 +664,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T ..Default::default() }, ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); + }, + ); client_a .fs() @@ -858,25 +829,14 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes cx_b.update(editor::init); - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { name: "the-language-server", ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); + }, + ); client_a .fs() @@ -1152,20 +1112,10 @@ async fn test_on_input_format_from_host_to_guest( .await; let active_call_a = cx_a.read(ActiveCall::global); - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { capabilities: lsp::ServerCapabilities { document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { first_trigger_character: ":".to_string(), @@ -1174,9 +1124,8 @@ async fn test_on_input_format_from_host_to_guest( ..Default::default() }, ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); + }, + ); client_a .fs() @@ -1283,20 +1232,10 @@ async fn test_on_input_format_from_guest_to_host( .await; let active_call_a = cx_a.read(ActiveCall::global); - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { capabilities: lsp::ServerCapabilities { document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { first_trigger_character: ":".to_string(), @@ -1305,9 +1244,8 @@ async fn test_on_input_format_from_guest_to_host( ..Default::default() }, ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); + }, + ); client_a .fs() @@ -1426,6 +1364,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( store.update_user_settings::(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: false, show_other_hints: true, @@ -1438,6 +1378,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( store.update_user_settings::(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: false, show_other_hints: true, @@ -1446,29 +1388,18 @@ async fn test_mutual_editor_inlay_hint_cache_update( }); }); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + client_a.language_registry().add(rust_lang()); + client_b.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), ..Default::default() }, ..Default::default() - })) - .await; - let language = Arc::new(language); - client_a.language_registry().add(Arc::clone(&language)); - client_b.language_registry().add(language); + }, + ); // Client A opens a project. client_a @@ -1695,6 +1626,8 @@ async fn test_inlay_hint_refresh_is_forwarded( store.update_user_settings::(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: false, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: false, show_parameter_hints: false, show_other_hints: false, @@ -1707,6 +1640,8 @@ async fn test_inlay_hint_refresh_is_forwarded( store.update_user_settings::(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, @@ -1715,29 +1650,18 @@ async fn test_inlay_hint_refresh_is_forwarded( }); }); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + client_a.language_registry().add(rust_lang()); + client_b.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), ..Default::default() }, ..Default::default() - })) - .await; - let language = Arc::new(language); - client_a.language_registry().add(Arc::clone(&language)); - client_b.language_registry().add(language); + }, + ); client_a .fs() diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index acc129e675..57e5388045 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1,5 +1,6 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use call::{ActiveCall, ParticipantLocation}; +use client::ChannelId; use collab_ui::{ channel_view::ChannelView, notifications::project_shared_notification::ProjectSharedNotification, @@ -309,7 +310,7 @@ async fn test_basic_following( let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { let editor = cx.new_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); - workspace.add_item(Box::new(editor.clone()), cx); + workspace.add_item_to_active_pane(Box::new(editor.clone()), cx); editor }); executor.run_until_parked(); @@ -1436,14 +1437,13 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut }); executor.run_until_parked(); - let window_b_project_a = cx_b + let window_b_project_a = *cx_b .windows() .iter() .max_by_key(|window| window.window_id()) - .unwrap() - .clone(); + .unwrap(); - let mut cx_b2 = VisualTestContext::from_window(window_b_project_a.clone(), cx_b); + let mut cx_b2 = VisualTestContext::from_window(window_b_project_a, cx_b); let workspace_b_project_a = window_b_project_a .downcast::() @@ -1534,7 +1534,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut executor.run_until_parked(); assert_eq!(visible_push_notifications(cx_a).len(), 1); cx_a.update(|cx| { - workspace::join_remote_project( + workspace::join_in_room_project( project_b_id, client_b.user_id().unwrap(), client_a.app_state.clone(), @@ -1547,13 +1547,12 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut executor.run_until_parked(); assert_eq!(visible_push_notifications(cx_a).len(), 0); - let window_a_project_b = cx_a + let window_a_project_b = *cx_a .windows() .iter() .max_by_key(|window| window.window_id()) - .unwrap() - .clone(); - let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b.clone(), cx_a); + .unwrap(); + let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a); let workspace_a_project_b = window_a_project_b .downcast::() .unwrap() @@ -1577,7 +1576,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut #[gpui::test] async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { - let (_, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await; + let (_server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await; let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await; client_a @@ -2000,7 +1999,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( } async fn join_channel( - channel_id: u64, + channel_id: ChannelId, client: &TestClient, cx: &mut TestAppContext, ) -> anyhow::Result<()> { @@ -2023,7 +2022,7 @@ async fn test_following_to_channel_notes_other_workspace( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - let (_, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await; + let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await; let mut cx_a2 = cx_a.clone(); let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await; @@ -2080,7 +2079,7 @@ async fn test_following_to_channel_notes_other_workspace( #[gpui::test] async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { - let (_, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await; + let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await; let mut cx_a2 = cx_a.clone(); let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await; diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 746f5aeeaf..443617bbe3 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1,6 +1,6 @@ use crate::{ rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - tests::{channel_id, room_participants, RoomParticipants, TestClient, TestServer}, + tests::{channel_id, room_participants, rust_lang, RoomParticipants, TestClient, TestServer}, }; use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; @@ -22,7 +22,6 @@ use project::{ search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath, }; use rand::prelude::*; -use rpc::proto::ChannelRole; use serde_json::json; use settings::SettingsStore; use std::{ @@ -2821,8 +2820,8 @@ async fn test_git_status_sync( ) .await; - const A_TXT: &'static str = "a.txt"; - const B_TXT: &'static str = "b.txt"; + const A_TXT: &str = "a.txt"; + const B_TXT: &str = "b.txt"; client_a.fs().set_status_for_repo_via_git_operation( Path::new("/dir/.git"), @@ -3742,7 +3741,6 @@ async fn test_leaving_project( client_b.user_store().clone(), client_b.language_registry().clone(), FakeFs::new(cx.background_executor().clone()), - ChannelRole::Member, cx, ) }) @@ -3785,8 +3783,7 @@ async fn test_collaborating_with_diagnostics( .await; let active_call_a = cx_a.read(ActiveCall::global); - // Set up a fake language server. - let mut language = Language::new( + client_a.language_registry().add(Arc::new(Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { @@ -3796,9 +3793,10 @@ async fn test_collaborating_with_diagnostics( ..Default::default() }, Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); + ))); + let mut fake_language_servers = client_a + .language_registry() + .register_fake_lsp_adapter("Rust", Default::default()); // Share a project as client A client_a @@ -3885,7 +3883,6 @@ async fn test_collaborating_with_diagnostics( DiagnosticSummary { error_count: 1, warning_count: 0, - ..Default::default() }, )] ) @@ -3922,7 +3919,6 @@ async fn test_collaborating_with_diagnostics( DiagnosticSummary { error_count: 1, warning_count: 0, - ..Default::default() }, )] ); @@ -4066,26 +4062,15 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering( .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()), disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()], ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); + }, + ); let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"]; client_a @@ -4298,20 +4283,10 @@ async fn test_formatting_buffer( .await; let active_call_a = cx_a.read(ActiveCall::global); - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a + .language_registry() + .register_fake_lsp_adapter("Rust", FakeLspAdapter::default()); // Here we insert a fake tree with a directory that exists on disk. This is needed // because later we'll invoke a command, which requires passing a working directory @@ -4406,8 +4381,9 @@ async fn test_prettier_formatting_buffer( .await; let active_call_a = cx_a.read(ActiveCall::global); - // Set up a fake language server. - let mut language = Language::new( + let test_plugin = "test_plugin"; + + client_a.language_registry().add(Arc::new(Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { @@ -4418,16 +4394,14 @@ async fn test_prettier_formatting_buffer( ..Default::default() }, Some(tree_sitter_rust::language()), - ); - let test_plugin = "test_plugin"; - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + ))); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { prettier_plugins: vec![test_plugin], ..Default::default() - })) - .await; - let language = Arc::new(language); - client_a.language_registry().add(Arc::clone(&language)); + }, + ); // Here we insert a fake tree with a directory that exists on disk. This is needed // because later we'll invoke a command, which requires passing a working directory @@ -4525,20 +4499,10 @@ async fn test_definition( .await; let active_call_a = cx_a.read(ActiveCall::global); - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); + let mut fake_language_servers = client_a + .language_registry() + .register_fake_lsp_adapter("Rust", Default::default()); + client_a.language_registry().add(rust_lang()); client_a .fs() @@ -4672,20 +4636,10 @@ async fn test_references( .await; let active_call_a = cx_a.read(ActiveCall::global); - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a + .language_registry() + .register_fake_lsp_adapter("Rust", Default::default()); client_a .fs() @@ -4872,20 +4826,10 @@ async fn test_document_highlights( ) .await; - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); + let mut fake_language_servers = client_a + .language_registry() + .register_fake_lsp_adapter("Rust", Default::default()); + client_a.language_registry().add(rust_lang()); let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let project_id = active_call_a @@ -4978,20 +4922,10 @@ async fn test_lsp_hover( ) .await; - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a + .language_registry() + .register_fake_lsp_adapter("Rust", Default::default()); let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let project_id = active_call_a @@ -5077,20 +5011,10 @@ async fn test_project_symbols( .await; let active_call_a = cx_a.read(ActiveCall::global); - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a + .language_registry() + .register_fake_lsp_adapter("Rust", Default::default()); client_a .fs() @@ -5189,20 +5113,10 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( .await; let active_call_a = cx_a.read(ActiveCall::global); - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a + .language_registry() + .register_fake_lsp_adapter("Rust", Default::default()); client_a .fs() diff --git a/crates/collab/src/tests/notification_tests.rs b/crates/collab/src/tests/notification_tests.rs index f6066e6409..ddbc1d197b 100644 --- a/crates/collab/src/tests/notification_tests.rs +++ b/crates/collab/src/tests/notification_tests.rs @@ -137,7 +137,7 @@ async fn test_notifications( assert_eq!( entry.notification, Notification::ChannelInvitation { - channel_id, + channel_id: channel_id.0, channel_name: "the-channel".to_string(), inviter_id: client_a.id() } diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index f980f7d908..0eacc56ffb 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -253,7 +253,7 @@ impl RandomizedTest for RandomChannelBufferTest { .channel_buffers() .deref() .iter() - .find(|b| b.read(cx).channel_id == channel_id.to_proto()) + .find(|b| b.read(cx).channel_id.0 == channel_id.to_proto()) { let channel_buffer = channel_buffer.read(cx); diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 37103a3382..3cb270e6ef 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -996,7 +996,7 @@ impl RandomizedTest for ProjectCollaborationTest { let statuses = statuses .iter() - .map(|(path, val)| (path.as_path(), val.clone())) + .map(|(path, val)| (path.as_path(), *val)) .collect::>(); if client.fs().metadata(&dot_git_dir).await?.is_none() { @@ -1021,7 +1021,7 @@ impl RandomizedTest for ProjectCollaborationTest { } async fn on_client_added(client: &Rc, _: &mut TestAppContext) { - let mut language = Language::new( + client.language_registry().add(Arc::new(Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { @@ -1031,9 +1031,10 @@ impl RandomizedTest for ProjectCollaborationTest { ..Default::default() }, None, - ); - language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + ))); + client.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { name: "the-fake-language-server", capabilities: lsp::LanguageServer::full_capabilities(), initializer: Some(Box::new({ @@ -1132,9 +1133,8 @@ impl RandomizedTest for ProjectCollaborationTest { } })), ..Default::default() - })) - .await; - client.app_state.languages.add(Arc::new(language)); + }, + ); } async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc, TestAppContext)]) { diff --git a/crates/collab/src/tests/randomized_test_helpers.rs b/crates/collab/src/tests/randomized_test_helpers.rs index 69bec62460..edce9c184d 100644 --- a/crates/collab/src/tests/randomized_test_helpers.rs +++ b/crates/collab/src/tests/randomized_test_helpers.rs @@ -11,6 +11,7 @@ use rand::prelude::*; use rpc::RECEIVE_TIMEOUT; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use settings::SettingsStore; +use std::sync::OnceLock; use std::{ env, path::PathBuf, @@ -21,16 +22,32 @@ use std::{ }, }; -lazy_static::lazy_static! { - static ref PLAN_LOAD_PATH: Option = path_env_var("LOAD_PLAN"); - static ref PLAN_SAVE_PATH: Option = path_env_var("SAVE_PLAN"); - static ref MAX_PEERS: usize = env::var("MAX_PEERS") - .map(|i| i.parse().expect("invalid `MAX_PEERS` variable")) - .unwrap_or(3); - static ref MAX_OPERATIONS: usize = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); +fn plan_load_path() -> &'static Option { + static PLAN_LOAD_PATH: OnceLock> = OnceLock::new(); + PLAN_LOAD_PATH.get_or_init(|| path_env_var("LOAD_PLAN")) +} +fn plan_save_path() -> &'static Option { + static PLAN_SAVE_PATH: OnceLock> = OnceLock::new(); + PLAN_SAVE_PATH.get_or_init(|| path_env_var("SAVE_PLAN")) +} + +fn max_peers() -> usize { + static MAX_PEERS: OnceLock = OnceLock::new(); + *MAX_PEERS.get_or_init(|| { + env::var("MAX_PEERS") + .map(|i| i.parse().expect("invalid `MAX_PEERS` variable")) + .unwrap_or(3) + }) +} + +fn max_operations() -> usize { + static MAX_OPERATIONS: OnceLock = OnceLock::new(); + *MAX_OPERATIONS.get_or_init(|| { + env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10) + }) } static LOADED_PLAN_JSON: Mutex>> = Mutex::new(None); @@ -175,7 +192,7 @@ pub async fn run_randomized_test( } executor.run_until_parked(); - if let Some(path) = &*PLAN_SAVE_PATH { + if let Some(path) = plan_save_path() { eprintln!("saved test plan to path {:?}", path); std::fs::write(path, plan.lock().serialize()).unwrap(); } @@ -183,7 +200,7 @@ pub async fn run_randomized_test( pub fn save_randomized_test_plan() { if let Some(serialize_plan) = LAST_PLAN.lock().take() { - if let Some(path) = &*PLAN_SAVE_PATH { + if let Some(path) = plan_save_path() { eprintln!("saved test plan to path {:?}", path); std::fs::write(path, serialize_plan()).unwrap(); } @@ -197,7 +214,7 @@ impl TestPlan { let allow_client_disconnection = rng.gen_bool(0.1); let mut users = Vec::new(); - for ix in 0..*MAX_PEERS { + for ix in 0..max_peers() { let username = format!("user-{}", ix + 1); let user_id = server .app_state @@ -234,12 +251,12 @@ impl TestPlan { stored_operations: Vec::new(), operation_ix: 0, next_batch_id: 0, - max_operations: *MAX_OPERATIONS, + max_operations: max_operations(), users, rng, })); - if let Some(path) = &*PLAN_LOAD_PATH { + if let Some(path) = plan_load_path() { let json = LOADED_PLAN_JSON .lock() .get_or_insert_with(|| { @@ -447,6 +464,7 @@ impl TestPlan { }) } + #[allow(clippy::too_many_arguments)] async fn apply_server_operation( plan: Arc>, deterministic: BackgroundExecutor, diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 39292ead44..469c4176ab 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -1,15 +1,17 @@ use crate::{ db::{tests::TestDb, NewUserParams, UserId}, executor::Executor, - rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, + rpc::{Server, ZedVersion, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, AppState, Config, }; use anyhow::anyhow; use call::ActiveCall; use channel::{ChannelBuffer, ChannelStore}; use client::{ - self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, + self, proto::PeerId, ChannelId, Client, Connection, Credentials, EstablishConnectionError, + UserStore, }; +use clock::FakeSystemClock; use collab_ui::channel_view::ChannelView; use collections::{HashMap, HashSet}; use fs::FakeFs; @@ -37,7 +39,7 @@ use std::{ Arc, }, }; -use util::http::FakeHttpClient; +use util::{http::FakeHttpClient, SemanticVersion}; use workspace::{Workspace, WorkspaceStore}; pub struct TestServer { @@ -119,7 +121,7 @@ impl TestServer { pub async fn start2( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, - ) -> (TestServer, TestClient, TestClient, u64) { + ) -> (TestServer, TestClient, TestClient, ChannelId) { let mut server = Self::start(cx_a.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -136,7 +138,7 @@ impl TestServer { (server, client_a, client_b, channel_id) } - pub async fn start1<'a>(cx: &'a mut TestAppContext) -> TestClient { + pub async fn start1(cx: &mut TestAppContext) -> TestClient { let mut server = Self::start(cx.executor().clone()).await; server.create_client(cx, "user_a").await } @@ -163,6 +165,7 @@ impl TestServer { client::init_settings(cx); }); + let clock = Arc::new(FakeSystemClock::default()); let http = FakeHttpClient::with_404_response(); let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await { @@ -185,7 +188,7 @@ impl TestServer { .user_id }; let client_name = name.to_string(); - let mut client = cx.update(|cx| Client::new(http.clone(), cx)); + let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx)); let server = self.server.clone(); let db = self.app_state.db.clone(); let connection_killers = self.connection_killers.clone(); @@ -231,12 +234,18 @@ impl TestServer { server_conn, client_name, user, + ZedVersion(SemanticVersion::new(1, 0, 0)), None, Some(connection_id_tx), Executor::Deterministic(cx.background_executor().clone()), )) .detach(); - let connection_id = connection_id_rx.await.unwrap(); + let connection_id = connection_id_rx.await.map_err(|e| { + EstablishConnectionError::Other(anyhow!( + "{} (is server shutting down?)", + e + )) + })?; connection_killers .lock() .insert(connection_id.into(), killed); @@ -273,7 +282,7 @@ impl TestServer { collab_ui::init(&app_state, cx); file_finder::init(cx); menu::init(); - settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap(); + settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap(); }); client @@ -350,10 +359,10 @@ impl TestServer { pub async fn make_channel( &self, channel: &str, - parent: Option, + parent: Option, admin: (&TestClient, &mut TestAppContext), members: &mut [(&TestClient, &mut TestAppContext)], - ) -> u64 { + ) -> ChannelId { let (_, admin_cx) = admin; let channel_id = admin_cx .read(ChannelStore::global) @@ -396,7 +405,7 @@ impl TestServer { channel: &str, client: &TestClient, cx: &mut TestAppContext, - ) -> u64 { + ) -> ChannelId { let channel_id = self .make_channel(channel, None, (client, cx), &mut []) .await; @@ -420,7 +429,7 @@ impl TestServer { &self, channels: &[(&str, Option<&str>)], creator: (&TestClient, &mut TestAppContext), - ) -> Vec { + ) -> Vec { let mut observed_channels = HashMap::default(); let mut result = Vec::new(); for (channel, parent) in channels { @@ -457,7 +466,7 @@ impl TestServer { let active_call_a = cx_a.read(ActiveCall::global); for (client_b, cx_b) in right { - let user_id_b = client_b.current_user_id(*cx_b).to_proto(); + let user_id_b = client_b.current_user_id(cx_b).to_proto(); active_call_a .update(*cx_a, |call, cx| call.invite(user_id_b, None, cx)) .await @@ -480,6 +489,7 @@ impl TestServer { db: test_db.db().clone(), live_kit_client: Some(Arc::new(fake_server.create_api_client())), blob_store_client: None, + clickhouse_client: None, config: Config { http_port: 0, database_url: "".into(), @@ -497,6 +507,12 @@ impl TestServer { blob_store_access_key: None, blob_store_secret_key: None, blob_store_bucket: None, + clickhouse_url: None, + clickhouse_user: None, + clickhouse_password: None, + clickhouse_database: None, + zed_client_checksum_seed: None, + slack_panics_webhook: None, }, }) } @@ -573,19 +589,19 @@ impl TestClient { .await; } - pub fn local_projects<'a>(&'a self) -> impl Deref>> + 'a { + pub fn local_projects(&self) -> impl Deref>> + '_ { Ref::map(self.state.borrow(), |state| &state.local_projects) } - pub fn remote_projects<'a>(&'a self) -> impl Deref>> + 'a { + pub fn remote_projects(&self) -> impl Deref>> + '_ { Ref::map(self.state.borrow(), |state| &state.remote_projects) } - pub fn local_projects_mut<'a>(&'a self) -> impl DerefMut>> + 'a { + pub fn local_projects_mut(&self) -> impl DerefMut>> + '_ { RefMut::map(self.state.borrow_mut(), |state| &mut state.local_projects) } - pub fn remote_projects_mut<'a>(&'a self) -> impl DerefMut>> + 'a { + pub fn remote_projects_mut(&self) -> impl DerefMut>> + '_ { RefMut::map(self.state.borrow_mut(), |state| &mut state.remote_projects) } @@ -598,16 +614,14 @@ impl TestClient { }) } - pub fn buffers<'a>( - &'a self, - ) -> impl DerefMut, HashSet>>> + 'a + pub fn buffers( + &self, + ) -> impl DerefMut, HashSet>>> + '_ { RefMut::map(self.state.borrow_mut(), |state| &mut state.buffers) } - pub fn channel_buffers<'a>( - &'a self, - ) -> impl DerefMut>> + 'a { + pub fn channel_buffers(&self) -> impl DerefMut>> + '_ { RefMut::map(self.state.borrow_mut(), |state| &mut state.channel_buffers) } @@ -668,7 +682,7 @@ impl TestClient { pub async fn host_workspace( &self, workspace: &View, - channel_id: u64, + channel_id: ChannelId, cx: &mut VisualTestContext, ) { cx.update(|cx| { @@ -689,7 +703,7 @@ impl TestClient { pub async fn join_workspace<'a>( &'a self, - channel_id: u64, + channel_id: ChannelId, cx: &'a mut TestAppContext, ) -> (View, &'a mut VisualTestContext) { cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx)) @@ -768,7 +782,7 @@ impl TestClient { } pub fn open_channel_notes( - channel_id: u64, + channel_id: ChannelId, cx: &mut VisualTestContext, ) -> Task>> { let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 26237de4c6..e88d191af5 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -34,19 +34,17 @@ clock.workspace = true collections.workspace = true db.workspace = true editor.workspace = true -feature_flags.workspace = true +extensions_ui.workspace = true feedback.workspace = true futures.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true lazy_static.workspace = true -log.workspace = true menu.workspace = true notifications.workspace = true parking_lot.workspace = true picker.workspace = true -postage.workspace = true project.workspace = true recent_projects.workspace = true rich_text.workspace = true @@ -60,13 +58,13 @@ smallvec.workspace = true story = { workspace = true, optional = true } theme.workspace = true theme_selector.workspace = true +time_format.workspace = true time.workspace = true ui.workspace = true util.workspace = true vcs_menu.workspace = true workspace.workspace = true zed_actions.workspace = true -sys-locale.workspace = true [dev-dependencies] call = { workspace = true, features = ["test-support"] } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 939276bf1b..ac0793715f 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,9 +1,9 @@ use anyhow::Result; use call::report_call_event_for_channel; -use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; +use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelStore}; use client::{ proto::{self, PeerId}, - Collaborator, ParticipantIndex, + ChannelId, Collaborator, ParticipantIndex, }; use collections::HashMap; use editor::{ @@ -171,10 +171,8 @@ impl ChannelView { let this = this.clone(); Some(ui::ContextMenu::build(cx, move |menu, _| { menu.entry("Copy link to section", None, move |cx| { - this.update(cx, |this, cx| { - this.copy_link_for_position(position.clone(), cx) - }) - .ok(); + this.update(cx, |this, cx| this.copy_link_for_position(position, cx)) + .ok(); }) })) }); @@ -267,7 +265,7 @@ impl ChannelView { return; }; - let link = channel.notes_link(closest_heading.map(|heading| heading.text)); + let link = channel.notes_link(closest_heading.map(|heading| heading.text), cx); cx.write_to_clipboard(ClipboardItem::new(link)); self.workspace .update(cx, |workspace, cx| { @@ -378,7 +376,7 @@ impl Item for ChannelView { (_, false) => format!("#{} (disconnected)", channel.name), } } else { - format!("channel notes (disconnected)") + "channel notes (disconnected)".to_string() }; Label::new(label) .color(if selected { @@ -454,7 +452,7 @@ impl FollowableItem for ChannelView { Some(proto::view::Variant::ChannelView( proto::view::ChannelView { - channel_id: channel_buffer.channel_id, + channel_id: channel_buffer.channel_id.0, editor: if let Some(proto::view::Variant::Editor(proto)) = self.editor.read(cx).to_state_proto(cx) { @@ -480,7 +478,8 @@ impl FollowableItem for ChannelView { unreachable!() }; - let open = ChannelView::open_in_pane(state.channel_id, None, pane, workspace, cx); + let open = + ChannelView::open_in_pane(ChannelId(state.channel_id), None, pane, workspace, cx); Some(cx.spawn(|mut cx| async move { let this = open.await?; diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 0181ed8587..6d2f3ea487 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -2,14 +2,14 @@ use crate::{collab_panel, ChatPanelSettings}; use anyhow::Result; use call::{room, ActiveCall}; use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore}; -use client::Client; +use client::{ChannelId, Client}; use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use gpui::{ - actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, CursorStyle, - DismissEvent, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, - HighlightStyle, ListOffset, ListScrollEvent, ListState, Model, Render, StyledText, + actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, ClipboardItem, + CursorStyle, DismissEvent, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, + FontWeight, HighlightStyle, ListOffset, ListScrollEvent, ListState, Model, Render, StyledText, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use language::LanguageRegistry; @@ -34,7 +34,7 @@ use workspace::{ mod message_editor; const MESSAGE_LOADING_THRESHOLD: usize = 50; -const CHAT_PANEL_KEY: &'static str = "ChatPanel"; +const CHAT_PANEL_KEY: &str = "ChatPanel"; pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _| { @@ -63,6 +63,7 @@ pub struct ChatPanel { focus_handle: FocusHandle, open_context_menu: Option<(u64, Subscription)>, highlighted_message: Option<(u64, Task<()>)>, + last_acknowledged_message_id: Option, } #[derive(Serialize, Deserialize)] @@ -126,6 +127,7 @@ impl ChatPanel { focus_handle: cx.focus_handle(), open_context_menu: None, highlighted_message: None, + last_acknowledged_message_id: None, }; if let Some(channel_id) = ActiveCall::global(cx) @@ -167,7 +169,7 @@ impl ChatPanel { }) } - pub fn channel_id(&self, cx: &AppContext) -> Option { + pub fn channel_id(&self, cx: &AppContext) -> Option { self.active_chat .as_ref() .map(|(chat, _)| chat.read(cx).channel_id) @@ -202,7 +204,7 @@ impl ChatPanel { let panel = Self::new(workspace, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width; + panel.width = serialized_panel.width.map(|r| r.round()); cx.notify(); }); } @@ -281,6 +283,13 @@ impl ChatPanel { fn acknowledge_last_message(&mut self, cx: &mut ViewContext) { if self.active && self.is_scrolled_to_bottom { if let Some((chat, _)) = &self.active_chat { + if let Some(channel_id) = self.channel_id(cx) { + self.last_acknowledged_message_id = self + .channel_store + .read(cx) + .last_acknowledge_message_id(channel_id); + } + chat.update(cx, |chat, cx| { chat.acknowledge_last_message(cx); }); @@ -362,9 +371,9 @@ impl ChatPanel { .px_1() .py_0p5() .mb_1() - .overflow_hidden() .child( div() + .overflow_hidden() .max_h_12() .child(reply_to_message_body.element(body_element_id, cx)), ), @@ -435,8 +444,7 @@ impl ChatPanel { let reply_to_message = message .reply_to_message_id - .map(|id| active_chat.read(cx).find_loaded_message(id)) - .flatten() + .and_then(|id| active_chat.read(cx).find_loaded_message(id)) .cloned(); let replied_to_you = @@ -454,120 +462,144 @@ impl ChatPanel { cx.theme().colors().panel_background }; - v_flex().w_full().relative().child( - div() - .bg(background) - .rounded_md() - .overflow_hidden() - .px_1() - .py_0p5() - .when(!is_continuation_from_previous, |this| { - this.mt_2().child( - h_flex() - .text_ui_sm() - .child(div().absolute().child( - Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)), - )) - .child( - div() - .pl(cx.rem_size() + px(6.0)) - .pr(px(8.0)) - .font_weight(FontWeight::BOLD) - .child(Label::new(message.sender.github_login.clone())), - ) - .child( - Label::new(format_timestamp( - OffsetDateTime::now_utc(), - message.timestamp, - self.local_timezone, - None, + v_flex() + .w_full() + .relative() + .child( + div() + .bg(background) + .rounded_md() + .overflow_hidden() + .px_1() + .py_0p5() + .when(!is_continuation_from_previous, |this| { + this.mt_2().child( + h_flex() + .text_ui_sm() + .child(div().absolute().child( + Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)), )) - .size(LabelSize::Small) - .color(Color::Muted), - ), + .child( + div() + .pl(cx.rem_size() + px(6.0)) + .pr(px(8.0)) + .font_weight(FontWeight::BOLD) + .child(Label::new(message.sender.github_login.clone())), + ) + .child( + Label::new(time_format::format_localized_timestamp( + OffsetDateTime::now_utc(), + message.timestamp, + self.local_timezone, + )) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + }) + .when( + message.reply_to_message_id.is_some() && reply_to_message.is_none(), + |this| { + const MESSAGE_DELETED: &str = "Message has been deleted"; + + let body_text = StyledText::new(MESSAGE_DELETED).with_highlights( + &cx.text_style(), + vec![( + 0..MESSAGE_DELETED.len(), + HighlightStyle { + font_style: Some(FontStyle::Italic), + ..Default::default() + }, + )], + ); + + this.child( + div() + .border_l_2() + .text_ui_xs() + .border_color(cx.theme().colors().border) + .px_1() + .py_0p5() + .child(body_text), + ) + }, ) - }) - .when( - message.reply_to_message_id.is_some() && reply_to_message.is_none(), - |this| { - const MESSAGE_DELETED: &str = "Message has been deleted"; - - let body_text = StyledText::new(MESSAGE_DELETED).with_highlights( - &cx.text_style(), - vec![( - 0..MESSAGE_DELETED.len(), - HighlightStyle { - font_style: Some(FontStyle::Italic), - ..Default::default() - }, - )], - ); - - this.child( - div() - .border_l_2() - .text_ui_xs() - .border_color(cx.theme().colors().border) - .px_1() - .py_0p5() - .child(body_text), + .when_some(reply_to_message, |el, reply_to_message| { + el.child(self.render_replied_to_message( + Some(message.id), + &reply_to_message, + cx, + )) + }) + .when(mentioning_you || replied_to_you, |this| this.my_0p5()) + .map(|el| { + let text = self.markdown_data.entry(message.id).or_insert_with(|| { + Self::render_markdown_with_mentions( + &self.languages, + self.client.id(), + &message, + ) + }); + el.child( + v_flex() + .w_full() + .text_ui_sm() + .id(element_id) + .group("") + .child(text.element("body".into(), cx)) + .child( + div() + .absolute() + .z_index(1) + .right_0() + .w_6() + .bg(background) + .when(!self.has_open_menu(message_id), |el| { + el.visible_on_hover("") + }) + .when_some(message_id, |el, message_id| { + el.child( + popover_menu(("menu", message_id)) + .trigger(IconButton::new( + ("trigger", message_id), + IconName::Ellipsis, + )) + .menu(move |cx| { + Some(Self::render_message_menu( + &this, + message_id, + can_delete_message, + cx, + )) + }), + ) + }), + ), ) - }, - ) - .when_some(reply_to_message, |el, reply_to_message| { - el.child(self.render_replied_to_message( - Some(message.id), - &reply_to_message, - cx, - )) - }) - .when(mentioning_you || replied_to_you, |this| this.my_0p5()) - .map(|el| { - let text = self.markdown_data.entry(message.id).or_insert_with(|| { - Self::render_markdown_with_mentions( - &self.languages, - self.client.id(), - &message, - ) - }); - el.child( - v_flex() - .w_full() - .text_ui_sm() - .id(element_id) - .group("") - .child(text.element("body".into(), cx)) + }), + ) + .when( + self.last_acknowledged_message_id + .is_some_and(|l| Some(l) == message_id), + |this| { + this.child( + h_flex() + .py_2() + .gap_1() + .items_center() + .child(div().w_full().h_0p5().bg(cx.theme().colors().border)) .child( div() - .absolute() - .z_index(1) - .right_0() - .w_6() - .bg(background) - .when(!self.has_open_menu(message_id), |el| { - el.visible_on_hover("") - }) - .when_some(message_id, |el, message_id| { - el.child( - popover_menu(("menu", message_id)) - .trigger(IconButton::new( - ("trigger", message_id), - IconName::Ellipsis, - )) - .menu(move |cx| { - Some(Self::render_message_menu( - &this, - message_id, - can_delete_message, - cx, - )) - }), - ) - }), - ), + .px_1() + .rounded_md() + .text_ui_xs() + .bg(cx.theme().colors().background) + .child("New messages"), + ) + .child(div().w_full().h_0p5().bg(cx.theme().colors().border)), ) - }), - ) + }, + ) } fn has_open_menu(&self, message_id: Option) -> bool { @@ -595,6 +627,18 @@ impl ChatPanel { }) }), ) + .entry( + "Copy message text", + None, + cx.handler_for(&this, move |this, cx| { + if let Some(message) = this.active_chat().and_then(|active_chat| { + active_chat.read(cx).find_loaded_message(message_id) + }) { + let text = message.body.clone(); + cx.write_to_clipboard(ClipboardItem::new(text)) + } + }), + ) .when(can_delete_message, move |menu| { menu.entry( "Delete message", @@ -663,7 +707,7 @@ impl ChatPanel { pub fn select_channel( &mut self, - selected_channel_id: u64, + selected_channel_id: ChannelId, scroll_to_message_id: Option, cx: &mut ViewContext, ) -> Task> { @@ -682,8 +726,11 @@ impl ChatPanel { cx.spawn(|this, mut cx| async move { let chat = open_chat.await?; - this.update(&mut cx, |this, cx| { + let highlight_message_id = scroll_to_message_id; + let scroll_to_message_id = this.update(&mut cx, |this, cx| { this.set_active_chat(chat.clone(), cx); + + scroll_to_message_id.or_else(|| this.last_acknowledged_message_id) })?; if let Some(message_id) = scroll_to_message_id { @@ -691,21 +738,22 @@ impl ChatPanel { ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone()) .await { - let task = cx.spawn({ - let this = this.clone(); - - |mut cx| async move { - cx.background_executor().timer(Duration::from_secs(2)).await; - this.update(&mut cx, |this, cx| { - this.highlighted_message.take(); - cx.notify(); - }) - .ok(); - } - }); - this.update(&mut cx, |this, cx| { - this.highlighted_message = Some((message_id, task)); + if let Some(highlight_message_id) = highlight_message_id { + let task = cx.spawn({ + |this, mut cx| async move { + cx.background_executor().timer(Duration::from_secs(2)).await; + this.update(&mut cx, |this, cx| { + this.highlighted_message.take(); + cx.notify(); + }) + .ok(); + } + }); + + this.highlighted_message = Some((highlight_message_id, task)); + } + if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) { this.message_list.scroll_to(ListOffset { item_ix, @@ -788,32 +836,30 @@ impl Render for ChatPanel { .when_some(reply_to_message_id, |el, reply_to_message_id| { let reply_message = self .active_chat() - .map(|active_chat| { - active_chat.read(cx).messages().iter().find_map(|m| { - if m.id == ChannelMessageId::Saved(reply_to_message_id) { - Some(m) - } else { - None - } + .and_then(|active_chat| { + active_chat.read(cx).messages().iter().find(|message| { + message.id == ChannelMessageId::Saved(reply_to_message_id) }) }) - .flatten() .cloned(); el.when_some(reply_message, |el, reply_message| { el.child( - div() + h_flex() .when(!self.is_scrolled_to_bottom, |el| { el.border_t_1().border_color(cx.theme().colors().border) }) - .flex() - .w_full() - .items_start() + .justify_between() .overflow_hidden() + .items_start() .py_1() .px_2() .bg(cx.theme().colors().background) - .child(self.render_replied_to_message(None, &reply_message, cx)) + .child( + div().flex_shrink().overflow_hidden().child( + self.render_replied_to_message(None, &reply_message, cx), + ), + ) .child( IconButton::new("close-reply-preview", IconName::Close) .shape(ui::IconButtonShape::Square) @@ -918,94 +964,13 @@ impl Panel for ChatPanel { impl EventEmitter for ChatPanel {} -fn is_12_hour_clock(locale: String) -> bool { - [ - "es-MX", "es-CO", "es-SV", "es-NI", - "es-HN", // Mexico, Colombia, El Salvador, Nicaragua, Honduras - "en-US", "en-CA", "en-AU", "en-NZ", // U.S, Canada, Australia, New Zealand - "ar-SA", "ar-EG", "ar-JO", // Saudi Arabia, Egypt, Jordan - "en-IN", "hi-IN", // India, Hindu - "en-PK", "ur-PK", // Pakistan, Urdu - "en-PH", "fil-PH", // Philippines, Filipino - "bn-BD", "ccp-BD", // Bangladesh, Chakma - "en-IE", "ga-IE", // Ireland, Irish - "en-MY", "ms-MY", // Malaysia, Malay - ] - .contains(&locale.as_str()) -} - -fn format_timestamp( - reference: OffsetDateTime, - timestamp: OffsetDateTime, - timezone: UtcOffset, - locale: Option, -) -> String { - let locale = match locale { - Some(locale) => locale, - None => sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")), - }; - let timestamp_local = timestamp.to_offset(timezone); - let timestamp_local_hour = timestamp_local.hour(); - let timestamp_local_minute = timestamp_local.minute(); - - let (hour, meridiem) = if is_12_hour_clock(locale) { - let meridiem = if timestamp_local_hour >= 12 { - "pm" - } else { - "am" - }; - - let hour_12 = match timestamp_local_hour { - 0 => 12, // Midnight - 13..=23 => timestamp_local_hour - 12, // PM hours - _ => timestamp_local_hour, // AM hours - }; - - (hour_12, Some(meridiem)) - } else { - (timestamp_local_hour, None) - }; - - let formatted_time = match meridiem { - Some(meridiem) => format!("{:02}:{:02} {}", hour, timestamp_local_minute, meridiem), - None => format!("{:02}:{:02}", hour, timestamp_local_minute), - }; - - let reference_local = reference.to_offset(timezone); - let reference_local_date = reference_local.date(); - let timestamp_local_date = timestamp_local.date(); - - if timestamp_local_date == reference_local_date { - return formatted_time; - } - - if reference_local_date.previous_day() == Some(timestamp_local_date) { - return format!("yesterday at {}", formatted_time); - } - - match meridiem { - Some(_) => format!( - "{:02}/{:02}/{}", - timestamp_local_date.month() as u32, - timestamp_local_date.day(), - timestamp_local_date.year() - ), - None => format!( - "{:02}/{:02}/{}", - timestamp_local_date.day(), - timestamp_local_date.month() as u32, - timestamp_local_date.year() - ), - } -} - #[cfg(test)] mod tests { use super::*; use gpui::HighlightStyle; use pretty_assertions::assert_eq; use rich_text::Highlight; - use time::{Date, OffsetDateTime, Time, UtcOffset}; + use time::OffsetDateTime; use util::test::marked_text_ranges; #[gpui::test] @@ -1056,149 +1021,104 @@ mod tests { ); } - #[test] - fn test_format_locale() { - let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0); - let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0); + #[gpui::test] + fn test_render_markdown_with_auto_detect_links() { + let language_registry = Arc::new(LanguageRegistry::test()); + let message = channel::ChannelMessage { + id: ChannelMessageId::Saved(0), + body: "Here is a link https://zed.dev to zeds website".to_string(), + timestamp: OffsetDateTime::now_utc(), + sender: Arc::new(client::User { + github_login: "fgh".into(), + avatar_uri: "avatar_fgh".into(), + id: 103, + }), + nonce: 5, + mentions: Vec::new(), + reply_to_message_id: None, + }; + let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message); + + // Note that the "'" was replaced with ’ due to smart punctuation. + let (body, ranges) = + marked_text_ranges("Here is a link Β«https://zed.devΒ» to zeds website", false); + assert_eq!(message.text, body); + assert_eq!(1, ranges.len()); assert_eq!( - format_timestamp( - reference, - timestamp, - test_timezone(), - Some(String::from("en-GB")) - ), - "15:30" + message.highlights, + vec![( + ranges[0].clone(), + HighlightStyle { + underline: Some(gpui::UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + } + .into() + ),] ); } - #[test] - fn test_format_today() { - let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0); - let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0); + #[gpui::test] + fn test_render_markdown_with_auto_detect_links_and_additional_formatting() { + let language_registry = Arc::new(LanguageRegistry::test()); + let message = channel::ChannelMessage { + id: ChannelMessageId::Saved(0), + body: "**Here is a link https://zed.dev to zeds website**".to_string(), + timestamp: OffsetDateTime::now_utc(), + sender: Arc::new(client::User { + github_login: "fgh".into(), + avatar_uri: "avatar_fgh".into(), + id: 103, + }), + nonce: 5, + mentions: Vec::new(), + reply_to_message_id: None, + }; - assert_eq!( - format_timestamp( - reference, - timestamp, - test_timezone(), - Some(String::from("en-US")) - ), - "03:30 pm" + let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message); + + // Note that the "'" was replaced with ’ due to smart punctuation. + let (body, ranges) = marked_text_ranges( + "Β«Here is a link »«https://zed.dev»« to zeds websiteΒ»", + false, ); - } - - #[test] - fn test_format_yesterday() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 9, 0, 0); - + assert_eq!(message.text, body); + assert_eq!(3, ranges.len()); assert_eq!( - format_timestamp( - reference, - timestamp, - test_timezone(), - Some(String::from("en-US")) - ), - "yesterday at 09:00 am" + message.highlights, + vec![ + ( + ranges[0].clone(), + HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + } + .into() + ), + ( + ranges[1].clone(), + HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + underline: Some(gpui::UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + } + .into() + ), + ( + ranges[2].clone(), + HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + } + .into() + ), + ] ); } - - #[test] - fn test_format_yesterday_less_than_24_hours_ago() { - let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 20, 0, 0); - - assert_eq!( - format_timestamp( - reference, - timestamp, - test_timezone(), - Some(String::from("en-US")) - ), - "yesterday at 08:00 pm" - ); - } - - #[test] - fn test_format_yesterday_more_than_24_hours_ago() { - let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 18, 0, 0); - - assert_eq!( - format_timestamp( - reference, - timestamp, - test_timezone(), - Some(String::from("en-US")) - ), - "yesterday at 06:00 pm" - ); - } - - #[test] - fn test_format_yesterday_over_midnight() { - let reference = create_offset_datetime(1990, 4, 12, 0, 5, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 23, 55, 0); - - assert_eq!( - format_timestamp( - reference, - timestamp, - test_timezone(), - Some(String::from("en-US")) - ), - "yesterday at 11:55 pm" - ); - } - - #[test] - fn test_format_yesterday_over_month() { - let reference = create_offset_datetime(1990, 4, 2, 9, 0, 0); - let timestamp = create_offset_datetime(1990, 4, 1, 20, 0, 0); - - assert_eq!( - format_timestamp( - reference, - timestamp, - test_timezone(), - Some(String::from("en-US")) - ), - "yesterday at 08:00 pm" - ); - } - - #[test] - fn test_format_before_yesterday() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - let timestamp = create_offset_datetime(1990, 4, 10, 20, 20, 0); - - assert_eq!( - format_timestamp( - reference, - timestamp, - test_timezone(), - Some(String::from("en-US")) - ), - "04/10/1990" - ); - } - - fn test_timezone() -> UtcOffset { - UtcOffset::from_hms(0, 0, 0).expect("Valid timezone offset") - } - - fn create_offset_datetime( - year: i32, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - ) -> OffsetDateTime { - let date = - Date::from_calendar_date(year, time::Month::try_from(month).unwrap(), day).unwrap(); - let time = Time::from_hms(hour, minute, second).unwrap(); - date.with_time(time).assume_utc() // Assume UTC for simplicity - } } diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index ed47c7a54a..21c49c97dd 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams}; -use client::UserId; +use channel::{ChannelMembership, ChannelStore, MessageParams}; +use client::{ChannelId, UserId}; use collections::{HashMap, HashSet}; use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle}; use fuzzy::StringMatchCandidate; @@ -131,15 +131,15 @@ impl MessageEditor { pub fn set_channel( &mut self, - channel_id: u64, + channel_id: ChannelId, channel_name: Option, cx: &mut ViewContext, ) { self.editor.update(cx, |editor, cx| { if let Some(channel_name) = channel_name { - editor.set_placeholder_text(format!("Message #{}", channel_name), cx); + editor.set_placeholder_text(format!("Message #{channel_name}"), cx); } else { - editor.set_placeholder_text(format!("Message Channel"), cx); + editor.set_placeholder_text("Message Channel", cx); } }); self.channel_id = Some(channel_id); @@ -310,7 +310,7 @@ impl MessageEditor { for range in ranges { text.clear(); text.extend(buffer.text_for_range(range.clone())); - if let Some(username) = text.strip_prefix("@") { + if let Some(username) = text.strip_prefix('@') { if let Some(user_id) = this.channel_members.get(username) { let start = multi_buffer.anchor_after(range.start); let end = multi_buffer.anchor_after(range.end); @@ -357,7 +357,7 @@ impl Render for MessageEditor { font_size: UiTextSize::Small.rems().into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, - line_height: relative(1.3).into(), + line_height: relative(1.3), background_color: None, underline: None, strikethrough: None, @@ -385,6 +385,7 @@ impl Render for MessageEditor { mod tests { use super::*; use client::{Client, User, UserStore}; + use clock::FakeSystemClock; use gpui::TestAppContext; use language::{Language, LanguageConfig}; use rpc::proto; @@ -455,8 +456,9 @@ mod tests { let settings = SettingsStore::test(cx); cx.set_global(settings); + let clock = Arc::new(FakeSystemClock::default()); let http = FakeHttpClient::with_404_response(); - let client = Client::new(http.clone(), cx); + let client = Client::new(clock, http.clone(), cx); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); theme::init(theme::LoadThemes::JustBase, cx); language::init(cx); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index d84fa78d4f..4f242d68bb 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -7,8 +7,8 @@ use crate::{ CollaborationPanelSettings, }; use call::ActiveCall; -use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; -use client::{Client, Contact, User, UserStore}; +use channel::{Channel, ChannelEvent, ChannelStore}; +use client::{ChannelId, Client, Contact, HostedProjectId, User, UserStore}; use contact_finder::ContactFinder; use db::kvp::KEY_VALUE_STORE; use editor::{Editor, EditorElement, EditorStyle}; @@ -34,7 +34,7 @@ use std::{mem, sync::Arc}; use theme::{ActiveTheme, ThemeSettings}; use ui::{ prelude::*, tooltip_container, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, - Icon, IconButton, IconName, IconSize, Label, ListHeader, ListItem, Tooltip, + Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip, }; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -62,7 +62,7 @@ struct ChannelMoveClipboard { channel_id: ChannelId, } -const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; +const COLLABORATION_PANEL_KEY: &str = "CollaborationPanel"; pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _| { @@ -184,6 +184,10 @@ enum ListEntry { ChannelEditor { depth: usize, }, + HostedProject { + id: HostedProjectId, + name: SharedString, + }, Contact { contact: Arc, calling: bool, @@ -323,10 +327,13 @@ impl CollabPanel { let panel = CollabPanel::new(workspace, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width; + panel.width = serialized_panel.width.map(|w| w.round()); panel.collapsed_channels = serialized_panel .collapsed_channels - .unwrap_or_else(|| Vec::new()); + .unwrap_or_else(|| Vec::new()) + .iter() + .map(|cid| ChannelId(*cid)) + .collect(); cx.notify(); }); } @@ -344,7 +351,9 @@ impl CollabPanel { COLLABORATION_PANEL_KEY.into(), serde_json::to_string(&SerializedCollabPanel { width, - collapsed_channels: Some(collapsed_channels), + collapsed_channels: Some( + collapsed_channels.iter().map(|cid| cid.0).collect(), + ), })?, ) .await?; @@ -432,17 +441,13 @@ impl CollabPanel { // Populate remote participants. self.match_candidates.clear(); self.match_candidates - .extend( - room.remote_participants() - .iter() - .filter_map(|(_, participant)| { - Some(StringMatchCandidate { - id: participant.user.id as usize, - string: participant.user.github_login.clone(), - char_bag: participant.user.github_login.chars().collect(), - }) - }), - ); + .extend(room.remote_participants().values().map(|participant| { + StringMatchCandidate { + id: participant.user.id as usize, + string: participant.user.github_login.clone(), + char_bag: participant.user.github_login.chars().collect(), + } + })); let mut matches = executor.block(match_strings( &self.match_candidates, &query, @@ -563,6 +568,7 @@ impl CollabPanel { } } + let hosted_projects = channel_store.projects_for_id(channel.id); let has_children = channel_store .channel_at_index(mat.candidate_id + 1) .map_or(false, |next_channel| { @@ -596,6 +602,10 @@ impl CollabPanel { }); } } + + for (name, id) in hosted_projects { + self.entries.push(ListEntry::HostedProject { id, name }) + } } } @@ -854,6 +864,10 @@ impl CollabPanel { .into_any_element() } else if role == proto::ChannelRole::Guest { Label::new("Guest").color(Color::Muted).into_any_element() + } else if role == proto::ChannelRole::Talker { + Label::new("Mic only") + .color(Color::Muted) + .into_any_element() } else { div().into_any_element() }) @@ -897,7 +911,7 @@ impl CollabPanel { this.workspace .update(cx, |workspace, cx| { let app_state = workspace.app_state().clone(); - workspace::join_remote_project(project_id, host_user_id, app_state, cx) + workspace::join_in_room_project(project_id, host_user_id, app_state, cx) .detach_and_prompt_err("Failed to join project", cx, |_, _| None); }) .ok(); @@ -938,7 +952,7 @@ impl CollabPanel { }) .ok(); })) - .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx)) + .tooltip(move |cx| Tooltip::text("Open shared screen", cx)) }) } @@ -959,6 +973,8 @@ impl CollabPanel { is_selected: bool, cx: &mut ViewContext, ) -> impl IntoElement { + let channel_store = self.channel_store.read(cx); + let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id); ListItem::new("channel-notes") .selected(is_selected) .on_click(cx.listener(move |this, _, cx| { @@ -966,9 +982,19 @@ impl CollabPanel { })) .start_slot( h_flex() + .relative() .gap_1() .child(render_tree_branch(false, true, cx)) - .child(IconButton::new(0, IconName::File)), + .child(IconButton::new(0, IconName::File)) + .children(has_channel_buffer_changed.then(|| { + div() + .w_1p5() + .z_index(1) + .absolute() + .right(px(2.)) + .top(px(2.)) + .child(Indicator::dot().color(Color::Info)) + })), ) .child(Label::new("notes")) .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) @@ -980,6 +1006,8 @@ impl CollabPanel { is_selected: bool, cx: &mut ViewContext, ) -> impl IntoElement { + let channel_store = self.channel_store.read(cx); + let has_messages_notification = channel_store.has_new_messages(channel_id); ListItem::new("channel-chat") .selected(is_selected) .on_click(cx.listener(move |this, _, cx| { @@ -987,14 +1015,58 @@ impl CollabPanel { })) .start_slot( h_flex() + .relative() .gap_1() .child(render_tree_branch(false, false, cx)) - .child(IconButton::new(0, IconName::MessageBubbles)), + .child(IconButton::new(0, IconName::MessageBubbles)) + .children(has_messages_notification.then(|| { + div() + .w_1p5() + .z_index(1) + .absolute() + .right(px(2.)) + .top(px(4.)) + .child(Indicator::dot().color(Color::Info)) + })), ) .child(Label::new("chat")) .tooltip(move |cx| Tooltip::text("Open Chat", cx)) } + fn render_channel_project( + &self, + id: HostedProjectId, + name: &SharedString, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + ListItem::new(ElementId::NamedInteger( + "channel-project".into(), + id.0 as usize, + )) + .indent_level(2) + .indent_step_size(px(20.)) + .selected(is_selected) + .on_click(cx.listener(move |this, _, cx| { + if let Some(workspace) = this.workspace.upgrade() { + let app_state = workspace.read(cx).app_state().clone(); + workspace::join_hosted_project(id, app_state, cx).detach_and_prompt_err( + "Failed to open project", + cx, + |_, _| None, + ) + } + })) + .start_slot( + h_flex() + .relative() + .gap_1() + .child(IconButton::new(0, IconName::FileTree)), + ) + .child(Label::new(name.clone())) + .tooltip(move |cx| Tooltip::text("Open Project", cx)) + } + fn has_subchannels(&self, ix: usize) -> bool { self.entries.get(ix).map_or(false, |entry| { if let ListEntry::Channel { has_children, .. } = entry { @@ -1013,13 +1085,38 @@ impl CollabPanel { cx: &mut ViewContext, ) { let this = cx.view().clone(); - if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Member) { + if !(role == proto::ChannelRole::Guest + || role == proto::ChannelRole::Talker + || role == proto::ChannelRole::Member) + { return; } - let context_menu = ContextMenu::build(cx, |context_menu, cx| { + let context_menu = ContextMenu::build(cx, |mut context_menu, cx| { if role == proto::ChannelRole::Guest { - context_menu.entry( + context_menu = context_menu.entry( + "Grant Mic Access", + None, + cx.handler_for(&this, move |_, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| { + let Some(room) = call.room() else { + return Task::ready(Ok(())); + }; + room.update(cx, |room, cx| { + room.set_participant_role( + user_id, + proto::ChannelRole::Talker, + cx, + ) + }) + }) + .detach_and_prompt_err("Failed to grant mic access", cx, |_, _| None) + }), + ); + } + if role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker { + context_menu = context_menu.entry( "Grant Write Access", None, cx.handler_for(&this, move |_, cx| { @@ -1043,10 +1140,16 @@ impl CollabPanel { } }) }), - ) - } else if role == proto::ChannelRole::Member { - context_menu.entry( - "Revoke Write Access", + ); + } + if role == proto::ChannelRole::Member || role == proto::ChannelRole::Talker { + let label = if role == proto::ChannelRole::Talker { + "Mute" + } else { + "Revoke Access" + }; + context_menu = context_menu.entry( + label, None, cx.handler_for(&this, move |_, cx| { ActiveCall::global(cx) @@ -1062,12 +1165,12 @@ impl CollabPanel { ) }) }) - .detach_and_prompt_err("Failed to revoke write access", cx, |_, _| None) + .detach_and_prompt_err("Failed to revoke access", cx, |_, _| None) }), - ) - } else { - unreachable!() + ); } + + context_menu }); cx.focus_view(&context_menu); @@ -1365,7 +1468,7 @@ impl CollabPanel { } => { if let Some(workspace) = self.workspace.upgrade() { let app_state = workspace.read(cx).app_state().clone(); - workspace::join_remote_project( + workspace::join_in_room_project( *project_id, *host_user_id, app_state, @@ -1427,6 +1530,12 @@ impl CollabPanel { ListEntry::ChannelChat { channel_id } => { self.join_channel_chat(*channel_id, cx) } + ListEntry::HostedProject { + id: _id, + name: _name, + } => { + // todo() + } ListEntry::OutgoingRequest(_) => {} ListEntry::ChannelEditor { .. } => {} @@ -1527,7 +1636,7 @@ impl CollabPanel { self.toggle_channel_collapsed(id, cx) } - fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + fn toggle_channel_collapsed(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { match self.collapsed_channels.binary_search(&channel_id) { Ok(ix) => { self.collapsed_channels.remove(ix); @@ -1864,7 +1973,7 @@ impl CollabPanel { fn respond_to_channel_invite( &mut self, - channel_id: u64, + channel_id: ChannelId, accept: bool, cx: &mut ViewContext, ) { @@ -1883,7 +1992,7 @@ impl CollabPanel { .detach_and_prompt_err("Call failed", cx, |_, _| None); } - fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { + fn join_channel(&self, channel_id: ChannelId, cx: &mut ViewContext) { let Some(workspace) = self.workspace.upgrade() else { return; }; @@ -1921,7 +2030,7 @@ impl CollabPanel { let Some(channel) = channel_store.channel_for_id(channel_id) else { return; }; - let item = ClipboardItem::new(channel.link()); + let item = ClipboardItem::new(channel.link(cx)); cx.write_to_clipboard(item) } @@ -2030,6 +2139,10 @@ impl CollabPanel { ListEntry::ChannelChat { channel_id } => self .render_channel_chat(*channel_id, is_selected, cx) .into_any_element(), + + ListEntry::HostedProject { id, name } => self + .render_channel_project(*id, name, is_selected, cx) + .into_any_element(), } } @@ -2065,7 +2178,7 @@ impl CollabPanel { font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, - line_height: relative(1.3).into(), + line_height: relative(1.3), background_color: None, underline: None, strikethrough: None, @@ -2100,7 +2213,7 @@ impl CollabPanel { let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; - channel_link = Some(channel.link()); + channel_link = Some(channel.link(cx)); (channel_icon, channel_tooltip_text) = match channel.visibility { proto::ChannelVisibility::Public => { (Some("icons/public.svg"), Some("Copy public channel link.")) @@ -2114,7 +2227,7 @@ impl CollabPanel { }); if let Some(name) = channel_name { - SharedString::from(format!("{}", name)) + SharedString::from(name.to_string()) } else { SharedString::from("Current Call") } @@ -2346,7 +2459,7 @@ impl CollabPanel { .tooltip(|cx| Tooltip::text("Accept invite", cx)), ]; - ListItem::new(("channel-invite", channel.id as usize)) + ListItem::new(("channel-invite", channel.id.0 as usize)) .selected(is_selected) .child( h_flex() @@ -2400,7 +2513,7 @@ impl CollabPanel { .map(|channel| channel.visibility) == Some(proto::ChannelVisibility::Public); let disclosed = - has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); + has_children.then(|| self.collapsed_channels.binary_search(&channel.id).is_err()); let has_messages_notification = channel_store.has_new_messages(channel_id); let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id); @@ -2438,7 +2551,7 @@ impl CollabPanel { div() .h_6() - .id(channel_id as usize) + .id(channel_id.0 as usize) .group("") .flex() .w_full() @@ -2466,7 +2579,7 @@ impl CollabPanel { this.move_channel(dragged_channel.id, channel_id, cx); })) .child( - ListItem::new(channel_id as usize) + ListItem::new(channel_id.0 as usize) // Add one level of depth for the disclosure arrow. .indent_level(depth + 1) .indent_step_size(px(20.)) @@ -2490,17 +2603,30 @@ impl CollabPanel { }, )) .start_slot( - Icon::new(if is_public { - IconName::Public - } else { - IconName::Hash - }) - .size(IconSize::Small) - .color(Color::Muted), + div() + .relative() + .child( + Icon::new(if is_public { + IconName::Public + } else { + IconName::Hash + }) + .size(IconSize::Small) + .color(Color::Muted), + ) + .children(has_notes_notification.then(|| { + div() + .w_1p5() + .z_index(1) + .absolute() + .right(px(-1.)) + .top(px(-1.)) + .child(Indicator::dot().color(Color::Info)) + })), ) .child( h_flex() - .id(channel_id as usize) + .id(channel_id.0 as usize) .child(Label::new(channel.name.clone())) .children(face_pile.map(|face_pile| face_pile.p_1())), ), @@ -2530,9 +2656,7 @@ impl CollabPanel { this.join_channel_chat(channel_id, cx) })) .tooltip(|cx| Tooltip::text("Open channel chat", cx)) - .when(!has_messages_notification, |this| { - this.visible_on_hover("") - }), + .visible_on_hover(""), ) .child( IconButton::new("channel_notes", IconName::File) @@ -2548,9 +2672,7 @@ impl CollabPanel { this.open_channel_notes(channel_id, cx) })) .tooltip(|cx| Tooltip::text("Open channel notes", cx)) - .when(!has_notes_notification, |this| { - this.visible_on_hover("") - }), + .visible_on_hover(""), ), ), ) @@ -2560,6 +2682,7 @@ impl CollabPanel { cx.new_view(|_| JoinChannelTooltip { channel_store: channel_store.clone(), channel_id, + has_notes_notification, }) .into() } @@ -2757,6 +2880,11 @@ impl PartialEq for ListEntry { return channel_1.id == channel_2.id; } } + ListEntry::HostedProject { id, .. } => { + if let ListEntry::HostedProject { id: other_id, .. } = other { + return id == other_id; + } + } ListEntry::ChannelNotes { channel_id } => { if let ListEntry::ChannelNotes { channel_id: other_id, @@ -2845,17 +2973,25 @@ impl Render for DraggedChannelView { struct JoinChannelTooltip { channel_store: Model, channel_id: ChannelId, + has_notes_notification: bool, } impl Render for JoinChannelTooltip { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - tooltip_container(cx, |div, cx| { + tooltip_container(cx, |container, cx| { let participants = self .channel_store .read(cx) .channel_participants(self.channel_id); - div.child(Label::new("Join Channel")) + container + .child(Label::new("Join channel")) + .children(self.has_notes_notification.then(|| { + h_flex() + .gap_2() + .child(Indicator::dot().color(Color::Info)) + .child(Label::new("Unread notes")) + })) .children(participants.iter().map(|participant| { h_flex() .gap_2() diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 501524501e..4f9b18198b 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,7 +1,7 @@ -use channel::{ChannelId, ChannelMembership, ChannelStore}; +use channel::{ChannelMembership, ChannelStore}; use client::{ proto::{self, ChannelRole, ChannelVisibility}, - User, UserId, UserStore, + ChannelId, User, UserId, UserStore, }; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -197,7 +197,7 @@ impl Render for ChannelModal { .read(cx) .channel_for_id(channel_id) { - let item = ClipboardItem::new(channel.link()); + let item = ClipboardItem::new(channel.link(cx)); cx.write_to_clipboard(item); } })), @@ -266,7 +266,7 @@ pub struct ChannelModalDelegate { impl PickerDelegate for ChannelModalDelegate { type ListItem = ListItem; - fn placeholder_text(&self) -> Arc { + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Search collaborator by username...".into() } diff --git a/crates/collab_ui/src/collab_panel/contact_finder.rs b/crates/collab_ui/src/collab_panel/contact_finder.rs index 6a1932a843..ff58c833f1 100644 --- a/crates/collab_ui/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui/src/collab_panel/contact_finder.rs @@ -84,7 +84,7 @@ impl PickerDelegate for ContactFinderDelegate { self.selected_index = ix; } - fn placeholder_text(&self) -> Arc { + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Search collaborator by username...".into() } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index f6e1299437..2df240564e 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -187,9 +187,10 @@ impl Render for CollabTitlebarItem { let is_muted = room.is_muted(); let is_deafened = room.is_deafened().unwrap_or(false); let is_screen_sharing = room.is_screen_sharing(); - let read_only = room.read_only(); + let can_use_microphone = room.can_use_microphone(); + let can_share_projects = room.can_share_projects(); - this.when(is_local && !read_only, |this| { + this.when(is_local && can_share_projects, |this| { this.child( Button::new( "toggle_sharing", @@ -235,7 +236,7 @@ impl Render for CollabTitlebarItem { ) .pr_2(), ) - .when(!read_only, |this| { + .when(can_use_microphone, |this| { this.child( IconButton::new( "mute-microphone", @@ -276,7 +277,7 @@ impl Render for CollabTitlebarItem { .icon_size(IconSize::Small) .selected(is_deafened) .tooltip(move |cx| { - if !read_only { + if can_use_microphone { Tooltip::with_meta( "Deafen Audio", None, @@ -289,7 +290,7 @@ impl Render for CollabTitlebarItem { }) .on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)), ) - .when(!read_only, |this| { + .when(can_share_projects, |this| { this.child( IconButton::new("screen-share", ui::IconName::Screen) .style(ButtonStyle::Subtle) @@ -402,7 +403,7 @@ impl CollabTitlebarItem { ) }) .on_click({ - let host_peer_id = host.peer_id.clone(); + let host_peer_id = host.peer_id; cx.listener(move |this, _, cx| { this.workspace .update(cx, |workspace, cx| { @@ -421,14 +422,20 @@ impl CollabTitlebarItem { worktree.root_name() }); - names.next().unwrap_or("") + names.next() + }; + let is_project_selected = name.is_some(); + let name = if let Some(name) = name { + util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH) + } else { + "Open recent project".to_string() }; - let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH); let workspace = self.workspace.clone(); popover_menu("project_name_trigger") .trigger( Button::new("project_name_trigger", name) + .when(!is_project_selected, |b| b.color(Color::Muted)) .style(ButtonStyle::Subtle) .label_size(LabelSize::Small) .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), @@ -471,6 +478,7 @@ impl CollabTitlebarItem { ) } + #[allow(clippy::too_many_arguments)] fn render_collaborator( &self, user: &Arc, @@ -689,6 +697,7 @@ impl CollabTitlebarItem { .menu(|cx| { ContextMenu::build(cx, |menu, _| { menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) + .action("Extensions", extensions_ui::Extensions.boxed_clone()) .action("Theme", theme_selector::Toggle.boxed_clone()) .separator() .action("Share Feedback", feedback::GiveFeedback.boxed_clone()) @@ -714,6 +723,7 @@ impl CollabTitlebarItem { ContextMenu::build(cx, |menu, _| { menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) .action("Theme", theme_selector::Toggle.boxed_clone()) + .action("Extensions", extensions_ui::Extensions.boxed_clone()) .separator() .action("Share Feedback", feedback::GiveFeedback.boxed_clone()) }) diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 54699a3440..6bd4abe588 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -1,7 +1,7 @@ use crate::{chat_panel::ChatPanel, NotificationPanelSettings}; use anyhow::Result; use channel::ChannelStore; -use client::{Client, Notification, User, UserStore}; +use client::{ChannelId, Client, Notification, User, UserStore}; use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::StreamExt; @@ -29,7 +29,7 @@ use workspace::{ const LOADING_THRESHOLD: usize = 30; const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1); const TOAST_DURATION: Duration = Duration::from_secs(5); -const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel"; +const NOTIFICATION_PANEL_KEY: &str = "NotificationPanel"; pub struct NotificationPanel { client: Arc, @@ -183,7 +183,7 @@ impl NotificationPanel { let panel = Self::new(workspace, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width; + panel.width = serialized_panel.width.map(|w| w.round()); cx.notify(); }); } @@ -357,7 +357,7 @@ impl NotificationPanel { "{} invited you to join the #{channel_name} channel", inviter.github_login ), - needs_response: channel_store.has_channel_invitation(channel_id), + needs_response: channel_store.has_channel_invitation(ChannelId(channel_id)), actor: Some(inviter), can_navigate: false, }) @@ -368,7 +368,7 @@ impl NotificationPanel { message_id, } => { let sender = user_store.get_cached_user(sender_id)?; - let channel = channel_store.channel_for_id(channel_id)?; + let channel = channel_store.channel_for_id(ChannelId(channel_id))?; let message = self .notification_store .read(cx) @@ -432,7 +432,7 @@ impl NotificationPanel { if let Some(panel) = workspace.focus_panel::(cx) { panel.update(cx, |panel, cx| { panel - .select_channel(channel_id, Some(message_id), cx) + .select_channel(ChannelId(channel_id), Some(message_id), cx) .detach_and_log_err(cx); }); } @@ -454,7 +454,7 @@ impl NotificationPanel { panel.is_scrolled_to_bottom() && panel .active_chat() - .map_or(false, |chat| chat.read(cx).channel_id == *channel_id) + .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id) } else { false }; @@ -778,7 +778,7 @@ fn format_timestamp( "just now".to_string() } } else if date.next_day() == Some(today) { - format!("yesterday") + "yesterday".to_string() } else { format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year()) } diff --git a/crates/collab_ui/src/notifications/incoming_call_notification.rs b/crates/collab_ui/src/notifications/incoming_call_notification.rs index f66194c52a..a8ba20c1e5 100644 --- a/crates/collab_ui/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui/src/notifications/incoming_call_notification.rs @@ -82,7 +82,7 @@ impl IncomingCallNotificationState { if let Some(project_id) = initial_project_id { cx.update(|cx| { if let Some(app_state) = app_state.upgrade() { - workspace::join_remote_project( + workspace::join_in_room_project( project_id, caller_user_id, app_state, @@ -119,7 +119,7 @@ impl Render for IncomingCallNotification { let theme_settings = ThemeSettings::get_global(cx); ( theme_settings.ui_font.family.clone(), - theme_settings.ui_font_size.clone(), + theme_settings.ui_font_size, ) }; diff --git a/crates/collab_ui/src/notifications/project_shared_notification.rs b/crates/collab_ui/src/notifications/project_shared_notification.rs index b8ceefcd76..46c5c8ce8a 100644 --- a/crates/collab_ui/src/notifications/project_shared_notification.rs +++ b/crates/collab_ui/src/notifications/project_shared_notification.rs @@ -98,7 +98,7 @@ impl ProjectSharedNotification { fn join(&mut self, cx: &mut ViewContext) { if let Some(app_state) = self.app_state.upgrade() { - workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx) + workspace::join_in_room_project(self.project_id, self.owner.id, app_state, cx) .detach_and_log_err(cx); } } @@ -123,7 +123,7 @@ impl Render for ProjectSharedNotification { let theme_settings = ThemeSettings::get_global(cx); ( theme_settings.ui_font.family.clone(), - theme_settings.ui_font_size.clone(), + theme_settings.ui_font_size, ) }; diff --git a/crates/color/Cargo.toml b/crates/color/Cargo.toml index e8530befdf..d3e4a125f4 100644 --- a/crates/color/Cargo.toml +++ b/crates/color/Cargo.toml @@ -7,13 +7,10 @@ license = "GPL-3.0-or-later" [features] default = [] -stories = ["dep:itertools", "dep:story"] [lib] path = "src/color.rs" doctest = true [dependencies] -itertools = { version = "0.11.0", optional = true } -palette = "0.7.3" -story = { workspace = true, optional = true } +palette.workspace = true diff --git a/crates/color/src/color.rs b/crates/color/src/color.rs index 43bbc1c032..4f5946fbde 100644 --- a/crates/color/src/color.rs +++ b/crates/color/src/color.rs @@ -14,7 +14,7 @@ //! Once we have a good idea of the needs of the theme system and color in gpui in general I see 3 paths: //! 1. Use `palette` (or another color library) directly in gpui and everywhere else, rather than rolling our own color system. //! 2. Keep this crate as a thin wrapper around `palette` and use it everywhere except gpui, and convert to gpui's color system when needed. -//! 3. Build the needed functionality into gpui and keep using it's color system everywhere. +//! 3. Build the needed functionality into gpui and keep using its color system everywhere. //! //! I'm leaning towards 2 in the short term and 1 in the long term, but we'll need to discuss it more. //! @@ -197,7 +197,7 @@ pub struct ColorStates { /// Returns a set of colors for different states of an element. /// -/// todo!("This should take a theme and use appropriate colors from it") +/// todo("This should take a theme and use appropriate colors from it") pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates { let adjustment_factor = if is_light { 0.1 } else { -0.1 }; let hover_adjustment = 1.0 - adjustment_factor; diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index cf11502741..565939e4a3 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -10,17 +10,14 @@ path = "src/command_palette.rs" doctest = false [dependencies] -anyhow.workspace = true client.workspace = true collections.workspace = true -# HACK: We're only depending on `copilot` here for `CommandPaletteFilter`. See the attached comment on that type. -copilot.workspace = true -editor.workspace = true +command_palette_hooks.workspace = true fuzzy.workspace = true gpui.workspace = true picker.workspace = true +postage.workspace = true project.workspace = true -release_channel.workspace = true serde.workspace = true settings.workspace = true theme.workspace = true diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 2a7a94b544..0cd7f581de 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -1,19 +1,22 @@ use std::{ cmp::{self, Reverse}, sync::Arc, + time::Duration, }; -use client::telemetry::Telemetry; +use client::{parse_zed_link, telemetry::Telemetry}; use collections::HashMap; -use copilot::CommandPaletteFilter; +use command_palette_hooks::{ + CommandInterceptResult, CommandPaletteFilter, CommandPaletteInterceptor, +}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global, - ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, + ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; -use release_channel::{parse_zed_link, ReleaseChannel}; +use postage::{sink::Sink, stream::Stream}; use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ModalView, Workspace}; @@ -22,6 +25,7 @@ use zed_actions::OpenZedUrl; actions!(command_palette, [Toggle]); pub fn init(cx: &mut AppContext) { + client::init_settings(cx); cx.set_global(HitCounts::default()); cx.set_global(CommandPaletteFilter::default()); cx.observe_new_views(CommandPalette::register).detach(); @@ -33,6 +37,24 @@ pub struct CommandPalette { picker: View>, } +fn trim_consecutive_whitespaces(input: &str) -> String { + let mut result = String::with_capacity(input.len()); + let mut last_char_was_whitespace = false; + + for char in input.trim().chars() { + if char.is_whitespace() { + if !last_char_was_whitespace { + result.push(char); + } + last_char_was_whitespace = true; + } else { + result.push(char); + last_char_was_whitespace = false; + } + } + result +} + impl CommandPalette { fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|workspace, _: &Toggle, cx| { @@ -99,18 +121,6 @@ impl Render for CommandPalette { } } -pub struct CommandPaletteInterceptor( - pub Box Option>, -); - -impl Global for CommandPaletteInterceptor {} - -pub struct CommandInterceptResult { - pub action: Box, - pub string: String, - pub positions: Vec, -} - pub struct CommandPaletteDelegate { command_palette: WeakView, all_commands: Vec, @@ -119,6 +129,10 @@ pub struct CommandPaletteDelegate { selected_ix: usize, telemetry: Arc, previous_focus_handle: FocusHandle, + updating_matches: Option<( + Task<()>, + postage::dispatch::Receiver<(Vec, Vec)>, + )>, } struct Command { @@ -138,7 +152,7 @@ impl Clone for Command { /// Hit count for each command in the palette. /// We only account for commands triggered directly via command palette and not by e.g. keystrokes because /// if a user already knows a keystroke for a command, they are unlikely to use a command palette to look for it. -#[derive(Default)] +#[derive(Default, Clone)] struct HitCounts(HashMap); impl Global for HitCounts {} @@ -158,6 +172,66 @@ impl CommandPaletteDelegate { selected_ix: 0, telemetry, previous_focus_handle, + updating_matches: None, + } + } + + fn matches_updated( + &mut self, + query: String, + mut commands: Vec, + mut matches: Vec, + cx: &mut ViewContext>, + ) { + self.updating_matches.take(); + + let mut intercept_result = + if let Some(interceptor) = cx.try_global::() { + (interceptor.0)(&query, cx) + } else { + None + }; + + if parse_zed_link(&query, cx).is_some() { + intercept_result = Some(CommandInterceptResult { + action: OpenZedUrl { url: query.clone() }.boxed_clone(), + string: query.clone(), + positions: vec![], + }) + } + + if let Some(CommandInterceptResult { + action, + string, + positions, + }) = intercept_result + { + if let Some(idx) = matches + .iter() + .position(|m| commands[m.candidate_id].action.type_id() == action.type_id()) + { + matches.remove(idx); + } + commands.push(Command { + name: string.clone(), + action, + }); + matches.insert( + 0, + StringMatch { + candidate_id: commands.len() - 1, + string, + positions, + score: 0.0, + }, + ) + } + self.commands = commands; + self.matches = matches; + if self.matches.is_empty() { + self.selected_ix = 0; + } else { + self.selected_ix = cmp::min(self.selected_ix, self.matches.len() - 1); } } } @@ -165,7 +239,7 @@ impl CommandPaletteDelegate { impl PickerDelegate for CommandPaletteDelegate { type ListItem = ListItem; - fn placeholder_text(&self) -> Arc { + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Execute a command...".into() } @@ -186,113 +260,98 @@ impl PickerDelegate for CommandPaletteDelegate { query: String, cx: &mut ViewContext>, ) -> gpui::Task<()> { - let mut commands = self.all_commands.clone(); - - cx.spawn(move |picker, mut cx| async move { - cx.read_global::(|hit_counts, _| { + let (mut tx, mut rx) = postage::dispatch::channel(1); + let task = cx.background_executor().spawn({ + let mut commands = self.all_commands.clone(); + let hit_counts = cx.global::().clone(); + let executor = cx.background_executor().clone(); + let query = trim_consecutive_whitespaces(&query.as_str()); + async move { commands.sort_by_key(|action| { ( Reverse(hit_counts.0.get(&action.name).cloned()), action.name.clone(), ) }); - }) - .ok(); - let candidates = commands - .iter() - .enumerate() - .map(|(ix, command)| StringMatchCandidate { - id: ix, - string: command.name.to_string(), - char_bag: command.name.chars().collect(), - }) - .collect::>(); - let mut matches = if query.is_empty() { - candidates - .into_iter() + let candidates = commands + .iter() .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, + .map(|(ix, command)| StringMatchCandidate { + id: ix, + string: command.name.to_string(), + char_bag: command.name.chars().collect(), }) - .collect() - } else { - fuzzy::match_strings( - &candidates, - &query, - true, - 10000, - &Default::default(), - cx.background_executor().clone(), - ) - .await + .collect::>(); + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + let ret = fuzzy::match_strings( + &candidates, + &query, + true, + 10000, + &Default::default(), + executor, + ) + .await; + ret + }; + + tx.send((commands, matches)).await.log_err(); + } + }); + self.updating_matches = Some((task, rx.clone())); + + cx.spawn(move |picker, mut cx| async move { + let Some((commands, matches)) = rx.recv().await else { + return; }; - let mut intercept_result = cx - .try_read_global(|interceptor: &CommandPaletteInterceptor, cx| { - (interceptor.0)(&query, cx) - }) - .flatten(); - let release_channel = cx - .update(|cx| ReleaseChannel::try_global(cx)) - .ok() - .flatten(); - if release_channel == Some(ReleaseChannel::Dev) { - if parse_zed_link(&query).is_some() { - intercept_result = Some(CommandInterceptResult { - action: OpenZedUrl { url: query.clone() }.boxed_clone(), - string: query.clone(), - positions: vec![], - }) - } - } - - if let Some(CommandInterceptResult { - action, - string, - positions, - }) = intercept_result - { - if let Some(idx) = matches - .iter() - .position(|m| commands[m.candidate_id].action.type_id() == action.type_id()) - { - matches.remove(idx); - } - commands.push(Command { - name: string.clone(), - action, - }); - matches.insert( - 0, - StringMatch { - candidate_id: commands.len() - 1, - string, - positions, - score: 0.0, - }, - ) - } - picker - .update(&mut cx, |picker, _| { - let delegate = &mut picker.delegate; - delegate.commands = commands; - delegate.matches = matches; - if delegate.matches.is_empty() { - delegate.selected_ix = 0; - } else { - delegate.selected_ix = - cmp::min(delegate.selected_ix, delegate.matches.len() - 1); - } + .update(&mut cx, |picker, cx| { + picker + .delegate + .matches_updated(query, commands, matches, cx) }) .log_err(); }) } + fn finalize_update_matches( + &mut self, + query: String, + duration: Duration, + cx: &mut ViewContext>, + ) -> bool { + let Some((task, rx)) = self.updating_matches.take() else { + return true; + }; + + match cx + .background_executor() + .block_with_timeout(duration, rx.clone().recv()) + { + Ok(Some((commands, matches))) => { + self.matches_updated(query, commands, matches, cx); + true + } + _ => { + self.updating_matches = Some((task, rx)); + false + } + } + } + fn dismissed(&mut self, cx: &mut ViewContext>) { self.command_palette .update(cx, |_, cx| cx.emit(DismissEvent)) @@ -426,7 +485,7 @@ mod tests { }); workspace.update(cx, |workspace, cx| { - workspace.add_item(Box::new(editor.clone()), cx); + workspace.add_item_to_active_pane(Box::new(editor.clone()), cx); editor.update(cx, |editor, cx| editor.focus(cx)) }); diff --git a/crates/command_palette_hooks/Cargo.toml b/crates/command_palette_hooks/Cargo.toml new file mode 100644 index 0000000000..8dd8b7bf69 --- /dev/null +++ b/crates/command_palette_hooks/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "command_palette_hooks" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lib] +path = "src/command_palette_hooks.rs" +doctest = false + +[dependencies] +collections.workspace = true +gpui.workspace = true diff --git a/crates/plugin/LICENSE-GPL b/crates/command_palette_hooks/LICENSE-GPL similarity index 100% rename from crates/plugin/LICENSE-GPL rename to crates/command_palette_hooks/LICENSE-GPL diff --git a/crates/command_palette_hooks/src/command_palette_hooks.rs b/crates/command_palette_hooks/src/command_palette_hooks.rs new file mode 100644 index 0000000000..a24955a255 --- /dev/null +++ b/crates/command_palette_hooks/src/command_palette_hooks.rs @@ -0,0 +1,24 @@ +use std::any::TypeId; + +use collections::HashSet; +use gpui::{Action, AppContext, Global}; + +#[derive(Default)] +pub struct CommandPaletteFilter { + pub hidden_namespaces: HashSet<&'static str>, + pub hidden_action_types: HashSet, +} + +impl Global for CommandPaletteFilter {} + +pub struct CommandPaletteInterceptor( + pub Box Option>, +); + +impl Global for CommandPaletteInterceptor {} + +pub struct CommandInterceptResult { + pub action: Box, + pub string: String, + pub positions: Vec, +} diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 8ad50e898a..eca15d3c56 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -22,22 +22,23 @@ test-support = [ [dependencies] anyhow.workspace = true async-compression.workspace = true -async-tar = "0.4.2" +async-tar.workspace = true collections.workspace = true +command_palette_hooks.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true -log.workspace = true lsp.workspace = true node_runtime.workspace = true parking_lot.workspace = true serde.workspace = true -serde_derive.workspace = true settings.workspace = true smol.workspace = true -theme.workspace = true util.workspace = true +[target.'cfg(windows)'.dependencies] +async-std = { version = "1.12.0", features = ["unstable"] } + [dev-dependencies] clock.workspace = true collections = { workspace = true, features = ["test-support"] } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index bce829fa59..e6552cdc33 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -3,6 +3,7 @@ use anyhow::{anyhow, Context as _, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use collections::{HashMap, HashSet}; +use command_palette_hooks::CommandPaletteFilter; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; use gpui::{ actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Global, Model, @@ -32,17 +33,6 @@ use util::{ ResultExt, }; -// HACK: This type is only defined in `copilot` since it is the earliest ancestor -// of the crates that use it. -// -// This is not great. Let's find a better place for it to live. -#[derive(Default)] -pub struct CommandPaletteFilter { - pub hidden_namespaces: HashSet<&'static str>, - pub hidden_action_types: HashSet, -} - -impl Global for CommandPaletteFilter {} actions!( copilot, [ @@ -393,8 +383,16 @@ impl Copilot { use lsp::FakeLanguageServer; use node_runtime::FakeNodeRuntime; - let (server, fake_server) = - FakeLanguageServer::new("copilot".into(), Default::default(), cx.to_async()); + let (server, fake_server) = FakeLanguageServer::new( + LanguageServerBinary { + path: "path/to/copilot".into(), + arguments: vec![], + env: None, + }, + "copilot".into(), + Default::default(), + cx.to_async(), + ); let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); let node_runtime = FakeNodeRuntime::new(); let this = cx.new_model(|cx| Self { @@ -428,6 +426,8 @@ impl Copilot { let binary = LanguageServerBinary { path: node_path, arguments, + // TODO: We could set HTTP_PROXY etc here and fix the copilot issue. + env: None, }; let server = LanguageServer::new( @@ -512,7 +512,7 @@ impl Copilot { .await?; match sign_in { request::SignInInitiateResult::AlreadySignedIn { user } => { - Ok(request::SignInStatus::Ok { user }) + Ok(request::SignInStatus::Ok { user: Some(user) }) } request::SignInInitiateResult::PromptUserDeviceFlow(flow) => { this.update(&mut cx, |this, cx| { @@ -920,7 +920,7 @@ impl Copilot { if let Ok(server) = self.server.as_running() { match lsp_status { - request::SignInStatus::Ok { .. } + request::SignInStatus::Ok { user: Some(_) } | request::SignInStatus::MaybeOk { .. } | request::SignInStatus::AlreadySignedIn { .. } => { server.sign_in_status = SignInStatus::Authorized; @@ -936,7 +936,7 @@ impl Copilot { self.unregister_buffer(&buffer); } } - request::SignInStatus::NotSignedIn => { + request::SignInStatus::Ok { user: None } | request::SignInStatus::NotSignedIn => { server.sign_in_status = SignInStatus::SignedOut; for buffer in self.buffers.iter().cloned().collect::>() { self.unregister_buffer(&buffer); @@ -971,7 +971,7 @@ async fn clear_copilot_dir() { } async fn get_copilot_lsp(http: Arc) -> anyhow::Result { - const SERVER_PATH: &'static str = "dist/agent.js"; + const SERVER_PATH: &str = "dist/agent.js"; ///Check for the latest copilot language server and download it if we haven't already async fn fetch_latest(http: Arc) -> anyhow::Result { diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index 0f9a478b91..0deabe16d1 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -52,7 +52,7 @@ pub struct SignInConfirmParams { pub enum SignInStatus { #[serde(rename = "OK")] Ok { - user: String, + user: Option, }, MaybeOk { user: String, diff --git a/crates/copilot_ui/Cargo.toml b/crates/copilot_ui/Cargo.toml index 9ce26eef4c..cc83e09aa6 100644 --- a/crates/copilot_ui/Cargo.toml +++ b/crates/copilot_ui/Cargo.toml @@ -14,12 +14,9 @@ anyhow.workspace = true copilot.workspace = true editor.workspace = true fs.workspace = true -futures.workspace = true gpui.workspace = true language.workspace = true settings.workspace = true -smol.workspace = true -theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/copilot_ui/src/copilot_button.rs b/crates/copilot_ui/src/copilot_button.rs index 8ea106a3ff..afc39c3d19 100644 --- a/crates/copilot_ui/src/copilot_button.rs +++ b/crates/copilot_ui/src/copilot_button.rs @@ -119,7 +119,9 @@ impl Render for CopilotButton { impl CopilotButton { pub fn new(fs: Arc, cx: &mut ViewContext) -> Self { - Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach()); + if let Some(copilot) = Copilot::global(cx) { + cx.observe(&copilot, |_, _, cx| cx.notify()).detach() + } cx.observe_global::(move |_, cx| cx.notify()) .detach(); @@ -238,7 +240,7 @@ impl CopilotButton { impl StatusItemView for CopilotButton { fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { - if let Some(editor) = item.map(|item| item.act_as::(cx)).flatten() { + if let Some(editor) = item.and_then(|item| item.act_as::(cx)) { self.editor_subscription = Some(( cx.observe(&editor, Self::update_enabled), editor.entity_id().as_u64() as usize, @@ -309,7 +311,7 @@ async fn configure_disabled_globs( fn toggle_copilot_globally(fs: Arc, cx: &mut AppContext) { let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None); update_settings_file::(fs, cx, move |file| { - file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) + file.defaults.show_copilot_suggestions = Some(!show_copilot_suggestions) }); } @@ -330,7 +332,7 @@ fn hide_copilot(fs: Arc, cx: &mut AppContext) { }); } -fn initiate_sign_in(cx: &mut WindowContext) { +pub fn initiate_sign_in(cx: &mut WindowContext) { let Some(copilot) = Copilot::global(cx) else { return; }; diff --git a/crates/copilot_ui/src/copilot_ui.rs b/crates/copilot_ui/src/copilot_ui.rs index 64dd068d5a..f55090ebcb 100644 --- a/crates/copilot_ui/src/copilot_ui.rs +++ b/crates/copilot_ui/src/copilot_ui.rs @@ -1,4 +1,4 @@ -mod copilot_button; +pub mod copilot_button; mod sign_in; pub use copilot_button::*; diff --git a/crates/copilot_ui/src/sign_in.rs b/crates/copilot_ui/src/sign_in.rs index 1ac0469f2d..976d5f8b3e 100644 --- a/crates/copilot_ui/src/sign_in.rs +++ b/crates/copilot_ui/src/sign_in.rs @@ -7,7 +7,7 @@ use gpui::{ use ui::{prelude::*, Button, IconName, Label}; use workspace::ModalView; -const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; +const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot"; pub struct CopilotCodeVerification { status: Status, diff --git a/crates/db/Cargo.toml b/crates/db/Cargo.toml index 138eba776a..0b01e691b1 100644 --- a/crates/db/Cargo.toml +++ b/crates/db/Cargo.toml @@ -14,22 +14,16 @@ test-support = [] [dependencies] anyhow.workspace = true -async-trait.workspace = true -collections.workspace = true gpui.workspace = true indoc.workspace = true lazy_static.workspace = true log.workspace = true -parking_lot.workspace = true release_channel.workspace = true -serde.workspace = true -serde_derive.workspace = true smol.workspace = true sqlez.workspace = true sqlez_macros.workspace = true util.workspace = true [dev-dependencies] -env_logger.workspace = true gpui = { workspace = true, features = ["test-support"] } tempfile.workspace = true diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index b0ed1606d8..2e6b325fee 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -22,20 +22,20 @@ use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use util::{async_maybe, ResultExt}; -const CONNECTION_INITIALIZE_QUERY: &'static str = sql!( +const CONNECTION_INITIALIZE_QUERY: &str = sql!( PRAGMA foreign_keys=TRUE; ); -const DB_INITIALIZE_QUERY: &'static str = sql!( +const DB_INITIALIZE_QUERY: &str = sql!( PRAGMA journal_mode=WAL; PRAGMA busy_timeout=1; PRAGMA case_sensitive_like=TRUE; PRAGMA synchronous=NORMAL; ); -const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB"; +const FALLBACK_DB_NAME: &str = "FALLBACK_MEMORY_DB"; -const DB_FILE_NAME: &'static str = "db.sqlite"; +const DB_FILE_NAME: &str = "db.sqlite"; lazy_static::lazy_static! { pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()); diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 8a7980e10c..e4e408b665 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -18,13 +18,10 @@ gpui.workspace = true language.workspace = true log.workspace = true lsp.workspace = true -postage.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true -serde_derive.workspace = true settings.workspace = true -smallvec.workspace = true theme.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index e737b69717..6d6a946fa0 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -202,7 +202,7 @@ impl ProjectDiagnosticsEditor { let diagnostics = cx.new_view(|cx| { ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx) }); - workspace.add_item(Box::new(diagnostics), cx); + workspace.add_item_to_active_pane(Box::new(diagnostics), cx); } } @@ -347,7 +347,7 @@ impl ProjectDiagnosticsEditor { .diagnostic_groups .last() .unwrap(); - prev_path_last_group.excerpts.last().unwrap().clone() + *prev_path_last_group.excerpts.last().unwrap() } else { ExcerptId::min() }; @@ -451,10 +451,10 @@ impl ProjectDiagnosticsEditor { .pop() .unwrap(); - prev_excerpt_id = excerpt_id.clone(); - first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone()); - group_state.excerpts.push(excerpt_id.clone()); - let header_position = (excerpt_id.clone(), language::Anchor::MIN); + prev_excerpt_id = excerpt_id; + first_excerpt_id.get_or_insert_with(|| prev_excerpt_id); + group_state.excerpts.push(excerpt_id); + let header_position = (excerpt_id, language::Anchor::MIN); if is_first_excerpt_for_group { is_first_excerpt_for_group = false; @@ -483,7 +483,7 @@ impl ProjectDiagnosticsEditor { if !diagnostic.message.is_empty() { group_state.block_count += 1; blocks_to_add.push(BlockProperties { - position: (excerpt_id.clone(), entry.range.start), + position: (excerpt_id, entry.range.start), height: diagnostic.message.matches('\n').count() as u8 + 1, style: BlockStyle::Fixed, render: diagnostic_block_renderer(diagnostic, true), @@ -506,8 +506,8 @@ impl ProjectDiagnosticsEditor { group_ixs_to_remove.push(group_ix); blocks_to_remove.extend(group_state.blocks.iter().copied()); } else if let Some((_, group)) = to_keep { - prev_excerpt_id = group.excerpts.last().unwrap().clone(); - first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone()); + prev_excerpt_id = *group.excerpts.last().unwrap(); + first_excerpt_id.get_or_insert_with(|| prev_excerpt_id); } } @@ -591,7 +591,7 @@ impl ProjectDiagnosticsEditor { if let Some(group) = groups.get(group_ix) { let offset = excerpts_snapshot .anchor_in_excerpt( - group.excerpts[group.primary_excerpt_ix].clone(), + group.excerpts[group.primary_excerpt_ix], group.primary_diagnostic.range.start, ) .to_offset(&excerpts_snapshot); @@ -735,8 +735,13 @@ impl Item for ProjectDiagnosticsEditor { true } - fn save(&mut self, project: Model, cx: &mut ViewContext) -> Task> { - self.editor.save(project, cx) + fn save( + &mut self, + format: bool, + project: Model, + cx: &mut ViewContext, + ) -> Task> { + self.editor.save(format, project, cx) } fn save_as( @@ -797,7 +802,7 @@ impl Item for ProjectDiagnosticsEditor { fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { let (message, code_ranges) = highlight_diagnostic_message(&diagnostic); - let message: SharedString = message.into(); + let message: SharedString = message; Arc::new(move |cx| { let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into(); h_flex() @@ -885,7 +890,7 @@ mod tests { use super::*; use editor::{ display_map::{BlockContext, TransformBlock}, - DisplayPoint, + DisplayPoint, GutterDimensions, }; use gpui::{px, TestAppContext, VisualTestContext, WindowContext}; use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped}; @@ -1584,7 +1589,6 @@ mod tests { } fn editor_blocks(editor: &View, cx: &mut WindowContext) -> Vec<(u32, SharedString)> { - let editor_view = editor.clone(); editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); snapshot @@ -1593,19 +1597,16 @@ mod tests { .filter_map(|(ix, (row, block))| { let name: SharedString = match block { TransformBlock::Custom(block) => cx.with_element_context({ - let editor_view = editor_view.clone(); |cx| -> Option { block .render(&mut BlockContext { context: cx, anchor_x: px(0.), - gutter_padding: px(0.), - gutter_width: px(0.), + gutter_dimensions: &GutterDimensions::default(), line_height: px(0.), em_width: px(0.), max_width: px(0.), block_id: ix, - view: editor_view, editor_style: &editor::EditorStyle::default(), }) .inner_id()? diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index a838dd6572..8266e12ba7 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -21,6 +21,7 @@ test-support = [ "workspace/test-support", "tree-sitter-rust", "tree-sitter-typescript", + "tree-sitter-html" ] [dependencies] @@ -37,29 +38,25 @@ fuzzy.workspace = true git.workspace = true gpui.workspace = true indoc = "1.0.4" -itertools = "0.10" +itertools.workspace = true language.workspace = true lazy_static.workspace = true -linkify = "0.10.0" +linkify.workspace = true log.workspace = true lsp.workspace = true multi_buffer.workspace = true ordered-float.workspace = true parking_lot.workspace = true -postage.workspace = true project.workspace = true rand.workspace = true -rich_text.workspace = true rpc.workspace = true schemars.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true settings.workspace = true smallvec.workspace = true smol.workspace = true snippet.workspace = true -sqlez.workspace = true sum_tree.workspace = true text.workspace = true theme.workspace = true @@ -87,7 +84,6 @@ text = { workspace = true, features = ["test-support"] } tree-sitter-html.workspace = true tree-sitter-rust.workspace = true tree-sitter-typescript.workspace = true -tree-sitter.workspace = true unindent.workspace = true util = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index e6c4d36984..d0bbab0335 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -119,6 +119,7 @@ impl_actions!( gpui::actions!( editor, [ + AcceptPartialCopilotSuggestion, AddSelectionAbove, AddSelectionBelow, Backspace, @@ -165,6 +166,8 @@ gpui::actions!( GoToPrevHunk, GoToTypeDefinition, GoToTypeDefinitionSplit, + GoToImplementation, + GoToImplementationSplit, OpenUrl, HalfPageDown, HalfPageUp, @@ -195,6 +198,7 @@ gpui::actions!( NewlineBelow, NextScreen, OpenExcerpts, + OpenExcerptsSplit, OpenPermalinkToLine, Outdent, PageDown, @@ -236,6 +240,7 @@ gpui::actions!( TabPrev, ToggleInlayHints, ToggleSoftWrap, + ToggleLineNumbers, Transpose, Undo, UndoSelection, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 2f4c4ddc44..c0213bc331 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -24,10 +24,7 @@ mod tab_map; mod wrap_map; use crate::EditorStyle; -use crate::{ - hover_links::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt, InlayId, - MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, -}; +use crate::{hover_links::InlayHighlight, movement::TextLayoutDetails, InlayId}; pub use block_map::{BlockMap, BlockPoint}; use collections::{BTreeMap, HashMap, HashSet}; use fold_map::FoldMap; @@ -37,6 +34,7 @@ use language::{ language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, }; use lsp::DiagnosticSeverity; +use multi_buffer::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; use tab_map::TabMap; @@ -328,7 +326,7 @@ impl DisplayMap { .read(cx) .as_singleton() .and_then(|buffer| buffer.read(cx).language()); - language_settings(language.as_deref(), None, cx).tab_size + language_settings(language, None, cx).tab_size } #[cfg(test)] @@ -504,7 +502,7 @@ impl DisplaySnapshot { /// Returns text chunks starting at the end of the given display row in reverse until the start of the file pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { - (0..=display_row).into_iter().rev().flat_map(|row| { + (0..=display_row).rev().flat_map(|row| { self.block_snapshot .chunks(row..row + 1, false, Highlights::default()) .map(|h| h.text) @@ -514,13 +512,13 @@ impl DisplaySnapshot { }) } - pub fn chunks<'a>( - &'a self, + pub fn chunks( + &self, display_rows: Range, language_aware: bool, inlay_highlight_style: Option, suggestion_highlight_style: Option, - ) -> DisplayChunks<'a> { + ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, @@ -849,7 +847,7 @@ impl DisplaySnapshot { self.block_snapshot.longest_row() } - pub fn fold_for_line(self: &Self, buffer_row: u32) -> Option { + pub fn fold_for_line(&self, buffer_row: u32) -> Option { if self.is_line_folded(buffer_row) { Some(FoldStatus::Folded) } else if self.is_foldable(buffer_row) { @@ -859,7 +857,7 @@ impl DisplaySnapshot { } } - pub fn is_foldable(self: &Self, buffer_row: u32) -> bool { + pub fn is_foldable(&self, buffer_row: u32) -> bool { let max_row = self.buffer_snapshot.max_buffer_row(); if buffer_row >= max_row { return false; @@ -882,7 +880,7 @@ impl DisplaySnapshot { false } - pub fn foldable_range(self: &Self, buffer_row: u32) -> Option> { + pub fn foldable_range(&self, buffer_row: u32) -> Option> { let start = Point::new(buffer_row, self.buffer_snapshot.line_len(buffer_row)); if self.is_foldable(start.row) && !self.is_line_folded(start.row) { let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row); @@ -1293,7 +1291,7 @@ pub mod tests { let mut cx = EditorTestContext::new(cx).await; let editor = cx.editor.clone(); - let window = cx.window.clone(); + let window = cx.window; _ = cx.update_window(window, |_, cx| { let text_layout_details = @@ -1457,10 +1455,8 @@ pub mod tests { }"# .unindent(); - let theme = SyntaxTheme::new_test(vec![ - ("mod.body", Hsla::red().into()), - ("fn.name", Hsla::blue().into()), - ]); + let theme = + SyntaxTheme::new_test(vec![("mod.body", Hsla::red()), ("fn.name", Hsla::blue())]); let language = Arc::new( Language::new( LanguageConfig { @@ -1547,10 +1543,8 @@ pub mod tests { }"# .unindent(); - let theme = SyntaxTheme::new_test(vec![ - ("mod.body", Hsla::red().into()), - ("fn.name", Hsla::blue().into()), - ]); + let theme = + SyntaxTheme::new_test(vec![("mod.body", Hsla::red()), ("fn.name", Hsla::blue())]); let language = Arc::new( Language::new( LanguageConfig { @@ -1618,10 +1612,8 @@ pub mod tests { async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) { cx.update(|cx| init_test(cx, |_| {})); - let theme = SyntaxTheme::new_test(vec![ - ("operator", Hsla::red().into()), - ("string", Hsla::green().into()), - ]); + let theme = + SyntaxTheme::new_test(vec![("operator", Hsla::red()), ("string", Hsla::green())]); let language = Arc::new( Language::new( LanguageConfig { @@ -1834,10 +1826,10 @@ pub mod tests { ) } - fn syntax_chunks<'a>( + fn syntax_chunks( rows: Range, map: &Model, - theme: &'a SyntaxTheme, + theme: &SyntaxTheme, cx: &mut AppContext, ) -> Vec<(String, Option)> { chunks(rows, map, theme, cx) @@ -1846,10 +1838,10 @@ pub mod tests { .collect() } - fn chunks<'a>( + fn chunks( rows: Range, map: &Model, - theme: &'a SyntaxTheme, + theme: &SyntaxTheme, cx: &mut AppContext, ) -> Vec<(String, Option, Option)> { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index a17dc4a6c6..2d7280114f 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2,10 +2,11 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, Highlights, }; -use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _}; +use crate::{EditorStyle, GutterDimensions}; use collections::{Bound, HashMap, HashSet}; -use gpui::{AnyElement, ElementContext, Pixels, View}; +use gpui::{AnyElement, ElementContext, Pixels}; use language::{BufferSnapshot, Chunk, Patch, Point}; +use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ToPoint as _}; use parking_lot::Mutex; use std::{ cell::RefCell, @@ -85,11 +86,9 @@ pub enum BlockStyle { pub struct BlockContext<'a, 'b> { pub context: &'b mut ElementContext<'a>, - pub view: View, pub anchor_x: Pixels, pub max_width: Pixels, - pub gutter_width: Pixels, - pub gutter_padding: Pixels, + pub gutter_dimensions: &'b GutterDimensions, pub em_width: Pixels, pub line_height: Pixels, pub block_id: usize, @@ -988,7 +987,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) { if row >= target { break; } - offset += line.len() as usize; + offset += line.len(); } (row, offset) } diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 07d40fcc12..29da2b41bb 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -2,9 +2,9 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, Highlights, }; -use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use gpui::{ElementId, HighlightStyle, Hsla}; use language::{Chunk, Edit, Point, TextSummary}; +use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use std::{ any::TypeId, cmp::{self, Ordering}, @@ -884,10 +884,10 @@ impl sum_tree::Item for Fold { fn summary(&self) -> Self::Summary { FoldSummary { - start: self.range.start.clone(), - end: self.range.end.clone(), - min_start: self.range.start.clone(), - max_end: self.range.end.clone(), + start: self.range.start, + end: self.range.end, + min_start: self.range.start, + max_end: self.range.end, count: 1, } } @@ -919,10 +919,10 @@ impl sum_tree::Summary for FoldSummary { fn add_summary(&mut self, other: &Self, buffer: &Self::Context) { if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less { - self.min_start = other.min_start.clone(); + self.min_start = other.min_start; } if other.max_end.cmp(&self.max_end, buffer) == Ordering::Greater { - self.max_end = other.max_end.clone(); + self.max_end = other.max_end; } #[cfg(debug_assertions)] @@ -934,16 +934,16 @@ impl sum_tree::Summary for FoldSummary { } } - self.start = other.start.clone(); - self.end = other.end.clone(); + self.start = other.start; + self.end = other.end; self.count += other.count; } } impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange { fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) { - self.0.start = summary.start.clone(); - self.0.end = summary.end.clone(); + self.0.start = summary.start; + self.0.end = summary.end; } } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index c26684a07b..19c6e8970d 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,8 +1,8 @@ -use crate::{Anchor, InlayId, MultiBufferSnapshot, ToOffset}; +use crate::InlayId; use collections::{BTreeMap, BTreeSet}; use gpui::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; -use multi_buffer::{MultiBufferChunks, MultiBufferRows}; +use multi_buffer::{Anchor, MultiBufferChunks, MultiBufferRows, MultiBufferSnapshot, ToOffset}; use std::{ any::TypeId, cmp, @@ -283,7 +283,7 @@ impl<'a> Iterator for InlayChunks<'a> { self.output_offset.0 += prefix.len(); let mut prefix = Chunk { text: prefix, - ..chunk.clone() + ..*chunk }; if !self.active_highlights.is_empty() { let mut highlight_style = HighlightStyle::default(); @@ -322,7 +322,7 @@ impl<'a> Iterator for InlayChunks<'a> { next_inlay_highlight_endpoint = range.end - offset_in_inlay.0; highlight_style .get_or_insert_with(|| Default::default()) - .highlight(style.clone()); + .highlight(*style); } } else { next_inlay_highlight_endpoint = usize::MAX; @@ -982,7 +982,7 @@ impl InlaySnapshot { summary } - pub fn buffer_rows<'a>(&'a self, row: u32) -> InlayBufferRows<'a> { + pub fn buffer_rows(&self, row: u32) -> InlayBufferRows<'_> { let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(); let inlay_point = InlayPoint::new(row, 0); cursor.seek(&inlay_point, Bias::Left, &()); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 7efc6b193b..018797386d 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -2,8 +2,8 @@ use super::{ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, Highlights, }; -use crate::MultiBufferSnapshot; use language::{Chunk, Point}; +use multi_buffer::MultiBufferSnapshot; use std::{cmp, mem, num::NonZeroU32, ops::Range}; use sum_tree::Bias; @@ -296,7 +296,7 @@ impl TabSnapshot { let (collapsed, expanded_char_column, to_next_stop) = self.collapse_tabs(chars, expanded, bias); ( - FoldPoint::new(output.row(), collapsed as u32), + FoldPoint::new(output.row(), collapsed), expanded_char_column, to_next_stop, ) @@ -513,7 +513,7 @@ impl<'a> Iterator for TabChunks<'a> { } else { self.chunk.text = &self.chunk.text[1..]; let tab_size = if self.input_column < self.max_expansion_column { - self.tab_size.get() as u32 + self.tab_size.get() } else { 1 }; diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index bdf5815f13..65d3bf49e2 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -3,10 +3,10 @@ use super::{ tab_map::{self, TabEdit, TabPoint, TabSnapshot}, Highlights, }; -use crate::MultiBufferSnapshot; use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task}; use language::{Chunk, Point}; use lazy_static::lazy_static; +use multi_buffer::MultiBufferSnapshot; use smol::future::yield_now; use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; use sum_tree::{Bias, Cursor, SumTree}; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f6fc5b4de0..ce15b36f6a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11,7 +11,7 @@ //! //! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s). //! -//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides it's behaviour. +//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behaviour. pub mod actions; mod blink_manager; pub mod display_map; @@ -88,6 +88,7 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; +use project::project_settings::{GitGutterSetting, ProjectSettings}; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::prelude::*; use rpc::proto::*; @@ -121,7 +122,7 @@ use ui::{ }; use util::{maybe, post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::Toast; -use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace}; +use workspace::{searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace}; use crate::hover_links::find_url; @@ -355,7 +356,6 @@ type InlayBackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec); /// /// See the [module level documentation](self) for more information. pub struct Editor { - handle: WeakView, focus_handle: FocusHandle, /// The text buffer being edited buffer: Model, @@ -443,7 +443,8 @@ pub struct EditorSnapshot { } pub struct GutterDimensions { - pub padding: Pixels, + pub left_padding: Pixels, + pub right_padding: Pixels, pub width: Pixels, pub margin: Pixels, } @@ -451,7 +452,8 @@ pub struct GutterDimensions { impl Default for GutterDimensions { fn default() -> Self { Self { - padding: Pixels::ZERO, + left_padding: Pixels::ZERO, + right_padding: Pixels::ZERO, width: Pixels::ZERO, margin: Pixels::ZERO, } @@ -955,14 +957,14 @@ impl CompletionsMenu { .selected(item_ix == selected_item) .on_click(cx.listener(move |editor, _event, cx| { cx.stop_propagation(); - editor - .confirm_completion( - &ConfirmCompletion { - item_ix: Some(item_ix), - }, - cx, - ) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = editor.confirm_completion( + &ConfirmCompletion { + item_ix: Some(item_ix), + }, + cx, + ) { + task.detach_and_log_err(cx) + } })) .child(h_flex().overflow_hidden().child(completion_label)) .end_slot::
(documentation_label), @@ -1173,14 +1175,14 @@ impl CodeActionsMenu { MouseButton::Left, cx.listener(move |editor, _, cx| { cx.stop_propagation(); - editor - .confirm_code_action( - &ConfirmCodeAction { - item_ix: Some(item_ix), - }, - cx, - ) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = editor.confirm_code_action( + &ConfirmCodeAction { + item_ix: Some(item_ix), + }, + cx, + ) { + task.detach_and_log_err(cx) + } }), ) // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here. @@ -1346,6 +1348,7 @@ pub(crate) struct NavigationData { enum GotoDefinitionKind { Symbol, Type, + Implementation, } #[derive(Debug, Clone)] @@ -1357,6 +1360,7 @@ enum InlayHintRefreshReason { RefreshRequested, ExcerptsRemoved(Vec), } + impl InlayHintRefreshReason { fn description(&self) -> &'static str { match self { @@ -1484,7 +1488,6 @@ impl Editor { cx.on_blur(&focus_handle, Self::handle_blur).detach(); let mut this = Self { - handle: cx.view().downgrade(), focus_handle, buffer: buffer.clone(), display_map: display_map.clone(), @@ -1624,6 +1627,10 @@ impl Editor { key_context.set("extension", extension.to_string()); } + if self.has_active_copilot_suggestion(cx) { + key_context.add("copilot_suggestion"); + } + key_context } @@ -1639,7 +1646,7 @@ impl Editor { .update(cx, |project, cx| project.create_buffer("", None, cx)) .log_err() { - workspace.add_item( + workspace.add_item_to_active_pane( Box::new(cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), cx, ); @@ -1682,10 +1689,6 @@ impl Editor { self.workspace.as_ref()?.0.upgrade() } - pub fn pane(&self, cx: &AppContext) -> Option> { - self.workspace()?.read(cx).pane_for(&self.handle.upgrade()?) - } - pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> { self.buffer().read(cx).title(cx) } @@ -1702,18 +1705,14 @@ impl Editor { } } - pub fn language_at<'a, T: ToOffset>( - &self, - point: T, - cx: &'a AppContext, - ) -> Option> { + pub fn language_at(&self, point: T, cx: &AppContext) -> Option> { self.buffer.read(cx).language_at(point, cx) } - pub fn file_at<'a, T: ToOffset>( + pub fn file_at( &self, point: T, - cx: &'a AppContext, + cx: &AppContext, ) -> Option> { self.buffer.read(cx).read(cx).file_at(point).cloned() } @@ -1751,7 +1750,7 @@ impl Editor { self.completion_provider = Some(hub); } - pub fn placeholder_text(&self) -> Option<&str> { + pub fn placeholder_text(&self, _cx: &mut WindowContext) -> Option<&str> { self.placeholder_text.as_deref() } @@ -1874,7 +1873,7 @@ impl Editor { let new_cursor_position = self.selections.newest_anchor().head(); self.push_to_nav_history( - old_cursor_position.clone(), + *old_cursor_position, Some(new_cursor_position.to_point(buffer)), cx, ); @@ -1893,8 +1892,7 @@ impl Editor { if let Some(completion_menu) = completion_menu { let cursor_position = new_cursor_position.to_offset(buffer); - let (word_range, kind) = - buffer.surrounding_word(completion_menu.initial_position.clone()); + let (word_range, kind) = buffer.surrounding_word(completion_menu.initial_position); if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) { @@ -2115,7 +2113,7 @@ impl Editor { match click_count { 1 => { start = buffer.anchor_before(position.to_point(&display_map)); - end = start.clone(); + end = start; mode = SelectMode::Character; auto_scroll = true; } @@ -2123,7 +2121,7 @@ impl Editor { let range = movement::surrounding_word(&display_map, position); start = buffer.anchor_before(range.start.to_point(&display_map)); end = buffer.anchor_before(range.end.to_point(&display_map)); - mode = SelectMode::Word(start.clone()..end.clone()); + mode = SelectMode::Word(start..end); auto_scroll = true; } 3 => { @@ -2137,7 +2135,7 @@ impl Editor { ); start = buffer.anchor_before(line_start); end = buffer.anchor_before(next_line_start); - mode = SelectMode::Line(start.clone()..end.clone()); + mode = SelectMode::Line(start..end); auto_scroll = true; } _ => { @@ -2343,32 +2341,11 @@ impl Editor { } pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - if self.take_rename(false, cx).is_some() { - return; - } - - if hide_hover(self, cx) { - return; - } - - if self.hide_context_menu(cx).is_some() { - return; - } - - if self.discard_copilot_suggestion(cx) { - return; - } - - if self.snippet_stack.pop().is_some() { + if self.dismiss_menus_and_popups(cx) { return; } if self.mode == EditorMode::Full { - if self.active_diagnostics.is_some() { - self.dismiss_diagnostics(cx); - return; - } - if self.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel()) { return; } @@ -2377,6 +2354,37 @@ impl Editor { cx.propagate(); } + pub fn dismiss_menus_and_popups(&mut self, cx: &mut ViewContext) -> bool { + if self.take_rename(false, cx).is_some() { + return true; + } + + if hide_hover(self, cx) { + return true; + } + + if self.hide_context_menu(cx).is_some() { + return true; + } + + if self.discard_copilot_suggestion(cx) { + return true; + } + + if self.snippet_stack.pop().is_some() { + return true; + } + + if self.mode == EditorMode::Full { + if self.active_diagnostics.is_some() { + self.dismiss_diagnostics(cx); + return true; + } + } + + false + } + pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext) { let text: Arc = text.into(); @@ -2723,9 +2731,8 @@ impl Editor { let mut edits = Vec::new(); let mut rows = Vec::new(); - let mut rows_inserted = 0; - for selection in self.selections.all_adjusted(cx) { + for (rows_inserted, selection) in self.selections.all_adjusted(cx).into_iter().enumerate() { let cursor = selection.head(); let row = cursor.row; @@ -2734,8 +2741,7 @@ impl Editor { let newline = "\n".to_string(); edits.push((start_of_line..start_of_line, newline)); - rows.push(row + rows_inserted); - rows_inserted += 1; + rows.push(row + rows_inserted as u32); } self.transact(cx, |editor, cx| { @@ -3021,6 +3027,12 @@ impl Editor { } let reason_description = reason.description(); + let ignore_debounce = matches!( + reason, + InlayHintRefreshReason::SettingsChange(_) + | InlayHintRefreshReason::Toggle(_) + | InlayHintRefreshReason::ExcerptsRemoved(_) + ); let (invalidate_cache, required_languages) = match reason { InlayHintRefreshReason::Toggle(enabled) => { self.inlay_hint_cache.enabled = enabled; @@ -3083,6 +3095,7 @@ impl Editor { reason_description, self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), invalidate_cache, + ignore_debounce, cx, ) { self.splice_inlay_hints(to_remove, to_insert, cx); @@ -3192,7 +3205,7 @@ impl Editor { let (buffer, buffer_position) = self .buffer .read(cx) - .text_anchor_for_position(position.clone(), cx)?; + .text_anchor_for_position(position, cx)?; // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances, // hence we do LSP request & edit on host side only — add formats to host's history. @@ -3236,17 +3249,14 @@ impl Editor { }; let position = self.selections.newest_anchor().head(); - let (buffer, buffer_position) = if let Some(output) = self - .buffer - .read(cx) - .text_anchor_for_position(position.clone(), cx) - { - output - } else { - return; - }; + let (buffer, buffer_position) = + if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) { + output + } else { + return; + }; - let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone()); + let query = Self::completion_query(&self.buffer.read(cx).read(cx), position); let completions = provider.completions(&buffer, buffer_position, cx); let id = post_inc(&mut self.next_completion_id); @@ -3623,7 +3633,7 @@ impl Editor { let project = workspace.project().clone(); let editor = cx.new_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); - workspace.add_item(Box::new(editor.clone()), cx); + workspace.add_item_to_active_pane(Box::new(editor.clone()), cx); editor.update(cx, |editor, cx| { editor.highlight_background::( ranges_to_highlight, @@ -3684,7 +3694,7 @@ impl Editor { let newest_selection = self.selections.newest_anchor().clone(); let cursor_position = newest_selection.head(); let (cursor_buffer, cursor_buffer_position) = - buffer.text_anchor_for_position(cursor_position.clone(), cx)?; + buffer.text_anchor_for_position(cursor_position, cx)?; let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?; if cursor_buffer != tail_buffer { return None; @@ -3742,7 +3752,7 @@ impl Editor { let range = Anchor { buffer_id, - excerpt_id: excerpt_id.clone(), + excerpt_id: excerpt_id, text_anchor: start, }..Anchor { buffer_id, @@ -3953,6 +3963,39 @@ impl Editor { } } + fn accept_partial_copilot_suggestion( + &mut self, + _: &AcceptPartialCopilotSuggestion, + cx: &mut ViewContext, + ) { + if self.selections.count() == 1 && self.has_active_copilot_suggestion(cx) { + if let Some(suggestion) = self.take_active_copilot_suggestion(cx) { + let mut partial_suggestion = suggestion + .text + .chars() + .by_ref() + .take_while(|c| c.is_alphabetic()) + .collect::(); + if partial_suggestion.is_empty() { + partial_suggestion = suggestion + .text + .chars() + .by_ref() + .take_while(|c| c.is_whitespace() || !c.is_alphabetic()) + .collect::(); + } + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: None, + text: partial_suggestion.clone().into(), + }); + self.insert_with_autoindent_mode(&partial_suggestion, None, cx); + self.refresh_copilot_suggestions(true, cx); + cx.notify(); + } + } + } + fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { if let Some(suggestion) = self.take_active_copilot_suggestion(cx) { if let Some(copilot) = Copilot::global(cx) { @@ -4057,7 +4100,8 @@ impl Editor { if self.available_code_actions.is_some() { Some( IconButton::new("code_actions_indicator", ui::IconName::Bolt) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) + .size(ui::ButtonSize::None) .icon_color(Color::Muted) .selected(is_active) .on_click(cx.listener(|editor, _e, cx| { @@ -4090,7 +4134,7 @@ impl Editor { fold_data .map(|(fold_status, buffer_row, active)| { (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { - IconButton::new(ix as usize, ui::IconName::ChevronDown) + IconButton::new(ix, ui::IconName::ChevronDown) .on_click({ let view = editor_view.clone(); move |_e, cx| { @@ -4206,8 +4250,43 @@ impl Editor { active_index: 0, ranges: tabstops, }); - } + // Check whether the just-entered snippet ends with an auto-closable bracket. + if self.autoclose_regions.is_empty() { + let snapshot = self.buffer.read(cx).snapshot(cx); + for selection in &mut self.selections.all::(cx) { + let selection_head = selection.head(); + let Some(scope) = snapshot.language_scope_at(selection_head) else { + continue; + }; + + let mut bracket_pair = None; + let next_chars = snapshot.chars_at(selection_head).collect::(); + let prev_chars = snapshot + .reversed_chars_at(selection_head) + .collect::(); + for (pair, enabled) in scope.brackets() { + if enabled + && pair.close + && prev_chars.starts_with(pair.start.as_str()) + && next_chars.starts_with(pair.end.as_str()) + { + bracket_pair = Some(pair.clone()); + break; + } + } + if let Some(pair) = bracket_pair { + let start = snapshot.anchor_after(selection_head); + let end = snapshot.anchor_after(selection_head); + self.autoclose_regions.push(AutocloseRegion { + selection_id: selection.id, + range: start..end, + pair, + }); + } + } + } + } Ok(()) } @@ -4422,6 +4501,9 @@ impl Editor { } pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext) { + if self.read_only(cx) { + return; + } let mut selections = self.selections.all::(cx); let mut prev_edited_row = 0; let mut row_delta = 0; @@ -4464,7 +4546,7 @@ impl Editor { // If a selection ends at the beginning of a line, don't indent // that last line. - if selection.end.column == 0 { + if selection.end.column == 0 && selection.end.row > selection.start.row { end_row -= 1; } @@ -4515,6 +4597,9 @@ impl Editor { } pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { + if self.read_only(cx) { + return; + } let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(cx); let mut deletion_ranges = Vec::new(); @@ -4655,6 +4740,9 @@ impl Editor { } pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext) { + if self.read_only(cx) { + return; + } let mut row_ranges = Vec::>::new(); for selection in self.selections.all::(cx) { let start = selection.start.row; @@ -4680,7 +4768,7 @@ impl Editor { row_range.end - 1, snapshot.line_len(row_range.end - 1), )); - cursor_positions.push(anchor.clone()..anchor); + cursor_positions.push(anchor..anchor); } self.transact(cx, |this, cx| { @@ -4784,7 +4872,7 @@ impl Editor { .text_for_range(start_point..end_point) .collect::(); - let mut lines = text.split("\n").collect_vec(); + let mut lines = text.split('\n').collect_vec(); let lines_before = lines.len(); callback(&mut lines); @@ -4852,7 +4940,7 @@ impl Editor { self.manipulate_text(cx, |text| { // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary // https://github.com/rutrum/convert-case/issues/16 - text.split("\n") + text.split('\n') .map(|line| line.to_case(Case::Title)) .join("\n") }) @@ -4874,7 +4962,7 @@ impl Editor { self.manipulate_text(cx, |text| { // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary // https://github.com/rutrum/convert-case/issues/16 - text.split("\n") + text.split('\n') .map(|line| line.to_case(Case::UpperCamel)) .join("\n") }) @@ -6387,10 +6475,9 @@ impl Editor { && !movement::is_inside_word(&display_map, display_range.end)) { // TODO: This is n^2, because we might check all the selections - if selections + if !selections .iter() - .find(|selection| selection.range().overlaps(&offset_range)) - .is_none() + .any(|selection| selection.range().overlaps(&offset_range)) { next_selected_range = Some(offset_range); break; @@ -7317,6 +7404,18 @@ impl Editor { self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx); } + pub fn go_to_implementation(&mut self, _: &GoToImplementation, cx: &mut ViewContext) { + self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, false, cx); + } + + pub fn go_to_implementation_split( + &mut self, + _: &GoToImplementationSplit, + cx: &mut ViewContext, + ) { + self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, true, cx); + } + pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext) { self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx); } @@ -7354,12 +7453,14 @@ impl Editor { let definitions = project.update(cx, |project, cx| match kind { GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx), GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx), + GotoDefinitionKind::Implementation => project.implementation(&buffer, head, cx), }); cx.spawn(|editor, mut cx| async move { let definitions = definitions.await?; editor.update(&mut cx, |editor, cx| { editor.navigate_to_hover_links( + Some(kind), definitions.into_iter().map(HoverLink::Text).collect(), split, cx, @@ -7372,10 +7473,8 @@ impl Editor { pub fn open_url(&mut self, _: &OpenUrl, cx: &mut ViewContext) { let position = self.selections.newest_anchor().head(); - let Some((buffer, buffer_position)) = self - .buffer - .read(cx) - .text_anchor_for_position(position.clone(), cx) + let Some((buffer, buffer_position)) = + self.buffer.read(cx).text_anchor_for_position(position, cx) else { return; }; @@ -7392,8 +7491,9 @@ impl Editor { .detach(); } - pub fn navigate_to_hover_links( + pub(crate) fn navigate_to_hover_links( &mut self, + kind: Option, mut definitions: Vec, split: bool, cx: &mut ViewContext, @@ -7430,11 +7530,13 @@ impl Editor { cx.window_context().defer(move |cx| { let target_editor: View = workspace.update(cx, |workspace, cx| { - if split { - workspace.split_project_item(target.buffer.clone(), cx) + let pane = if split { + workspace.adjacent_pane(cx) } else { - workspace.open_project_item(target.buffer.clone(), cx) - } + workspace.active_pane().clone() + }; + + workspace.open_project_item(pane, target.buffer.clone(), cx) }); target_editor.update(cx, |target_editor, cx| { // When selecting a definition in a different buffer, disable the nav history @@ -7462,13 +7564,18 @@ impl Editor { cx.spawn(|editor, mut cx| async move { let (title, location_tasks, workspace) = editor .update(&mut cx, |editor, cx| { + let tab_kind = match kind { + Some(GotoDefinitionKind::Implementation) => "Implementations", + _ => "Definitions", + }; let title = definitions .iter() .find_map(|definition| match definition { HoverLink::Text(link) => link.origin.as_ref().map(|origin| { let buffer = origin.buffer.read(cx); format!( - "Definitions for {}", + "{} for {}", + tab_kind, buffer .text_for_range(origin.range.clone()) .collect::() @@ -7477,7 +7584,7 @@ impl Editor { HoverLink::InlayHint(_, _) => None, HoverLink::Url(_) => None, }) - .unwrap_or("Definitions".to_string()); + .unwrap_or(tab_kind.to_string()); let location_tasks = definitions .into_iter() .map(|definition| match definition { @@ -7580,12 +7687,68 @@ impl Editor { let workspace = self.workspace()?; let project = workspace.read(cx).project().clone(); let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); - Some(cx.spawn(|_, mut cx| async move { - let locations = references.await?; + Some(cx.spawn(|editor, mut cx| async move { + let mut locations = references.await?; + let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?; + let head_offset = text::ToOffset::to_offset(&head, &snapshot); + + // LSP may return references that contain the item itself we requested `find_all_references` for (eg. rust-analyzer) + // So we will remove it from locations + // If there is only one reference, we will not do this filter cause it may make locations empty + if locations.len() > 1 { + cx.update(|cx| { + locations.retain(|location| { + // fn foo(x : i64) { + // ^ + // println!(x); + // } + // It is ok to find reference when caret being at ^ (the end of the word) + // So we turn offset into inclusive to include the end of the word + !location + .range + .to_offset(location.buffer.read(cx)) + .to_inclusive() + .contains(&head_offset) + }); + })?; + } + if locations.is_empty() { return Ok(()); } + // If there is one reference, just open it directly + if locations.len() == 1 { + let target = locations.pop().unwrap(); + + return editor.update(&mut cx, |editor, cx| { + let range = target.range.to_offset(target.buffer.read(cx)); + let range = editor.range_for_match(&range); + + if Some(&target.buffer) == editor.buffer().read(cx).as_singleton().as_ref() { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }); + } else { + cx.window_context().defer(move |cx| { + let target_editor: View = + workspace.update(cx, |workspace, cx| { + workspace.open_project_item( + workspace.active_pane().clone(), + target.buffer.clone(), + cx, + ) + }); + target_editor.update(cx, |target_editor, cx| { + target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }) + }) + }) + } + }); + } + workspace.update(&mut cx, |workspace, cx| { let title = locations .first() @@ -7666,7 +7829,7 @@ impl Editor { if split { workspace.split_item(SplitDirection::Right, Box::new(editor), cx); } else { - workspace.add_item(Box::new(editor), cx); + workspace.add_item_to_active_pane(Box::new(editor), cx); } } @@ -7773,7 +7936,7 @@ impl Editor { let block_id = this.insert_blocks( [BlockProperties { style: BlockStyle::Flex, - position: range.start.clone(), + position: range.start, height: 1, render: Arc::new({ let rename_editor = rename_editor.clone(); @@ -7837,11 +8000,11 @@ impl Editor { let (start_buffer, start) = self .buffer .read(cx) - .text_anchor_for_position(rename.range.start.clone(), cx)?; + .text_anchor_for_position(rename.range.start, cx)?; let (end_buffer, end) = self .buffer .read(cx) - .text_anchor_for_position(rename.range.end.clone(), cx)?; + .text_anchor_for_position(rename.range.end, cx)?; if start_buffer != end_buffer { return None; } @@ -8447,6 +8610,12 @@ impl Editor { cx.notify(); } + pub fn toggle_line_numbers(&mut self, _: &ToggleLineNumbers, cx: &mut ViewContext) { + let mut editor_settings = EditorSettings::get_global(cx).clone(); + editor_settings.gutter.line_numbers = !editor_settings.gutter.line_numbers; + EditorSettings::override_global(editor_settings, cx); + } + pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext) { self.show_gutter = show_gutter; cx.notify(); @@ -8499,7 +8668,7 @@ impl Editor { let mut cwd = worktree.read(cx).abs_path().to_path_buf(); cwd.push(".git"); - const REMOTE_NAME: &'static str = "origin"; + const REMOTE_NAME: &str = "origin"; let repo = project .fs() .open_repo(&cwd) @@ -8672,7 +8841,6 @@ impl Editor { Ok(i) | Err(i) => i, }; - let right_position = right_position.clone(); ranges[start_ix..] .iter() .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) @@ -8986,7 +9154,15 @@ impl Editor { self.searchable } + fn open_excerpts_in_split(&mut self, _: &OpenExcerptsSplit, cx: &mut ViewContext) { + self.open_excerpts_common(true, cx) + } + fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { + self.open_excerpts_common(false, cx) + } + + fn open_excerpts_common(&mut self, split: bool, cx: &mut ViewContext) { let buffer = self.buffer.read(cx); if buffer.is_singleton() { cx.propagate(); @@ -9013,18 +9189,20 @@ impl Editor { } } - self.push_to_nav_history(self.selections.newest_anchor().head(), None, cx); - // We defer the pane interaction because we ourselves are a workspace item // and activating a new item causes the pane to call a method on us reentrantly, // which panics if we're on the stack. cx.window_context().defer(move |cx| { workspace.update(cx, |workspace, cx| { - let pane = workspace.active_pane().clone(); + let pane = if split { + workspace.adjacent_pane(cx) + } else { + workspace.active_pane().clone() + }; pane.update(cx, |pane, _| pane.disable_history()); for (buffer, ranges) in new_selections_by_buffer.into_iter() { - let editor = workspace.open_project_item::(buffer, cx); + let editor = workspace.open_project_item::(pane.clone(), buffer, cx); editor.update(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::newest()), cx, |s| { s.select_ranges(ranges); @@ -9232,7 +9410,7 @@ impl Editor { let highlight = chunk .syntax_highlight_id .and_then(|id| id.name(&style.syntax)); - let mut chunk_lines = chunk.text.split("\n").peekable(); + let mut chunk_lines = chunk.text.split('\n').peekable(); while let Some(text) = chunk_lines.next() { let mut merged_with_last_token = false; if let Some(last_token) = line.back_mut() { @@ -9564,7 +9742,7 @@ impl EditorSnapshot { self.is_focused } - pub fn placeholder_text(&self) -> Option<&Arc> { + pub fn placeholder_text(&self, _cx: &mut WindowContext) -> Option<&Arc> { self.placeholder_text.as_ref() } @@ -9580,23 +9758,50 @@ impl EditorSnapshot { max_line_number_width: Pixels, cx: &AppContext, ) -> GutterDimensions { - if self.show_gutter { - let descent = cx.text_system().descent(font_id, font_size); - let gutter_padding_factor = 4.0; - let gutter_padding = (em_width * gutter_padding_factor).round(); + if !self.show_gutter { + return GutterDimensions::default(); + } + let descent = cx.text_system().descent(font_id, font_size); + + let show_git_gutter = matches!( + ProjectSettings::get_global(cx).git.git_gutter, + Some(GitGutterSetting::TrackedFiles) + ); + let gutter_settings = EditorSettings::get_global(cx).gutter; + + let line_gutter_width = if gutter_settings.line_numbers { // Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines. let min_width_for_number_on_gutter = em_width * 4.0; - let gutter_width = - max_line_number_width.max(min_width_for_number_on_gutter) + gutter_padding * 2.0; - let gutter_margin = -descent; - - GutterDimensions { - padding: gutter_padding, - width: gutter_width, - margin: gutter_margin, - } + max_line_number_width.max(min_width_for_number_on_gutter) } else { - GutterDimensions::default() + 0.0.into() + }; + + let left_padding = if gutter_settings.code_actions { + em_width * 3.0 + } else if show_git_gutter && gutter_settings.line_numbers { + em_width * 2.0 + } else if show_git_gutter || gutter_settings.line_numbers { + em_width + } else { + px(0.) + }; + + let right_padding = if gutter_settings.folds && gutter_settings.line_numbers { + em_width * 4.0 + } else if gutter_settings.folds { + em_width * 3.0 + } else if gutter_settings.line_numbers { + em_width + } else { + px(0.) + }; + + GutterDimensions { + left_padding, + right_padding, + width: line_gutter_width + left_padding + right_padding, + margin: -descent, } } } @@ -9703,7 +9908,6 @@ impl Render for Editor { status: cx.theme().status().clone(), inlays_style: HighlightStyle { color: Some(cx.theme().status().hint), - font_weight: Some(FontWeight::BOLD), ..HighlightStyle::default() }, suggestions_style: HighlightStyle { @@ -9879,7 +10083,7 @@ impl ViewInputHandler for Editor { .disjoint_anchors() .iter() .map(|selection| { - selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot) + selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot) }) .collect::>() }; @@ -10103,9 +10307,14 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren .group(group_id.clone()) .relative() .size_full() - .pl(cx.gutter_width) - .w(cx.max_width + cx.gutter_width) - .child(div().flex().w(cx.anchor_x - cx.gutter_width).flex_shrink()) + .pl(cx.gutter_dimensions.width) + .w(cx.max_width + cx.gutter_dimensions.width) + .child( + div() + .flex() + .w(cx.anchor_x - cx.gutter_dimensions.width) + .flex_shrink(), + ) .child(div().flex().flex_shrink_0().child( StyledText::new(text_without_backticks.clone()).with_highlights( &text_style, @@ -10226,7 +10435,7 @@ pub fn styled_runs_for_code_label<'a>( }) } -pub(crate) fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator + 'a { +pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator + '_ { let mut index = 0; let mut codepoints = text.char_indices().peekable(); diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 074003492f..ca12112d5f 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -2,7 +2,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; -#[derive(Deserialize)] +#[derive(Deserialize, Clone)] pub struct EditorSettings { pub cursor_blink: bool, pub hover_popover_enabled: bool, @@ -12,6 +12,7 @@ pub struct EditorSettings { pub use_on_type_format: bool, pub toolbar: Toolbar, pub scrollbar: Scrollbar, + pub gutter: Gutter, pub vertical_scroll_margin: f32, pub relative_line_numbers: bool, pub seed_search_query_from_cursor: SeedQuerySetting, @@ -45,6 +46,13 @@ pub struct Scrollbar { pub diagnostics: bool, } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct Gutter { + pub line_numbers: bool, + pub code_actions: bool, + pub folds: bool, +} + /// When to show the scrollbar in the editor. /// /// Default: auto @@ -97,6 +105,8 @@ pub struct EditorSettingsContent { pub toolbar: Option, /// Scrollbar related settings pub scrollbar: Option, + /// Gutter related settings + pub gutter: Option, /// The number of lines to keep above/below the cursor when auto-scrolling. /// @@ -157,6 +167,23 @@ pub struct ScrollbarContent { pub diagnostics: Option, } +/// Gutter related settings +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct GutterContent { + /// Whether to show line numbers in the gutter. + /// + /// Default: true + pub line_numbers: Option, + /// Whether to show code action buttons in the gutter. + /// + /// Default: true + pub code_actions: Option, + /// Whether to show fold buttons in the gutter. + /// + /// Default: true + pub folds: Option, +} + impl Settings for EditorSettings { const KEY: Option<&'static str> = None; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1c9c5f1db6..36daaec5d9 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3999,7 +3999,7 @@ async fn test_select_all_matches(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx).await; cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_all_matches(&SelectAllMatches::default(), cx)) + cx.update_editor(|e, cx| e.select_all_matches(&SelectAllMatches, cx)) .unwrap(); cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); } @@ -5233,32 +5233,24 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + let fs = FakeFs::new(cx.executor()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { capabilities: lsp::ServerCapabilities { document_formatting_provider: Some(lsp::OneOf::Left(true)), ..Default::default() }, ..Default::default() - })) - .await; + }, + ); - let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - _ = project.update(cx, |project, _| project.languages().add(Arc::new(language))); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) .await @@ -5273,7 +5265,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { assert!(cx.read(|cx| editor.is_dirty(cx))); let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) .unwrap(); fake_server .handle_request::(move |params, _| async move { @@ -5311,7 +5303,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { unreachable!() }); let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); cx.executor().start_waiting(); @@ -5334,7 +5326,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { }); let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) .unwrap(); fake_server .handle_request::(move |params, _| async move { @@ -5355,32 +5347,24 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + let fs = FakeFs::new(cx.executor()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { capabilities: lsp::ServerCapabilities { document_range_formatting_provider: Some(lsp::OneOf::Left(true)), ..Default::default() }, ..Default::default() - })) - .await; + }, + ); - let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - _ = project.update(cx, |project, _| project.languages().add(Arc::new(language))); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) .await @@ -5395,7 +5379,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { assert!(cx.read(|cx| editor.is_dirty(cx))); let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) .unwrap(); fake_server .handle_request::(move |params, _| async move { @@ -5434,7 +5418,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { }, ); let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); cx.executor().start_waiting(); @@ -5457,7 +5441,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { }); let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) + .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) .unwrap(); fake_server .handle_request::(move |params, _| async move { @@ -5480,7 +5464,13 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer) }); - let mut language = Language::new( + let fs = FakeFs::new(cx.executor()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(Arc::new(Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { @@ -5493,24 +5483,18 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { ..Default::default() }, Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + ))); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { capabilities: lsp::ServerCapabilities { document_formatting_provider: Some(lsp::OneOf::Left(true)), ..Default::default() }, ..Default::default() - })) - .await; + }, + ); - let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - _ = project.update(cx, |project, _| { - project.languages().add(Arc::new(language)); - }); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) .await @@ -6768,8 +6752,8 @@ async fn test_following(cx: &mut gpui::TestAppContext) { cx.open_window( WindowOptions { bounds: WindowBounds::Fixed(Bounds::from_corners( - gpui::Point::new((0. as f64).into(), (0. as f64).into()), - gpui::Point::new((10. as f64).into(), (80. as f64).into()), + gpui::Point::new(0_f64.into(), 0_f64.into()), + gpui::Point::new(10_f64.into(), 80_f64.into()), )), ..Default::default() }, @@ -6790,7 +6774,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { move |_, leader, event, cx| { leader .read(cx) - .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + .add_event_to_update_proto(event, &mut update.borrow_mut(), cx); }, ) .detach(); @@ -6959,7 +6943,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { cx.subscribe(&leader, move |_, leader, event, cx| { leader .read(cx) - .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + .add_event_to_update_proto(event, &mut update.borrow_mut(), cx); }) .detach(); } @@ -7326,7 +7310,7 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) #[test] fn test_split_words() { - fn split<'a>(text: &'a str) -> Vec<&'a str> { + fn split(text: &str) -> Vec<&str> { split_words(text).collect() } @@ -7639,6 +7623,128 @@ async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContex }); } +#[gpui::test(iterations = 10)] +async fn test_accept_partial_copilot_suggestion( + executor: BackgroundExecutor, + cx: &mut gpui::TestAppContext, +) { + // flaky + init_test(cx, |_| {}); + + let (copilot, copilot_lsp) = Copilot::fake(cx); + _ = cx.update(|cx| Copilot::set_global(copilot, cx)); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + // Setup the editor with a completion request. + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + let _ = handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec![], + ); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.copilot1".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), + ..Default::default() + }], + vec![], + ); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + + // Accepting the first word of the suggestion should only accept the first word and still show the rest. + editor.accept_partial_copilot_suggestion(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n"); + assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + + // Accepting next word should accept the non-word and copilot suggestion should be gone + editor.accept_partial_copilot_suggestion(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n"); + assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); + }); + + // Reset the editor and check non-word and whitespace completion + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + let _ = handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec![], + ); + handle_copilot_completion_request( + &copilot_lsp, + vec![copilot::request::Completion { + text: "one.123. copilot\n 456".into(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), + ..Default::default() + }], + vec![], + ); + executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + cx.update_editor(|editor, cx| { + assert!(editor.has_active_copilot_suggestion(cx)); + + // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest. + editor.accept_partial_copilot_suggestion(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n"); + assert_eq!( + editor.display_text(cx), + "one.123. copilot\n 456\ntwo\nthree\n" + ); + + // Accepting next word should accept the next word and copilot suggestion should still exist + editor.accept_partial_copilot_suggestion(&Default::default(), cx); + assert!(editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n"); + assert_eq!( + editor.display_text(cx), + "one.123. copilot\n 456\ntwo\nthree\n" + ); + + // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone + editor.accept_partial_copilot_suggestion(&Default::default(), cx); + assert!(!editor.has_active_copilot_suggestion(cx)); + assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n"); + assert_eq!( + editor.display_text(cx), + "one.123. copilot\n 456\ntwo\nthree\n" + ); + }); +} + #[gpui::test] async fn test_copilot_completion_invalidation( executor: BackgroundExecutor, @@ -7912,7 +8018,19 @@ async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - let mut language = Language::new( + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { let a = 5; }", + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(Arc::new(Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { @@ -7931,9 +8049,10 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { ..Default::default() }, Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + ))); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { capabilities: lsp::ServerCapabilities { document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { first_trigger_character: "{".to_string(), @@ -7942,20 +8061,9 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { ..Default::default() }, ..Default::default() - })) - .await; + }, + ); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/a", - json!({ - "main.rs": "fn main() { let a = 5; }", - "other.rs": "// Test file", - }), - ) - .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; - _ = project.update(cx, |project, _| project.languages().add(Arc::new(language))); let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let cx = &mut VisualTestContext::from_window(*workspace, cx); @@ -8026,8 +8134,25 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { let a = 5; }", + "other.rs": "// Test file", + }), + ) + .await; + + let project = Project::test(fs, ["/a".as_ref()], cx).await; + + let server_restarts = Arc::new(AtomicUsize::new(0)); + let closure_restarts = Arc::clone(&server_restarts); + let language_server_name = "test language server"; let language_name: Arc = "Rust".into(); - let mut language = Language::new( + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(Arc::new(Language::new( LanguageConfig { name: Arc::clone(&language_name), matcher: LanguageMatcher { @@ -8037,13 +8162,10 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test ..Default::default() }, Some(tree_sitter_rust::language()), - ); - - let server_restarts = Arc::new(AtomicUsize::new(0)); - let closure_restarts = Arc::clone(&server_restarts); - let language_server_name = "test language server"; - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + ))); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { name: language_server_name, initialization_options: Some(json!({ "testOptionValue": true @@ -8056,20 +8178,9 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test }); })), ..Default::default() - })) - .await; + }, + ); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/a", - json!({ - "main.rs": "fn main() { let a = 5; }", - "other.rs": "// Test file", - }), - ) - .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; - _ = project.update(cx, |project, _| project.languages().add(Arc::new(language))); let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let _buffer = project .update(cx, |project, cx| { @@ -8098,6 +8209,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test project_settings.lsp.insert( "Some other server name".into(), LspSettings { + settings: None, initialization_options: Some(json!({ "some other init value": false })), @@ -8115,6 +8227,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test project_settings.lsp.insert( language_server_name.into(), LspSettings { + settings: None, initialization_options: Some(json!({ "anotherInitValue": false })), @@ -8132,6 +8245,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test project_settings.lsp.insert( language_server_name.into(), LspSettings { + settings: None, initialization_options: Some(json!({ "anotherInitValue": false })), @@ -8149,6 +8263,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test project_settings.lsp.insert( language_server_name.into(), LspSettings { + settings: None, initialization_options: None, }, ); @@ -8361,7 +8476,13 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { settings.defaults.formatter = Some(language_settings::Formatter::Prettier) }); - let mut language = Language::new( + let fs = FakeFs::new(cx.executor()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + + language_registry.add(Arc::new(Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { @@ -8372,24 +8493,18 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { ..Default::default() }, Some(tree_sitter_rust::language()), - ); + ))); let test_plugin = "test_plugin"; - let _ = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + let _ = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { prettier_plugins: vec![test_plugin], ..Default::default() - })) - .await; + }, + ); - let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; - _ = project.update(cx, |project, _| { - project.languages().add(Arc::new(language)); - }); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) .await @@ -8426,6 +8541,105 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_find_all_references(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + fn foo(«paramˇ»: i64) { + println!(param); + } + "}); + + cx.lsp + .handle_request::(move |_, _| async move { + Ok(Some(vec![ + lsp::Location { + uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), + range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 12)), + }, + lsp::Location { + uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), + range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 18)), + }, + ])) + }); + + let references = cx + .update_editor(|editor, cx| editor.find_all_references(&FindAllReferences, cx)) + .unwrap(); + + cx.executor().run_until_parked(); + + cx.executor().start_waiting(); + references.await.unwrap(); + + cx.assert_editor_state(indoc! {" + fn foo(param: i64) { + println!(«paramˇ»); + } + "}); + + let references = cx + .update_editor(|editor, cx| editor.find_all_references(&FindAllReferences, cx)) + .unwrap(); + + cx.executor().run_until_parked(); + + cx.executor().start_waiting(); + references.await.unwrap(); + + cx.assert_editor_state(indoc! {" + fn foo(«paramˇ»: i64) { + println!(param); + } + "}); + + cx.set_state(indoc! {" + fn foo(param: i64) { + let a = param; + let aˇ = param; + let a = param; + println!(param); + } + "}); + + cx.lsp + .handle_request::(move |_, _| async move { + Ok(Some(vec![lsp::Location { + uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), + range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9)), + }])) + }); + + let references = cx + .update_editor(|editor, cx| editor.find_all_references(&FindAllReferences, cx)) + .unwrap(); + + cx.executor().run_until_parked(); + + cx.executor().start_waiting(); + references.await.unwrap(); + + cx.assert_editor_state(indoc! {" + fn foo(param: i64) { + let a = param; + let «aˇ» = param; + let a = param; + println!(param); + } + "}); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point @@ -8582,3 +8796,17 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC update_test_language_settings(cx, f); } + +pub(crate) fn rust_lang() -> Arc { + Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )) +} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4431d2315e..d46f0f2a31 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -12,9 +12,9 @@ use crate::{ mouse_context_menu, scroll::scroll_amount::ScrollAmount, CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, - EditorSettings, EditorSnapshot, EditorStyle, HalfPageDown, HalfPageUp, HoveredCursor, LineDown, - LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, SoftWrap, ToPoint, - CURSORS_VISIBLE_FOR, MAX_LINE_LEN, + EditorSettings, EditorSnapshot, EditorStyle, GutterDimensions, HalfPageDown, HalfPageUp, + HoveredCursor, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, + SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN, }; use anyhow::Result; use collections::{BTreeMap, HashMap}; @@ -260,6 +260,8 @@ impl EditorElement { register_action(view, cx, Editor::go_to_prev_hunk); register_action(view, cx, Editor::go_to_definition); register_action(view, cx, Editor::go_to_definition_split); + register_action(view, cx, Editor::go_to_implementation); + register_action(view, cx, Editor::go_to_implementation_split); register_action(view, cx, Editor::go_to_type_definition); register_action(view, cx, Editor::go_to_type_definition_split); register_action(view, cx, Editor::open_url); @@ -271,7 +273,9 @@ impl EditorElement { register_action(view, cx, Editor::show_completions); register_action(view, cx, Editor::toggle_code_actions); register_action(view, cx, Editor::open_excerpts); + register_action(view, cx, Editor::open_excerpts_in_split); register_action(view, cx, Editor::toggle_soft_wrap); + register_action(view, cx, Editor::toggle_line_numbers); register_action(view, cx, Editor::toggle_inlay_hints); register_action(view, cx, hover_popover::hover); register_action(view, cx, Editor::reveal_in_finder); @@ -334,6 +338,7 @@ impl EditorElement { register_action(view, cx, Editor::display_cursor_names); register_action(view, cx, Editor::unique_lines_case_insensitive); register_action(view, cx, Editor::unique_lines_case_sensitive); + register_action(view, cx, Editor::accept_partial_copilot_suggestion); } fn register_key_listeners( @@ -714,20 +719,27 @@ impl EditorElement { let scroll_position = layout.position_map.snapshot.scroll_position(); let scroll_top = scroll_position.y * line_height; - let show_gutter = matches!( + if bounds.contains(&cx.mouse_position()) { + let stacking_order = cx.stacking_order().clone(); + cx.set_cursor_style(CursorStyle::Arrow, stacking_order); + } + + let show_git_gutter = matches!( ProjectSettings::get_global(cx).git.git_gutter, Some(GitGutterSetting::TrackedFiles) ); - if show_gutter { + if show_git_gutter { Self::paint_diff_hunks(bounds, layout, cx); } + let gutter_settings = EditorSettings::get_global(cx).gutter; + for (ix, line) in layout.line_numbers.iter().enumerate() { if let Some(line) = line { let line_origin = bounds.origin + point( - bounds.size.width - line.width - layout.gutter_padding, + bounds.size.width - line.width - layout.gutter_dimensions.right_padding, ix as f32 * line_height - (scroll_top % line_height), ); @@ -738,6 +750,7 @@ impl EditorElement { cx.with_z_index(1, |cx| { for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() { if let Some(fold_indicator) = fold_indicator { + debug_assert!(gutter_settings.folds); let mut fold_indicator = fold_indicator.into_any_element(); let available_space = size( AvailableSpace::MinContent, @@ -746,11 +759,12 @@ impl EditorElement { let fold_indicator_size = fold_indicator.measure(available_space, cx); let position = point( - bounds.size.width - layout.gutter_padding, + bounds.size.width - layout.gutter_dimensions.right_padding, ix as f32 * line_height - (scroll_top % line_height), ); let centering_offset = point( - (layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width) + (layout.gutter_dimensions.right_padding + layout.gutter_dimensions.margin + - fold_indicator_size.width) / 2., (line_height - fold_indicator_size.height) / 2., ); @@ -760,6 +774,7 @@ impl EditorElement { } if let Some(indicator) = layout.code_actions_indicator.take() { + debug_assert!(gutter_settings.code_actions); let mut button = indicator.button.into_any_element(); let available_space = size( AvailableSpace::MinContent, @@ -770,7 +785,9 @@ impl EditorElement { let mut x = Pixels::ZERO; let mut y = indicator.row as f32 * line_height - scroll_top; // Center indicator. - x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.; + x += (layout.gutter_dimensions.margin + layout.gutter_dimensions.left_padding + - indicator_size.width) + / 2.; y += (line_height - indicator_size.height) / 2.; button.draw(bounds.origin + point(x, y), available_space, cx); @@ -885,7 +902,9 @@ impl EditorElement { cx: &mut ElementContext, ) { let start_row = layout.visible_display_row_range.start; - let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); + // Offset the content_bounds from the text_bounds by the gutter margin (which is roughly half a character wide) to make hit testing work more like how we want. + let content_origin = + text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO); let line_end_overshoot = 0.15 * layout.position_map.line_height; let whitespace_setting = self .editor @@ -904,7 +923,7 @@ impl EditorElement { bounds: text_bounds, stacking_order: cx.stacking_order().clone(), }; - if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) { + if text_bounds.contains(&cx.mouse_position()) { if self .editor .read(cx) @@ -912,9 +931,15 @@ impl EditorElement { .as_ref() .is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty()) { - cx.set_cursor_style(CursorStyle::PointingHand); + cx.set_cursor_style( + CursorStyle::PointingHand, + interactive_text_bounds.stacking_order.clone(), + ); } else { - cx.set_cursor_style(CursorStyle::IBeam); + cx.set_cursor_style( + CursorStyle::IBeam, + interactive_text_bounds.stacking_order.clone(), + ); } } @@ -1154,7 +1179,8 @@ impl EditorElement { layout: &LayoutState, cx: &mut ElementContext, ) { - let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); + let content_origin = + text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO); let line_end_overshoot = layout.line_end_overshoot(); // A softer than perfect black @@ -1180,7 +1206,8 @@ impl EditorElement { layout: &mut LayoutState, cx: &mut ElementContext, ) { - let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); + let content_origin = + text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO); let start_row = layout.visible_display_row_range.start; if let Some((position, mut context_menu)) = layout.context_menu.take() { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); @@ -1538,8 +1565,11 @@ impl EditorElement { stacking_order: cx.stacking_order().clone(), }; let mut mouse_position = cx.mouse_position(); - if interactive_track_bounds.visibly_contains(&mouse_position, cx) { - cx.set_cursor_style(CursorStyle::Arrow); + if track_bounds.contains(&mouse_position) { + cx.set_cursor_style( + CursorStyle::Arrow, + interactive_track_bounds.stacking_order.clone(), + ); } cx.on_mouse_event({ @@ -1557,7 +1587,7 @@ impl EditorElement { let new_y = event.position.y; if (track_bounds.top()..track_bounds.bottom()).contains(&y) { let mut position = editor.scroll_position(cx); - position.y += (new_y - y) * (max_row as f32) / height; + position.y += (new_y - y) * max_row / height; if position.y < 0.0 { position.y = 0.0; } @@ -1604,8 +1634,7 @@ impl EditorElement { let y = event.position.y; if y < thumb_top || thumb_bottom < y { - let center_row = - ((y - top) * max_row as f32 / height).round() as u32; + let center_row = ((y - top) * max_row / height).round() as u32; let top_row = center_row .saturating_sub((row_range.end - row_range.start) as u32 / 2); let mut position = editor.scroll_position(cx); @@ -1817,7 +1846,10 @@ impl EditorElement { Vec>, ) { let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); - let include_line_numbers = snapshot.mode == EditorMode::Full; + let include_line_numbers = + EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full; + let include_fold_statuses = + EditorSettings::get_global(cx).gutter.folds && snapshot.mode == EditorMode::Full; let mut shaped_line_numbers = Vec::with_capacity(rows.len()); let mut fold_statuses = Vec::with_capacity(rows.len()); let mut line_number = String::new(); @@ -1862,6 +1894,8 @@ impl EditorElement { .shape_line(line_number.clone().into(), font_size, &[run]) .unwrap(); shaped_line_numbers.push(Some(shaped_line)); + } + if include_fold_statuses { fold_statuses.push( is_singleton .then(|| { @@ -1886,7 +1920,7 @@ impl EditorElement { rows: Range, line_number_layouts: &[Option], snapshot: &EditorSnapshot, - cx: &ViewContext, + cx: &mut ViewContext, ) -> Vec { if rows.start >= rows.end { return Vec::new(); @@ -1896,7 +1930,7 @@ impl EditorElement { if snapshot.is_empty() { let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); let placeholder_color = cx.theme().colors().text_placeholder; - let placeholder_text = snapshot.placeholder_text(); + let placeholder_text = snapshot.placeholder_text(cx); let placeholder_lines = placeholder_text .as_ref() @@ -1930,7 +1964,7 @@ impl EditorElement { chunks, &self.style.text, MAX_LINE_LEN, - rows.len() as usize, + rows.len(), line_number_layouts, snapshot.mode, cx, @@ -1958,14 +1992,20 @@ impl EditorElement { .unwrap() .width; - let gutter_dimensions = snapshot.gutter_dimensions(font_id, font_size, em_width, self.max_line_number_width(&snapshot, cx), cx); + let gutter_dimensions = snapshot.gutter_dimensions( + font_id, + font_size, + em_width, + self.max_line_number_width(&snapshot, cx), + cx, + ); editor.gutter_width = gutter_dimensions.width; let text_width = bounds.size.width - gutter_dimensions.width; let overscroll = size(em_width, px(0.)); let _snapshot = { - editor.set_visible_line_count((bounds.size.height / line_height).into(), cx); + editor.set_visible_line_count(bounds.size.height / line_height, cx); let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width; let wrap_width = match editor.soft_wrap_mode(cx) { @@ -1998,7 +2038,7 @@ impl EditorElement { // The scroll position is a fractional point, the whole number of which represents // the top of the window in terms of display rows. let start_row = scroll_position.y as u32; - let height_in_lines = f32::from(bounds.size.height / line_height); + let height_in_lines = bounds.size.height / line_height; let max_row = snapshot.max_point().row(); // Add 1 to ensure selections bleed off screen @@ -2051,7 +2091,7 @@ impl EditorElement { editor.cursor_shape, &snapshot.display_snapshot, is_newest, - true, + editor.leader_peer_id.is_none(), None, ); if is_newest { @@ -2202,7 +2242,6 @@ impl EditorElement { .width; let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; - let editor_view = cx.view().clone(); let (scroll_width, blocks) = cx.with_element_context(|cx| { cx.with_element_id(Some("editor_blocks"), |cx| { self.layout_blocks( @@ -2211,22 +2250,20 @@ impl EditorElement { bounds.size.width, scroll_width, text_width, - gutter_dimensions.padding, - gutter_dimensions.width, + &gutter_dimensions, em_width, gutter_dimensions.width + gutter_dimensions.margin, line_height, &style, &line_layouts, editor, - editor_view, cx, ) }) }); let scroll_max = point( - f32::from((scroll_width - text_size.width) / em_width).max(0.0), + ((scroll_width - text_size.width) / em_width).max(0.0), max_row as f32, ); @@ -2249,6 +2286,8 @@ impl EditorElement { snapshot = editor.snapshot(cx); } + let gutter_settings = EditorSettings::get_global(cx).gutter; + let mut context_menu = None; let mut code_actions_indicator = None; if let Some(newest_selection_head) = newest_selection_head { @@ -2270,12 +2309,14 @@ impl EditorElement { Some(crate::ContextMenu::CodeActions(_)) ); - code_actions_indicator = editor - .render_code_actions_indicator(&style, active, cx) - .map(|element| CodeActionsIndicator { - row: newest_selection_head.row(), - button: element, - }); + if gutter_settings.code_actions { + code_actions_indicator = editor + .render_code_actions_indicator(&style, active, cx) + .map(|element| CodeActionsIndicator { + row: newest_selection_head.row(), + button: element, + }); + } } } @@ -2293,29 +2334,32 @@ impl EditorElement { None } else { editor.hover_state.render( - &snapshot, - &style, - visible_rows, - max_size, - editor.workspace.as_ref().map(|(w, _)| w.clone()), - cx, - ) + &snapshot, + &style, + visible_rows, + max_size, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ) }; let editor_view = cx.view().clone(); - let fold_indicators = cx.with_element_context(|cx| { - - cx.with_element_id(Some("gutter_fold_indicators"), |_cx| { - editor.render_fold_indicators( - fold_statuses, - &style, - editor.gutter_hovered, - line_height, - gutter_dimensions.margin, - editor_view, - ) - }) - }); + let fold_indicators = if gutter_settings.folds { + cx.with_element_context(|cx| { + cx.with_element_id(Some("gutter_fold_indicators"), |_cx| { + editor.render_fold_indicators( + fold_statuses, + &style, + editor.gutter_hovered, + line_height, + gutter_dimensions.margin, + editor_view, + ) + }) + }) + } else { + Vec::new() + }; let invisible_symbol_font_size = font_size / 2.; let tab_invisible = cx @@ -2368,13 +2412,12 @@ impl EditorElement { visible_display_row_range: start_row..end_row, wrap_guides, gutter_size, - gutter_padding: gutter_dimensions.padding, + gutter_dimensions, text_size, scrollbar_row_range, show_scrollbars, is_singleton, max_row, - gutter_margin: gutter_dimensions.margin, active_rows, highlighted_rows, highlighted_ranges, @@ -2401,15 +2444,13 @@ impl EditorElement { editor_width: Pixels, scroll_width: Pixels, text_width: Pixels, - gutter_padding: Pixels, - gutter_width: Pixels, + gutter_dimensions: &GutterDimensions, em_width: Pixels, text_x: Pixels, line_height: Pixels, style: &EditorStyle, line_layouts: &[LineWithInvisibles], editor: &mut Editor, - editor_view: View, cx: &mut ElementContext, ) -> (Pixels, Vec) { let mut block_id = 0; @@ -2445,13 +2486,11 @@ impl EditorElement { block.render(&mut BlockContext { context: cx, anchor_x, - gutter_padding, + gutter_dimensions, line_height, - gutter_width, em_width, block_id, max_width: scroll_width.max(text_width), - view: editor_view.clone(), editor_style: &self.style, }) } @@ -2551,12 +2590,14 @@ impl EditorElement { h_flex() .id(("collapsed context", block_id)) .size_full() - .gap(gutter_padding) + .gap(gutter_dimensions.left_padding + gutter_dimensions.right_padding) .child( h_flex() .justify_end() .flex_none() - .w(gutter_width - gutter_padding) + .w(gutter_dimensions.width + - (gutter_dimensions.left_padding + + gutter_dimensions.right_padding)) .h_full() .text_buffer(cx) .text_color(cx.theme().colors().editor_line_number) @@ -2617,7 +2658,7 @@ impl EditorElement { BlockStyle::Sticky => editor_width, BlockStyle::Flex => editor_width .max(fixed_block_max_width) - .max(gutter_width + scroll_width), + .max(gutter_dimensions.width + scroll_width), BlockStyle::Fixed => unreachable!(), }; let available_space = size( @@ -2634,7 +2675,7 @@ impl EditorElement { }); } ( - scroll_width.max(fixed_block_max_width - gutter_width), + scroll_width.max(fixed_block_max_width - gutter_dimensions.width), blocks, ) } @@ -2682,11 +2723,8 @@ impl EditorElement { }; let scroll_position = position_map.snapshot.scroll_position(); - let x = f32::from( - (scroll_position.x * max_glyph_width - delta.x) / max_glyph_width, - ); - let y = - f32::from((scroll_position.y * line_height - delta.y) / line_height); + let x = (scroll_position.x * max_glyph_width - delta.x) / max_glyph_width; + let y = (scroll_position.y * line_height - delta.y) / line_height; let scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max); editor.scroll(scroll_position, axis, cx); @@ -2843,7 +2881,7 @@ impl LineWithInvisibles { .unwrap(); layouts.push(Self { line: shaped_line, - invisibles: invisibles.drain(..).collect(), + invisibles: std::mem::take(&mut invisibles), }); line.clear(); @@ -2952,6 +2990,7 @@ impl LineWithInvisibles { ); } + #[allow(clippy::too_many_arguments)] fn draw_invisibles( &self, selection_ranges: &[Range], @@ -3151,8 +3190,7 @@ type BufferRow = u32; pub struct LayoutState { position_map: Arc, gutter_size: Size, - gutter_padding: Pixels, - gutter_margin: Pixels, + gutter_dimensions: GutterDimensions, text_size: gpui::Size, mode: EditorMode, wrap_guides: SmallVec<[(Pixels, bool); 2]>, @@ -3228,12 +3266,12 @@ impl PositionMap { let position = position - text_bounds.origin; let y = position.y.max(px(0.)).min(self.size.height); let x = position.x + (scroll_position.x * self.em_width); - let row = (f32::from(y / self.line_height) + scroll_position.y) as u32; + let row = ((y / self.line_height) + scroll_position.y) as u32; let (column, x_overshoot_after_line_end) = if let Some(line) = self .line_layouts .get(row as usize - scroll_position.y as usize) - .map(|&LineWithInvisibles { ref line, .. }| line) + .map(|LineWithInvisibles { line, .. }| line) { if let Some(ix) = line.index_for_x(x) { (ix as u32, px(0.)) @@ -4044,8 +4082,7 @@ mod tests { .position_map .line_layouts .iter() - .map(|line_with_invisibles| &line_with_invisibles.invisibles) - .flatten() + .flat_map(|line_with_invisibles| &line_with_invisibles.invisibles) .cloned() .collect() } diff --git a/crates/editor/src/git/permalink.rs b/crates/editor/src/git/permalink.rs index 39edae0dce..90704b43d0 100644 --- a/crates/editor/src/git/permalink.rs +++ b/crates/editor/src/git/permalink.rs @@ -8,6 +8,9 @@ enum GitHostingProvider { Github, Gitlab, Gitee, + Bitbucket, + Sourcehut, + Codeberg, } impl GitHostingProvider { @@ -16,6 +19,9 @@ impl GitHostingProvider { Self::Github => "https://github.com", Self::Gitlab => "https://gitlab.com", Self::Gitee => "https://gitee.com", + Self::Bitbucket => "https://bitbucket.org", + Self::Sourcehut => "https://git.sr.ht", + Self::Codeberg => "https://codeberg.org", }; Url::parse(&base_url).unwrap() @@ -28,16 +34,21 @@ impl GitHostingProvider { let line = selection.start.row + 1; match self { - Self::Github | Self::Gitlab | Self::Gitee => format!("L{}", line), + Self::Github | Self::Gitlab | Self::Gitee | Self::Sourcehut | Self::Codeberg => { + format!("L{}", line) + } + Self::Bitbucket => format!("lines-{}", line), } } else { let start_line = selection.start.row + 1; let end_line = selection.end.row + 1; match self { - Self::Github => format!("L{}-L{}", start_line, end_line), - Self::Gitlab => format!("L{}-{}", start_line, end_line), - Self::Gitee => format!("L{}-{}", start_line, end_line), + Self::Github | Self::Codeberg => format!("L{}-L{}", start_line, end_line), + Self::Gitlab | Self::Gitee | Self::Sourcehut => { + format!("L{}-{}", start_line, end_line) + } + Self::Bitbucket => format!("lines-{}:{}", start_line, end_line), } } } @@ -69,6 +80,9 @@ pub fn build_permalink(params: BuildPermalinkParams) -> Result { GitHostingProvider::Github => format!("{owner}/{repo}/blob/{sha}/{path}"), GitHostingProvider::Gitlab => format!("{owner}/{repo}/-/blob/{sha}/{path}"), GitHostingProvider::Gitee => format!("{owner}/{repo}/blob/{sha}/{path}"), + GitHostingProvider::Bitbucket => format!("{owner}/{repo}/src/{sha}/{path}"), + GitHostingProvider::Sourcehut => format!("~{owner}/{repo}/tree/{sha}/item/{path}"), + GitHostingProvider::Codeberg => format!("{owner}/{repo}/src/commit/{sha}/{path}"), }; let line_fragment = selection.map(|selection| provider.line_fragment(&selection)); @@ -91,7 +105,7 @@ fn parse_git_remote_url(url: &str) -> Option { .trim_start_matches("https://github.com/") .trim_end_matches(".git"); - let (owner, repo) = repo_with_owner.split_once("/")?; + let (owner, repo) = repo_with_owner.split_once('/')?; return Some(ParsedGitRemote { provider: GitHostingProvider::Github, @@ -106,7 +120,7 @@ fn parse_git_remote_url(url: &str) -> Option { .trim_start_matches("https://gitlab.com/") .trim_end_matches(".git"); - let (owner, repo) = repo_with_owner.split_once("/")?; + let (owner, repo) = repo_with_owner.split_once('/')?; return Some(ParsedGitRemote { provider: GitHostingProvider::Gitlab, @@ -121,7 +135,7 @@ fn parse_git_remote_url(url: &str) -> Option { .trim_start_matches("https://gitee.com/") .trim_end_matches(".git"); - let (owner, repo) = repo_with_owner.split_once("/")?; + let (owner, repo) = repo_with_owner.split_once('/')?; return Some(ParsedGitRemote { provider: GitHostingProvider::Gitee, @@ -130,6 +144,52 @@ fn parse_git_remote_url(url: &str) -> Option { }); } + if url.contains("bitbucket.org") { + let (_, repo_with_owner) = url.trim_end_matches(".git").split_once("bitbucket.org")?; + let (owner, repo) = repo_with_owner + .trim_start_matches('/') + .trim_start_matches(':') + .split_once('/')?; + + return Some(ParsedGitRemote { + provider: GitHostingProvider::Bitbucket, + owner, + repo, + }); + } + + if url.starts_with("git@git.sr.ht:") || url.starts_with("https://git.sr.ht/") { + // sourcehut indicates a repo with '.git' suffix as a separate repo. + // For example, "git@git.sr.ht:~username/repo" and "git@git.sr.ht:~username/repo.git" + // are two distinct repositories. + let repo_with_owner = url + .trim_start_matches("git@git.sr.ht:~") + .trim_start_matches("https://git.sr.ht/~"); + + let (owner, repo) = repo_with_owner.split_once('/')?; + + return Some(ParsedGitRemote { + provider: GitHostingProvider::Sourcehut, + owner, + repo, + }); + } + + if url.starts_with("git@codeberg.org:") || url.starts_with("https://codeberg.org/") { + let repo_with_owner = url + .trim_start_matches("git@codeberg.org:") + .trim_start_matches("https://codeberg.org/") + .trim_end_matches(".git"); + + let (owner, repo) = repo_with_owner.split_once('/')?; + + return Some(ParsedGitRemote { + provider: GitHostingProvider::Codeberg, + owner, + repo, + }); + } + None } @@ -387,4 +447,257 @@ mod tests { let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L24-48"; assert_eq!(permalink.to_string(), expected_url.to_string()) } + + #[test] + fn test_parse_git_remote_url_bitbucket_https_with_username() { + let url = "https://thorstenballzed@bitbucket.org/thorstenzed/testingrepo.git"; + let parsed = parse_git_remote_url(url).unwrap(); + assert!(matches!(parsed.provider, GitHostingProvider::Bitbucket)); + assert_eq!(parsed.owner, "thorstenzed"); + assert_eq!(parsed.repo, "testingrepo"); + } + + #[test] + fn test_parse_git_remote_url_bitbucket_https_without_username() { + let url = "https://bitbucket.org/thorstenzed/testingrepo.git"; + let parsed = parse_git_remote_url(url).unwrap(); + assert!(matches!(parsed.provider, GitHostingProvider::Bitbucket)); + assert_eq!(parsed.owner, "thorstenzed"); + assert_eq!(parsed.repo, "testingrepo"); + } + + #[test] + fn test_parse_git_remote_url_bitbucket_git() { + let url = "git@bitbucket.org:thorstenzed/testingrepo.git"; + let parsed = parse_git_remote_url(url).unwrap(); + assert!(matches!(parsed.provider, GitHostingProvider::Bitbucket)); + assert_eq!(parsed.owner, "thorstenzed"); + assert_eq!(parsed.repo, "testingrepo"); + } + + #[test] + fn test_build_bitbucket_permalink_from_ssh_url() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git", + sha: "f00b4r", + path: "main.rs", + selection: None, + }) + .unwrap(); + + let expected_url = "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_bitbucket_permalink_from_ssh_url_single_line_selection() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git", + sha: "f00b4r", + path: "main.rs", + selection: Some(Point::new(6, 1)..Point::new(6, 10)), + }) + .unwrap(); + + let expected_url = + "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_bitbucket_permalink_from_ssh_url_multi_line_selection() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git", + sha: "f00b4r", + path: "main.rs", + selection: Some(Point::new(23, 1)..Point::new(47, 10)), + }) + .unwrap(); + + let expected_url = + "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-24:48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_ssh_url() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "git@git.sr.ht:~rajveermalviya/zed", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: None, + }) + .unwrap(); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_ssh_url_with_git_prefix() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "git@git.sr.ht:~rajveermalviya/zed.git", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: None, + }) + .unwrap(); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed.git/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_ssh_url_single_line_selection() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "git@git.sr.ht:~rajveermalviya/zed", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: Some(Point::new(6, 1)..Point::new(6, 10)), + }) + .unwrap(); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_ssh_url_multi_line_selection() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "git@git.sr.ht:~rajveermalviya/zed", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: Some(Point::new(23, 1)..Point::new(47, 10)), + }) + .unwrap(); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L24-48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_https_url() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "https://git.sr.ht/~rajveermalviya/zed", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: None, + }) + .unwrap(); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_https_url_single_line_selection() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "https://git.sr.ht/~rajveermalviya/zed", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: Some(Point::new(6, 1)..Point::new(6, 10)), + }) + .unwrap(); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_sourcehut_permalink_from_https_url_multi_line_selection() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "https://git.sr.ht/~rajveermalviya/zed", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: Some(Point::new(23, 1)..Point::new(47, 10)), + }) + .unwrap(); + + let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs#L24-48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_codeberg_permalink_from_ssh_url() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "git@codeberg.org:rajveermalviya/zed.git", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: None, + }) + .unwrap(); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_codeberg_permalink_from_ssh_url_single_line_selection() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "git@codeberg.org:rajveermalviya/zed.git", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: Some(Point::new(6, 1)..Point::new(6, 10)), + }) + .unwrap(); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_codeberg_permalink_from_ssh_url_multi_line_selection() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "git@codeberg.org:rajveermalviya/zed.git", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/editor/src/git/permalink.rs", + selection: Some(Point::new(23, 1)..Point::new(47, 10)), + }) + .unwrap(); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L24-L48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_codeberg_permalink_from_https_url() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "https://codeberg.org/rajveermalviya/zed.git", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: None, + }) + .unwrap(); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_codeberg_permalink_from_https_url_single_line_selection() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "https://codeberg.org/rajveermalviya/zed.git", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: Some(Point::new(6, 1)..Point::new(6, 10)), + }) + .unwrap(); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L7"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } + + #[test] + fn test_build_codeberg_permalink_from_https_url_multi_line_selection() { + let permalink = build_permalink(BuildPermalinkParams { + remote_url: "https://codeberg.org/rajveermalviya/zed.git", + sha: "faa6f979be417239b2e070dbbf6392b909224e0b", + path: "crates/zed/src/main.rs", + selection: Some(Point::new(23, 1)..Point::new(47, 10)), + }) + .unwrap(); + + let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L24-L48"; + assert_eq!(permalink.to_string(), expected_url.to_string()) + } } diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index 787be1999e..1d0b304913 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -17,7 +17,7 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon let snapshot = editor.snapshot(cx); if let Some((opening_range, closing_range)) = snapshot .buffer_snapshot - .innermost_enclosing_bracket_ranges(head..head) + .innermost_enclosing_bracket_ranges(head..head, None) { editor.highlight_background::( vec![ diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 5baf6e9e96..e1afaa6591 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -138,7 +138,7 @@ impl Editor { cx.focus(&self.focus_handle); } - self.navigate_to_hover_links(hovered_link_state.links, modifiers.alt, cx); + self.navigate_to_hover_links(None, hovered_link_state.links, modifiers.alt, cx); return; } } @@ -381,7 +381,7 @@ pub fn show_link_definition( let Some((buffer, buffer_position)) = editor .buffer .read(cx) - .text_anchor_for_position(trigger_anchor.clone(), cx) + .text_anchor_for_position(*trigger_anchor, cx) else { return; }; @@ -389,7 +389,7 @@ pub fn show_link_definition( let Some((excerpt_id, _, _)) = editor .buffer() .read(cx) - .excerpt_containing(trigger_anchor.clone(), cx) + .excerpt_containing(*trigger_anchor, cx) else { return; }; @@ -424,9 +424,8 @@ pub fn show_link_definition( TriggerPoint::Text(_) => { if let Some((url_range, url)) = find_url(&buffer, buffer_position, cx.clone()) { this.update(&mut cx, |_, _| { - let start = - snapshot.anchor_in_excerpt(excerpt_id.clone(), url_range.start); - let end = snapshot.anchor_in_excerpt(excerpt_id.clone(), url_range.end); + let start = snapshot.anchor_in_excerpt(excerpt_id, url_range.start); + let end = snapshot.anchor_in_excerpt(excerpt_id, url_range.end); ( Some(RangeInEditor::Text(start..end)), vec![HoverLink::Url(url)], @@ -451,14 +450,10 @@ pub fn show_link_definition( ( definition_result.iter().find_map(|link| { link.origin.as_ref().map(|origin| { - let start = snapshot.anchor_in_excerpt( - excerpt_id.clone(), - origin.range.start, - ); - let end = snapshot.anchor_in_excerpt( - excerpt_id.clone(), - origin.range.end, - ); + let start = snapshot + .anchor_in_excerpt(excerpt_id, origin.range.start); + let end = snapshot + .anchor_in_excerpt(excerpt_id, origin.range.end); RangeInEditor::Text(start..end) }) }), @@ -984,6 +979,8 @@ mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 8bed85305a..b2a168fb83 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -117,7 +117,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie // Highlight the selected symbol using a background highlight this.highlight_inlay_background::( vec![inlay_hover.range], - |theme| theme.element_hover, // todo!("use a proper background here") + |theme| theme.element_hover, // todo("use a proper background here") cx, ); this.hover_state.info_popover = Some(hover_popover); @@ -199,9 +199,10 @@ fn show_hover( if symbol_range .as_text_range() .map(|range| { - range - .to_offset(&snapshot.buffer_snapshot) - .contains(&multibuffer_offset) + let hover_range = range.to_offset(&snapshot.buffer_snapshot); + // LSP returns a hover result for the end index of ranges that should be hovered, so we need to + // use an inclusive range here to check if we should dismiss the popover + (hover_range.start..=hover_range.end).contains(&multibuffer_offset) }) .unwrap_or(false) { @@ -296,10 +297,10 @@ fn show_hover( let range = if let Some(range) = hover_result.range { let start = snapshot .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), range.start); + .anchor_in_excerpt(excerpt_id, range.start); let end = snapshot .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), range.end); + .anchor_in_excerpt(excerpt_id, range.end); start..end } else { @@ -331,7 +332,7 @@ fn show_hover( // Highlight the selected symbol using a background highlight this.highlight_background::( vec![symbol_range], - |theme| theme.element_hover, // todo! update theme + |theme| theme.element_hover, // todo update theme cx, ); } else { @@ -596,7 +597,7 @@ impl DiagnosticPopover { .as_ref() .unwrap_or(&self.local_diagnostic); - (entry.diagnostic.group_id, entry.range.start.clone()) + (entry.diagnostic.group_id, entry.range.start) } } @@ -1065,6 +1066,8 @@ mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index dc2616c6ef..a796c39a25 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -37,6 +37,9 @@ pub struct InlayHintCache { version: usize, pub(super) enabled: bool, update_tasks: HashMap, + refresh_task: Option>, + invalidate_debounce: Option, + append_debounce: Option, lsp_request_limiter: Arc, } @@ -267,6 +270,9 @@ impl InlayHintCache { enabled: inlay_hint_settings.enabled, hints: HashMap::default(), update_tasks: HashMap::default(), + refresh_task: None, + invalidate_debounce: debounce_value(inlay_hint_settings.edit_debounce_ms), + append_debounce: debounce_value(inlay_hint_settings.scroll_debounce_ms), version: 0, lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)), } @@ -282,6 +288,8 @@ impl InlayHintCache { visible_hints: Vec, cx: &mut ViewContext, ) -> ControlFlow> { + self.invalidate_debounce = debounce_value(new_hint_settings.edit_debounce_ms); + self.append_debounce = debounce_value(new_hint_settings.scroll_debounce_ms); let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds(); match (self.enabled, new_hint_settings.enabled) { (false, false) => { @@ -332,15 +340,15 @@ impl InlayHintCache { /// This way, concequent refresh invocations are less likely to trigger LSP queries for the invisible ranges. pub(super) fn spawn_hint_refresh( &mut self, - reason: &'static str, + reason_description: &'static str, excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, + ignore_debounce: bool, cx: &mut ViewContext, ) -> Option { if !self.enabled { return None; } - let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { self.update_tasks @@ -358,12 +366,23 @@ impl InlayHintCache { } let cache_version = self.version + 1; - cx.spawn(|editor, mut cx| async move { + let debounce_duration = if ignore_debounce { + None + } else if invalidate.should_invalidate() { + self.invalidate_debounce + } else { + self.append_debounce + }; + self.refresh_task = Some(cx.spawn(|editor, mut cx| async move { + if let Some(debounce_duration) = debounce_duration { + cx.background_executor().timer(debounce_duration).await; + } + editor .update(&mut cx, |editor, cx| { spawn_new_update_tasks( editor, - reason, + reason_description, excerpts_to_query, invalidate, cache_version, @@ -371,8 +390,7 @@ impl InlayHintCache { ) }) .ok(); - }) - .detach(); + })); if invalidated_hints.is_empty() { None @@ -612,6 +630,14 @@ impl InlayHintCache { } } +fn debounce_value(debounce_ms: u64) -> Option { + if debounce_ms > 0 { + Some(Duration::from_millis(debounce_ms)) + } else { + None + } +} + fn spawn_new_update_tasks( editor: &mut Editor, reason: &'static str, @@ -1259,6 +1285,8 @@ pub mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), show_other_hints: allowed_hint_kinds.contains(&None), @@ -1389,6 +1417,8 @@ pub mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, @@ -1506,6 +1536,8 @@ pub mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, @@ -1521,12 +1553,14 @@ pub mod tests { }), ) .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); let mut rs_fake_servers = None; let mut md_fake_servers = None; for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { - let mut language = Language::new( + language_registry.add(Arc::new(Language::new( LanguageConfig { name: name.into(), matcher: LanguageMatcher { @@ -1536,25 +1570,23 @@ pub mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ); - let fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + ))); + let fake_servers = language_registry.register_fake_lsp_adapter( + name, + FakeLspAdapter { name, capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), ..Default::default() }, ..Default::default() - })) - .await; + }, + ); match name { "Rust" => rs_fake_servers = Some(fake_servers), "Markdown" => md_fake_servers = Some(fake_servers), _ => unreachable!(), } - project.update(cx, |project, _| { - project.languages().add(Arc::new(language)); - }); } let rs_buffer = project @@ -1734,6 +1766,8 @@ pub mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), show_other_hints: allowed_hint_kinds.contains(&None), @@ -1895,6 +1929,8 @@ pub mod tests { update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), show_parameter_hints: new_allowed_hint_kinds .contains(&Some(InlayHintKind::Parameter)), @@ -1939,6 +1975,8 @@ pub mod tests { update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: false, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), show_parameter_hints: another_allowed_hint_kinds .contains(&Some(InlayHintKind::Parameter)), @@ -1997,6 +2035,8 @@ pub mod tests { update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), show_parameter_hints: final_allowed_hint_kinds .contains(&Some(InlayHintKind::Parameter)), @@ -2071,6 +2111,8 @@ pub mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, @@ -2155,7 +2197,7 @@ pub mod tests { "another change #3", ] { expected_changes.push(async_later_change); - let task_editor = editor.clone(); + let task_editor = editor; edits.push(cx.spawn(|mut cx| async move { task_editor .update(&mut cx, |editor, cx| { @@ -2203,32 +2245,14 @@ pub mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, }) }); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/a", @@ -2238,8 +2262,22 @@ pub mod tests { }), ) .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(crate::editor_tests::rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }, + ); + let buffer = project .update(cx, |project, cx| { project.open_local_buffer("/a/main.rs", cx) @@ -2361,6 +2399,11 @@ pub mod tests { editor .update(cx, |editor, cx| { editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, cx| { editor.scroll_screen(&ScrollAmount::Page(1.0), cx); }) .unwrap(); @@ -2497,33 +2540,14 @@ pub mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, }) }); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - let language = Arc::new(language); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/a", @@ -2533,10 +2557,23 @@ pub mod tests { }), ) .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages().add(Arc::clone(&language)) - }); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let language = crate::editor_tests::rust_lang(); + language_registry.add(language); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }, + ); + let worktree_id = project.update(cx, |project, cx| { project.worktrees().next().unwrap().read(cx).id() }); @@ -2782,6 +2819,9 @@ pub mod tests { }); }) .unwrap(); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.executor().run_until_parked(); editor.update(cx, |editor, cx| { let expected_hints = vec![ @@ -2816,12 +2856,12 @@ pub mod tests { cx.executor().run_until_parked(); editor.update(cx, |editor, cx| { let expected_hints = vec![ - "main hint(edited) #0".to_string(), - "main hint(edited) #1".to_string(), - "main hint(edited) #2".to_string(), - "main hint(edited) #3".to_string(), - "main hint(edited) #4".to_string(), - "main hint(edited) #5".to_string(), + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), "other hint(edited) #0".to_string(), "other hint(edited) #1".to_string(), ]; @@ -2834,11 +2874,12 @@ pub mod tests { assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let current_cache_version = editor.inlay_hint_cache().version; - let expected_version = last_scroll_update_version + expected_hints.len(); - assert!( - current_cache_version == expected_version || current_cache_version == expected_version + 1 , - // TODO we sometimes get an extra cache version bump, why? - "We should have updated cache N times == N of new hints arrived (separately from each excerpt), or hit a bug and do that one extra time" + // We expect two new hints for the excerpts from `other.rs`: + let expected_version = last_scroll_update_version + 2; + assert_eq!( + current_cache_version, + expected_version, + "We should have updated cache N times == N of new hints arrived (separately from each edited excerpt)" ); }).unwrap(); } @@ -2848,33 +2889,14 @@ pub mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: false, show_parameter_hints: false, show_other_hints: false, }) }); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - let language = Arc::new(language); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/a", @@ -2884,10 +2906,22 @@ pub mod tests { }), ) .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages().add(Arc::clone(&language)) - }); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(crate::editor_tests::rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }, + ); + let worktree_id = project.update(cx, |project, cx| { project.worktrees().next().unwrap().read(cx).id() }); @@ -3049,6 +3083,8 @@ pub mod tests { update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, @@ -3082,32 +3118,14 @@ pub mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, }) }); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/a", @@ -3117,8 +3135,22 @@ pub mod tests { }), ) .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(crate::editor_tests::rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }, + ); + let buffer = project .update(cx, |project, cx| { project.open_local_buffer("/a/main.rs", cx) @@ -3180,6 +3212,8 @@ pub mod tests { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: false, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, @@ -3258,6 +3292,8 @@ pub mod tests { update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, show_type_hints: true, show_parameter_hints: true, show_other_hints: true, @@ -3331,27 +3367,6 @@ pub mod tests { async fn prepare_test_objects( cx: &mut TestAppContext, ) -> (&'static str, WindowHandle, FakeLanguageServer) { - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/a", @@ -3363,7 +3378,30 @@ pub mod tests { .await; let project = Project::test(fs, ["/a".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ))); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }, + ); + let buffer = project .update(cx, |project, cx| { project.open_local_buffer("/a/main.rs", cx) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b5497db1e8..216df09f03 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -593,7 +593,7 @@ impl Item for Editor { None } - fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option { + fn tab_description(&self, detail: usize, cx: &AppContext) -> Option { let path = path_for_buffer(&self.buffer, detail, true, cx)?; Some(path.to_string_lossy().to_string().into()) } @@ -702,14 +702,21 @@ impl Item for Editor { } } - fn save(&mut self, project: Model, cx: &mut ViewContext) -> Task> { + fn save( + &mut self, + format: bool, + project: Model, + cx: &mut ViewContext, + ) -> Task> { self.report_editor_event("save", None, cx); let buffers = self.buffer().clone().read(cx).all_buffers(); cx.spawn(|this, mut cx| async move { - this.update(&mut cx, |this, cx| { - this.perform_format(project.clone(), FormatTrigger::Save, cx) - })? - .await?; + if format { + this.update(&mut cx, |this, cx| { + this.perform_format(project.clone(), FormatTrigger::Save, cx) + })? + .await?; + } if buffers.len() == 1 { project @@ -952,14 +959,14 @@ impl Item for Editor { let buffer = project_item .downcast::() .map_err(|_| anyhow!("Project item at stored path was not a buffer"))?; - Ok(pane.update(&mut cx, |_, cx| { + pane.update(&mut cx, |_, cx| { cx.new_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project), cx); editor.read_scroll_position_from_db(item_id, workspace_id, cx); editor }) - })?) + }) }) }) .unwrap_or_else(|error| Task::ready(Err(error))) @@ -1147,8 +1154,8 @@ impl SearchableItem for Editor { let end = excerpt .buffer .anchor_before(excerpt_range.start + range.end); - buffer.anchor_in_excerpt(excerpt.id.clone(), start) - ..buffer.anchor_in_excerpt(excerpt.id.clone(), end) + buffer.anchor_in_excerpt(excerpt.id, start) + ..buffer.anchor_in_excerpt(excerpt.id, end) }), ); } diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index bda55e01ed..e7f0397485 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -1,6 +1,6 @@ use crate::{ - DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition, - Rename, RevealInFinder, SelectMode, ToggleCodeActions, + DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation, + GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions, }; use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext}; @@ -48,6 +48,7 @@ pub fn deploy_context_menu( menu.action("Rename Symbol", Box::new(Rename)) .action("Go to Definition", Box::new(GoToDefinition)) .action("Go to Type Definition", Box::new(GoToTypeDefinition)) + .action("Go to Implementation", Box::new(GoToImplementation)) .action("Find All References", Box::new(FindAllReferences)) .action( "Code Actions", diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index c00d12668b..34299ffad6 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -12,7 +12,7 @@ use std::{ops::Range, sync::Arc}; /// Defines search strategy for items in `movement` module. /// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas /// `FindRange::MultiLine` keeps going until the end of a string. -#[derive(Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FindRange { SingleLine, MultiLine, @@ -688,31 +688,30 @@ mod tests { // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary let mut id = 0; let inlays = (0..buffer_snapshot.len()) - .map(|offset| { + .flat_map(|offset| { [ Inlay { id: InlayId::Suggestion(post_inc(&mut id)), position: buffer_snapshot.anchor_at(offset, Bias::Left), - text: format!("test").into(), + text: "test".into(), }, Inlay { id: InlayId::Suggestion(post_inc(&mut id)), position: buffer_snapshot.anchor_at(offset, Bias::Right), - text: format!("test").into(), + text: "test".into(), }, Inlay { id: InlayId::Hint(post_inc(&mut id)), position: buffer_snapshot.anchor_at(offset, Bias::Left), - text: format!("test").into(), + text: "test".into(), }, Inlay { id: InlayId::Hint(post_inc(&mut id)), position: buffer_snapshot.anchor_at(offset, Bias::Right), - text: format!("test").into(), + text: "test".into(), }, ] }) - .flatten() .collect(); let snapshot = display_map.update(cx, |map, cx| { map.splice_inlays(Vec::new(), inlays, cx); @@ -841,7 +840,7 @@ mod tests { surrounding_word(&snapshot, display_points[1]), display_points[0]..display_points[2], "{}", - marked_text.to_string() + marked_text ); } @@ -863,7 +862,7 @@ mod tests { let mut cx = EditorTestContext::new(cx).await; let editor = cx.editor.clone(); - let window = cx.window.clone(); + let window = cx.window; _ = cx.update_window(window, |_, cx| { let text_layout_details = editor.update(cx, |editor, cx| editor.text_layout_details(cx)); diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index 067d09d9ce..1a950e0ea4 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -62,7 +62,6 @@ pub fn expand_macro_recursively( project .read(cx) .language_servers_for_buffer(buffer.read(cx), cx) - .into_iter() .find_map(|(adapter, server)| { if adapter.name.0.as_ref() == "rust-analyzer" { Some(( @@ -105,7 +104,7 @@ pub fn expand_macro_recursively( let buffer = cx.new_model(|cx| { MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name) }); - workspace.add_item( + workspace.add_item_to_active_pane( Box::new(cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))), cx, ); diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index c354f98150..4ac7374651 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -470,7 +470,7 @@ impl Editor { .buffer() .read(cx) .snapshot(cx) - .anchor_at(Point::new(top_row as u32, 0), Bias::Left); + .anchor_at(Point::new(top_row, 0), Bias::Left); let scroll_anchor = ScrollAnchor { offset: gpui::Point::new(x, y), anchor: top_anchor, diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 191dbd04dc..3dfe2d250a 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -62,7 +62,7 @@ impl Editor { line_height: Pixels, cx: &mut ViewContext, ) -> bool { - let visible_lines = f32::from(viewport_height / line_height); + let visible_lines = viewport_height / line_height; let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut scroll_position = self.scroll_manager.scroll_position(&display_map); let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { @@ -241,11 +241,10 @@ impl Editor { let scroll_right = scroll_left + viewport_width; if target_left < scroll_left { - self.scroll_manager.anchor.offset.x = (target_left / max_glyph_width).into(); + self.scroll_manager.anchor.offset.x = target_left / max_glyph_width; true } else if target_right > scroll_right { - self.scroll_manager.anchor.offset.x = - ((target_right - viewport_width) / max_glyph_width).into(); + self.scroll_manager.anchor.offset.x = (target_right - viewport_width) / max_glyph_width; true } else { false diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index b0595ee778..d4d40b8563 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -478,7 +478,7 @@ impl<'a> MutableSelectionsCollection<'a> { if !oldest.start.cmp(&oldest.end, &self.buffer()).is_eq() { let head = oldest.head(); - oldest.start = head.clone(); + oldest.start = head; oldest.end = head; self.collection.disjoint = Arc::from([oldest]); self.selections_changed = true; @@ -794,8 +794,8 @@ impl<'a> MutableSelectionsCollection<'a> { let adjusted_disjoint: Vec<_> = anchors_with_status .chunks(2) .map(|selection_anchors| { - let (anchor_ix, start, kept_start) = selection_anchors[0].clone(); - let (_, end, kept_end) = selection_anchors[1].clone(); + let (anchor_ix, start, kept_start) = selection_anchors[0]; + let (_, end, kept_end) = selection_anchors[1]; let selection = &self.disjoint[anchor_ix / 2]; let kept_head = if selection.reversed { kept_start @@ -826,8 +826,8 @@ impl<'a> MutableSelectionsCollection<'a> { let buffer = self.buffer(); let anchors = buffer.refresh_anchors([&pending.selection.start, &pending.selection.end]); - let (_, start, kept_start) = anchors[0].clone(); - let (_, end, kept_end) = anchors[1].clone(); + let (_, start, kept_start) = anchors[0]; + let (_, end, kept_end) = anchors[1]; let kept_head = if pending.selection.reversed { kept_start } else { diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index b083e63890..848e47a2ea 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -32,7 +32,7 @@ pub struct EditorLspTestContext { impl EditorLspTestContext { pub async fn new( - mut language: Language, + language: Language, capabilities: lsp::ServerCapabilities, cx: &mut gpui::TestAppContext, ) -> EditorLspTestContext { @@ -53,16 +53,17 @@ impl EditorLspTestContext { .expect("language must have a path suffix for EditorLspTestContext") ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities, - ..Default::default() - })) - .await; - let project = Project::test(app_state.fs.clone(), [], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + language.name().as_ref(), + FakeLspAdapter { + capabilities, + ..Default::default() + }, + ); + language_registry.add(Arc::new(language)); app_state .fs @@ -214,6 +215,22 @@ impl EditorLspTestContext { Self::new(language, capabilities, cx).await } + pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self { + let language = Language::new( + LanguageConfig { + name: "HTML".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["html".into()], + ..Default::default() + }, + block_comment: Some(("".into())), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ); + Self::new(language, Default::default(), cx).await + } + // Constructs lsp range using a marked string with '[', ']' range delimiters pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { let ranges = self.ranges(marked_text); diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 9ad0839088..d4a5ee7c6e 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -147,7 +147,7 @@ impl EditorTestContext { self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); let keystroke = Keystroke::parse(keystroke_text).unwrap(); - self.cx.dispatch_keystroke(self.window, keystroke, false); + self.cx.dispatch_keystroke(self.window, keystroke); keystroke_under_test_handle } @@ -236,7 +236,7 @@ impl EditorTestContext { pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { let state_context = self.add_assertion_context(format!( "Initial Editor State: \"{}\"", - marked_text.escape_debug().to_string() + marked_text.escape_debug() )); let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); self.editor.update(&mut self.cx, |editor, cx| { @@ -252,7 +252,7 @@ impl EditorTestContext { pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle { let state_context = self.add_assertion_context(format!( "Initial Editor State: \"{}\"", - marked_text.escape_debug().to_string() + marked_text.escape_debug() )); let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); self.editor.update(&mut self.cx, |editor, cx| { diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index 32ed59d2a3..7c0e2d7afa 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -16,14 +16,16 @@ path = "src/extension_json_schemas.rs" anyhow.workspace = true async-compression.workspace = true async-tar.workspace = true -client.workspace = true +async-trait.workspace = true collections.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true log.workspace = true -parking_lot.workspace = true +lsp.workspace = true +node_runtime.workspace = true +project.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true @@ -31,9 +33,12 @@ settings.workspace = true theme.workspace = true toml.workspace = true util.workspace = true +wasmtime = { workspace = true, features = ["async"] } +wasmtime-wasi.workspace = true +wasmparser.workspace = true [dev-dependencies] -client = { workspace = true, features = ["test-support"] } fs = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension/src/extension_lsp_adapter.rs new file mode 100644 index 0000000000..a981facef0 --- /dev/null +++ b/crates/extension/src/extension_lsp_adapter.rs @@ -0,0 +1,90 @@ +use crate::wasm_host::{wit::LanguageServerConfig, WasmExtension}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::{Future, FutureExt}; +use gpui::AsyncAppContext; +use language::{Language, LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use std::{ + any::Any, + path::{Path, PathBuf}, + pin::Pin, + sync::Arc, +}; +use wasmtime_wasi::preview2::WasiView as _; + +pub struct ExtensionLspAdapter { + pub(crate) extension: WasmExtension, + pub(crate) config: LanguageServerConfig, + pub(crate) work_dir: PathBuf, +} + +#[async_trait] +impl LspAdapter for ExtensionLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName(self.config.name.clone().into()) + } + + fn get_language_server_command<'a>( + self: Arc, + _: Arc, + _: Arc, + delegate: Arc, + _: futures::lock::MutexGuard<'a, Option>, + _: &'a mut AsyncAppContext, + ) -> Pin>>> { + async move { + let command = self + .extension + .call({ + let this = self.clone(); + |extension, store| { + async move { + let resource = store.data_mut().table().push(delegate)?; + extension + .call_language_server_command(store, &this.config, resource) + .await + } + .boxed() + } + }) + .await? + .map_err(|e| anyhow!("{}", e))?; + + Ok(LanguageServerBinary { + path: self.work_dir.join(&command.command), + arguments: command.args.into_iter().map(|arg| arg.into()).collect(), + env: Some(command.env.into_iter().collect()), + }) + } + .boxed_local() + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + unreachable!("get_language_server_command is overridden") + } + + async fn fetch_server_binary( + &self, + _: Box, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + unreachable!("get_language_server_command is overridden") + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + unreachable!("get_language_server_command is overridden") + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } +} diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 59cbe7a0fd..e80ac6f5a4 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -1,50 +1,118 @@ -use anyhow::{anyhow, bail, Context as _, Result}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use client::ClientSettings; -use collections::{BTreeMap, HashSet}; -use fs::{Fs, RemoveOptions}; -use futures::channel::mpsc::unbounded; -use futures::StreamExt as _; -use futures::{io::BufReader, AsyncReadExt as _}; -use gpui::{actions, AppContext, Context, Global, Model, ModelContext, Task}; -use language::{ - LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES, -}; -use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use settings::Settings as _; -use std::cmp::Ordering; -use std::{ - ffi::OsStr, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -use theme::{ThemeRegistry, ThemeSettings}; -use util::http::AsyncBody; -use util::TryFutureExt; -use util::{http::HttpClient, paths::EXTENSIONS_DIR, ResultExt}; +mod extension_lsp_adapter; +mod wasm_host; #[cfg(test)] mod extension_store_test; -#[derive(Deserialize)] -pub struct ExtensionsApiResponse { - pub data: Vec, -} +use anyhow::{anyhow, bail, Context as _, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use collections::{BTreeMap, HashSet}; +use fs::{Fs, RemoveOptions}; +use futures::{channel::mpsc::unbounded, io::BufReader, AsyncReadExt as _, StreamExt as _}; +use gpui::{actions, AppContext, Context, Global, Model, ModelContext, Task}; +use language::{ + LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, LanguageServerName, + QUERY_FILENAME_PREFIXES, +}; +use node_runtime::NodeRuntime; +use serde::{Deserialize, Serialize}; +use std::{ + cmp::Ordering, + ffi::OsStr, + path::{self, Path, PathBuf}, + sync::Arc, + time::Duration, +}; +use theme::{ThemeRegistry, ThemeSettings}; +use util::{ + http::{AsyncBody, HttpClient, HttpClientWithUrl}, + paths::EXTENSIONS_DIR, + ResultExt, TryFutureExt, +}; +use wasm_host::{WasmExtension, WasmHost}; + +use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit}; #[derive(Deserialize)] -pub struct Extension { +pub struct ExtensionsApiResponse { + pub data: Vec, +} + +#[derive(Clone, Deserialize)] +pub struct ExtensionApiResponse { pub id: Arc, - pub version: Arc, pub name: String, + pub version: Arc, pub description: Option, pub authors: Vec, pub repository: String, pub download_count: usize, } +/// This is the old version of the extension manifest, from when it was `extension.json`. +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct OldExtensionManifest { + pub name: String, + pub version: Arc, + + #[serde(default)] + pub description: Option, + #[serde(default)] + pub repository: Option, + #[serde(default)] + pub authors: Vec, + + #[serde(default)] + pub themes: BTreeMap, PathBuf>, + #[serde(default)] + pub languages: BTreeMap, PathBuf>, + #[serde(default)] + pub grammars: BTreeMap, PathBuf>, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct ExtensionManifest { + pub id: Arc, + pub name: String, + pub version: Arc, + + #[serde(default)] + pub description: Option, + #[serde(default)] + pub repository: Option, + #[serde(default)] + pub authors: Vec, + #[serde(default)] + pub lib: LibManifestEntry, + + #[serde(default)] + pub themes: Vec, + #[serde(default)] + pub languages: Vec, + #[serde(default)] + pub grammars: BTreeMap, GrammarManifestEntry>, + #[serde(default)] + pub language_servers: BTreeMap, +} + +#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct LibManifestEntry { + path: Option, +} + +#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct GrammarManifestEntry { + repository: String, + #[serde(alias = "commit")] + rev: String, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct LanguageServerManifestEntry { + language: Arc, +} + #[derive(Clone)] pub enum ExtensionStatus { NotInstalled, @@ -54,17 +122,33 @@ pub enum ExtensionStatus { Removing, } +impl ExtensionStatus { + pub fn is_installing(&self) -> bool { + matches!(self, Self::Installing) + } + + pub fn is_upgrading(&self) -> bool { + matches!(self, Self::Upgrading) + } + + pub fn is_removing(&self) -> bool { + matches!(self, Self::Removing) + } +} + pub struct ExtensionStore { - manifest: Arc>, + extension_index: ExtensionIndex, fs: Arc, - http_client: Arc, + http_client: Arc, extensions_dir: PathBuf, extensions_being_installed: HashSet>, extensions_being_uninstalled: HashSet>, manifest_path: PathBuf, language_registry: Arc, theme_registry: Arc, - extension_changes: ExtensionChanges, + modified_extensions: HashSet>, + wasm_host: Arc, + wasm_extensions: Vec<(Arc, WasmExtension)>, reload_task: Option>>, needs_reload: bool, _watch_extensions_dir: [Task<()>; 2], @@ -74,56 +158,44 @@ struct GlobalExtensionStore(Model); impl Global for GlobalExtensionStore {} -#[derive(Debug, Deserialize, Serialize, Default)] -pub struct Manifest { - pub extensions: BTreeMap, Arc>, - pub grammars: BTreeMap, GrammarManifestEntry>, - pub languages: BTreeMap, LanguageManifestEntry>, - pub themes: BTreeMap, ThemeManifestEntry>, +#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)] +pub struct ExtensionIndex { + pub extensions: BTreeMap, Arc>, + pub themes: BTreeMap, ExtensionIndexEntry>, + pub languages: BTreeMap, ExtensionIndexLanguageEntry>, } -#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Deserialize, Serialize)] -pub struct GrammarManifestEntry { - extension: String, +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] +pub struct ExtensionIndexEntry { + extension: Arc, path: PathBuf, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] -pub struct LanguageManifestEntry { - extension: String, +pub struct ExtensionIndexLanguageEntry { + extension: Arc, path: PathBuf, matcher: LanguageMatcher, grammar: Option>, } -#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] -pub struct ThemeManifestEntry { - extension: String, - path: PathBuf, -} - -#[derive(Default)] -struct ExtensionChanges { - languages: HashSet>, - grammars: HashSet>, - themes: HashSet>, -} - actions!(zed, [ReloadExtensions]); pub fn init( fs: Arc, - http_client: Arc, + http_client: Arc, + node_runtime: Arc, language_registry: Arc, theme_registry: Arc, cx: &mut AppContext, ) { - let store = cx.new_model(|cx| { + let store = cx.new_model(move |cx| { ExtensionStore::new( EXTENSIONS_DIR.clone(), - fs.clone(), - http_client.clone(), - language_registry.clone(), + fs, + http_client, + node_runtime, + language_registry, theme_registry, cx, ) @@ -145,20 +217,29 @@ impl ExtensionStore { pub fn new( extensions_dir: PathBuf, fs: Arc, - http_client: Arc, + http_client: Arc, + node_runtime: Arc, language_registry: Arc, theme_registry: Arc, cx: &mut ModelContext, ) -> Self { let mut this = Self { - manifest: Default::default(), + extension_index: Default::default(), extensions_dir: extensions_dir.join("installed"), manifest_path: extensions_dir.join("manifest.json"), extensions_being_installed: Default::default(), extensions_being_uninstalled: Default::default(), reload_task: None, + wasm_host: WasmHost::new( + fs.clone(), + http_client.clone(), + node_runtime, + language_registry.clone(), + extensions_dir.join("work"), + ), + wasm_extensions: Vec::new(), needs_reload: false, - extension_changes: ExtensionChanges::default(), + modified_extensions: Default::default(), fs, http_client, language_registry, @@ -182,7 +263,8 @@ impl ExtensionStore { if let Some(manifest_content) = manifest_content.log_err() { if let Some(manifest) = serde_json::from_str(&manifest_content).log_err() { - self.manifest_updated(manifest, cx); + // TODO: don't detach + self.extensions_updated(manifest, cx).detach(); } } @@ -209,11 +291,15 @@ impl ExtensionStore { return ExtensionStatus::Removing; } - let installed_version = self.manifest.read().extensions.get(extension_id).cloned(); + let installed_version = self + .extension_index + .extensions + .get(extension_id) + .map(|manifest| manifest.version.clone()); let is_installing = self.extensions_being_installed.contains(extension_id); match (installed_version, is_installing) { (Some(_), true) => ExtensionStatus::Upgrading, - (Some(version), false) => ExtensionStatus::Installed(version.clone()), + (Some(version), false) => ExtensionStatus::Installed(version), (None, true) => ExtensionStatus::Installing, (None, false) => ExtensionStatus::NotInstalled, } @@ -223,15 +309,13 @@ impl ExtensionStore { &self, search: Option<&str>, cx: &mut ModelContext, - ) -> Task>> { - let url = format!( - "{}/{}{query}", - ClientSettings::get_global(cx).server_url, - "api/extensions", + ) -> Task>> { + let url = self.http_client.build_zed_api_url(&format!( + "/extensions{query}", query = search .map(|search| format!("?filter={search}")) .unwrap_or_default() - ); + )); let http_client = self.http_client.clone(); cx.spawn(move |_, _| async move { let mut response = http_client.get(&url, AsyncBody::empty(), true).await?; @@ -264,10 +348,9 @@ impl ExtensionStore { cx: &mut ModelContext, ) { log::info!("installing extension {extension_id} {version}"); - let url = format!( - "{}/api/extensions/{extension_id}/{version}/download", - ClientSettings::get_global(cx).server_url - ); + let url = self + .http_client + .build_zed_api_url(&format!("/extensions/{extension_id}/{version}/download")); let extensions_dir = self.extensions_dir(); let http_client = self.http_client.clone(); @@ -326,7 +409,11 @@ impl ExtensionStore { /// no longer in the manifest, or whose files have changed on disk. /// Then it loads any themes, languages, or grammars that are newly /// added to the manifest, or whose files have changed on disk. - fn manifest_updated(&mut self, manifest: Manifest, cx: &mut ModelContext) { + fn extensions_updated( + &mut self, + new_index: ExtensionIndex, + cx: &mut ModelContext, + ) -> Task> { fn diff<'a, T, I1, I2>( old_keys: I1, new_keys: I2, @@ -370,50 +457,105 @@ impl ExtensionStore { } } - let old_manifest = self.manifest.read(); - let (languages_to_remove, languages_to_add) = diff( - old_manifest.languages.iter(), - manifest.languages.iter(), - &self.extension_changes.languages, + let old_index = &self.extension_index; + let (extensions_to_unload, extensions_to_load) = diff( + old_index.extensions.iter(), + new_index.extensions.iter(), + &self.modified_extensions, ); - let (grammars_to_remove, grammars_to_add) = diff( - old_manifest.grammars.iter(), - manifest.grammars.iter(), - &self.extension_changes.grammars, - ); - let (themes_to_remove, themes_to_add) = diff( - old_manifest.themes.iter(), - manifest.themes.iter(), - &self.extension_changes.themes, - ); - self.extension_changes.clear(); - drop(old_manifest); + self.modified_extensions.clear(); - let themes_to_remove = &themes_to_remove - .into_iter() - .map(|theme| theme.into()) + let themes_to_remove = old_index + .themes + .iter() + .filter_map(|(name, entry)| { + if extensions_to_unload.contains(&entry.extension) { + Some(name.clone().into()) + } else { + None + } + }) .collect::>(); + let languages_to_remove = old_index + .languages + .iter() + .filter_map(|(name, entry)| { + if extensions_to_unload.contains(&entry.extension) { + Some(name.clone()) + } else { + None + } + }) + .collect::>(); + let empty = Default::default(); + let grammars_to_remove = extensions_to_unload + .iter() + .flat_map(|extension_id| { + old_index + .extensions + .get(extension_id) + .map_or(&empty, |extension| &extension.grammars) + .keys() + .cloned() + }) + .collect::>(); + + self.wasm_extensions + .retain(|(extension, _)| !extensions_to_unload.contains(&extension.id)); + + for extension_id in &extensions_to_unload { + if let Some(extension) = old_index.extensions.get(extension_id) { + for (language_server_name, config) in extension.language_servers.iter() { + self.language_registry + .remove_lsp_adapter(config.language.as_ref(), language_server_name); + } + } + } + self.theme_registry.remove_user_themes(&themes_to_remove); self.language_registry .remove_languages(&languages_to_remove, &grammars_to_remove); - self.language_registry - .register_wasm_grammars(grammars_to_add.iter().map(|grammar_name| { - let grammar = manifest.grammars.get(grammar_name).unwrap(); + let languages_to_add = new_index + .languages + .iter() + .filter(|(_, entry)| extensions_to_load.contains(&entry.extension)) + .collect::>(); + let mut grammars_to_add = Vec::new(); + let mut themes_to_add = Vec::new(); + for extension_id in &extensions_to_load { + let Some(extension) = new_index.extensions.get(extension_id) else { + continue; + }; + + grammars_to_add.extend(extension.grammars.keys().map(|grammar_name| { let mut grammar_path = self.extensions_dir.clone(); - grammar_path.extend([grammar.extension.as_ref(), grammar.path.as_path()]); + grammar_path.extend([extension_id.as_ref(), "grammars"]); + grammar_path.push(grammar_name.as_ref()); + grammar_path.set_extension("wasm"); (grammar_name.clone(), grammar_path) })); + themes_to_add.extend(extension.themes.iter().map(|theme_path| { + let mut path = self.extensions_dir.clone(); + path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]); + path + })); + } - for language_name in &languages_to_add { - let language = manifest.languages.get(language_name.as_ref()).unwrap(); + self.language_registry + .register_wasm_grammars(grammars_to_add); + + for (language_name, language) in languages_to_add { let mut language_path = self.extensions_dir.clone(); - language_path.extend([language.extension.as_ref(), language.path.as_path()]); + language_path.extend([ + Path::new(language.extension.as_ref()), + language.path.as_path(), + ]); self.language_registry.register_language( language_name.clone(), language.grammar.clone(), language.matcher.clone(), - vec![], + None, move || { let config = std::fs::read_to_string(language_path.join("config.toml"))?; let config: LanguageConfig = ::toml::from_str(&config)?; @@ -423,107 +565,119 @@ impl ExtensionStore { ); } - let (reload_theme_tx, mut reload_theme_rx) = unbounded(); let fs = self.fs.clone(); + let wasm_host = self.wasm_host.clone(); let root_dir = self.extensions_dir.clone(); let theme_registry = self.theme_registry.clone(); - let themes = themes_to_add + let extension_manifests = extensions_to_load .iter() - .filter_map(|name| manifest.themes.get(name).cloned()) + .filter_map(|name| new_index.extensions.get(name).cloned()) .collect::>(); - cx.background_executor() - .spawn(async move { - for theme in &themes { - let mut theme_path = root_dir.clone(); - theme_path.extend([theme.extension.as_ref(), theme.path.as_path()]); - theme_registry - .load_user_theme(&theme_path, fs.clone()) - .await - .log_err(); - } - - reload_theme_tx.unbounded_send(()).ok(); - }) - .detach(); - - cx.spawn(|_, cx| async move { - while let Some(_) = reload_theme_rx.next().await { - if cx - .update(|cx| ThemeSettings::reload_current_theme(cx)) - .is_err() - { - break; - } - } - }) - .detach(); - - *self.manifest.write() = manifest; + self.extension_index = new_index; cx.notify(); + + cx.spawn(|this, mut cx| async move { + cx.background_executor() + .spawn({ + let fs = fs.clone(); + async move { + for theme_path in &themes_to_add { + theme_registry + .load_user_theme(&theme_path, fs.clone()) + .await + .log_err(); + } + } + }) + .await; + + let mut wasm_extensions = Vec::new(); + for extension_manifest in extension_manifests { + let Some(wasm_path) = &extension_manifest.lib.path else { + continue; + }; + + let mut path = root_dir.clone(); + path.extend([ + Path::new(extension_manifest.id.as_ref()), + wasm_path.as_path(), + ]); + let mut wasm_file = fs + .open_sync(&path) + .await + .context("failed to open wasm file")?; + let mut wasm_bytes = Vec::new(); + wasm_file + .read_to_end(&mut wasm_bytes) + .context("failed to read wasm")?; + let wasm_extension = wasm_host + .load_extension( + wasm_bytes, + extension_manifest.clone(), + cx.background_executor().clone(), + ) + .await + .context("failed to load wasm extension")?; + wasm_extensions.push((extension_manifest.clone(), wasm_extension)); + } + + this.update(&mut cx, |this, cx| { + for (manifest, wasm_extension) in &wasm_extensions { + for (language_server_name, language_server_config) in &manifest.language_servers + { + this.language_registry.register_lsp_adapter( + language_server_config.language.clone(), + Arc::new(ExtensionLspAdapter { + extension: wasm_extension.clone(), + work_dir: this.wasm_host.work_dir.join(manifest.id.as_ref()), + config: wit::LanguageServerConfig { + name: language_server_name.0.to_string(), + language_name: language_server_config.language.to_string(), + }, + }), + ); + } + } + this.wasm_extensions.extend(wasm_extensions); + ThemeSettings::reload_current_theme(cx) + }) + .ok(); + Ok(()) + }) } fn watch_extensions_dir(&self, cx: &mut ModelContext) -> [Task<()>; 2] { - let manifest = self.manifest.clone(); let fs = self.fs.clone(); let extensions_dir = self.extensions_dir.clone(); - - let (changes_tx, mut changes_rx) = unbounded(); + let (changed_extensions_tx, mut changed_extensions_rx) = unbounded(); let events_task = cx.background_executor().spawn(async move { let mut events = fs.watch(&extensions_dir, Duration::from_millis(250)).await; while let Some(events) = events.next().await { - let mut changed_grammars = HashSet::default(); - let mut changed_languages = HashSet::default(); - let mut changed_themes = HashSet::default(); + for event in events { + let Ok(event_path) = event.path.strip_prefix(&extensions_dir) else { + continue; + }; - { - let manifest = manifest.read(); - for event in events { - for (grammar_name, grammar) in &manifest.grammars { - let mut grammar_path = extensions_dir.clone(); - grammar_path - .extend([grammar.extension.as_ref(), grammar.path.as_path()]); - if event.path.starts_with(&grammar_path) || event.path == grammar_path { - changed_grammars.insert(grammar_name.clone()); - } - } - - for (language_name, language) in &manifest.languages { - let mut language_path = extensions_dir.clone(); - language_path - .extend([language.extension.as_ref(), language.path.as_path()]); - if event.path.starts_with(&language_path) || event.path == language_path - { - changed_languages.insert(language_name.clone()); - } - } - - for (theme_name, theme) in &manifest.themes { - let mut theme_path = extensions_dir.clone(); - theme_path.extend([theme.extension.as_ref(), theme.path.as_path()]); - if event.path.starts_with(&theme_path) || event.path == theme_path { - changed_themes.insert(theme_name.clone()); - } + if let Some(path::Component::Normal(extension_dir_name)) = + event_path.components().next() + { + if let Some(extension_id) = extension_dir_name.to_str() { + changed_extensions_tx + .unbounded_send(Arc::from(extension_id)) + .ok(); } } } - - changes_tx - .unbounded_send(ExtensionChanges { - languages: changed_languages, - grammars: changed_grammars, - themes: changed_themes, - }) - .ok(); } }); let reload_task = cx.spawn(|this, mut cx| async move { - while let Some(changes) = changes_rx.next().await { + while let Some(changed_extension_id) = changed_extensions_rx.next().await { if this .update(&mut cx, |this, cx| { - this.extension_changes.merge(changes); + this.modified_extensions.insert(changed_extension_id); this.reload(cx); }) .is_err() @@ -543,16 +697,18 @@ impl ExtensionStore { } let fs = self.fs.clone(); + let work_dir = self.wasm_host.work_dir.clone(); let extensions_dir = self.extensions_dir.clone(); let manifest_path = self.manifest_path.clone(); self.needs_reload = false; self.reload_task = Some(cx.spawn(|this, mut cx| { async move { - let manifest = cx + let extension_index = cx .background_executor() .spawn(async move { - let mut manifest = Manifest::default(); + let mut index = ExtensionIndex::default(); + fs.create_dir(&work_dir).await.log_err(); fs.create_dir(&extensions_dir).await.log_err(); let extension_paths = fs.read_dir(&extensions_dir).await; @@ -561,20 +717,16 @@ impl ExtensionStore { let Ok(extension_dir) = extension_dir else { continue; }; - Self::add_extension_to_manifest( - fs.clone(), - extension_dir, - &mut manifest, - ) - .await - .log_err(); + Self::add_extension_to_index(fs.clone(), extension_dir, &mut index) + .await + .log_err(); } } - if let Ok(manifest_json) = serde_json::to_string_pretty(&manifest) { + if let Ok(index_json) = serde_json::to_string_pretty(&index) { fs.save( &manifest_path, - &manifest_json.as_str().into(), + &index_json.as_str().into(), Default::default(), ) .await @@ -582,12 +734,17 @@ impl ExtensionStore { .log_err(); } - manifest + index }) .await; + if let Ok(task) = this.update(&mut cx, |this, cx| { + this.extensions_updated(extension_index, cx) + }) { + task.await.log_err(); + } + this.update(&mut cx, |this, cx| { - this.manifest_updated(manifest, cx); this.reload_task.take(); if this.needs_reload { this.reload(cx); @@ -598,52 +755,65 @@ impl ExtensionStore { })); } - async fn add_extension_to_manifest( + async fn add_extension_to_index( fs: Arc, extension_dir: PathBuf, - manifest: &mut Manifest, + index: &mut ExtensionIndex, ) -> Result<()> { let extension_name = extension_dir .file_name() .and_then(OsStr::to_str) .ok_or_else(|| anyhow!("invalid extension name"))?; - #[derive(Deserialize)] - struct ExtensionJson { - pub version: String, - } + let mut extension_manifest_path = extension_dir.join("extension.json"); + let mut extension_manifest; + if fs.is_file(&extension_manifest_path).await { + let manifest_content = fs + .load(&extension_manifest_path) + .await + .with_context(|| format!("failed to load {extension_name} extension.json"))?; + let manifest_json = serde_json::from_str::(&manifest_content) + .with_context(|| { + format!("invalid extension.json for extension {extension_name}") + })?; - let extension_json_path = extension_dir.join("extension.json"); - let extension_json = fs - .load(&extension_json_path) - .await - .context("failed to load extension.json")?; - let extension_json: ExtensionJson = - serde_json::from_str(&extension_json).context("invalid extension.json")?; - - manifest - .extensions - .insert(extension_name.into(), extension_json.version.into()); - - if let Ok(mut grammar_paths) = fs.read_dir(&extension_dir.join("grammars")).await { - while let Some(grammar_path) = grammar_paths.next().await { - let grammar_path = grammar_path?; - let Ok(relative_path) = grammar_path.strip_prefix(&extension_dir) else { - continue; - }; - let Some(grammar_name) = grammar_path.file_stem().and_then(OsStr::to_str) else { - continue; - }; - - manifest.grammars.insert( - grammar_name.into(), - GrammarManifestEntry { - extension: extension_name.into(), - path: relative_path.into(), - }, - ); - } - } + extension_manifest = ExtensionManifest { + id: extension_name.into(), + name: manifest_json.name, + version: manifest_json.version, + description: manifest_json.description, + repository: manifest_json.repository, + authors: manifest_json.authors, + lib: Default::default(), + themes: { + let mut themes = manifest_json.themes.into_values().collect::>(); + themes.sort(); + themes.dedup(); + themes + }, + languages: { + let mut languages = manifest_json.languages.into_values().collect::>(); + languages.sort(); + languages.dedup(); + languages + }, + grammars: manifest_json + .grammars + .into_keys() + .map(|grammar_name| (grammar_name, Default::default())) + .collect(), + language_servers: Default::default(), + }; + } else { + extension_manifest_path.set_extension("toml"); + let manifest_content = fs + .load(&extension_manifest_path) + .await + .with_context(|| format!("failed to load {extension_name} extension.toml"))?; + extension_manifest = ::toml::from_str(&manifest_content).with_context(|| { + format!("invalid extension.json for extension {extension_name}") + })?; + }; if let Ok(mut language_paths) = fs.read_dir(&extension_dir.join("languages")).await { while let Some(language_path) = language_paths.next().await { @@ -651,14 +821,25 @@ impl ExtensionStore { let Ok(relative_path) = language_path.strip_prefix(&extension_dir) else { continue; }; + let Ok(Some(fs_metadata)) = fs.metadata(&language_path).await else { + continue; + }; + if !fs_metadata.is_dir { + continue; + } let config = fs.load(&language_path.join("config.toml")).await?; let config = ::toml::from_str::(&config)?; - manifest.languages.insert( + let relative_path = relative_path.to_path_buf(); + if !extension_manifest.languages.contains(&relative_path) { + extension_manifest.languages.push(relative_path.clone()); + } + + index.languages.insert( config.name.clone(), - LanguageManifestEntry { + ExtensionIndexLanguageEntry { extension: extension_name.into(), - path: relative_path.into(), + path: relative_path, matcher: config.matcher, grammar: config.grammar, }, @@ -680,35 +861,39 @@ impl ExtensionStore { continue; }; - for theme in theme_family.themes { - let location = ThemeManifestEntry { - extension: extension_name.into(), - path: relative_path.into(), - }; + let relative_path = relative_path.to_path_buf(); + if !extension_manifest.themes.contains(&relative_path) { + extension_manifest.themes.push(relative_path.clone()); + } - manifest.themes.insert(theme.name.into(), location); + for theme in theme_family.themes { + index.themes.insert( + theme.name.into(), + ExtensionIndexEntry { + extension: extension_name.into(), + path: relative_path.clone(), + }, + ); } } } + let default_extension_wasm_path = extension_dir.join("extension.wasm"); + if fs.is_file(&default_extension_wasm_path).await { + extension_manifest + .lib + .path + .get_or_insert(default_extension_wasm_path); + } + + index + .extensions + .insert(extension_name.into(), Arc::new(extension_manifest)); + Ok(()) } } -impl ExtensionChanges { - fn clear(&mut self) { - self.grammars.clear(); - self.languages.clear(); - self.themes.clear(); - } - - fn merge(&mut self, other: Self) { - self.grammars.extend(other.grammars); - self.languages.extend(other.languages); - self.themes.extend(other.themes); - } -} - fn load_plugin_queries(root_path: &Path) -> LanguageQueries { let mut result = LanguageQueries::default(); if let Some(entries) = std::fs::read_dir(root_path).log_err() { diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index a9ff4fe443..4f72bf2f87 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/crates/extension/src/extension_store_test.rs @@ -1,14 +1,27 @@ use crate::{ - ExtensionStore, GrammarManifestEntry, LanguageManifestEntry, Manifest, ThemeManifestEntry, + ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionManifest, + ExtensionStore, GrammarManifestEntry, }; -use fs::FakeFs; +use async_compression::futures::bufread::GzipEncoder; +use collections::BTreeMap; +use fs::{FakeFs, Fs}; +use futures::{io::BufReader, AsyncReadExt, StreamExt}; use gpui::{Context, TestAppContext}; -use language::{LanguageMatcher, LanguageRegistry}; +use language::{ + Language, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, + LanguageServerName, +}; +use node_runtime::FakeNodeRuntime; +use project::Project; use serde_json::json; use settings::SettingsStore; -use std::{path::PathBuf, sync::Arc}; +use std::{ + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; use theme::ThemeRegistry; -use util::http::FakeHttpClient; +use util::http::{FakeHttpClient, Response}; #[gpui::test] async fn test_extension_store(cx: &mut TestAppContext) { @@ -29,7 +42,13 @@ async fn test_extension_store(cx: &mut TestAppContext) { "extension.json": r#"{ "id": "zed-monokai", "name": "Zed Monokai", - "version": "2.0.0" + "version": "2.0.0", + "themes": { + "Monokai Dark": "themes/monokai.json", + "Monokai Light": "themes/monokai.json", + "Monokai Pro Dark": "themes/monokai-pro.json", + "Monokai Pro Light": "themes/monokai-pro.json" + } }"#, "themes": { "monokai.json": r#"{ @@ -70,7 +89,15 @@ async fn test_extension_store(cx: &mut TestAppContext) { "extension.json": r#"{ "id": "zed-ruby", "name": "Zed Ruby", - "version": "1.0.0" + "version": "1.0.0", + "grammars": { + "ruby": "grammars/ruby.wasm", + "embedded_template": "grammars/embedded_template.wasm" + }, + "languages": { + "ruby": "languages/ruby", + "erb": "languages/erb" + } }"#, "grammars": { "ruby.wasm": "", @@ -100,27 +127,49 @@ async fn test_extension_store(cx: &mut TestAppContext) { ) .await; - let mut expected_manifest = Manifest { + let mut expected_index = ExtensionIndex { extensions: [ - ("zed-ruby".into(), "1.0.0".into()), - ("zed-monokai".into(), "2.0.0".into()), - ] - .into_iter() - .collect(), - grammars: [ ( - "embedded_template".into(), - GrammarManifestEntry { - extension: "zed-ruby".into(), - path: "grammars/embedded_template.wasm".into(), - }, + "zed-ruby".into(), + ExtensionManifest { + id: "zed-ruby".into(), + name: "Zed Ruby".into(), + version: "1.0.0".into(), + description: None, + authors: Vec::new(), + repository: None, + themes: Default::default(), + lib: Default::default(), + languages: vec!["languages/erb".into(), "languages/ruby".into()], + grammars: [ + ("embedded_template".into(), GrammarManifestEntry::default()), + ("ruby".into(), GrammarManifestEntry::default()), + ] + .into_iter() + .collect(), + language_servers: BTreeMap::default(), + } + .into(), ), ( - "ruby".into(), - GrammarManifestEntry { - extension: "zed-ruby".into(), - path: "grammars/ruby.wasm".into(), - }, + "zed-monokai".into(), + ExtensionManifest { + id: "zed-monokai".into(), + name: "Zed Monokai".into(), + version: "2.0.0".into(), + description: None, + authors: vec![], + repository: None, + themes: vec![ + "themes/monokai-pro.json".into(), + "themes/monokai.json".into(), + ], + lib: Default::default(), + languages: Default::default(), + grammars: BTreeMap::default(), + language_servers: BTreeMap::default(), + } + .into(), ), ] .into_iter() @@ -128,7 +177,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { languages: [ ( "ERB".into(), - LanguageManifestEntry { + ExtensionIndexLanguageEntry { extension: "zed-ruby".into(), path: "languages/erb".into(), grammar: Some("embedded_template".into()), @@ -140,7 +189,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { ), ( "Ruby".into(), - LanguageManifestEntry { + ExtensionIndexLanguageEntry { extension: "zed-ruby".into(), path: "languages/ruby".into(), grammar: Some("ruby".into()), @@ -156,28 +205,28 @@ async fn test_extension_store(cx: &mut TestAppContext) { themes: [ ( "Monokai Dark".into(), - ThemeManifestEntry { + ExtensionIndexEntry { extension: "zed-monokai".into(), path: "themes/monokai.json".into(), }, ), ( "Monokai Light".into(), - ThemeManifestEntry { + ExtensionIndexEntry { extension: "zed-monokai".into(), path: "themes/monokai.json".into(), }, ), ( "Monokai Pro Dark".into(), - ThemeManifestEntry { + ExtensionIndexEntry { extension: "zed-monokai".into(), path: "themes/monokai-pro.json".into(), }, ), ( "Monokai Pro Light".into(), - ThemeManifestEntry { + ExtensionIndexEntry { extension: "zed-monokai".into(), path: "themes/monokai-pro.json".into(), }, @@ -189,12 +238,14 @@ async fn test_extension_store(cx: &mut TestAppContext) { let language_registry = Arc::new(LanguageRegistry::test()); let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); + let node_runtime = FakeNodeRuntime::new(); let store = cx.new_model(|cx| { ExtensionStore::new( PathBuf::from("/the-extension-dir"), fs.clone(), http_client.clone(), + node_runtime.clone(), language_registry.clone(), theme_registry.clone(), cx, @@ -203,10 +254,10 @@ async fn test_extension_store(cx: &mut TestAppContext) { cx.executor().run_until_parked(); store.read_with(cx, |store, _| { - let manifest = store.manifest.read(); - assert_eq!(manifest.grammars, expected_manifest.grammars); - assert_eq!(manifest.languages, expected_manifest.languages); - assert_eq!(manifest.themes, expected_manifest.themes); + let index = &store.extension_index; + assert_eq!(index.extensions, expected_index.extensions); + assert_eq!(index.languages, expected_index.languages); + assert_eq!(index.themes, expected_index.themes); assert_eq!( language_registry.language_names(), @@ -230,7 +281,10 @@ async fn test_extension_store(cx: &mut TestAppContext) { "extension.json": r#"{ "id": "zed-gruvbox", "name": "Zed Gruvbox", - "version": "1.0.0" + "version": "1.0.0", + "themes": { + "Gruvbox": "themes/gruvbox.json" + } }"#, "themes": { "gruvbox.json": r#"{ @@ -249,9 +303,26 @@ async fn test_extension_store(cx: &mut TestAppContext) { ) .await; - expected_manifest.themes.insert( + expected_index.extensions.insert( + "zed-gruvbox".into(), + ExtensionManifest { + id: "zed-gruvbox".into(), + name: "Zed Gruvbox".into(), + version: "1.0.0".into(), + description: None, + authors: vec![], + repository: None, + themes: vec!["themes/gruvbox.json".into()], + lib: Default::default(), + languages: Default::default(), + grammars: BTreeMap::default(), + language_servers: BTreeMap::default(), + } + .into(), + ); + expected_index.themes.insert( "Gruvbox".into(), - ThemeManifestEntry { + ExtensionIndexEntry { extension: "zed-gruvbox".into(), path: "themes/gruvbox.json".into(), }, @@ -261,10 +332,10 @@ async fn test_extension_store(cx: &mut TestAppContext) { cx.executor().run_until_parked(); store.read_with(cx, |store, _| { - let manifest = store.manifest.read(); - assert_eq!(manifest.grammars, expected_manifest.grammars); - assert_eq!(manifest.languages, expected_manifest.languages); - assert_eq!(manifest.themes, expected_manifest.themes); + let index = &store.extension_index; + assert_eq!(index.extensions, expected_index.extensions); + assert_eq!(index.languages, expected_index.languages); + assert_eq!(index.themes, expected_index.themes); assert_eq!( theme_registry.list_names(false), @@ -289,6 +360,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { PathBuf::from("/the-extension-dir"), fs.clone(), http_client.clone(), + node_runtime.clone(), language_registry.clone(), theme_registry.clone(), cx, @@ -297,11 +369,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { cx.executor().run_until_parked(); store.read_with(cx, |store, _| { - let manifest = store.manifest.read(); - assert_eq!(manifest.grammars, expected_manifest.grammars); - assert_eq!(manifest.languages, expected_manifest.languages); - assert_eq!(manifest.themes, expected_manifest.themes); - + assert_eq!(store.extension_index, expected_index); assert_eq!( language_registry.language_names(), ["ERB", "Plain Text", "Ruby"] @@ -333,19 +401,204 @@ async fn test_extension_store(cx: &mut TestAppContext) { }); cx.executor().run_until_parked(); - expected_manifest.extensions.remove("zed-ruby"); - expected_manifest.languages.remove("Ruby"); - expected_manifest.languages.remove("ERB"); - expected_manifest.grammars.remove("ruby"); - expected_manifest.grammars.remove("embedded_template"); + expected_index.extensions.remove("zed-ruby"); + expected_index.languages.remove("Ruby"); + expected_index.languages.remove("ERB"); store.read_with(cx, |store, _| { - let manifest = store.manifest.read(); - assert_eq!(manifest.grammars, expected_manifest.grammars); - assert_eq!(manifest.languages, expected_manifest.languages); - assert_eq!(manifest.themes, expected_manifest.themes); - + assert_eq!(store.extension_index, expected_index); assert_eq!(language_registry.language_names(), ["Plain Text"]); assert_eq!(language_registry.grammar_names(), []); }); } + +#[gpui::test] +async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { + init_test(cx); + + let gleam_extension_dir = PathBuf::from_iter([ + env!("CARGO_MANIFEST_DIR"), + "..", + "..", + "extensions", + "gleam", + ]) + .canonicalize() + .unwrap(); + + compile_extension("zed_gleam", &gleam_extension_dir); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/the-extension-dir", json!({ "installed": {} })) + .await; + fs.insert_tree_from_real_fs("/the-extension-dir/installed/gleam", gleam_extension_dir) + .await; + + fs.insert_tree( + "/the-project-dir", + json!({ + ".tool-versions": "rust 1.73.0", + "test.gleam": "" + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/the-project-dir".as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); + let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); + let node_runtime = FakeNodeRuntime::new(); + + let mut status_updates = language_registry.language_server_binary_statuses(); + + let http_client = FakeHttpClient::create({ + move |request| async move { + match request.uri().to_string().as_str() { + "https://api.github.com/repos/gleam-lang/gleam/releases" => Ok(Response::new( + json!([ + { + "tag_name": "v1.2.3", + "prerelease": false, + "tarball_url": "", + "zipball_url": "", + "assets": [ + { + "name": "gleam-v1.2.3-aarch64-apple-darwin.tar.gz", + "browser_download_url": "http://example.com/the-download" + } + ] + } + ]) + .to_string() + .into(), + )), + + "http://example.com/the-download" => { + let mut bytes = Vec::::new(); + let mut archive = async_tar::Builder::new(&mut bytes); + let mut header = async_tar::Header::new_gnu(); + let content = "the-gleam-binary-contents".as_bytes(); + header.set_size(content.len() as u64); + archive + .append_data(&mut header, "gleam", content) + .await + .unwrap(); + archive.into_inner().await.unwrap(); + + let mut gzipped_bytes = Vec::new(); + let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice())); + encoder.read_to_end(&mut gzipped_bytes).await.unwrap(); + + Ok(Response::new(gzipped_bytes.into())) + } + + _ => Ok(Response::builder().status(404).body("not found".into())?), + } + } + }); + + let _store = cx.new_model(|cx| { + ExtensionStore::new( + PathBuf::from("/the-extension-dir"), + fs.clone(), + http_client.clone(), + node_runtime, + language_registry.clone(), + theme_registry.clone(), + cx, + ) + }); + + cx.executor().run_until_parked(); + + let mut fake_servers = language_registry.fake_language_servers("Gleam"); + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/the-project-dir/test.gleam", cx) + }) + .await + .unwrap(); + + project.update(cx, |project, cx| { + project.set_language_for_buffer( + &buffer, + Arc::new(Language::new( + LanguageConfig { + name: "Gleam".into(), + ..Default::default() + }, + None, + )), + cx, + ) + }); + + let fake_server = fake_servers.next().await.unwrap(); + + assert_eq!( + fs.load("/the-extension-dir/work/gleam/gleam-v1.2.3/gleam".as_ref()) + .await + .unwrap(), + "the-gleam-binary-contents" + ); + + assert_eq!( + fake_server.binary.path, + PathBuf::from("/the-extension-dir/work/gleam/gleam-v1.2.3/gleam") + ); + assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]); + + assert_eq!( + [ + status_updates.next().await.unwrap(), + status_updates.next().await.unwrap(), + status_updates.next().await.unwrap(), + ], + [ + ( + LanguageServerName("gleam".into()), + LanguageServerBinaryStatus::CheckingForUpdate + ), + ( + LanguageServerName("gleam".into()), + LanguageServerBinaryStatus::Downloading + ), + ( + LanguageServerName("gleam".into()), + LanguageServerBinaryStatus::Downloaded + ) + ] + ); +} + +fn compile_extension(name: &str, extension_dir_path: &Path) { + let output = std::process::Command::new("cargo") + .args(["component", "build", "--target-dir"]) + .arg(extension_dir_path.join("target")) + .current_dir(&extension_dir_path) + .output() + .unwrap(); + + assert!( + output.status.success(), + "failed to build component {}", + String::from_utf8_lossy(&output.stderr) + ); + + let mut wasm_path = PathBuf::from(extension_dir_path); + wasm_path.extend(["target", "wasm32-wasi", "debug", name]); + wasm_path.set_extension("wasm"); + + std::fs::rename(wasm_path, extension_dir_path.join("extension.wasm")).unwrap(); +} + +fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let store = SettingsStore::test(cx); + cx.set_global(store); + theme::init(theme::LoadThemes::JustBase, cx); + Project::init_settings(cx); + language::init(cx); + }); +} diff --git a/crates/extension/src/wasm_host.rs b/crates/extension/src/wasm_host.rs new file mode 100644 index 0000000000..611cd9c9b0 --- /dev/null +++ b/crates/extension/src/wasm_host.rs @@ -0,0 +1,405 @@ +use crate::ExtensionManifest; +use anyhow::{anyhow, bail, Context as _, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use fs::Fs; +use futures::{ + channel::{mpsc::UnboundedSender, oneshot}, + future::BoxFuture, + io::BufReader, + Future, FutureExt, StreamExt as _, +}; +use gpui::BackgroundExecutor; +use language::{LanguageRegistry, LanguageServerBinaryStatus, LspAdapterDelegate}; +use node_runtime::NodeRuntime; +use std::{ + path::PathBuf, + sync::{Arc, OnceLock}, +}; +use util::{http::HttpClient, SemanticVersion}; +use wasmtime::{ + component::{Component, Linker, Resource, ResourceTable}, + Engine, Store, +}; +use wasmtime_wasi::preview2::{command as wasi_command, WasiCtx, WasiCtxBuilder, WasiView}; + +pub mod wit { + wasmtime::component::bindgen!({ + async: true, + path: "../extension_api/wit", + with: { + "worktree": super::ExtensionWorktree, + }, + }); +} + +pub type ExtensionWorktree = Arc; + +pub(crate) struct WasmHost { + engine: Engine, + linker: Arc>, + http_client: Arc, + node_runtime: Arc, + language_registry: Arc, + fs: Arc, + pub(crate) work_dir: PathBuf, +} + +#[derive(Clone)] +pub struct WasmExtension { + tx: UnboundedSender, + #[allow(unused)] + zed_api_version: SemanticVersion, +} + +pub(crate) struct WasmState { + manifest: Arc, + table: ResourceTable, + ctx: WasiCtx, + host: Arc, +} + +type ExtensionCall = Box< + dyn Send + + for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store) -> BoxFuture<'a, ()>, +>; + +static WASM_ENGINE: OnceLock = OnceLock::new(); + +impl WasmHost { + pub fn new( + fs: Arc, + http_client: Arc, + node_runtime: Arc, + language_registry: Arc, + work_dir: PathBuf, + ) -> Arc { + let engine = WASM_ENGINE + .get_or_init(|| { + let mut config = wasmtime::Config::new(); + config.wasm_component_model(true); + config.async_support(true); + wasmtime::Engine::new(&config).unwrap() + }) + .clone(); + let mut linker = Linker::new(&engine); + wasi_command::add_to_linker(&mut linker).unwrap(); + wit::Extension::add_to_linker(&mut linker, |state: &mut WasmState| state).unwrap(); + Arc::new(Self { + engine, + linker: Arc::new(linker), + fs, + work_dir, + http_client, + node_runtime, + language_registry, + }) + } + + pub fn load_extension( + self: &Arc, + wasm_bytes: Vec, + manifest: Arc, + executor: BackgroundExecutor, + ) -> impl 'static + Future> { + let this = self.clone(); + async move { + let component = Component::from_binary(&this.engine, &wasm_bytes) + .context("failed to compile wasm component")?; + + let mut zed_api_version = None; + for part in wasmparser::Parser::new(0).parse_all(&wasm_bytes) { + if let wasmparser::Payload::CustomSection(s) = part? { + if s.name() == "zed:api-version" { + if s.data().len() != 6 { + bail!( + "extension {} has invalid zed:api-version section: {:?}", + manifest.id, + s.data() + ); + } + + let major = u16::from_be_bytes(s.data()[0..2].try_into().unwrap()) as _; + let minor = u16::from_be_bytes(s.data()[2..4].try_into().unwrap()) as _; + let patch = u16::from_be_bytes(s.data()[4..6].try_into().unwrap()) as _; + zed_api_version = Some(SemanticVersion { + major, + minor, + patch, + }) + } + } + } + + let Some(zed_api_version) = zed_api_version else { + bail!("extension {} has no zed:api-version section", manifest.id); + }; + + let mut store = wasmtime::Store::new( + &this.engine, + WasmState { + manifest, + table: ResourceTable::new(), + ctx: WasiCtxBuilder::new() + .inherit_stdio() + .env("RUST_BACKTRACE", "1") + .build(), + host: this.clone(), + }, + ); + let (mut extension, instance) = + wit::Extension::instantiate_async(&mut store, &component, &this.linker) + .await + .context("failed to instantiate wasm component")?; + let (tx, mut rx) = futures::channel::mpsc::unbounded::(); + executor + .spawn(async move { + extension.call_init_extension(&mut store).await.unwrap(); + + let _instance = instance; + while let Some(call) = rx.next().await { + (call)(&mut extension, &mut store).await; + } + }) + .detach(); + Ok(WasmExtension { + tx, + zed_api_version, + }) + } + } +} + +impl WasmExtension { + pub async fn call(&self, f: Fn) -> T + where + T: 'static + Send, + Fn: 'static + + Send + + for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store) -> BoxFuture<'a, T>, + { + let (return_tx, return_rx) = oneshot::channel(); + self.tx + .clone() + .unbounded_send(Box::new(move |extension, store| { + async { + let result = f(extension, store).await; + return_tx.send(result).ok(); + } + .boxed() + })) + .expect("wasm extension channel should not be closed yet"); + return_rx.await.expect("wasm extension channel") + } +} + +#[async_trait] +impl wit::HostWorktree for WasmState { + async fn read_text_file( + &mut self, + delegate: Resource>, + path: String, + ) -> wasmtime::Result> { + let delegate = self.table().get(&delegate)?; + Ok(delegate + .read_text_file(path.into()) + .await + .map_err(|error| error.to_string())) + } + + fn drop(&mut self, _worktree: Resource) -> Result<()> { + // we only ever hand out borrows of worktrees + Ok(()) + } +} + +#[async_trait] +impl wit::ExtensionImports for WasmState { + async fn npm_package_latest_version( + &mut self, + package_name: String, + ) -> wasmtime::Result> { + async fn inner(this: &mut WasmState, package_name: String) -> anyhow::Result { + this.host + .node_runtime + .npm_package_latest_version(&package_name) + .await + } + + Ok(inner(self, package_name) + .await + .map_err(|err| err.to_string())) + } + + async fn latest_github_release( + &mut self, + repo: String, + options: wit::GithubReleaseOptions, + ) -> wasmtime::Result> { + async fn inner( + this: &mut WasmState, + repo: String, + options: wit::GithubReleaseOptions, + ) -> anyhow::Result { + let release = util::github::latest_github_release( + &repo, + options.require_assets, + options.pre_release, + this.host.http_client.clone(), + ) + .await?; + Ok(wit::GithubRelease { + version: release.tag_name, + assets: release + .assets + .into_iter() + .map(|asset| wit::GithubReleaseAsset { + name: asset.name, + download_url: asset.browser_download_url, + }) + .collect(), + }) + } + + Ok(inner(self, repo, options) + .await + .map_err(|err| err.to_string())) + } + + async fn current_platform(&mut self) -> Result<(wit::Os, wit::Architecture)> { + Ok(( + match std::env::consts::OS { + "macos" => wit::Os::Mac, + "linux" => wit::Os::Linux, + "windows" => wit::Os::Windows, + _ => panic!("unsupported os"), + }, + match std::env::consts::ARCH { + "aarch64" => wit::Architecture::Aarch64, + "x86" => wit::Architecture::X86, + "x86_64" => wit::Architecture::X8664, + _ => panic!("unsupported architecture"), + }, + )) + } + + async fn set_language_server_installation_status( + &mut self, + server_name: String, + status: wit::LanguageServerInstallationStatus, + ) -> wasmtime::Result<()> { + let status = match status { + wit::LanguageServerInstallationStatus::CheckingForUpdate => { + LanguageServerBinaryStatus::CheckingForUpdate + } + wit::LanguageServerInstallationStatus::Downloading => { + LanguageServerBinaryStatus::Downloading + } + wit::LanguageServerInstallationStatus::Downloaded => { + LanguageServerBinaryStatus::Downloaded + } + wit::LanguageServerInstallationStatus::Cached => LanguageServerBinaryStatus::Cached, + wit::LanguageServerInstallationStatus::Failed(error) => { + LanguageServerBinaryStatus::Failed { error } + } + }; + + self.host + .language_registry + .update_lsp_status(language::LanguageServerName(server_name.into()), status); + Ok(()) + } + + async fn download_file( + &mut self, + url: String, + filename: String, + file_type: wit::DownloadedFileType, + ) -> wasmtime::Result> { + async fn inner( + this: &mut WasmState, + url: String, + filename: String, + file_type: wit::DownloadedFileType, + ) -> anyhow::Result<()> { + this.host.fs.create_dir(&this.host.work_dir).await?; + let container_dir = this.host.work_dir.join(this.manifest.id.as_ref()); + let destination_path = container_dir.join(&filename); + + let mut response = this + .host + .http_client + .get(&url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + let body = BufReader::new(response.body_mut()); + + match file_type { + wit::DownloadedFileType::Uncompressed => { + futures::pin_mut!(body); + this.host + .fs + .create_file_with(&destination_path, body) + .await?; + } + wit::DownloadedFileType::Gzip => { + let body = GzipDecoder::new(body); + futures::pin_mut!(body); + this.host + .fs + .create_file_with(&destination_path, body) + .await?; + } + wit::DownloadedFileType::GzipTar => { + let body = GzipDecoder::new(body); + futures::pin_mut!(body); + this.host + .fs + .extract_tar_file(&destination_path, Archive::new(body)) + .await?; + } + wit::DownloadedFileType::Zip => { + let zip_filename = format!("{filename}.zip"); + let mut zip_path = destination_path.clone(); + zip_path.set_file_name(zip_filename); + futures::pin_mut!(body); + this.host.fs.create_file_with(&zip_path, body).await?; + + let unzip_status = std::process::Command::new("unzip") + .current_dir(&container_dir) + .arg(&zip_path) + .output()? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip {filename} archive"))?; + } + } + } + + Ok(()) + } + + Ok(inner(self, url, filename, file_type) + .await + .map(|_| ()) + .map_err(|err| err.to_string())) + } +} + +impl WasiView for WasmState { + fn table(&mut self) -> &mut ResourceTable { + &mut self.table + } + + fn ctx(&mut self) -> &mut WasiCtx { + &mut self.ctx + } +} diff --git a/crates/extension_api/Cargo.toml b/crates/extension_api/Cargo.toml new file mode 100644 index 0000000000..1adbd0c0ee --- /dev/null +++ b/crates/extension_api/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "zed_extension_api" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +path = "src/extension_api.rs" + +[dependencies] +wit-bindgen = "0.18" + +[package.metadata.component] +target = { path = "wit" } diff --git a/crates/extension_api/LICENSE-APACHE b/crates/extension_api/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/crates/extension_api/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/extension_api/build.rs b/crates/extension_api/build.rs new file mode 100644 index 0000000000..4637257dee --- /dev/null +++ b/crates/extension_api/build.rs @@ -0,0 +1,15 @@ +fn main() { + let version = std::env::var("CARGO_PKG_VERSION").unwrap(); + let out_dir = std::env::var("OUT_DIR").unwrap(); + + let mut parts = version.split(|c: char| !c.is_digit(10)); + let major = parts.next().unwrap().parse::().unwrap().to_be_bytes(); + let minor = parts.next().unwrap().parse::().unwrap().to_be_bytes(); + let patch = parts.next().unwrap().parse::().unwrap().to_be_bytes(); + + std::fs::write( + std::path::Path::new(&out_dir).join("version_bytes"), + [major[0], major[1], minor[0], minor[1], patch[0], patch[1]], + ) + .unwrap(); +} diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs new file mode 100644 index 0000000000..20332a85e3 --- /dev/null +++ b/crates/extension_api/src/extension_api.rs @@ -0,0 +1,62 @@ +pub struct Guest; +pub use wit::*; + +pub type Result = core::result::Result; + +pub trait Extension: Send + Sync { + fn new() -> Self + where + Self: Sized; + + fn language_server_command( + &mut self, + config: wit::LanguageServerConfig, + worktree: &wit::Worktree, + ) -> Result; +} + +#[macro_export] +macro_rules! register_extension { + ($extension_type:ty) => { + #[export_name = "init-extension"] + pub extern "C" fn __init_extension() { + zed_extension_api::register_extension(|| { + Box::new(<$extension_type as zed_extension_api::Extension>::new()) + }); + } + }; +} + +#[doc(hidden)] +pub fn register_extension(build_extension: fn() -> Box) { + unsafe { EXTENSION = Some((build_extension)()) } +} + +fn extension() -> &'static mut dyn Extension { + unsafe { EXTENSION.as_deref_mut().unwrap() } +} + +static mut EXTENSION: Option> = None; + +#[cfg(target_arch = "wasm32")] +#[link_section = "zed:api-version"] +#[doc(hidden)] +pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes")); + +mod wit { + wit_bindgen::generate!({ + exports: { world: super::Component }, + skip: ["init-extension"] + }); +} + +struct Component; + +impl wit::Guest for Component { + fn language_server_command( + config: wit::LanguageServerConfig, + worktree: &wit::Worktree, + ) -> Result { + extension().language_server_command(config, worktree) + } +} diff --git a/crates/extension_api/wit/extension.wit b/crates/extension_api/wit/extension.wit new file mode 100644 index 0000000000..1b00874698 --- /dev/null +++ b/crates/extension_api/wit/extension.wit @@ -0,0 +1,80 @@ +package zed:extension; + +world extension { + export init-extension: func(); + + record github-release { + version: string, + assets: list, + } + + record github-release-asset { + name: string, + download-url: string, + } + + record github-release-options { + require-assets: bool, + pre-release: bool, + } + + enum os { + mac, + linux, + windows, + } + + enum architecture { + aarch64, + x86, + x8664, + } + + enum downloaded-file-type { + gzip, + gzip-tar, + zip, + uncompressed, + } + + variant language-server-installation-status { + checking-for-update, + downloaded, + downloading, + cached, + failed(string), + } + + /// Gets the current operating system and architecture + import current-platform: func() -> tuple; + + /// Gets the latest version of the given NPM package. + import npm-package-latest-version: func(package-name: string) -> result; + + /// Gets the latest release for the given GitHub repository. + import latest-github-release: func(repo: string, options: github-release-options) -> result; + + /// Downloads a file from the given url, and saves it to the given filename within the extension's + /// working directory. Extracts the file according to the given file type. + import download-file: func(url: string, output-filename: string, file-type: downloaded-file-type) -> result<_, string>; + + /// Updates the installation status for the given language server. + import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status); + + record command { + command: string, + args: list, + env: list>, + } + + resource worktree { + read-text-file: func(path: string) -> result; + } + + record language-server-config { + name: string, + language-name: string, + } + + export language-server-command: func(config: language-server-config, worktree: borrow) -> result; +} diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index 04af02a4c9..df55a5091a 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -12,26 +12,13 @@ path = "src/extensions_ui.rs" test-support = [] [dependencies] -anyhow.workspace = true -async-compression.workspace = true -async-tar.workspace = true client.workspace = true -db.workspace = true editor.workspace = true extension.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true gpui.workspace = true -log.workspace = true -picker.workspace = true -project.workspace = true -serde.workspace = true -serde_json.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true -util.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index c062fbb01f..cd07f8244e 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -1,6 +1,6 @@ use client::telemetry::Telemetry; use editor::{Editor, EditorElement, EditorStyle}; -use extension::{Extension, ExtensionStatus, ExtensionStore}; +use extension::{ExtensionApiResponse, ExtensionStatus, ExtensionStore}; use gpui::{ actions, canvas, uniform_list, AnyElement, AppContext, AvailableSpace, EventEmitter, FocusableView, FontStyle, FontWeight, InteractiveElement, KeyContext, ParentElement, Render, @@ -11,7 +11,7 @@ use settings::Settings; use std::time::Duration; use std::{ops::Range, sync::Arc}; use theme::ThemeSettings; -use ui::prelude::*; +use ui::{prelude::*, ToggleButton, Tooltip}; use workspace::{ item::{Item, ItemEvent}, @@ -24,17 +24,25 @@ pub fn init(cx: &mut AppContext) { cx.observe_new_views(move |workspace: &mut Workspace, _cx| { workspace.register_action(move |workspace, _: &Extensions, cx| { let extensions_page = ExtensionsPage::new(workspace, cx); - workspace.add_item(Box::new(extensions_page), cx) + workspace.add_item_to_active_pane(Box::new(extensions_page), cx) }); }) .detach(); } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +enum ExtensionFilter { + All, + Installed, + NotInstalled, +} + pub struct ExtensionsPage { list: UniformListScrollHandle, telemetry: Arc, is_fetching_extensions: bool, - extensions_entries: Vec, + filter: ExtensionFilter, + extension_entries: Vec, query_editor: View, query_contains_error: bool, _subscription: gpui::Subscription, @@ -47,14 +55,19 @@ impl ExtensionsPage { let store = ExtensionStore::global(cx); let subscription = cx.observe(&store, |_, _, cx| cx.notify()); - let query_editor = cx.new_view(|cx| Editor::single_line(cx)); + let query_editor = cx.new_view(|cx| { + let mut input = Editor::single_line(cx); + input.set_placeholder_text("Search extensions...", cx); + input + }); cx.subscribe(&query_editor, Self::on_query_change).detach(); let mut this = Self { list: UniformListScrollHandle::new(), telemetry: workspace.client().telemetry().clone(), is_fetching_extensions: false, - extensions_entries: Vec::new(), + filter: ExtensionFilter::All, + extension_entries: Vec::new(), query_contains_error: false, extension_fetch_task: None, _subscription: subscription, @@ -65,6 +78,28 @@ impl ExtensionsPage { }) } + fn filtered_extension_entries(&self, cx: &mut ViewContext) -> Vec { + let extension_store = ExtensionStore::global(cx).read(cx); + + self.extension_entries + .iter() + .filter(|extension| match self.filter { + ExtensionFilter::All => true, + ExtensionFilter::Installed => { + let status = extension_store.extension_status(&extension.id); + + matches!(status, ExtensionStatus::Installed(_)) + } + ExtensionFilter::NotInstalled => { + let status = extension_store.extension_status(&extension.id); + + matches!(status, ExtensionStatus::NotInstalled) + } + }) + .cloned() + .collect::>() + } + fn install_extension( &self, extension_id: Arc, @@ -94,7 +129,7 @@ impl ExtensionsPage { let fetch_result = extensions.await; match fetch_result { Ok(extensions) => this.update(&mut cx, |this, cx| { - this.extensions_entries = extensions; + this.extension_entries = extensions; this.is_fetching_extensions = false; cx.notify(); }), @@ -113,13 +148,13 @@ impl ExtensionsPage { } fn render_extensions(&mut self, range: Range, cx: &mut ViewContext) -> Vec
{ - self.extensions_entries[range] + self.filtered_extension_entries(cx)[range] .iter() .map(|extension| self.render_entry(extension, cx)) .collect() } - fn render_entry(&self, extension: &Extension, cx: &mut ViewContext) -> Div { + fn render_entry(&self, extension: &ExtensionApiResponse, cx: &mut ViewContext) -> Div { let status = ExtensionStore::global(cx) .read(cx) .extension_status(&extension.id); @@ -161,40 +196,53 @@ impl ExtensionsPage { }; let install_or_uninstall_button = match status { - ExtensionStatus::NotInstalled | ExtensionStatus::Installing => { - Button::new(SharedString::from(extension.id.clone()), "Install") - .on_click(cx.listener({ - let extension_id = extension.id.clone(); - let version = extension.version.clone(); - move |this, _, cx| { - this.telemetry - .report_app_event("extensions: install extension".to_string()); - this.install_extension(extension_id.clone(), version.clone(), cx); - } - })) - .disabled(matches!(status, ExtensionStatus::Installing)) - } + ExtensionStatus::NotInstalled | ExtensionStatus::Installing => Button::new( + SharedString::from(extension.id.clone()), + if status.is_installing() { + "Installing..." + } else { + "Install" + }, + ) + .on_click(cx.listener({ + let extension_id = extension.id.clone(); + let version = extension.version.clone(); + move |this, _, cx| { + this.telemetry + .report_app_event("extensions: install extension".to_string()); + this.install_extension(extension_id.clone(), version.clone(), cx); + } + })) + .disabled(status.is_installing()), ExtensionStatus::Installed(_) | ExtensionStatus::Upgrading - | ExtensionStatus::Removing => { - Button::new(SharedString::from(extension.id.clone()), "Uninstall") - .on_click(cx.listener({ - let extension_id = extension.id.clone(); - move |this, _, cx| { - this.telemetry - .report_app_event("extensions: uninstall extension".to_string()); - this.uninstall_extension(extension_id.clone(), cx); - } - })) - .disabled(matches!( - status, - ExtensionStatus::Upgrading | ExtensionStatus::Removing - )) - } + | ExtensionStatus::Removing => Button::new( + SharedString::from(extension.id.clone()), + if status.is_upgrading() { + "Upgrading..." + } else if status.is_removing() { + "Removing..." + } else { + "Uninstall" + }, + ) + .on_click(cx.listener({ + let extension_id = extension.id.clone(); + move |this, _, cx| { + this.telemetry + .report_app_event("extensions: uninstall extension".to_string()); + this.uninstall_extension(extension_id.clone(), cx); + } + })) + .disabled(matches!( + status, + ExtensionStatus::Upgrading | ExtensionStatus::Removing + )), } .color(Color::Accent); let repository_url = extension.repository.clone(); + let tooltip_text = Tooltip::text(repository_url.clone(), cx); div().w_full().child( v_flex() @@ -269,7 +317,8 @@ impl ExtensionsPage { .style(ButtonStyle::Filled) .on_click(cx.listener(move |_, _, cx| { cx.open_url(&repository_url); - })), + })) + .tooltip(move |_| tooltip_text.clone()), ), ), ) @@ -319,7 +368,7 @@ impl ExtensionsPage { font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, - line_height: relative(1.3).into(), + line_height: relative(1.3), background_color: None, underline: None, strikethrough: None, @@ -379,39 +428,120 @@ impl ExtensionsPage { Some(search) } } -} -impl Render for ExtensionsPage { - fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { - v_flex() - .size_full() - .p_4() - .gap_4() - .bg(cx.theme().colors().editor_background) - .child( - h_flex() - .w_full() - .child(Headline::new("Extensions").size(HeadlineSize::XLarge)), - ) - .child(h_flex().w_56().child(self.render_search(cx))) - .child(v_flex().size_full().overflow_y_hidden().map(|this| { - if self.extensions_entries.is_empty() { - let message = if self.is_fetching_extensions { - "Loading extensions..." - } else if self.search_query(cx).is_some() { + fn render_empty_state(&self, cx: &mut ViewContext) -> impl IntoElement { + let has_search = self.search_query(cx).is_some(); + + let message = if self.is_fetching_extensions { + "Loading extensions..." + } else { + match self.filter { + ExtensionFilter::All => { + if has_search { "No extensions that match your search." } else { "No extensions." - }; + } + } + ExtensionFilter::Installed => { + if has_search { + "No installed extensions that match your search." + } else { + "No installed extensions." + } + } + ExtensionFilter::NotInstalled => { + if has_search { + "No not installed extensions that match your search." + } else { + "No not installed extensions." + } + } + } + }; - return this.child(Label::new(message)); + Label::new(message) + } +} + +impl Render for ExtensionsPage { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + v_flex() + .size_full() + .bg(cx.theme().colors().editor_background) + .child( + v_flex() + .gap_4() + .p_4() + .border_b() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .child( + h_flex() + .w_full() + .child(Headline::new("Extensions").size(HeadlineSize::XLarge)), + ) + .child( + h_flex() + .w_full() + .gap_2() + .justify_between() + .child(h_flex().child(self.render_search(cx))) + .child( + h_flex() + .child( + ToggleButton::new("filter-all", "All") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .selected(self.filter == ExtensionFilter::All) + .on_click(cx.listener(|this, _event, _cx| { + this.filter = ExtensionFilter::All; + })) + .tooltip(move |cx| { + Tooltip::text("Show all extensions", cx) + }) + .first(), + ) + .child( + ToggleButton::new("filter-installed", "Installed") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .selected(self.filter == ExtensionFilter::Installed) + .on_click(cx.listener(|this, _event, _cx| { + this.filter = ExtensionFilter::Installed; + })) + .tooltip(move |cx| { + Tooltip::text("Show installed extensions", cx) + }) + .middle(), + ) + .child( + ToggleButton::new("filter-not-installed", "Not Installed") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .selected(self.filter == ExtensionFilter::NotInstalled) + .on_click(cx.listener(|this, _event, _cx| { + this.filter = ExtensionFilter::NotInstalled; + })) + .tooltip(move |cx| { + Tooltip::text("Show not installed extensions", cx) + }) + .last(), + ), + ), + ), + ) + .child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| { + let entries = self.filtered_extension_entries(cx); + if entries.is_empty() { + return this.py_4().child(self.render_empty_state(cx)); } this.child( canvas({ let view = cx.view().clone(); let scroll_handle = self.list.clone(); - let item_count = self.extensions_entries.len(); + let item_count = entries.len(); move |bounds, cx| { uniform_list::<_, Div, _>( view, @@ -420,6 +550,7 @@ impl Render for ExtensionsPage { Self::render_extensions, ) .size_full() + .pb_4() .track_scroll(scroll_handle) .into_any_element() .draw( diff --git a/crates/feature_flags/Cargo.toml b/crates/feature_flags/Cargo.toml index 78caa1dc72..cf0f9475af 100644 --- a/crates/feature_flags/Cargo.toml +++ b/crates/feature_flags/Cargo.toml @@ -9,5 +9,4 @@ license = "GPL-3.0-or-later" path = "src/feature_flags.rs" [dependencies] -anyhow.workspace = true gpui.workspace = true diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 0a3df28c69..700f70be78 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -8,7 +8,7 @@ struct FeatureFlags { impl FeatureFlags { fn has_flag(&self, flag: &str) -> bool { - self.staff || self.flags.iter().find(|f| f.as_str() == flag).is_some() + self.staff || self.flags.iter().any(|f| f.as_str() == flag) } } diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index 30b4773d6f..b3c89e2c54 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -13,7 +13,7 @@ test-support = [] [dependencies] anyhow.workspace = true -bitflags = "2.4.1" +bitflags.workspace = true client.workspace = true db.workspace = true editor.workspace = true @@ -22,22 +22,16 @@ gpui.workspace = true human_bytes = "0.4.1" isahc.workspace = true language.workspace = true -lazy_static.workspace = true log.workspace = true menu.workspace = true -postage.workspace = true project.workspace = true regex.workspace = true release_channel.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true -settings.workspace = true -smallvec.workspace = true smol.workspace = true sysinfo.workspace = true -theme.workspace = true -tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } ui.workspace = true urlencoding = "2.1.2" util.workspace = true diff --git a/crates/feedback/src/feedback_modal.rs b/crates/feedback/src/feedback_modal.rs index f06fe8ba05..9b76ea69a6 100644 --- a/crates/feedback/src/feedback_modal.rs +++ b/crates/feedback/src/feedback_modal.rs @@ -299,7 +299,7 @@ impl FeedbackModal { let installation_id = telemetry.installation_id(); let is_staff = telemetry.is_staff(); let http_client = zed_client.http_client(); - let feedback_endpoint = http_client.zed_url("/api/feedback"); + let feedback_endpoint = http_client.build_url("/api/feedback"); let request = FeedbackRequestBody { feedback_text: &feedback_text, email, diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 25e77d4dab..377bc2dc2a 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -18,10 +18,7 @@ gpui.workspace = true itertools = "0.11" menu.workspace = true picker.workspace = true -postage.workspace = true project.workspace = true -serde.workspace = true -settings.workspace = true text.workspace = true theme.workspace = true ui.workspace = true diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index fc8e5d1d99..a8349a6335 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -365,14 +365,7 @@ impl FileFinderDelegate { history_items: Vec, cx: &mut ViewContext, ) -> Self { - cx.observe(&project, |file_finder, _, cx| { - //todo We should probably not re-render on every project anything - file_finder - .picker - .update(cx, |picker, cx| picker.refresh(cx)) - }) - .detach(); - + Self::subscribe_to_updates(&project, cx); Self { file_finder, workspace, @@ -389,6 +382,20 @@ impl FileFinderDelegate { } } + fn subscribe_to_updates(project: &Model, cx: &mut ViewContext) { + cx.subscribe(project, |file_finder, _, event, cx| { + match event { + project::Event::WorktreeUpdatedEntries(_, _) + | project::Event::WorktreeAdded + | project::Event::WorktreeRemoved(_) => file_finder + .picker + .update(cx, |picker, cx| picker.refresh(cx)), + _ => {} + }; + }) + .detach(); + } + fn spawn_search( &mut self, query: PathLikeWithPosition, @@ -663,7 +670,7 @@ impl FileFinderDelegate { impl PickerDelegate for FileFinderDelegate { type ListItem = ListItem; - fn placeholder_text(&self) -> Arc { + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Search project files...".into() } @@ -694,7 +701,7 @@ impl PickerDelegate for FileFinderDelegate { raw_query: String, cx: &mut ViewContext>, ) -> Task<()> { - let raw_query = raw_query.replace(" ", ""); + let raw_query = raw_query.replace(' ', ""); let raw_query = raw_query.trim(); if raw_query.is_empty() { let project = self.project.read(cx); diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index f0e1626bd4..8342677d38 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -1,9 +1,10 @@ -use std::{assert_eq, path::Path, time::Duration}; +use std::{assert_eq, future::IntoFuture, path::Path, time::Duration}; use super::*; use editor::Editor; use gpui::{Entity, TestAppContext, VisualTestContext}; use menu::{Confirm, SelectNext}; +use project::worktree::FS_WATCH_LATENCY; use serde_json::json; use workspace::{AppState, Workspace}; @@ -1337,6 +1338,137 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) }); } +#[gpui::test] +async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "lib.rs": "// Lib file", + "main.rs": "// Bar file", + "read.me": "// Readme file", + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + + // Initial state + let picker = open_file_picker(&workspace, cx); + cx.simulate_input("rs"); + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 2); + assert_match_at_position(finder, 0, "lib.rs"); + assert_match_at_position(finder, 1, "main.rs"); + }); + + // Delete main.rs + app_state + .fs + .remove_file("/src/main.rs".as_ref(), Default::default()) + .await + .expect("unable to remove file"); + cx.executor().advance_clock(FS_WATCH_LATENCY); + + // main.rs is in not among search results anymore + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 1); + assert_match_at_position(finder, 0, "lib.rs"); + }); + + // Create util.rs + app_state + .fs + .create_file("/src/util.rs".as_ref(), Default::default()) + .await + .expect("unable to create file"); + cx.executor().advance_clock(FS_WATCH_LATENCY); + + // util.rs is among search results + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 2); + assert_match_at_position(finder, 0, "lib.rs"); + assert_match_at_position(finder, 1, "util.rs"); + }); +} + +#[gpui::test] +async fn test_search_results_refreshed_on_adding_and_removing_worktrees( + cx: &mut gpui::TestAppContext, +) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/test", + json!({ + "project_1": { + "bar.rs": "// Bar file", + "lib.rs": "// Lib file", + }, + "project_2": { + "Cargo.toml": "// Cargo file", + "main.rs": "// Main file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + let worktree_1_id = project.update(cx, |project, cx| { + let worktree = project.worktrees().last().expect("worktree not found"); + worktree.read(cx).id() + }); + + // Initial state + let picker = open_file_picker(&workspace, cx); + cx.simulate_input("rs"); + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 2); + assert_match_at_position(finder, 0, "bar.rs"); + assert_match_at_position(finder, 1, "lib.rs"); + }); + + // Add new worktree + project + .update(cx, |project, cx| { + project + .find_or_create_local_worktree("/test/project_2", true, cx) + .into_future() + }) + .await + .expect("unable to create workdir"); + cx.executor().advance_clock(FS_WATCH_LATENCY); + + // main.rs is among search results + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 3); + assert_match_at_position(finder, 0, "bar.rs"); + assert_match_at_position(finder, 1, "lib.rs"); + assert_match_at_position(finder, 2, "main.rs"); + }); + + // Remove the first worktree + project.update(cx, |project, cx| { + project.remove_worktree(worktree_1_id, cx); + }); + cx.executor().advance_clock(FS_WATCH_LATENCY); + + // Files from the first worktree are not in the search results anymore + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 1); + assert_match_at_position(finder, 0, "main.rs"); + }); +} + async fn open_close_queried_buffer( input: &str, expected_matches: usize, diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index d3f6d87d30..4b48dbc2bf 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -17,13 +17,13 @@ util.workspace = true sum_tree.workspace = true anyhow.workspace = true +async-tar.workspace = true async-trait.workspace = true futures.workspace = true tempfile.workspace = true lazy_static.workspace = true parking_lot.workspace = true smol.workspace = true -regex.workspace = true git2.workspace = true serde.workspace = true serde_derive.workspace = true @@ -32,11 +32,17 @@ log.workspace = true libc = "0.2" time.workspace = true -gpui = { workspace = true, optional = true} +gpui = { workspace = true, optional = true } [target.'cfg(not(target_os = "macos"))'.dependencies] notify = "6.1.1" +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.52", features = [ + "Win32_Foundation", + "Win32_Storage_FileSystem", +] } + [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 885b94e3a3..968ab68389 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -11,7 +11,11 @@ use fsevent::StreamFlags; #[cfg(not(target_os = "macos"))] use notify::{Config, EventKind, Watcher}; -use futures::{future::BoxFuture, Stream, StreamExt}; +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; + +use async_tar::Archive; +use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt}; use git2::Repository as LibGitRepository; use parking_lot::Mutex; use repository::GitRepository; @@ -21,14 +25,13 @@ use std::io::Write; use std::sync::Arc; use std::{ io, - os::unix::fs::MetadataExt, path::{Component, Path, PathBuf}, pin::Pin, time::{Duration, SystemTime}, }; use tempfile::{NamedTempFile, TempDir}; use text::LineEnding; -use util::ResultExt; +use util::{paths, ResultExt}; #[cfg(any(test, feature = "test-support"))] use collections::{btree_map, BTreeMap}; @@ -41,6 +44,16 @@ use std::ffi::OsStr; pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>; + async fn create_file_with( + &self, + path: &Path, + content: Pin<&mut (dyn AsyncRead + Send)>, + ) -> Result<()>; + async fn extract_tar_file( + &self, + path: &Path, + content: Archive>, + ) -> Result<()>; async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>; async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; @@ -123,6 +136,25 @@ impl Fs for RealFs { Ok(()) } + async fn create_file_with( + &self, + path: &Path, + content: Pin<&mut (dyn AsyncRead + Send)>, + ) -> Result<()> { + let mut file = smol::fs::File::create(&path).await?; + futures::io::copy(content, &mut file).await?; + Ok(()) + } + + async fn extract_tar_file( + &self, + path: &Path, + content: Archive>, + ) -> Result<()> { + content.unpack(path).await?; + Ok(()) + } + async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> { if !options.overwrite && smol::fs::metadata(target).await.is_ok() { if options.ignore_if_exists { @@ -187,7 +219,14 @@ impl Fs for RealFs { async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> { smol::unblock(move || { - let mut tmp_file = NamedTempFile::new()?; + let mut tmp_file = if cfg!(target_os = "linux") { + // Use the directory of the destination as temp dir to avoid + // invalid cross-device link error, and XDG_CACHE_DIR for fallback. + // See https://github.com/zed-industries/zed/pull/8437 for more details. + NamedTempFile::new_in(path.parent().unwrap_or(&paths::TEMP_DIR)) + } else { + NamedTempFile::new() + }?; tmp_file.write_all(data.as_bytes())?; tmp_file.persist(path)?; Ok::<(), anyhow::Error>(()) @@ -239,8 +278,15 @@ impl Fs for RealFs { } else { symlink_metadata }; + + #[cfg(unix)] + let inode = metadata.ino(); + + #[cfg(windows)] + let inode = file_id(path).await?; + Ok(Some(Metadata { - inode: metadata.ino(), + inode, mtime: metadata.modified().unwrap(), is_symlink, is_dir: metadata.file_type().is_dir(), @@ -330,10 +376,10 @@ impl Fs for RealFs { } fn open_repo(&self, dotgit_path: &Path) -> Option>> { - LibGitRepository::open(&dotgit_path) + LibGitRepository::open(dotgit_path) .log_err() - .and_then::>, _>(|libgit_repository| { - Some(Arc::new(Mutex::new(libgit_repository))) + .map::>, _>(|libgit_repository| { + Arc::new(Mutex::new(libgit_repository)) }) } @@ -413,7 +459,7 @@ enum FakeFsEntry { File { inode: u64, mtime: SystemTime, - content: String, + content: Vec, }, Dir { inode: u64, @@ -428,15 +474,15 @@ enum FakeFsEntry { #[cfg(any(test, feature = "test-support"))] impl FakeFsState { - fn read_path<'a>(&'a self, target: &Path) -> Result>> { + fn read_path(&self, target: &Path) -> Result>> { Ok(self .try_read_path(target, true) .ok_or_else(|| anyhow!("path does not exist: {}", target.display()))? .0) } - fn try_read_path<'a>( - &'a self, + fn try_read_path( + &self, target: &Path, follow_symlink: bool, ) -> Option<(Arc>, PathBuf)> { @@ -559,7 +605,7 @@ impl FakeFs { }) } - pub async fn insert_file(&self, path: impl AsRef, content: String) { + pub async fn insert_file(&self, path: impl AsRef, content: Vec) { self.write_file_internal(path, content).unwrap() } @@ -579,10 +625,10 @@ impl FakeFs { } }) .unwrap(); - state.emit_event(&[path]); + state.emit_event([path]); } - pub fn write_file_internal(&self, path: impl AsRef, content: String) -> Result<()> { + fn write_file_internal(&self, path: impl AsRef, content: Vec) -> Result<()> { let mut state = self.state.lock(); let path = path.as_ref(); let inode = state.next_inode; @@ -605,10 +651,20 @@ impl FakeFs { } Ok(()) })?; - state.emit_event(&[path]); + state.emit_event([path]); Ok(()) } + async fn load_internal(&self, path: impl AsRef) -> Result> { + let path = path.as_ref(); + let path = normalize_path(path); + self.simulate_random_delay().await; + let state = self.state.lock(); + let entry = state.read_path(&path)?; + let entry = entry.lock(); + entry.file_content(&path).cloned() + } + pub fn pause_events(&self) { self.state.lock().events_paused = true; } @@ -646,7 +702,7 @@ impl FakeFs { self.create_dir(path).await.unwrap(); } String(contents) => { - self.insert_file(&path, contents).await; + self.insert_file(&path, contents.into_bytes()).await; } _ => { panic!("JSON object must contain only objects, strings, or null"); @@ -656,6 +712,30 @@ impl FakeFs { .boxed() } + pub fn insert_tree_from_real_fs<'a>( + &'a self, + path: impl 'a + AsRef + Send, + src_path: impl 'a + AsRef + Send, + ) -> futures::future::BoxFuture<'a, ()> { + use futures::FutureExt as _; + + async move { + let path = path.as_ref(); + if std::fs::metadata(&src_path).unwrap().is_file() { + let contents = std::fs::read(src_path).unwrap(); + self.insert_file(path, contents).await; + } else { + self.create_dir(path).await.unwrap(); + for entry in std::fs::read_dir(&src_path).unwrap() { + let entry = entry.unwrap(); + self.insert_tree_from_real_fs(&path.join(entry.file_name()), &entry.path()) + .await; + } + } + } + .boxed() + } + pub fn with_git_state(&self, dot_git: &Path, emit_git_event: bool, f: F) where F: FnOnce(&mut FakeGitRepositoryState), @@ -705,7 +785,7 @@ impl FakeFs { state.worktree_statuses.extend( statuses .iter() - .map(|(path, content)| ((**path).into(), content.clone())), + .map(|(path, content)| ((**path).into(), *content)), ); }); self.state.lock().emit_event( @@ -725,7 +805,7 @@ impl FakeFs { state.worktree_statuses.extend( statuses .iter() - .map(|(path, content)| ((**path).into(), content.clone())), + .map(|(path, content)| ((**path).into(), *content)), ); }); } @@ -816,7 +896,7 @@ impl FakeFsEntry { matches!(self, Self::Symlink { .. }) } - fn file_content(&self, path: &Path) -> Result<&String> { + fn file_content(&self, path: &Path) -> Result<&Vec> { if let Self::File { content, .. } = self { Ok(content) } else { @@ -824,7 +904,7 @@ impl FakeFsEntry { } } - fn set_file_content(&mut self, path: &Path, new_content: String) -> Result<()> { + fn set_file_content(&mut self, path: &Path, new_content: Vec) -> Result<()> { if let Self::File { content, mtime, .. } = self { *mtime = SystemTime::now(); *content = new_content; @@ -893,7 +973,7 @@ impl Fs for FakeFs { let file = Arc::new(Mutex::new(FakeFsEntry::File { inode, mtime, - content: String::new(), + content: Vec::new(), })); state.write_path(path, |entry| { match entry { @@ -910,7 +990,37 @@ impl Fs for FakeFs { } Ok(()) })?; - state.emit_event(&[path]); + state.emit_event([path]); + Ok(()) + } + + async fn create_file_with( + &self, + path: &Path, + mut content: Pin<&mut (dyn AsyncRead + Send)>, + ) -> Result<()> { + let mut bytes = Vec::new(); + content.read_to_end(&mut bytes).await?; + self.write_file_internal(path, bytes)?; + Ok(()) + } + + async fn extract_tar_file( + &self, + path: &Path, + content: Archive>, + ) -> Result<()> { + let mut entries = content.entries()?; + while let Some(entry) = entries.next().await { + let mut entry = entry?; + if entry.header().entry_type().is_file() { + let path = path.join(entry.path()?.as_ref()); + let mut bytes = Vec::new(); + entry.read_to_end(&mut bytes).await?; + self.create_dir(path.parent().unwrap()).await?; + self.write_file_internal(&path, bytes)?; + } + } Ok(()) } @@ -984,7 +1094,7 @@ impl Fs for FakeFs { e.insert(Arc::new(Mutex::new(FakeFsEntry::File { inode, mtime, - content: String::new(), + content: Vec::new(), }))) .clone(), )), @@ -1063,35 +1173,30 @@ impl Fs for FakeFs { } async fn open_sync(&self, path: &Path) -> Result> { - let text = self.load(path).await?; - Ok(Box::new(io::Cursor::new(text))) + let bytes = self.load_internal(path).await?; + Ok(Box::new(io::Cursor::new(bytes))) } async fn load(&self, path: &Path) -> Result { - let path = normalize_path(path); - self.simulate_random_delay().await; - let state = self.state.lock(); - let entry = state.read_path(&path)?; - let entry = entry.lock(); - entry.file_content(&path).cloned() + let content = self.load_internal(path).await?; + Ok(String::from_utf8(content.clone())?) } async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> { self.simulate_random_delay().await; let path = normalize_path(path.as_path()); - self.write_file_internal(path, data.to_string())?; - + self.write_file_internal(path, data.into_bytes())?; Ok(()) } async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { self.simulate_random_delay().await; let path = normalize_path(path); - let content = chunks(text, line_ending).collect(); + let content = chunks(text, line_ending).collect::(); if let Some(path) = path.parent() { self.create_dir(path).await?; } - self.write_file_internal(path, content)?; + self.write_file_internal(path, content.into_bytes())?; Ok(()) } @@ -1327,6 +1432,41 @@ pub fn copy_recursive<'a>( .boxed() } +// todo(windows) +// can we get file id not open the file twice? +// https://github.com/rust-lang/rust/issues/63010 +#[cfg(target_os = "windows")] +async fn file_id(path: impl AsRef) -> Result { + use std::os::windows::io::AsRawHandle; + + use smol::fs::windows::OpenOptionsExt; + use windows_sys::Win32::{ + Foundation::HANDLE, + Storage::FileSystem::{ + GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS, + }, + }; + + let file = smol::fs::OpenOptions::new() + .read(true) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) + .open(path) + .await?; + + let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() }; + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle + // This function supports Windows XP+ + smol::unblock(move || { + let ret = unsafe { GetFileInformationByHandle(file.as_raw_handle() as HANDLE, &mut info) }; + if ret == 0 { + return Err(anyhow!(format!("{}", std::io::Error::last_os_error()))); + }; + + Ok(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64)) + }) + .await +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 66dd0503cf..ae69025f0a 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -363,19 +363,19 @@ impl GitFileStatus { ) -> Option { if prefer_other { return other; - } else { - match (this, other) { - (Some(GitFileStatus::Conflict), _) | (_, Some(GitFileStatus::Conflict)) => { - Some(GitFileStatus::Conflict) - } - (Some(GitFileStatus::Modified), _) | (_, Some(GitFileStatus::Modified)) => { - Some(GitFileStatus::Modified) - } - (Some(GitFileStatus::Added), _) | (_, Some(GitFileStatus::Added)) => { - Some(GitFileStatus::Added) - } - _ => None, + } + + match (this, other) { + (Some(GitFileStatus::Conflict), _) | (_, Some(GitFileStatus::Conflict)) => { + Some(GitFileStatus::Conflict) } + (Some(GitFileStatus::Modified), _) | (_, Some(GitFileStatus::Modified)) => { + Some(GitFileStatus::Modified) + } + (Some(GitFileStatus::Added), _) | (_, Some(GitFileStatus::Added)) => { + Some(GitFileStatus::Added) + } + _ => None, } } } @@ -428,7 +428,7 @@ pub struct RepoPathDescendants<'a>(pub &'a Path); impl<'a> MapSeekTarget for RepoPathDescendants<'a> { fn cmp_cursor(&self, key: &RepoPath) -> Ordering { - if key.starts_with(&self.0) { + if key.starts_with(self.0) { Ordering::Greater } else { self.0.cmp(key) diff --git a/crates/fsevent/Cargo.toml b/crates/fsevent/Cargo.toml index a684fca867..23490e8fa5 100644 --- a/crates/fsevent/Cargo.toml +++ b/crates/fsevent/Cargo.toml @@ -11,7 +11,7 @@ path = "src/fsevent.rs" doctest = false [dependencies] -bitflags = "1" +bitflags.workspace = true parking_lot.workspace = true [target.'cfg(target_os = "macos")'.dependencies] diff --git a/crates/fsevent/src/fsevent.rs b/crates/fsevent/src/fsevent.rs index 108b582bd0..e730e701f3 100644 --- a/crates/fsevent/src/fsevent.rs +++ b/crates/fsevent/src/fsevent.rs @@ -17,6 +17,7 @@ pub struct Event { // Synchronize with // /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Headers/FSEvents.h bitflags! { + #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[repr(C)] pub struct StreamFlags: u32 { const NONE = 0x00000000; diff --git a/crates/fuzzy/src/char_bag.rs b/crates/fuzzy/src/char_bag.rs index 8fc36368a1..ca40d730fb 100644 --- a/crates/fuzzy/src/char_bag.rs +++ b/crates/fuzzy/src/char_bag.rs @@ -10,14 +10,14 @@ impl CharBag { fn insert(&mut self, c: char) { let c = c.to_ascii_lowercase(); - if ('a'..='z').contains(&c) { + if c.is_ascii_lowercase() { let mut count = self.0; let idx = c as u8 - b'a'; count >>= idx * 2; count = ((count << 1) | 1) & 3; count <<= idx * 2; self.0 |= count; - } else if ('0'..='9').contains(&c) { + } else if c.is_ascii_digit() { let idx = c as u8 - b'0'; self.0 |= 1 << (idx + 52); } else if c == '-' { diff --git a/crates/fuzzy/src/paths.rs b/crates/fuzzy/src/paths.rs index e982195158..25927f1829 100644 --- a/crates/fuzzy/src/paths.rs +++ b/crates/fuzzy/src/paths.rs @@ -200,7 +200,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>( usize::MAX, |relative_to| { distance_between_paths( - candidate.path.as_ref(), + candidate.path, relative_to.as_ref(), ) }, diff --git a/crates/fuzzy/src/strings.rs b/crates/fuzzy/src/strings.rs index 5028a43fd7..e1f6de37a5 100644 --- a/crates/fuzzy/src/strings.rs +++ b/crates/fuzzy/src/strings.rs @@ -57,10 +57,10 @@ pub struct StringMatch { } impl StringMatch { - pub fn ranges<'a>(&'a self) -> impl 'a + Iterator> { + pub fn ranges(&self) -> impl '_ + Iterator> { let mut positions = self.positions.iter().peekable(); iter::from_fn(move || { - while let Some(start) = positions.next().copied() { + if let Some(start) = positions.next().copied() { let mut end = start + self.char_len_at_index(start); while let Some(next_start) = positions.peek() { if end == **next_start { diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 648ea336c2..720a0cdd32 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -9,19 +9,13 @@ license = "GPL-3.0-or-later" path = "src/git.rs" [dependencies] -anyhow.workspace = true -async-trait.workspace = true clock.workspace = true -collections.workspace = true -futures.workspace = true git2.workspace = true lazy_static.workspace = true log.workspace = true -parking_lot.workspace = true smol.workspace = true sum_tree.workspace = true text.workspace = true -util.workspace = true [dev-dependencies] unindent.workspace = true diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index 07b0240f60..20f425f42c 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -208,8 +208,8 @@ impl BufferDiff { } } - fn process_patch_hunk<'a>( - patch: &GitPatch<'a>, + fn process_patch_hunk( + patch: &GitPatch<'_>, hunk_index: usize, buffer: &text::BufferSnapshot, buffer_row_divergence: &mut i64, diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index e75ed95ad5..a009e27547 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -13,9 +13,6 @@ doctest = false editor.workspace = true gpui.workspace = true menu.workspace = true -postage.workspace = true -serde.workspace = true -settings.workspace = true text.workspace = true theme.workspace = true ui.workspace = true diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 8326294c75..de83868b35 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -10,7 +10,6 @@ license = "Apache-2.0" [features] test-support = [ "backtrace", - "dhat", "env_logger", "collections/test-support", "util/test-support", @@ -26,7 +25,6 @@ doctest = false anyhow.workspace = true async-task = "4.7" backtrace = { version = "0.3", optional = true } -bitflags = "2.4.0" blade-graphics = { workspace = true, optional = true } blade-macros = { workspace = true, optional = true } blade-rwh = { workspace = true, optional = true } @@ -34,23 +32,22 @@ bytemuck = { version = "1", optional = true } collections.workspace = true ctor.workspace = true derive_more.workspace = true -dhat = { version = "0.3", optional = true } env_logger = { version = "0.9", optional = true } etagere = "0.2" futures.workspace = true font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4" } gpui_macros.workspace = true image = "0.23" -itertools = "0.10" +itertools.workspace = true lazy_static.workspace = true linkme = "0.3" log.workspace = true num_cpus = "1.13" -ordered-float.workspace = true parking = "2.0.0" parking_lot.workspace = true pathfinder_geometry = "0.5" postage.workspace = true +profiling.workspace = true rand.workspace = true raw-window-handle = "0.6" refineable.workspace = true @@ -76,10 +73,7 @@ waker-fn = "1.1.0" [dev-dependencies] backtrace = "0.3" collections = { workspace = true, features = ["test-support"] } -dhat = "0.3" env_logger.workspace = true -png = "0.16" -simplelog = "0.9" util = { workspace = true, features = ["test-support"] } [build-dependencies] @@ -89,7 +83,7 @@ cbindgen = "0.26.0" [target.'cfg(target_os = "macos")'.dependencies] block = "0.1" cocoa = "0.25" -core-foundation = { version = "0.9.3", features = ["with-uuid"] } +core-foundation.workspace = true core-graphics = "0.23" core-text = "20.1" foreign-types = "0.5" @@ -98,20 +92,37 @@ media.workspace = true metal = "0.25" objc = "0.2" -[target.'cfg(target_os = "linux")'.dependencies] +[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies] flume = "0.11" -open = "5.0.1" -ashpd = "0.7.0" -# todo!(linux) - Technically do not use `randr`, but it doesn't compile otherwise -xcb = { version = "1.3", features = ["as-raw-xcb-connection", "present", "randr", "xkb"] } -wayland-client= { version = "0.31.2" } -wayland-protocols = { version = "0.31.2", features = ["client", "staging"] } -wayland-backend = { version = "0.3.3", features = ["client_system"] } -xkbcommon = { version = "0.7", features = ["wayland", "x11"] } -as-raw-xcb-connection = "1" #TODO: use these on all platforms blade-graphics.workspace = true blade-macros.workspace = true blade-rwh.workspace = true bytemuck = "1" cosmic-text = "0.10.0" + +[target.'cfg(target_os = "linux")'.dependencies] +open = "5.0.1" +ashpd = "0.7.0" +xcb = { version = "1.3", features = ["as-raw-xcb-connection", "randr", "xkb"] } +wayland-client= { version = "0.31.2" } +wayland-cursor = "0.31.1" +wayland-protocols = { version = "0.31.2", features = ["client", "staging", "unstable"] } +wayland-backend = { version = "0.3.3", features = ["client_system"] } +xkbcommon = { version = "0.7", features = ["wayland", "x11"] } +as-raw-xcb-connection = "1" +calloop = "0.12.4" +calloop-wayland-source = "0.2.0" +copypasta = "0.10.1" +oo7 = "0.3.0" + +[target.'cfg(windows)'.dependencies] +windows.workspace = true + +[[example]] +name = "hello_world" +path = "examples/hello_world.rs" + +[[example]] +name = "image" +path = "examples/image/image.rs" diff --git a/crates/gpui/README.md b/crates/gpui/README.md index 5278b024ae..ac2c04bf07 100644 --- a/crates/gpui/README.md +++ b/crates/gpui/README.md @@ -21,7 +21,7 @@ GPUI offers three different [registers](https://en.wikipedia.org/wiki/Register_( - High level, declarative UI with Views. All UI in GPUI starts with a View. A view is simply a model that can be rendered, via the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering. -- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information. +- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information. Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework. diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 6d6f384bc5..8f63787c8b 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -125,7 +125,7 @@ fn emit_stitched_shaders(header_path: &Path) { let shader_contents = std::fs::read_to_string(shader_path)?; let stitched_contents = format!("{header_contents}\n{shader_contents}"); let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("stitched_shaders.metal"); - let _ = std::fs::write(&out_path, stitched_contents)?; + std::fs::write(&out_path, stitched_contents)?; Ok(out_path) } let shader_source_path = "./src/platform/mac/shaders.metal"; diff --git a/crates/gpui/examples/hello_world.rs b/crates/gpui/examples/hello_world.rs index d0578e6681..9361cbf437 100644 --- a/crates/gpui/examples/hello_world.rs +++ b/crates/gpui/examples/hello_world.rs @@ -23,7 +23,15 @@ impl Render for HelloWorld { fn main() { App::new().run(|cx: &mut AppContext| { - cx.open_window(WindowOptions::default(), |cx| { + let options = WindowOptions { + bounds: WindowBounds::Fixed(Bounds { + size: size(px(600.0), px(600.0)).into(), + origin: Default::default(), + }), + center: true, + ..Default::default() + }; + cx.open_window(options, |cx| { cx.new_view(|_cx| HelloWorld { text: "World".into(), }) diff --git a/crates/gpui/examples/image/app-icon.png b/crates/gpui/examples/image/app-icon.png new file mode 100644 index 0000000000..08b6d8afa0 Binary files /dev/null and b/crates/gpui/examples/image/app-icon.png differ diff --git a/crates/gpui/examples/image.rs b/crates/gpui/examples/image/image.rs similarity index 91% rename from crates/gpui/examples/image.rs rename to crates/gpui/examples/image/image.rs index 48cc39df58..d6002888c9 100644 --- a/crates/gpui/examples/image.rs +++ b/crates/gpui/examples/image/image.rs @@ -64,9 +64,8 @@ fn main() { App::new().run(|cx: &mut AppContext| { cx.open_window(WindowOptions::default(), |cx| { cx.new_view(|_cx| ImageShowcase { - local_resource: Arc::new( - PathBuf::from_str("crates/zed/resources/app-icon.png").unwrap(), - ), + // Relative path to your root project path + local_resource: Arc::new(PathBuf::from_str("examples/image/app-icon.png").unwrap()), remote_resource: "https://picsum.photos/512/512".into(), }) }); diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index c6ea705b57..44e6ec17ff 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -39,7 +39,7 @@ use std::any::{Any, TypeId}; /// } /// register_action!(Paste); /// ``` -pub trait Action: 'static { +pub trait Action: 'static + Send { /// Clone the action into a new box fn boxed_clone(&self) -> Box; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 333948f956..9373ad66e8 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -226,6 +226,7 @@ pub struct AppContext { pub(crate) entities: EntityMap, pub(crate) new_view_observers: SubscriberSet, pub(crate) windows: SlotMap>, + pub(crate) window_handles: FxHashMap, pub(crate) keymap: Rc>, pub(crate) global_action_listeners: FxHashMap>>, @@ -285,6 +286,7 @@ impl AppContext { globals_by_type: FxHashMap::default(), entities, new_view_observers: SubscriberSet::new(), + window_handles: FxHashMap::default(), windows: SlotMap::with_key(), keymap: Rc::new(RefCell::new(Keymap::default())), global_action_listeners: FxHashMap::default(), @@ -324,6 +326,7 @@ impl AppContext { } self.windows.clear(); + self.window_handles.clear(); self.flush_effects(); let futures = futures::future::join_all(futures); @@ -468,8 +471,8 @@ impl AppContext { /// To find all windows of a given type, you could filter on pub fn windows(&self) -> Vec { self.windows - .values() - .filter_map(|window| Some(window.as_ref()?.handle)) + .keys() + .flat_map(|window_id| self.window_handles.get(&window_id).copied()) .collect() } @@ -492,6 +495,7 @@ impl AppContext { let mut window = Window::new(handle.into(), options, cx); let root_view = build_root_view(&mut WindowContext::new(cx, &mut window)); window.root_view.replace(root_view.into()); + cx.window_handles.insert(id, window.handle); cx.windows.get_mut(id).unwrap().replace(window); handle }) @@ -562,6 +566,14 @@ impl AppContext { self.platform.open_url(url); } + /// register_url_scheme requests that the given scheme (e.g. `zed` for `zed://` urls) + /// is opened by the current app. + /// On some platforms (e.g. macOS) you may be able to register URL schemes as part of app + /// distribution, but this method exists to let you register schemes at runtime. + pub fn register_url_scheme(&self, scheme: &str) -> Task> { + self.platform.register_url_scheme(scheme) + } + /// Returns the full pathname of the current app bundle. /// If the app is not being run from a bundle, returns an error. pub fn app_path(&self) -> Result { @@ -1239,12 +1251,13 @@ impl Context for AppContext { .get_mut(handle.id) .ok_or_else(|| anyhow!("window not found"))? .take() - .unwrap(); + .ok_or_else(|| anyhow!("window not found"))?; let root_view = window.root_view.clone().unwrap(); let result = update(root_view, &mut WindowContext::new(cx, &mut window)); if window.removed { + cx.window_handles.remove(&handle.id); cx.windows.remove(handle.id); } else { cx.windows diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 0f64a0690f..2b7d170e04 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -1,9 +1,9 @@ use crate::{ Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, - AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter, - ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, Modifiers, - ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, - Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, + AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Empty, Entity, + EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, + Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; @@ -160,7 +160,7 @@ impl TestAppContext { /// Gives you an `&AppContext` for the duration of the closure pub fn read(&self, f: impl FnOnce(&AppContext) -> R) -> R { let cx = self.app.borrow(); - f(&*cx) + f(&cx) } /// Adds a new window. The Window will always be backed by a `TestWindow` which @@ -177,7 +177,7 @@ impl TestAppContext { /// Adds a new window with no content. pub fn add_empty_window(&mut self) -> &mut VisualTestContext { let mut cx = self.app.borrow_mut(); - let window = cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| ())); + let window = cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| Empty)); drop(cx); let cx = VisualTestContext::from_window(*window.deref(), self).as_mut(); cx.run_until_parked(); @@ -331,11 +331,11 @@ impl TestAppContext { /// This will also run the background executor until it's parked. pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { for keystroke in keystrokes - .split(" ") + .split(' ') .map(Keystroke::parse) .map(Result::unwrap) { - self.dispatch_keystroke(window, keystroke.into(), false); + self.dispatch_keystroke(window, keystroke); } self.background_executor.run_until_parked() @@ -347,21 +347,16 @@ impl TestAppContext { /// This will also run the background executor until it's parked. pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) { for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) { - self.dispatch_keystroke(window, keystroke.into(), false); + self.dispatch_keystroke(window, keystroke); } self.background_executor.run_until_parked() } /// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`) - pub fn dispatch_keystroke( - &mut self, - window: AnyWindowHandle, - keystroke: Keystroke, - is_held: bool, - ) { - self.test_window(window) - .simulate_keystroke(keystroke, is_held) + pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) { + self.update_window(window, |_, cx| cx.dispatch_keystroke(keystroke)) + .unwrap(); } /// Returns the `TestWindow` backing the given handle. @@ -580,7 +575,7 @@ pub struct VisualTestContext { window: AnyWindowHandle, } -impl<'a> VisualTestContext { +impl VisualTestContext { /// Get the underlying window handle underlying this context. pub fn handle(&self) -> AnyWindowHandle { self.window diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index 4dbc7be652..dd343387ba 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -132,8 +132,10 @@ pub trait Render: 'static + Sized { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement; } -impl Render for () { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement {} +impl Render for Empty { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + Empty + } } /// You can derive [`IntoElement`] on any type that implements this trait. @@ -514,9 +516,9 @@ impl IntoElement for AnyElement { } /// The empty element, which renders nothing. -pub type Empty = (); +pub struct Empty; -impl IntoElement for () { +impl IntoElement for Empty { type Element = Self; fn element_id(&self) -> Option { @@ -528,7 +530,7 @@ impl IntoElement for () { } } -impl Element for () { +impl Element for Empty { type State = (); fn request_layout( diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index e166b99a8e..27a0e4615f 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -480,7 +480,7 @@ impl Interactivity { self.tooltip_builder = Some(Rc::new(build_tooltip)); } - /// Block the mouse from interacting with this element or any of it's children + /// Block the mouse from interacting with this element or any of its children /// The imperative API equivalent to [`InteractiveElement::block_mouse`] pub fn block_mouse(&mut self) { self.block_mouse = true; @@ -508,7 +508,7 @@ pub trait InteractiveElement: Sized { /// Track the focus state of the given focus handle on this element. /// If the focus handle is focused by the application, this element will - /// apply it's focused styles. + /// apply its focused styles. fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable { self.interactivity().focusable = true; self.interactivity().tracked_focus_handle = Some(focus_handle.clone()); @@ -834,7 +834,7 @@ pub trait InteractiveElement: Sized { self } - /// Block the mouse from interacting with this element or any of it's children + /// Block the mouse from interacting with this element or any of its children /// The fluent API equivalent to [`Interactivity::block_mouse`] fn block_mouse(mut self) -> Self { self.interactivity().block_mouse(); @@ -1221,7 +1221,8 @@ pub struct InteractiveBounds { } impl InteractiveBounds { - /// Checks whether this point was inside these bounds, and that these bounds where the topmost layer + /// Checks whether this point was inside these bounds in the rendered frame, and that these bounds where the topmost layer + /// Never call this during paint to perform hover calculations. It will reference the previous frame and could cause flicker. pub fn visibly_contains(&self, point: &Point, cx: &WindowContext) -> bool { self.bounds.contains(point) && cx.was_top_layer(point, &self.stacking_order) } @@ -1449,11 +1450,12 @@ impl Interactivity { if !cx.has_active_drag() { if let Some(mouse_cursor) = style.mouse_cursor { - let mouse_position = &cx.mouse_position(); - let hovered = - interactive_bounds.visibly_contains(mouse_position, cx); + let hovered = bounds.contains(&cx.mouse_position()); if hovered { - cx.set_cursor_style(mouse_cursor); + cx.set_cursor_style( + mouse_cursor, + interactive_bounds.stacking_order.clone(), + ); } } } @@ -1955,9 +1957,7 @@ impl Interactivity { if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx.deref_mut()) { - if group_bounds.contains(&mouse_position) - && cx.was_top_layer(&mouse_position, cx.stacking_order()) - { + if group_bounds.contains(&mouse_position) { style.refine(&group_hover.style); } } @@ -1967,7 +1967,6 @@ impl Interactivity { if bounds .intersect(&cx.content_mask().bounds) .contains(&mouse_position) - && cx.was_top_layer(&mouse_position, cx.stacking_order()) { style.refine(hover_style); } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 7961c0df84..f449862668 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -548,32 +548,28 @@ impl Element for List { let summary = state.items.summary(); let total_height = summary.height; - let all_rendered = summary.unrendered_count == 0; - if all_rendered { - cx.request_measured_layout( - style, - move |known_dimensions, available_space, _cx| { - let width = known_dimensions.width.unwrap_or(match available_space + cx.request_measured_layout( + style, + move |known_dimensions, available_space, _cx| { + let width = + known_dimensions .width - { - AvailableSpace::Definite(x) => x, - AvailableSpace::MinContent | AvailableSpace::MaxContent => { - max_element_width - } - }); - let height = match available_space.height { - AvailableSpace::Definite(height) => total_height.min(height), - AvailableSpace::MinContent | AvailableSpace::MaxContent => { - total_height - } - }; - size(width, height) - }, - ) - } else { - cx.request_layout(&style, None) - } + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + max_element_width + } + }); + let height = match available_space.height { + AvailableSpace::Definite(height) => total_height.min(height), + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + total_height + } + }; + size(width, height) + }, + ) }) } ListSizingBehavior::Auto => { diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 2b5bf9166e..c07f581910 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -427,9 +427,9 @@ impl Element for InteractiveText { .clickable_ranges .iter() .any(|range| range.contains(&ix)) - && cx.was_top_layer(&mouse_position, cx.stacking_order()) { - cx.set_cursor_style(crate::CursorStyle::PointingHand) + let stacking_order = cx.stacking_order().clone(); + cx.set_cursor_style(crate::CursorStyle::PointingHand, stacking_order); } } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index ff1daa59ea..ab9898d3da 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -35,7 +35,7 @@ //! //! - Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they //! provide a nice wrapper around an imperative API that provides as much flexibility and control as -//! you need. Elements have total control over how they and their child elements are rendered and and +//! you need. Elements have total control over how they and their child elements are rendered and //! can be used for making efficient views into large lists, implement custom layouting for a code editor, //! and anything else you can think of. See the [`element`] module for more information. //! diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 1bc32717c4..cfeaf6653e 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -1,5 +1,6 @@ use crate::{ - point, seal::Sealed, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, ViewContext, + point, seal::Sealed, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, + ViewContext, }; use smallvec::SmallVec; use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf}; @@ -343,7 +344,8 @@ impl ExternalPaths { impl Render for ExternalPaths { fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { - // Intentionally left empty because the platform will render icons for the dragged files + // the platform will render icons for the dragged files + Empty } } @@ -491,8 +493,8 @@ mod test { .update(cx, |test_view, cx| cx.focus(&test_view.focus_handle)) .unwrap(); - cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap(), false); - cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap(), false); + cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap()); + cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap()); window .update(cx, |test_view, _| { diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index 221f5d09f4..5e97e26cdd 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -2,7 +2,7 @@ use crate::{Action, KeyBindingContextPredicate, KeyMatch, Keystroke}; use anyhow::Result; use smallvec::SmallVec; -/// A keybinding and it's associated metadata, from the keymap. +/// A keybinding and its associated metadata, from the keymap. pub struct KeyBinding { pub(crate) action: Box, pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index de39ef61cc..1e1b66fffe 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1,5 +1,7 @@ -// todo!(linux): remove +// todo(linux): remove #![cfg_attr(target_os = "linux", allow(dead_code))] +// todo("windows"): remove +#![cfg_attr(windows, allow(dead_code))] mod app_menu; mod keystroke; @@ -10,12 +12,15 @@ mod linux; #[cfg(target_os = "macos")] mod mac; -#[cfg(any(target_os = "linux", feature = "macos-blade"))] +#[cfg(any(target_os = "linux", target_os = "windows", feature = "macos-blade"))] mod blade; #[cfg(any(test, feature = "test-support"))] mod test; +#[cfg(target_os = "windows")] +mod windows; + use crate::{ Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout, @@ -52,6 +57,8 @@ pub(crate) use mac::*; pub(crate) use test::*; use time::UtcOffset; pub use util::SemanticVersion; +#[cfg(target_os = "windows")] +pub(crate) use windows::*; #[cfg(target_os = "macos")] pub(crate) fn current_platform() -> Rc { @@ -61,6 +68,11 @@ pub(crate) fn current_platform() -> Rc { pub(crate) fn current_platform() -> Rc { Rc::new(LinuxPlatform::new()) } +// todo("windows") +#[cfg(target_os = "windows")] +pub(crate) fn current_platform() -> Rc { + Rc::new(WindowsPlatform::new()) +} pub(crate) trait Platform: 'static { fn background_executor(&self) -> BackgroundExecutor; @@ -89,6 +101,8 @@ pub(crate) trait Platform: 'static { fn open_url(&self, url: &str); fn on_open_urls(&self, callback: Box)>); + fn register_url_scheme(&self, url: &str) -> Task>; + fn prompt_for_paths( &self, options: PathPromptOptions, @@ -188,8 +202,8 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn on_appearance_changed(&self, callback: Box); fn is_topmost_for_position(&self, position: Point) -> bool; fn draw(&self, scene: &Scene); + fn sprite_atlas(&self) -> Arc; - fn set_graphics_profiler_enabled(&self, enabled: bool); #[cfg(any(test, feature = "test-support"))] fn as_test(&mut self) -> Option<&mut TestWindow> { @@ -412,7 +426,7 @@ impl PlatformInputHandler { .flatten() } - pub(crate) fn flush_pending_input(&mut self, input: &str, cx: &mut WindowContext) { + pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) { self.handler.replace_text_in_range(None, input, cx); } } diff --git a/crates/gpui/src/platform/blade/blade_atlas.rs b/crates/gpui/src/platform/blade/blade_atlas.rs index ccebc93b30..18c8a607b1 100644 --- a/crates/gpui/src/platform/blade/blade_atlas.rs +++ b/crates/gpui/src/platform/blade/blade_atlas.rs @@ -117,6 +117,7 @@ impl PlatformAtlas for BladeAtlas { if let Some(tile) = lock.tiles_by_key.get(key) { Ok(tile.clone()) } else { + profiling::scope!("new tile"); let (size, bytes) = build()?; let tile = lock.allocate(size, key.texture_kind()); lock.upload_texture(tile.texture_id, tile.bounds, &bytes); diff --git a/crates/gpui/src/platform/blade/blade_belt.rs b/crates/gpui/src/platform/blade/blade_belt.rs index 88f8cf8d80..322caaa3ee 100644 --- a/crates/gpui/src/platform/blade/blade_belt.rs +++ b/crates/gpui/src/platform/blade/blade_belt.rs @@ -39,6 +39,7 @@ impl BladeBelt { } } + #[profiling::function] pub fn alloc(&mut self, size: u64, gpu: &gpu::Context) -> gpu::BufferPiece { for &mut (ref rb, ref mut offset) in self.active.iter_mut() { let aligned = offset.next_multiple_of(self.desc.alignment); diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index d715bed895..554479ef1c 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -444,6 +444,7 @@ impl BladeRenderer { self.gpu.metal_layer().unwrap().as_ptr() } + #[profiling::function] fn rasterize_paths(&mut self, paths: &[Path]) { self.path_tiles.clear(); let mut vertices_by_texture_id = HashMap::default(); @@ -506,13 +507,16 @@ impl BladeRenderer { } pub fn draw(&mut self, scene: &Scene) { - let frame = self.gpu.acquire_frame(); self.command_encoder.start(); - self.command_encoder.init_texture(frame.texture()); - self.atlas.before_frame(&mut self.command_encoder); self.rasterize_paths(scene.paths()); + let frame = { + profiling::scope!("acquire frame"); + self.gpu.acquire_frame() + }; + self.command_encoder.init_texture(frame.texture()); + let globals = GlobalParams { viewport_size: [ self.viewport_size.width as f32, @@ -529,6 +533,7 @@ impl BladeRenderer { }], depth_stencil: None, }) { + profiling::scope!("render pass"); for batch in scene.batches() { match batch { PrimitiveBatch::Quads(quads) => { @@ -559,7 +564,7 @@ impl BladeRenderer { } PrimitiveBatch::Paths(paths) => { let mut encoder = pass.with(&self.pipelines.paths); - //todo!(linux): group by texture ID + // todo(linux): group by texture ID for path in paths { let tile = &self.path_tiles[&path.id]; let tex_info = self.atlas.get_texture_info(tile.texture_id); @@ -718,6 +723,7 @@ impl BladeRenderer { self.command_encoder.present(frame); let sync_point = self.gpu.submit(&mut self.command_encoder); + profiling::scope!("finish"); self.instance_belt.flush(&sync_point); self.atlas.after_frame(&sync_point); self.atlas.clear_textures(AtlasTextureKind::Path); diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index f342dfc92c..8413031655 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -215,6 +215,15 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { } let quad = b_quads[input.quad_id]; + // Fast path when the quad is not rounded and doesn't have any border. + if (quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 && + quad.corner_radii.top_right == 0.0 && + quad.corner_radii.bottom_right == 0.0 && quad.border_widths.top == 0.0 && + quad.border_widths.left == 0.0 && quad.border_widths.right == 0.0 && + quad.border_widths.bottom == 0.0) { + return input.background_color; + } + let half_size = quad.bounds.size / 2.0; let center = quad.bounds.origin + half_size; let center_to_point = input.position.xy - center; diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 9cdffd5e61..d6c4ea28a5 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -108,6 +108,35 @@ impl Keystroke { ime_key, }) } + + /// Returns a new keystroke with the ime_key filled. + /// This is used for dispatch_keystroke where we want users to + /// be able to simulate typing "space", etc. + pub fn with_simulated_ime(mut self) -> Self { + if self.ime_key.is_none() + && !self.modifiers.command + && !self.modifiers.control + && !self.modifiers.function + && !self.modifiers.alt + { + self.ime_key = match self.key.as_str() { + "space" => Some(" ".into()), + "tab" => Some("\t".into()), + "enter" => Some("\n".into()), + "up" | "down" | "left" | "right" | "pageup" | "pagedown" | "home" | "end" + | "delete" | "escape" | "backspace" | "f1" | "f2" | "f3" | "f4" | "f5" | "f6" + | "f7" | "f8" | "f9" | "f10" | "f11" | "f12" => None, + key => { + if self.modifiers.shift { + Some(key.to_uppercase()) + } else { + Some(key.into()) + } + } + } + } + self + } } impl std::fmt::Display for Keystroke { diff --git a/crates/gpui/src/platform/linux.rs b/crates/gpui/src/platform/linux.rs index f8d398587d..f334b23399 100644 --- a/crates/gpui/src/platform/linux.rs +++ b/crates/gpui/src/platform/linux.rs @@ -1,12 +1,12 @@ mod client; -mod client_dispatcher; mod dispatcher; mod platform; mod text_system; +mod util; mod wayland; mod x11; pub(crate) use dispatcher::*; pub(crate) use platform::*; pub(crate) use text_system::*; -pub(crate) use x11::*; +// pub(crate) use x11::*; diff --git a/crates/gpui/src/platform/linux/client.rs b/crates/gpui/src/platform/linux/client.rs index d314202972..b5d154b7a7 100644 --- a/crates/gpui/src/platform/linux/client.rs +++ b/crates/gpui/src/platform/linux/client.rs @@ -1,10 +1,12 @@ +use std::cell::RefCell; use std::rc::Rc; +use copypasta::ClipboardProvider; + use crate::platform::PlatformWindow; -use crate::{AnyWindowHandle, DisplayId, PlatformDisplay, WindowOptions}; +use crate::{AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, WindowOptions}; pub trait Client { - fn run(&self, on_finish_launching: Box); fn displays(&self) -> Vec>; fn display(&self, id: DisplayId) -> Option>; fn open_window( @@ -12,4 +14,7 @@ pub trait Client { handle: AnyWindowHandle, options: WindowOptions, ) -> Box; + fn set_cursor_style(&self, style: CursorStyle); + fn get_clipboard(&self) -> Rc>; + fn get_primary(&self) -> Rc>; } diff --git a/crates/gpui/src/platform/linux/client_dispatcher.rs b/crates/gpui/src/platform/linux/client_dispatcher.rs deleted file mode 100644 index 823e2df0b7..0000000000 --- a/crates/gpui/src/platform/linux/client_dispatcher.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub trait ClientDispatcher: Send + Sync { - fn dispatch_on_main_thread(&self); -} diff --git a/crates/gpui/src/platform/linux/dispatcher.rs b/crates/gpui/src/platform/linux/dispatcher.rs index bb96da6a8b..9dc0442035 100644 --- a/crates/gpui/src/platform/linux/dispatcher.rs +++ b/crates/gpui/src/platform/linux/dispatcher.rs @@ -1,49 +1,91 @@ #![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] #![allow(non_snake_case)] -//todo!(linux): remove +// todo(linux): remove #![allow(unused_variables)] -use crate::platform::linux::client_dispatcher::ClientDispatcher; use crate::{PlatformDispatcher, TaskLabel}; use async_task::Runnable; +use calloop::{ + channel::{self, Sender}, + timer::TimeoutAction, + EventLoop, +}; use parking::{Parker, Unparker}; use parking_lot::Mutex; -use std::{ - panic, - sync::Arc, - thread, - time::{Duration, Instant}, -}; +use std::{thread, time::Duration}; +use util::ResultExt; + +struct TimerAfter { + duration: Duration, + runnable: Runnable, +} pub(crate) struct LinuxDispatcher { - client_dispatcher: Arc, parker: Mutex, - timed_tasks: Mutex>, - main_sender: flume::Sender, + main_sender: Sender, + timer_sender: Sender, background_sender: flume::Sender, - _background_thread: thread::JoinHandle<()>, + _background_threads: Vec>, main_thread_id: thread::ThreadId, } impl LinuxDispatcher { - pub fn new( - main_sender: flume::Sender, - client_dispatcher: &Arc, - ) -> Self { + pub fn new(main_sender: Sender) -> Self { let (background_sender, background_receiver) = flume::unbounded::(); - let background_thread = thread::spawn(move || { - for runnable in background_receiver { - let _ignore_panic = panic::catch_unwind(|| runnable.run()); - } + let thread_count = std::thread::available_parallelism() + .map(|i| i.get()) + .unwrap_or(1); + + let mut background_threads = (0..thread_count) + .map(|_| { + let receiver = background_receiver.clone(); + std::thread::spawn(move || { + for runnable in receiver { + runnable.run(); + } + }) + }) + .collect::>(); + + let (timer_sender, timer_channel) = calloop::channel::channel::(); + let timer_thread = std::thread::spawn(|| { + let mut event_loop: EventLoop<()> = + EventLoop::try_new().expect("Failed to initialize timer loop!"); + + let handle = event_loop.handle(); + let timer_handle = event_loop.handle(); + handle + .insert_source(timer_channel, move |e, _, _| { + if let channel::Event::Msg(timer) = e { + // This has to be in an option to satisfy the borrow checker. The callback below should only be scheduled once. + let mut runnable = Some(timer.runnable); + timer_handle + .insert_source( + calloop::timer::Timer::from_duration(timer.duration), + move |e, _, _| { + if let Some(runnable) = runnable.take() { + runnable.run(); + } + TimeoutAction::Drop + }, + ) + .expect("Failed to start timer"); + } + }) + .expect("Failed to start timer thread"); + + event_loop.run(None, &mut (), |_| {}).log_err(); }); + + background_threads.push(timer_thread); + Self { - client_dispatcher: Arc::clone(client_dispatcher), parker: Mutex::new(Parker::new()), - timed_tasks: Mutex::new(Vec::new()), main_sender, + timer_sender, background_sender, - _background_thread: background_thread, + _background_threads: background_threads, main_thread_id: thread::current().id(), } } @@ -59,29 +101,19 @@ impl PlatformDispatcher for LinuxDispatcher { } fn dispatch_on_main_thread(&self, runnable: Runnable) { - self.main_sender.send(runnable).unwrap(); - self.client_dispatcher.dispatch_on_main_thread(); + self.main_sender + .send(runnable) + .expect("Main thread is gone"); } fn dispatch_after(&self, duration: Duration, runnable: Runnable) { - let moment = Instant::now() + duration; - let mut timed_tasks = self.timed_tasks.lock(); - timed_tasks.push((moment, runnable)); - timed_tasks.sort_unstable_by(|(a, _), (b, _)| b.cmp(a)); + self.timer_sender + .send(TimerAfter { duration, runnable }) + .expect("Timer thread has died"); } fn tick(&self, background_only: bool) -> bool { - let mut timed_tasks = self.timed_tasks.lock(); - let old_count = timed_tasks.len(); - while let Some(&(moment, _)) = timed_tasks.last() { - if moment <= Instant::now() { - let (_, runnable) = timed_tasks.pop().unwrap(); - runnable.run(); - } else { - break; - } - } - timed_tasks.len() != old_count + false } fn park(&self) { diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index e6f9dc4894..2a7b8bfb2e 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -1,5 +1,6 @@ #![allow(unused)] +use std::cell::RefCell; use std::env; use std::{ path::{Path, PathBuf}, @@ -8,8 +9,10 @@ use std::{ time::Duration, }; +use anyhow::anyhow; use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest}; use async_task::Runnable; +use calloop::{EventLoop, LoopHandle, LoopSignal}; use flume::{Receiver, Sender}; use futures::channel::oneshot; use parking_lot::Mutex; @@ -17,9 +20,7 @@ use time::UtcOffset; use wayland_client::Connection; use crate::platform::linux::client::Client; -use crate::platform::linux::client_dispatcher::ClientDispatcher; -use crate::platform::linux::wayland::{WaylandClient, WaylandClientDispatcher}; -use crate::platform::{X11Client, X11ClientDispatcher, XcbAtoms}; +use crate::platform::linux::wayland::WaylandClient; use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, Keymap, LinuxDispatcher, LinuxTextSystem, Menu, PathPromptOptions, @@ -27,12 +28,14 @@ use crate::{ SemanticVersion, Task, WindowOptions, }; +use super::x11::X11Client; + #[derive(Default)] pub(crate) struct Callbacks { open_urls: Option)>>, become_active: Option>, resign_active: Option>, - pub(crate) quit: Option>, + quit: Option>, reopen: Option>, event: Option bool>>, app_menu_action: Option>, @@ -41,12 +44,13 @@ pub(crate) struct Callbacks { } pub(crate) struct LinuxPlatformInner { + pub(crate) event_loop: RefCell>, + pub(crate) loop_handle: Rc>, + pub(crate) loop_signal: LoopSignal, pub(crate) background_executor: BackgroundExecutor, pub(crate) foreground_executor: ForegroundExecutor, - pub(crate) main_receiver: flume::Receiver, pub(crate) text_system: Arc, - pub(crate) callbacks: Mutex, - pub(crate) state: Mutex, + pub(crate) callbacks: RefCell, } pub(crate) struct LinuxPlatform { @@ -54,10 +58,6 @@ pub(crate) struct LinuxPlatform { inner: Rc, } -pub(crate) struct LinuxPlatformState { - pub(crate) quit_requested: bool, -} - impl Default for LinuxPlatform { fn default() -> Self { Self::new() @@ -69,94 +69,47 @@ impl LinuxPlatform { let wayland_display = env::var_os("WAYLAND_DISPLAY"); let use_wayland = wayland_display.is_some() && !wayland_display.unwrap().is_empty(); - let (main_sender, main_receiver) = flume::unbounded::(); + let (main_sender, main_receiver) = calloop::channel::channel::(); let text_system = Arc::new(LinuxTextSystem::new()); - let callbacks = Mutex::new(Callbacks::default()); - let state = Mutex::new(LinuxPlatformState { - quit_requested: false, + let callbacks = RefCell::new(Callbacks::default()); + + let event_loop = EventLoop::try_new().unwrap(); + event_loop + .handle() + .insert_source(main_receiver, |event, _, _| { + if let calloop::channel::Event::Msg(runnable) = event { + runnable.run(); + } + }); + + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender)); + + let inner = Rc::new(LinuxPlatformInner { + loop_handle: Rc::new(event_loop.handle()), + loop_signal: event_loop.get_signal(), + event_loop: RefCell::new(event_loop), + background_executor: BackgroundExecutor::new(dispatcher.clone()), + foreground_executor: ForegroundExecutor::new(dispatcher.clone()), + text_system, + callbacks, }); if use_wayland { - Self::new_wayland(main_sender, main_receiver, text_system, callbacks, state) + Self { + client: Rc::new(WaylandClient::new(Rc::clone(&inner))), + inner, + } } else { - Self::new_x11(main_sender, main_receiver, text_system, callbacks, state) - } - } - - fn new_wayland( - main_sender: Sender, - main_receiver: Receiver, - text_system: Arc, - callbacks: Mutex, - state: Mutex, - ) -> Self { - let conn = Arc::new(Connection::connect_to_env().unwrap()); - let client_dispatcher: Arc = - Arc::new(WaylandClientDispatcher::new(&conn)); - let dispatcher = Arc::new(LinuxDispatcher::new(main_sender, &client_dispatcher)); - let inner = Rc::new(LinuxPlatformInner { - background_executor: BackgroundExecutor::new(dispatcher.clone()), - foreground_executor: ForegroundExecutor::new(dispatcher.clone()), - main_receiver, - text_system, - callbacks, - state, - }); - let client = Rc::new(WaylandClient::new(Rc::clone(&inner), Arc::clone(&conn))); - Self { - client, - inner: Rc::clone(&inner), - } - } - - fn new_x11( - main_sender: Sender, - main_receiver: Receiver, - text_system: Arc, - callbacks: Mutex, - state: Mutex, - ) -> Self { - let (xcb_connection, x_root_index) = xcb::Connection::connect_with_extensions( - None, - &[xcb::Extension::Present, xcb::Extension::Xkb], - &[], - ) - .unwrap(); - - let xkb_ver = xcb_connection - .wait_for_reply(xcb_connection.send_request(&xcb::xkb::UseExtension { - wanted_major: xcb::xkb::MAJOR_VERSION as u16, - wanted_minor: xcb::xkb::MINOR_VERSION as u16, - })) - .unwrap(); - assert!(xkb_ver.supported()); - - let atoms = XcbAtoms::intern_all(&xcb_connection).unwrap(); - let xcb_connection = Arc::new(xcb_connection); - let client_dispatcher: Arc = - Arc::new(X11ClientDispatcher::new(&xcb_connection, x_root_index)); - let dispatcher = Arc::new(LinuxDispatcher::new(main_sender, &client_dispatcher)); - let inner = Rc::new(LinuxPlatformInner { - background_executor: BackgroundExecutor::new(dispatcher.clone()), - foreground_executor: ForegroundExecutor::new(dispatcher.clone()), - main_receiver, - text_system, - callbacks, - state, - }); - let client = Rc::new(X11Client::new( - Rc::clone(&inner), - xcb_connection, - x_root_index, - atoms, - )); - Self { - client, - inner: Rc::clone(&inner), + Self { + client: X11Client::new(Rc::clone(&inner)), + inner, + } } } } +const KEYRING_LABEL: &str = "zed-github-account"; + impl Platform for LinuxPlatform { fn background_executor(&self) -> BackgroundExecutor { self.inner.background_executor.clone() @@ -171,26 +124,36 @@ impl Platform for LinuxPlatform { } fn run(&self, on_finish_launching: Box) { - self.client.run(on_finish_launching) + on_finish_launching(); + + self.inner + .event_loop + .borrow_mut() + .run(None, &mut (), |&mut ()| {}) + .expect("Run loop failed"); + + if let Some(mut fun) = self.inner.callbacks.borrow_mut().quit.take() { + fun(); + } } fn quit(&self) { - self.inner.state.lock().quit_requested = true; + self.inner.loop_signal.stop(); } - //todo!(linux) + // todo(linux) fn restart(&self) {} - //todo!(linux) + // todo(linux) fn activate(&self, ignoring_other_apps: bool) {} - //todo!(linux) + // todo(linux) fn hide(&self) {} - //todo!(linux) + // todo(linux) fn hide_other_apps(&self) {} - //todo!(linux) + // todo(linux) fn unhide_other_apps(&self) {} fn displays(&self) -> Vec> { @@ -201,7 +164,7 @@ impl Platform for LinuxPlatform { self.client.display(id) } - //todo!(linux) + // todo(linux) fn active_window(&self) -> Option { None } @@ -219,7 +182,7 @@ impl Platform for LinuxPlatform { } fn on_open_urls(&self, callback: Box)>) { - self.inner.callbacks.lock().open_urls = Some(callback); + self.inner.callbacks.borrow_mut().open_urls = Some(callback); } fn prompt_for_paths( @@ -227,7 +190,8 @@ impl Platform for LinuxPlatform { options: PathPromptOptions, ) -> oneshot::Receiver>> { let (done_tx, done_rx) = oneshot::channel(); - self.foreground_executor() + self.inner + .foreground_executor .spawn(async move { let title = if options.multiple { if !options.files { @@ -270,7 +234,8 @@ impl Platform for LinuxPlatform { fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { let (done_tx, done_rx) = oneshot::channel(); let directory = directory.to_owned(); - self.foreground_executor() + self.inner + .foreground_executor .spawn(async move { let result = SaveFileRequest::default() .modal(true) @@ -294,39 +259,45 @@ impl Platform for LinuxPlatform { } fn reveal_path(&self, path: &Path) { - open::that(path); + if path.is_dir() { + open::that(path); + return; + } + // If `path` is a file, the system may try to open it in a text editor + let dir = path.parent().unwrap_or(Path::new("")); + open::that(dir); } fn on_become_active(&self, callback: Box) { - self.inner.callbacks.lock().become_active = Some(callback); + self.inner.callbacks.borrow_mut().become_active = Some(callback); } fn on_resign_active(&self, callback: Box) { - self.inner.callbacks.lock().resign_active = Some(callback); + self.inner.callbacks.borrow_mut().resign_active = Some(callback); } fn on_quit(&self, callback: Box) { - self.inner.callbacks.lock().quit = Some(callback); + self.inner.callbacks.borrow_mut().quit = Some(callback); } fn on_reopen(&self, callback: Box) { - self.inner.callbacks.lock().reopen = Some(callback); + self.inner.callbacks.borrow_mut().reopen = Some(callback); } fn on_event(&self, callback: Box bool>) { - self.inner.callbacks.lock().event = Some(callback); + self.inner.callbacks.borrow_mut().event = Some(callback); } fn on_app_menu_action(&self, callback: Box) { - self.inner.callbacks.lock().app_menu_action = Some(callback); + self.inner.callbacks.borrow_mut().app_menu_action = Some(callback); } fn on_will_open_app_menu(&self, callback: Box) { - self.inner.callbacks.lock().will_open_app_menu = Some(callback); + self.inner.callbacks.borrow_mut().will_open_app_menu = Some(callback); } fn on_validate_app_menu_command(&self, callback: Box bool>) { - self.inner.callbacks.lock().validate_app_menu_command = Some(callback); + self.inner.callbacks.borrow_mut().validate_app_menu_command = Some(callback); } fn os_name(&self) -> &'static str { @@ -353,52 +324,127 @@ impl Platform for LinuxPlatform { }) } + //todo!(linux) fn app_path(&self) -> Result { - unimplemented!() + Err(anyhow::Error::msg( + "Platform::app_path is not implemented yet", + )) } - //todo!(linux) + // todo(linux) fn set_menus(&self, menus: Vec, keymap: &Keymap) {} fn local_timezone(&self) -> UtcOffset { UtcOffset::UTC } + //todo!(linux) fn path_for_auxiliary_executable(&self, name: &str) -> Result { - unimplemented!() + Err(anyhow::Error::msg( + "Platform::path_for_auxiliary_executable is not implemented yet", + )) } - //todo!(linux) - fn set_cursor_style(&self, style: CursorStyle) {} + fn set_cursor_style(&self, style: CursorStyle) { + self.client.set_cursor_style(style) + } - //todo!(linux) + // todo(linux) fn should_auto_hide_scrollbars(&self) -> bool { false } - //todo!(linux) - fn write_to_clipboard(&self, item: ClipboardItem) {} + fn write_to_clipboard(&self, item: ClipboardItem) { + let clipboard = self.client.get_clipboard(); + clipboard.borrow_mut().set_contents(item.text); + } - //todo!(linux) fn read_from_clipboard(&self) -> Option { - None + let clipboard = self.client.get_clipboard(); + let contents = clipboard.borrow_mut().get_contents(); + match contents { + Ok(text) => Some(ClipboardItem { + metadata: None, + text, + }), + _ => None, + } } fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { - unimplemented!() + let url = url.to_string(); + let username = username.to_string(); + let password = password.to_vec(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + keyring + .create_item( + KEYRING_LABEL, + &vec![("url", &url), ("username", &username)], + password, + true, + ) + .await?; + Ok(()) + }) } + //todo!(linux): add trait methods for accessing the primary selection + fn read_credentials(&self, url: &str) -> Task)>>> { - unimplemented!() + let url = url.to_string(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + + let items = keyring.search_items(&vec![("url", &url)]).await?; + + for item in items.into_iter() { + if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) { + let attributes = item.attributes().await?; + let username = attributes + .get("username") + .ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?; + let secret = item.secret().await?; + + // we lose the zeroizing capabilities at this boundary, + // a current limitation GPUI's credentials api + return Ok(Some((username.to_string(), secret.to_vec()))); + } else { + continue; + } + } + Ok(None) + }) } fn delete_credentials(&self, url: &str) -> Task> { - unimplemented!() + let url = url.to_string(); + self.background_executor().spawn(async move { + let keyring = oo7::Keyring::new().await?; + keyring.unlock().await?; + + let items = keyring.search_items(&vec![("url", &url)]).await?; + + for item in items.into_iter() { + if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) { + item.delete().await?; + return Ok(()); + } + } + + Ok(()) + }) } fn window_appearance(&self) -> crate::WindowAppearance { crate::WindowAppearance::Light } + + fn register_url_scheme(&self, _: &str) -> Task> { + Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) + } } #[cfg(test)] diff --git a/crates/gpui/src/platform/linux/text_system.rs b/crates/gpui/src/platform/linux/text_system.rs index 683ef6a662..70caa7175b 100644 --- a/crates/gpui/src/platform/linux/text_system.rs +++ b/crates/gpui/src/platform/linux/text_system.rs @@ -1,22 +1,21 @@ -//todo!(linux) remove -#[allow(unused)] -use crate::{point, size, FontStyle, FontWeight, Point, ShapedGlyph}; use crate::{ - Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, GlyphId, LineLayout, - Pixels, PlatformTextSystem, RenderGlyphParams, SharedString, Size, + point, size, Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, + FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, + ShapedGlyph, SharedString, Size, }; -use anyhow::Ok; -use anyhow::Result; -use anyhow::{anyhow, Context}; +use anyhow::{anyhow, Context, Ok, Result}; use collections::HashMap; -use cosmic_text::fontdb::Query; use cosmic_text::{ - Attrs, AttrsList, BufferLine, CacheKey, Family, Font as CosmicTextFont, FontSystem, SwashCache, + fontdb::Query, Attrs, AttrsList, BufferLine, CacheKey, Family, Font as CosmicTextFont, + FontSystem, SwashCache, }; + +use itertools::Itertools; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; -use pathfinder_geometry::rect::RectF; -use pathfinder_geometry::rect::RectI; -use pathfinder_geometry::vector::{Vector2F, Vector2I}; +use pathfinder_geometry::{ + rect::{RectF, RectI}, + vector::{Vector2F, Vector2I}, +}; use smallvec::SmallVec; use std::{borrow::Cow, sync::Arc}; @@ -35,7 +34,7 @@ impl LinuxTextSystem { pub(crate) fn new() -> Self { let mut font_system = FontSystem::new(); - // todo!(linux) make font loading non-blocking + // todo(linux) make font loading non-blocking font_system.db_mut().load_system_fonts(); Self(RwLock::new(LinuxTextSystemState { @@ -62,7 +61,7 @@ impl PlatformTextSystem for LinuxTextSystem { self.0.write().add_fonts(fonts) } - // todo!(linux) ensure that this integrates with platform font loading + // todo(linux) ensure that this integrates with platform font loading // do we need to do more than call load_system_fonts()? fn all_font_names(&self) -> Vec { self.0 @@ -74,13 +73,18 @@ impl PlatformTextSystem for LinuxTextSystem { .collect() } - // todo!(linux) fn all_font_families(&self) -> Vec { - Vec::new() + self.0 + .read() + .font_system + .db() + .faces() + .filter_map(|face| Some(face.families.get(0)?.0.clone())) + .collect_vec() } fn font_id(&self, font: &Font) -> Result { - // todo!(linux): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit? + // todo(linux): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit? let lock = self.0.upgradable_read(); if let Some(font_id) = lock.font_selections.get(font) { Ok(*font_id) @@ -130,13 +134,13 @@ impl PlatformTextSystem for LinuxTextSystem { FontMetrics { units_per_em: metrics.units_per_em as u32, ascent: metrics.ascent, - descent: -metrics.descent, // todo!(linux) confirm this is correct + descent: -metrics.descent, // todo(linux) confirm this is correct line_gap: metrics.leading, underline_position: metrics.underline_offset, underline_thickness: metrics.stroke_size, cap_height: metrics.cap_height, x_height: metrics.x_height, - // todo!(linux): Compute this correctly + // todo(linux): Compute this correctly bounding_box: Bounds { origin: point(0.0, 0.0), size: size(metrics.max_width, metrics.ascent + metrics.descent), @@ -149,7 +153,7 @@ impl PlatformTextSystem for LinuxTextSystem { let metrics = lock.fonts[font_id.0].as_swash().metrics(&[]); let glyph_metrics = lock.fonts[font_id.0].as_swash().glyph_metrics(&[]); let glyph_id = glyph_id.0 as u16; - // todo!(linux): Compute this correctly + // todo(linux): Compute this correctly // see https://github.com/servo/font-kit/blob/master/src/loaders/freetype.rs#L614-L620 Ok(Bounds { origin: point(0.0, 0.0), @@ -184,7 +188,7 @@ impl PlatformTextSystem for LinuxTextSystem { self.0.write().layout_line(text, font_size, runs) } - // todo!(linux) Confirm that this has been superseded by the LineWrapper + // todo(linux) Confirm that this has been superseded by the LineWrapper fn wrap_line( &self, text: &str, @@ -197,6 +201,7 @@ impl PlatformTextSystem for LinuxTextSystem { } impl LinuxTextSystemState { + #[profiling::function] fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { let db = self.font_system.db_mut(); for bytes in fonts { @@ -212,6 +217,7 @@ impl LinuxTextSystemState { Ok(()) } + #[profiling::function] fn load_family( &mut self, name: &SharedString, @@ -257,7 +263,7 @@ impl LinuxTextSystemState { } fn is_emoji(&self, font_id: FontId) -> bool { - // todo!(linux): implement this correctly + // todo(linux): implement this correctly self.postscript_names_by_font_id .get(&font_id) .map_or(false, |postscript_name| { @@ -265,7 +271,7 @@ impl LinuxTextSystemState { }) } - // todo!(linux) both raster functions have problems because I am not sure this is the correct mapping from cosmic text to gpui system + // todo(linux) both raster functions have problems because I am not sure this is the correct mapping from cosmic text to gpui system fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result> { let font = &self.fonts[params.font_id.0]; let font_system = &mut self.font_system; @@ -289,6 +295,7 @@ impl LinuxTextSystemState { }) } + #[profiling::function] fn rasterize_glyph( &mut self, params: &RenderGlyphParams, @@ -297,7 +304,7 @@ impl LinuxTextSystemState { if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { Err(anyhow!("glyph bounds are empty")) } else { - // todo!(linux) handle subpixel variants + // todo(linux) handle subpixel variants let bitmap_size = glyph_bounds.size; let font = &self.fonts[params.font_id.0]; let font_system = &mut self.font_system; @@ -320,16 +327,17 @@ impl LinuxTextSystemState { } } - // todo!(linux) This is all a quick first pass, maybe we should be using cosmic_text::Buffer + // todo(linux) This is all a quick first pass, maybe we should be using cosmic_text::Buffer + #[profiling::function] fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { let mut attrs_list = AttrsList::new(Attrs::new()); let mut offs = 0; for run in font_runs { - // todo!(linux) We need to check we are doing utf properly + // todo(linux) We need to check we are doing utf properly let font = &self.fonts[run.font_id.0]; let font = self.font_system.db().face(font.id()).unwrap(); attrs_list.add_span( - offs..run.len, + offs..offs + run.len, Attrs::new() .family(Family::Name(&font.families.first().unwrap().0)) .stretch(font.stretch) @@ -342,11 +350,11 @@ impl LinuxTextSystemState { let layout = line.layout( &mut self.font_system, font_size.0, - f32::MAX, // todo!(linux) we don't have a width cause this should technically not be wrapped I believe + f32::MAX, // todo(linux) we don't have a width cause this should technically not be wrapped I believe cosmic_text::Wrap::None, ); let mut runs = Vec::new(); - // todo!(linux) what I think can happen is layout returns possibly multiple lines which means we should be probably working with it higher up in the text rendering + // todo(linux) what I think can happen is layout returns possibly multiple lines which means we should be probably working with it higher up in the text rendering let layout = layout.first().unwrap(); for glyph in &layout.glyphs { let font_id = glyph.font_id; @@ -357,7 +365,7 @@ impl LinuxTextSystemState { .unwrap(), ); let mut glyphs = SmallVec::new(); - // todo!(linux) this is definitely wrong, each glyph in glyphs from cosmic-text is a cluster with one glyph, ShapedRun takes a run of glyphs with the same font and direction + // todo(linux) this is definitely wrong, each glyph in glyphs from cosmic-text is a cluster with one glyph, ShapedRun takes a run of glyphs with the same font and direction glyphs.push(ShapedGlyph { id: GlyphId(glyph.glyph_id as u32), position: point((glyph.x).into(), glyph.y.into()), diff --git a/crates/gpui/src/platform/linux/util.rs b/crates/gpui/src/platform/linux/util.rs new file mode 100644 index 0000000000..f5b90541f0 --- /dev/null +++ b/crates/gpui/src/platform/linux/util.rs @@ -0,0 +1,90 @@ +use xkbcommon::xkb::{self, Keycode, Keysym, State}; + +use crate::{Keystroke, Modifiers}; + +impl Keystroke { + pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self { + let mut modifiers = modifiers; + + let key_utf32 = state.key_get_utf32(keycode); + let key_utf8 = state.key_get_utf8(keycode); + let key_sym = state.key_get_one_sym(keycode); + + // The logic here tries to replicate the logic in `../mac/events.rs` + // "Consumed" modifiers are modifiers that have been used to translate a key, for example + // pressing "shift" and "1" on US layout produces the key `!` but "consumes" the shift. + // Notes: + // - macOS gets the key character directly ("."), xkb gives us the key name ("period") + // - macOS logic removes consumed shift modifier for symbols: "{", not "shift-{" + // - macOS logic keeps consumed shift modifiers for letters: "shift-a", not "a" or "A" + + let mut handle_consumed_modifiers = true; + let key = match key_sym { + Keysym::Return => "enter".to_owned(), + Keysym::Prior => "pageup".to_owned(), + Keysym::Next => "pagedown".to_owned(), + + Keysym::comma => ",".to_owned(), + Keysym::period => ".".to_owned(), + Keysym::less => "<".to_owned(), + Keysym::greater => ">".to_owned(), + Keysym::slash => "/".to_owned(), + Keysym::question => "?".to_owned(), + + Keysym::semicolon => ";".to_owned(), + Keysym::colon => ":".to_owned(), + Keysym::apostrophe => "'".to_owned(), + Keysym::quotedbl => "\"".to_owned(), + + Keysym::bracketleft => "[".to_owned(), + Keysym::braceleft => "{".to_owned(), + Keysym::bracketright => "]".to_owned(), + Keysym::braceright => "}".to_owned(), + Keysym::backslash => "\\".to_owned(), + Keysym::bar => "|".to_owned(), + + Keysym::grave => "`".to_owned(), + Keysym::asciitilde => "~".to_owned(), + Keysym::exclam => "!".to_owned(), + Keysym::at => "@".to_owned(), + Keysym::numbersign => "#".to_owned(), + Keysym::dollar => "$".to_owned(), + Keysym::percent => "%".to_owned(), + Keysym::asciicircum => "^".to_owned(), + Keysym::ampersand => "&".to_owned(), + Keysym::asterisk => "*".to_owned(), + Keysym::parenleft => "(".to_owned(), + Keysym::parenright => ")".to_owned(), + Keysym::minus => "-".to_owned(), + Keysym::underscore => "_".to_owned(), + Keysym::equal => "=".to_owned(), + Keysym::plus => "+".to_owned(), + + _ => { + handle_consumed_modifiers = false; + xkb::keysym_get_name(key_sym).to_lowercase() + } + }; + + // Ignore control characters (and DEL) for the purposes of ime_key, + // but if key_utf32 is 0 then assume it isn't one + let ime_key = ((key_utf32 == 0 || (key_utf32 >= 32 && key_utf32 != 127)) + && !key_utf8.is_empty()) + .then_some(key_utf8); + + if handle_consumed_modifiers { + let mod_shift_index = state.get_keymap().mod_get_index(xkb::MOD_NAME_SHIFT); + let is_shift_consumed = state.mod_index_is_consumed(keycode, mod_shift_index); + + if modifiers.shift && is_shift_consumed { + modifiers.shift = false; + } + } + + Keystroke { + modifiers, + key, + ime_key, + } + } +} diff --git a/crates/gpui/src/platform/linux/wayland.rs b/crates/gpui/src/platform/linux/wayland.rs index 0078289ba2..ebb592d375 100644 --- a/crates/gpui/src/platform/linux/wayland.rs +++ b/crates/gpui/src/platform/linux/wayland.rs @@ -1,10 +1,9 @@ -//todo!(linux): remove this once the relevant functionality has been implemented +// todo(linux): remove this once the relevant functionality has been implemented #![allow(unused_variables)] pub(crate) use client::*; -pub(crate) use client_dispatcher::*; mod client; -mod client_dispatcher; +mod cursor; mod display; mod window; diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 0a20dfd871..e8f20ebc0e 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1,9 +1,16 @@ +use std::cell::RefCell; use std::rc::Rc; use std::sync::Arc; +use std::time::Duration; -use parking_lot::Mutex; +use calloop::timer::{TimeoutAction, Timer}; +use calloop::LoopHandle; +use calloop_wayland_source::WaylandSource; +use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary}; +use copypasta::ClipboardProvider; use wayland_backend::client::ObjectId; use wayland_backend::protocol::WEnum; +use wayland_client::globals::{registry_queue_init, GlobalListContents}; use wayland_client::protocol::wl_callback::WlCallback; use wayland_client::protocol::wl_pointer::AxisRelativeDirection; use wayland_client::{ @@ -13,67 +20,120 @@ use wayland_client::{ wl_shm, wl_shm_pool, wl_surface::{self, WlSurface}, }, - Connection, Dispatch, EventQueue, Proxy, QueueHandle, + Connection, Dispatch, Proxy, QueueHandle, }; use wayland_protocols::wp::fractional_scale::v1::client::{ wp_fractional_scale_manager_v1, wp_fractional_scale_v1, }; use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter}; +use wayland_protocols::xdg::decoration::zv1::client::{ + zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1, +}; use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; -use xkbcommon::xkb; use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; -use xkbcommon::xkb::{Keycode, KEYMAP_COMPILE_NO_FLAGS}; +use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS}; use crate::platform::linux::client::Client; -use crate::platform::linux::wayland::window::WaylandWindow; +use crate::platform::linux::wayland::cursor::Cursor; +use crate::platform::linux::wayland::window::{WaylandDecorationState, WaylandWindow}; use crate::platform::{LinuxPlatformInner, PlatformWindow}; -use crate::ScrollDelta; use crate::{ - platform::linux::wayland::window::WaylandWindowState, AnyWindowHandle, DisplayId, KeyDownEvent, - KeyUpEvent, Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - Pixels, PlatformDisplay, PlatformInput, Point, ScrollWheelEvent, TouchPhase, WindowOptions, + platform::linux::wayland::window::WaylandWindowState, AnyWindowHandle, CursorStyle, DisplayId, + KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, ScrollDelta, + ScrollWheelEvent, TouchPhase, WindowOptions, }; -const MIN_KEYCODE: u32 = 8; // used to convert evdev scancode to xkb scancode +/// Used to convert evdev scancode to xkb scancode +const MIN_KEYCODE: u32 = 8; -pub(crate) struct WaylandClientState { - compositor: Option, - buffer: Option, - wm_base: Option, +pub(crate) struct WaylandClientStateInner { + compositor: wl_compositor::WlCompositor, + wm_base: xdg_wm_base::XdgWmBase, + shm: wl_shm::WlShm, viewporter: Option, fractional_scale_manager: Option, + decoration_manager: Option, windows: Vec<(xdg_surface::XdgSurface, Rc)>, platform_inner: Rc, - wl_seat: Option, keymap_state: Option, + repeat: KeyRepeat, modifiers: Modifiers, scroll_direction: f64, mouse_location: Option>, button_pressed: Option, mouse_focused_window: Option>, keyboard_focused_window: Option>, + loop_handle: Rc>, +} + +pub(crate) struct CursorState { + cursor_icon_name: String, + cursor: Option, +} + +#[derive(Clone)] +pub(crate) struct WaylandClientState { + client_state_inner: Rc>, + cursor_state: Rc>, + clipboard: Rc>, + primary: Rc>, +} + +pub(crate) struct KeyRepeat { + characters_per_second: u32, + delay: Duration, + current_id: u64, + current_keysym: Option, } pub(crate) struct WaylandClient { platform_inner: Rc, - conn: Arc, - state: Mutex, - event_queue: Mutex>, + state: WaylandClientState, qh: Arc>, } +const WL_SEAT_VERSION: u32 = 4; + impl WaylandClient { - pub(crate) fn new(linux_platform_inner: Rc, conn: Arc) -> Self { - let state = WaylandClientState { - compositor: None, - buffer: None, - wm_base: None, - viewporter: None, - fractional_scale_manager: None, + pub(crate) fn new(linux_platform_inner: Rc) -> Self { + let conn = Connection::connect_to_env().unwrap(); + + let (globals, mut event_queue) = registry_queue_init::(&conn).unwrap(); + let qh = event_queue.handle(); + + globals.contents().with_list(|list| { + for global in list { + if global.interface == "wl_seat" { + globals.registry().bind::( + global.name, + WL_SEAT_VERSION, + &qh, + (), + ); + } + } + }); + + let display = conn.backend().display_ptr() as *mut std::ffi::c_void; + let (primary, clipboard) = unsafe { create_clipboards_from_external(display) }; + + let mut state_inner = Rc::new(RefCell::new(WaylandClientStateInner { + compositor: globals.bind(&qh, 1..=1, ()).unwrap(), + wm_base: globals.bind(&qh, 1..=1, ()).unwrap(), + shm: globals.bind(&qh, 1..=1, ()).unwrap(), + viewporter: globals.bind(&qh, 1..=1, ()).ok(), + fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), + decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), windows: Vec::new(), platform_inner: Rc::clone(&linux_platform_inner), - wl_seat: None, keymap_state: None, + repeat: KeyRepeat { + characters_per_second: 16, + delay: Duration::from_millis(500), + current_id: 0, + current_keysym: None, + }, modifiers: Modifiers { shift: false, control: false, @@ -86,41 +146,39 @@ impl WaylandClient { button_pressed: None, mouse_focused_window: None, keyboard_focused_window: None, + loop_handle: Rc::clone(&linux_platform_inner.loop_handle), + })); + + let mut cursor_state = Rc::new(RefCell::new(CursorState { + cursor_icon_name: "arrow".to_string(), + cursor: None, + })); + + let source = WaylandSource::new(conn, event_queue); + + let mut state = WaylandClientState { + client_state_inner: Rc::clone(&state_inner), + cursor_state: Rc::clone(&cursor_state), + clipboard: Rc::new(RefCell::new(clipboard)), + primary: Rc::new(RefCell::new(primary)), }; - let event_queue: EventQueue = conn.new_event_queue(); - let qh = event_queue.handle(); + let mut state_loop = state.clone(); + linux_platform_inner + .loop_handle + .insert_source(source, move |_, queue, _| { + queue.dispatch_pending(&mut state_loop) + }) + .unwrap(); + Self { platform_inner: linux_platform_inner, - conn, - state: Mutex::new(state), - event_queue: Mutex::new(event_queue), + state, qh: Arc::new(qh), } } } impl Client for WaylandClient { - fn run(&self, on_finish_launching: Box) { - let display = self.conn.display(); - let mut eq = self.event_queue.lock(); - let _registry = display.get_registry(&self.qh, ()); - - eq.roundtrip(&mut self.state.lock()).unwrap(); - - on_finish_launching(); - while !self.platform_inner.state.lock().quit_requested { - eq.flush().unwrap(); - eq.dispatch_pending(&mut self.state.lock()).unwrap(); - if let Some(guard) = self.conn.prepare_read() { - guard.read().unwrap(); - eq.dispatch_pending(&mut self.state.lock()).unwrap(); - } - if let Ok(runnable) = self.platform_inner.main_receiver.try_recv() { - runnable.run(); - } - } - } - fn displays(&self) -> Vec> { Vec::new() } @@ -134,15 +192,38 @@ impl Client for WaylandClient { handle: AnyWindowHandle, options: WindowOptions, ) -> Box { - let mut state = self.state.lock(); + let mut state = self.state.client_state_inner.borrow_mut(); - let wm_base = state.wm_base.as_ref().unwrap(); - let compositor = state.compositor.as_ref().unwrap(); - let wl_surface = compositor.create_surface(&self.qh, ()); - let xdg_surface = wm_base.get_xdg_surface(&wl_surface, &self.qh, ()); + let wl_surface = state.compositor.create_surface(&self.qh, ()); + let xdg_surface = state.wm_base.get_xdg_surface(&wl_surface, &self.qh, ()); let toplevel = xdg_surface.get_toplevel(&self.qh, ()); let wl_surface = Arc::new(wl_surface); + // Attempt to set up window decorations based on the requested configuration + // + // Note that wayland compositors may either not support decorations at all, or may + // support them but not allow clients to choose whether they are enabled or not. + // We attempt to account for these cases here. + + if let Some(decoration_manager) = state.decoration_manager.as_ref() { + // The protocol for managing decorations is present at least, but that doesn't + // mean that the compositor will allow us to use it. + + let decoration = + decoration_manager.get_toplevel_decoration(&toplevel, &self.qh, xdg_surface.id()); + + // todo(linux) - options.titlebar is lacking information required for wayland. + // Especially, whether a titlebar is wanted in itself. + // + // Removing the titlebar also removes the entire window frame (ie. the ability to + // close, move and resize the window [snapping still works]). This needs additional + // handling in Zed, in order to implement drag handlers on a titlebar element. + // + // Since all of this handling is not present, we request server-side decorations + // for now as a stopgap solution. + decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ServerSide); + } + let viewport = state .viewporter .as_ref() @@ -151,8 +232,7 @@ impl Client for WaylandClient { wl_surface.frame(&self.qh, wl_surface.clone()); wl_surface.commit(); - let window_state = Rc::new(WaylandWindowState::new( - &self.conn, + let window_state: Rc = Rc::new(WaylandWindowState::new( wl_surface.clone(), viewport, Arc::new(toplevel), @@ -166,52 +246,63 @@ impl Client for WaylandClient { state.windows.push((xdg_surface, Rc::clone(&window_state))); Box::new(WaylandWindow(window_state)) } + + fn set_cursor_style(&self, style: CursorStyle) { + let cursor_icon_name = match style { + CursorStyle::Arrow => "arrow".to_string(), + CursorStyle::IBeam => "text".to_string(), + CursorStyle::Crosshair => "crosshair".to_string(), + CursorStyle::ClosedHand => "grabbing".to_string(), + CursorStyle::OpenHand => "openhand".to_string(), + CursorStyle::PointingHand => "hand".to_string(), + CursorStyle::ResizeLeft => "w-resize".to_string(), + CursorStyle::ResizeRight => "e-resize".to_string(), + CursorStyle::ResizeLeftRight => "ew-resize".to_string(), + CursorStyle::ResizeUp => "n-resize".to_string(), + CursorStyle::ResizeDown => "s-resize".to_string(), + CursorStyle::ResizeUpDown => "ns-resize".to_string(), + CursorStyle::DisappearingItem => "grabbing".to_string(), // todo!(linux) - couldn't find equivalent icon in linux + CursorStyle::IBeamCursorForVerticalLayout => "vertical-text".to_string(), + CursorStyle::OperationNotAllowed => "not-allowed".to_string(), + CursorStyle::DragLink => "dnd-link".to_string(), + CursorStyle::DragCopy => "dnd-copy".to_string(), + CursorStyle::ContextualMenu => "context-menu".to_string(), + }; + + let mut cursor_state = self.state.cursor_state.borrow_mut(); + cursor_state.cursor_icon_name = cursor_icon_name; + } + + fn get_clipboard(&self) -> Rc> { + self.state.clipboard.clone() + } + + fn get_primary(&self) -> Rc> { + self.state.primary.clone() + } } -impl Dispatch for WaylandClientState { +impl Dispatch for WaylandClientState { fn event( state: &mut Self, registry: &wl_registry::WlRegistry, event: wl_registry::Event, - _: &(), + _: &GlobalListContents, _: &Connection, qh: &QueueHandle, ) { - if let wl_registry::Event::Global { - name, interface, .. - } = event - { - match &interface[..] { - "wl_compositor" => { - let compositor = - registry.bind::(name, 1, qh, ()); - state.compositor = Some(compositor); + match event { + wl_registry::Event::Global { + name, + interface, + version: _, + } => { + if interface.as_str() == "wl_seat" { + registry.bind::(name, 4, qh, ()); } - "xdg_wm_base" => { - let wm_base = registry.bind::(name, 1, qh, ()); - state.wm_base = Some(wm_base); - } - "wl_seat" => { - let seat = registry.bind::(name, 1, qh, ()); - state.wl_seat = Some(seat); - } - "wp_fractional_scale_manager_v1" => { - let manager = registry - .bind::( - name, - 1, - qh, - (), - ); - state.fractional_scale_manager = Some(manager); - } - "wp_viewporter" => { - let view_porter = - registry.bind::(name, 1, qh, ()); - state.viewporter = Some(view_porter); - } - _ => {} - }; + } + wl_registry::Event::GlobalRemove { name: _ } => {} + _ => {} } } } @@ -222,6 +313,7 @@ delegate_noop!(WaylandClientState: ignore wl_shm::WlShm); delegate_noop!(WaylandClientState: ignore wl_shm_pool::WlShmPool); delegate_noop!(WaylandClientState: ignore wl_buffer::WlBuffer); delegate_noop!(WaylandClientState: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1); +delegate_noop!(WaylandClientState: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); delegate_noop!(WaylandClientState: ignore wp_viewporter::WpViewporter); delegate_noop!(WaylandClientState: ignore wp_viewport::WpViewport); @@ -234,6 +326,7 @@ impl Dispatch> for WaylandClientState { _: &Connection, qh: &QueueHandle, ) { + let mut state = state.client_state_inner.borrow_mut(); if let wl_callback::Event::Done { .. } = event { for window in &state.windows { if window.1.surface.id() == surf.id() { @@ -255,6 +348,7 @@ impl Dispatch for WaylandClientState { _: &Connection, _: &QueueHandle, ) { + let mut state = state.client_state_inner.borrow_mut(); if let xdg_surface::Event::Configure { serial, .. } = event { xdg_surface.ack_configure(serial); for window in &state.windows { @@ -277,6 +371,7 @@ impl Dispatch for WaylandClientState { _: &Connection, _: &QueueHandle, ) { + let mut state = state.client_state_inner.borrow_mut(); if let xdg_toplevel::Event::Configure { width, height, @@ -302,7 +397,7 @@ impl Dispatch for WaylandClientState { true } }); - state.platform_inner.state.lock().quit_requested |= state.windows.is_empty(); + state.platform_inner.loop_signal.stop(); } } } @@ -347,14 +442,19 @@ impl Dispatch for WaylandClientState { impl Dispatch for WaylandClientState { fn event( - state: &mut Self, + this: &mut Self, keyboard: &wl_keyboard::WlKeyboard, event: wl_keyboard::Event, data: &(), conn: &Connection, qh: &QueueHandle, ) { + let mut state = this.client_state_inner.borrow_mut(); match event { + wl_keyboard::Event::RepeatInfo { rate, delay } => { + state.repeat.characters_per_second = rate as u32; + state.repeat.delay = Duration::from_millis(delay as u64); + } wl_keyboard::Event::Keymap { format: WEnum::Value(format), fd, @@ -380,12 +480,29 @@ impl Dispatch for WaylandClientState { state.keymap_state = Some(xkb::State::new(&keymap)); } wl_keyboard::Event::Enter { surface, .. } => { - for window in &state.windows { - if window.1.surface.id() == surface.id() { - state.keyboard_focused_window = Some(Rc::clone(&window.1)); - } + state.keyboard_focused_window = state + .windows + .iter() + .find(|&w| w.1.surface.id() == surface.id()) + .map(|w| w.1.clone()); + + if let Some(window) = &state.keyboard_focused_window { + window.set_focused(true); } } + wl_keyboard::Event::Leave { surface, .. } => { + let keyboard_focused_window = state + .windows + .iter() + .find(|&w| w.1.surface.id() == surface.id()) + .map(|w| w.1.clone()); + + if let Some(window) = keyboard_focused_window { + window.set_focused(false); + } + + state.keyboard_focused_window = None; + } wl_keyboard::Event::Modifiers { mods_depressed, mods_latched, @@ -395,71 +512,114 @@ impl Dispatch for WaylandClientState { } => { let keymap_state = state.keymap_state.as_mut().unwrap(); keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group); - state.modifiers.shift = + + let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE); - state.modifiers.alt = + let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE); - state.modifiers.control = + let control = keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE); - state.modifiers.command = + let command = keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE); + + state.modifiers.shift = shift; + state.modifiers.alt = alt; + state.modifiers.control = control; + state.modifiers.command = command; } wl_keyboard::Event::Key { key, state: WEnum::Value(key_state), .. } => { + let focused_window = &state.keyboard_focused_window; + let Some(focused_window) = focused_window else { + return; + }; + let keymap_state = state.keymap_state.as_ref().unwrap(); let keycode = Keycode::from(key + MIN_KEYCODE); - let key_utf32 = keymap_state.key_get_utf32(keycode); - let key_utf8 = keymap_state.key_get_utf8(keycode); - let key_sym = keymap_state.key_get_one_sym(keycode); - let key = xkb::keysym_get_name(key_sym).to_lowercase(); + let keysym = keymap_state.key_get_one_sym(keycode); - // Ignore control characters (and DEL) for the purposes of ime_key, - // but if key_utf32 is 0 then assume it isn't one - let ime_key = - (key_utf32 == 0 || (key_utf32 >= 32 && key_utf32 != 127)).then_some(key_utf8); + match key_state { + wl_keyboard::KeyState::Pressed => { + let input = PlatformInput::KeyDown(KeyDownEvent { + keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode), + is_held: false, // todo(linux) + }); - let focused_window = &state.keyboard_focused_window; - if let Some(focused_window) = focused_window { - match key_state { - wl_keyboard::KeyState::Pressed => { - focused_window.handle_input(PlatformInput::KeyDown(KeyDownEvent { - keystroke: Keystroke { - modifiers: state.modifiers, - key, - ime_key, - }, - is_held: false, // todo!(linux) - })); + focused_window.handle_input(input.clone()); + + if !keysym.is_modifier_key() { + state.repeat.current_id += 1; + state.repeat.current_keysym = Some(keysym); + + let rate = state.repeat.characters_per_second; + let delay = state.repeat.delay; + let id = state.repeat.current_id; + let this = this.clone(); + + let timer = Timer::from_duration(delay); + let state_ = Rc::clone(&this.client_state_inner); + state + .loop_handle + .insert_source(timer, move |event, _metadata, shared_data| { + let state_ = state_.borrow_mut(); + let is_repeating = id == state_.repeat.current_id + && state_.repeat.current_keysym.is_some() + && state_.keyboard_focused_window.is_some(); + + if !is_repeating { + return TimeoutAction::Drop; + } + + let focused_window = + state_.keyboard_focused_window.as_ref().unwrap().clone(); + + drop(state_); + + focused_window.handle_input(input.clone()); + + TimeoutAction::ToDuration(Duration::from_secs(1) / rate) + }) + .unwrap(); } - wl_keyboard::KeyState::Released => { - focused_window.handle_input(PlatformInput::KeyUp(KeyUpEvent { - keystroke: Keystroke { - modifiers: state.modifiers, - key, - ime_key, - }, - })); - } - _ => {} } + wl_keyboard::KeyState::Released => { + focused_window.handle_input(PlatformInput::KeyUp(KeyUpEvent { + keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode), + })); + + if !keysym.is_modifier_key() { + state.repeat.current_keysym = None; + } + } + _ => {} } } - wl_keyboard::Event::Leave { .. } => {} _ => {} } } } -fn linux_button_to_gpui(button: u32) -> MouseButton { - match button { - 0x110 => MouseButton::Left, - 0x111 => MouseButton::Right, - 0x112 => MouseButton::Middle, - _ => unimplemented!(), // todo!(linux) - } +fn linux_button_to_gpui(button: u32) -> Option { + // These values are coming from . + const BTN_LEFT: u32 = 0x110; + const BTN_RIGHT: u32 = 0x111; + const BTN_MIDDLE: u32 = 0x112; + const BTN_SIDE: u32 = 0x113; + const BTN_EXTRA: u32 = 0x114; + const BTN_FORWARD: u32 = 0x115; + const BTN_BACK: u32 = 0x116; + + Some(match button { + BTN_LEFT => MouseButton::Left, + BTN_RIGHT => MouseButton::Right, + BTN_MIDDLE => MouseButton::Middle, + BTN_BACK | BTN_SIDE => MouseButton::Navigate(NavigationDirection::Back), + BTN_FORWARD | BTN_EXTRA => MouseButton::Navigate(NavigationDirection::Forward), + _ => return None, + }) } impl Dispatch for WaylandClientState { @@ -471,18 +631,36 @@ impl Dispatch for WaylandClientState { conn: &Connection, qh: &QueueHandle, ) { + let mut cursor_state = state.cursor_state.borrow_mut(); + let mut state = state.client_state_inner.borrow_mut(); + + if cursor_state.cursor.is_none() { + cursor_state.cursor = Some(Cursor::new(&conn, &state.compositor, &qh, &state.shm, 24)); + } + let cursor_icon_name = cursor_state.cursor_icon_name.clone(); + let mut cursor: &mut Cursor = cursor_state.cursor.as_mut().unwrap(); + match event { wl_pointer::Event::Enter { + serial, surface, surface_x, surface_y, .. } => { + let mut mouse_focused_window = None; for window in &state.windows { if window.1.surface.id() == surface.id() { - state.mouse_focused_window = Some(Rc::clone(&window.1)); + window.1.set_focused(true); + mouse_focused_window = Some(Rc::clone(&window.1)); } } + if mouse_focused_window.is_some() { + state.mouse_focused_window = mouse_focused_window; + cursor.set_serial_id(serial); + cursor.set_icon(&wl_pointer, cursor_icon_name); + } + state.mouse_location = Some(Point { x: Pixels::from(surface_x), y: Pixels::from(surface_y), @@ -494,50 +672,56 @@ impl Dispatch for WaylandClientState { surface_y, .. } => { - let focused_window = &state.mouse_focused_window; - if let Some(focused_window) = focused_window { - state.mouse_location = Some(Point { - x: Pixels::from(surface_x), - y: Pixels::from(surface_y), - }); - focused_window.handle_input(PlatformInput::MouseMove(MouseMoveEvent { + if state.mouse_focused_window.is_none() { + return; + } + state.mouse_location = Some(Point { + x: Pixels::from(surface_x), + y: Pixels::from(surface_y), + }); + state.mouse_focused_window.as_ref().unwrap().handle_input( + PlatformInput::MouseMove(MouseMoveEvent { position: state.mouse_location.unwrap(), pressed_button: state.button_pressed, modifiers: state.modifiers, - })) - } + }), + ); + cursor.set_icon(&wl_pointer, cursor_icon_name); } wl_pointer::Event::Button { button, state: WEnum::Value(button_state), .. } => { - let focused_window = &state.mouse_focused_window; - let mouse_location = &state.mouse_location; - if let (Some(focused_window), Some(mouse_location)) = - (focused_window, mouse_location) - { - match button_state { - wl_pointer::ButtonState::Pressed => { - state.button_pressed = Some(linux_button_to_gpui(button)); - focused_window.handle_input(PlatformInput::MouseDown(MouseDownEvent { - button: linux_button_to_gpui(button), - position: *mouse_location, + let button = linux_button_to_gpui(button); + let Some(button) = button else { return }; + if state.mouse_focused_window.is_none() || state.mouse_location.is_none() { + return; + } + match button_state { + wl_pointer::ButtonState::Pressed => { + state.button_pressed = Some(button); + state.mouse_focused_window.as_ref().unwrap().handle_input( + PlatformInput::MouseDown(MouseDownEvent { + button, + position: state.mouse_location.unwrap(), modifiers: state.modifiers, click_count: 1, - })); - } - wl_pointer::ButtonState::Released => { - state.button_pressed = None; - focused_window.handle_input(PlatformInput::MouseUp(MouseUpEvent { - button: linux_button_to_gpui(button), - position: *mouse_location, + }), + ); + } + wl_pointer::ButtonState::Released => { + state.button_pressed = None; + state.mouse_focused_window.as_ref().unwrap().handle_input( + PlatformInput::MouseUp(MouseUpEvent { + button, + position: state.mouse_location.unwrap(), modifiers: Modifiers::default(), click_count: 1, - })); - } - _ => {} + }), + ); } + _ => {} } } wl_pointer::Event::AxisRelativeDirection { @@ -586,6 +770,7 @@ impl Dispatch for WaylandClientState { pressed_button: None, modifiers: Modifiers::default(), })); + focused_window.set_focused(false); } state.mouse_focused_window = None; state.mouse_location = None; @@ -604,6 +789,7 @@ impl Dispatch for Wayland _: &Connection, _: &QueueHandle, ) { + let mut state = state.client_state_inner.borrow_mut(); if let wp_fractional_scale_v1::Event::PreferredScale { scale, .. } = event { for window in &state.windows { if window.0.id() == *id { @@ -614,3 +800,43 @@ impl Dispatch for Wayland } } } + +impl Dispatch + for WaylandClientState +{ + fn event( + state: &mut Self, + _: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, + event: zxdg_toplevel_decoration_v1::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let mut state = state.client_state_inner.borrow_mut(); + if let zxdg_toplevel_decoration_v1::Event::Configure { mode, .. } = event { + for window in &state.windows { + if window.0.id() == *surface_id { + match mode { + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { + window + .1 + .set_decoration_state(WaylandDecorationState::Server); + } + WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => { + window + .1 + .set_decoration_state(WaylandDecorationState::Client); + } + WEnum::Value(_) => { + log::warn!("Unknown decoration mode"); + } + WEnum::Unknown(v) => { + log::warn!("Unknown decoration mode: {}", v); + } + } + return; + } + } + } + } +} diff --git a/crates/gpui/src/platform/linux/wayland/client_dispatcher.rs b/crates/gpui/src/platform/linux/wayland/client_dispatcher.rs deleted file mode 100644 index c9154b2f6a..0000000000 --- a/crates/gpui/src/platform/linux/wayland/client_dispatcher.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::sync::Arc; - -use wayland_client::{Connection, EventQueue}; - -use crate::platform::linux::client_dispatcher::ClientDispatcher; - -pub(crate) struct WaylandClientDispatcher { - conn: Arc, - event_queue: Arc>, -} - -impl WaylandClientDispatcher { - pub(crate) fn new(conn: &Arc) -> Self { - let event_queue = conn.new_event_queue(); - Self { - conn: Arc::clone(conn), - event_queue: Arc::new(event_queue), - } - } -} - -impl Drop for WaylandClientDispatcher { - fn drop(&mut self) { - //todo!(linux) - } -} - -impl ClientDispatcher for WaylandClientDispatcher { - fn dispatch_on_main_thread(&self) {} -} diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs new file mode 100644 index 0000000000..8b641972e3 --- /dev/null +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -0,0 +1,67 @@ +use crate::platform::linux::wayland::WaylandClientState; +use wayland_backend::client::InvalidId; +use wayland_client::protocol::wl_compositor::WlCompositor; +use wayland_client::protocol::wl_pointer::WlPointer; +use wayland_client::protocol::wl_shm::WlShm; +use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::{Connection, QueueHandle}; +use wayland_cursor::{CursorImageBuffer, CursorTheme}; + +pub(crate) struct Cursor { + theme: Result, + current_icon_name: String, + surface: WlSurface, + serial_id: u32, +} + +impl Cursor { + pub fn new( + connection: &Connection, + compositor: &WlCompositor, + qh: &QueueHandle, + shm: &WlShm, + size: u32, + ) -> Self { + Self { + theme: CursorTheme::load(&connection, shm.clone(), size), + current_icon_name: "".to_string(), + surface: compositor.create_surface(qh, ()), + serial_id: 0, + } + } + + pub fn set_serial_id(&mut self, serial_id: u32) { + self.serial_id = serial_id; + } + + pub fn set_icon(&mut self, wl_pointer: &WlPointer, cursor_icon_name: String) { + if self.current_icon_name != cursor_icon_name { + if self.theme.is_ok() { + if let Some(cursor) = self.theme.as_mut().unwrap().get_cursor(&cursor_icon_name) { + let buffer: &CursorImageBuffer = &cursor[0]; + let (width, height) = buffer.dimensions(); + let (hot_x, hot_y) = buffer.hotspot(); + + wl_pointer.set_cursor( + self.serial_id, + Some(&self.surface), + hot_x as i32, + hot_y as i32, + ); + self.surface.attach(Some(&buffer), 0, 0); + self.surface.damage(0, 0, width as i32, height as i32); + self.surface.commit(); + + self.current_icon_name = cursor_icon_name; + } else { + log::warn!( + "Linux: Wayland: Unable to get cursor icon: {}", + cursor_icon_name + ); + } + } else { + log::warn!("Linux: Wayland: Unable to load cursor themes"); + } + } + } +} diff --git a/crates/gpui/src/platform/linux/wayland/display.rs b/crates/gpui/src/platform/linux/wayland/display.rs index 0d8b6dbd3f..0b76a4b7dc 100644 --- a/crates/gpui/src/platform/linux/wayland/display.rs +++ b/crates/gpui/src/platform/linux/wayland/display.rs @@ -8,17 +8,17 @@ use crate::{Bounds, DisplayId, GlobalPixels, PlatformDisplay, Size}; pub(crate) struct WaylandDisplay {} impl PlatformDisplay for WaylandDisplay { - // todo!(linux) + // todo(linux) fn id(&self) -> DisplayId { DisplayId(123) // return some fake data so it doesn't panic } - // todo!(linux) + // todo(linux) fn uuid(&self) -> anyhow::Result { Ok(Uuid::from_bytes([0; 16])) // return some fake data so it doesn't panic } - // todo!(linux) + // todo(linux) fn bounds(&self) -> Bounds { Bounds { origin: Default::default(), diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 6cc8d2015e..11e1743b03 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -1,4 +1,5 @@ use std::any::Any; +use std::cell::RefCell; use std::ffi::c_void; use std::rc::Rc; use std::sync::Arc; @@ -6,7 +7,6 @@ use std::sync::Arc; use blade_graphics as gpu; use blade_rwh::{HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle}; use futures::channel::oneshot::Receiver; -use parking_lot::Mutex; use raw_window_handle::{ DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle, }; @@ -41,6 +41,7 @@ struct WaylandWindowInner { bounds: Bounds, scale: f32, input_handler: Option, + decoration_state: WaylandDecorationState, } struct RawWindow { @@ -65,14 +66,15 @@ unsafe impl HasRawDisplayHandle for RawWindow { } impl WaylandWindowInner { - fn new( - conn: &Arc, - wl_surf: &Arc, - bounds: Bounds, - ) -> Self { + fn new(wl_surf: &Arc, bounds: Bounds) -> Self { let raw = RawWindow { window: wl_surf.id().as_ptr().cast::(), - display: conn.backend().display_ptr().cast::(), + display: wl_surf + .backend() + .upgrade() + .unwrap() + .display_ptr() + .cast::(), }; let gpu = Arc::new( unsafe { @@ -96,14 +98,16 @@ impl WaylandWindowInner { bounds, scale: 1.0, input_handler: None, + + // On wayland, decorations are by default provided by the client + decoration_state: WaylandDecorationState::Client, } } } pub(crate) struct WaylandWindowState { - conn: Arc, - inner: Mutex, - pub(crate) callbacks: Mutex, + inner: RefCell, + pub(crate) callbacks: RefCell, pub(crate) surface: Arc, pub(crate) toplevel: Arc, viewport: Option, @@ -111,7 +115,6 @@ pub(crate) struct WaylandWindowState { impl WaylandWindowState { pub(crate) fn new( - conn: &Arc, wl_surf: Arc, viewport: Option, toplevel: Arc, @@ -129,40 +132,39 @@ impl WaylandWindowState { size: Size { width: 500, height: 500, - }, //todo!(implement) + }, // todo(implement) }, WindowBounds::Fixed(bounds) => bounds.map(|p| p.0 as i32), }; Self { - conn: Arc::clone(conn), surface: Arc::clone(&wl_surf), - inner: Mutex::new(WaylandWindowInner::new(&Arc::clone(conn), &wl_surf, bounds)), - callbacks: Mutex::new(Callbacks::default()), + inner: RefCell::new(WaylandWindowInner::new(&wl_surf, bounds)), + callbacks: RefCell::new(Callbacks::default()), toplevel, viewport, } } pub fn update(&self) { - let mut cb = self.callbacks.lock(); + let mut cb = self.callbacks.borrow_mut(); if let Some(mut fun) = cb.request_frame.take() { drop(cb); fun(); - self.callbacks.lock().request_frame = Some(fun); + self.callbacks.borrow_mut().request_frame = Some(fun); } } pub fn set_size_and_scale(&self, width: i32, height: i32, scale: f32) { - self.inner.lock().scale = scale; - self.inner.lock().bounds.size.width = width; - self.inner.lock().bounds.size.height = height; - self.inner.lock().renderer.update_drawable_size(size( + self.inner.borrow_mut().scale = scale; + self.inner.borrow_mut().bounds.size.width = width; + self.inner.borrow_mut().bounds.size.height = height; + self.inner.borrow_mut().renderer.update_drawable_size(size( width as f64 * scale as f64, height as f64 * scale as f64, )); - if let Some(ref mut fun) = self.callbacks.lock().resize { + if let Some(ref mut fun) = self.callbacks.borrow_mut().resize { fun( Size { width: px(width as f32), @@ -178,17 +180,31 @@ impl WaylandWindowState { } pub fn resize(&self, width: i32, height: i32) { - let scale = self.inner.lock().scale; + let scale = self.inner.borrow_mut().scale; self.set_size_and_scale(width, height, scale); } pub fn rescale(&self, scale: f32) { - let bounds = self.inner.lock().bounds; + let bounds = self.inner.borrow_mut().bounds; self.set_size_and_scale(bounds.size.width, bounds.size.height, scale) } + /// Notifies the window of the state of the decorations. + /// + /// # Note + /// + /// This API is indirectly called by the wayland compositor and + /// not meant to be called by a user who wishes to change the state + /// of the decorations. This is because the state of the decorations + /// is managed by the compositor and not the client. + pub fn set_decoration_state(&self, state: WaylandDecorationState) { + self.inner.borrow_mut().decoration_state = state; + log::trace!("Window decorations are now handled by {:?}", state); + // todo(linux) - Handle this properly + } + pub fn close(&self) { - let mut callbacks = self.callbacks.lock(); + let mut callbacks = self.callbacks.borrow_mut(); if let Some(fun) = callbacks.close.take() { fun() } @@ -196,13 +212,13 @@ impl WaylandWindowState { } pub fn handle_input(&self, input: PlatformInput) { - if let Some(ref mut fun) = self.callbacks.lock().input { + if let Some(ref mut fun) = self.callbacks.borrow_mut().input { if fun(input.clone()) { return; } } if let PlatformInput::KeyDown(event) = input { - let mut inner = self.inner.lock(); + let mut inner = self.inner.borrow_mut(); if let Some(ref mut input_handler) = inner.input_handler { if let Some(ime_key) = &event.keystroke.ime_key { input_handler.replace_text_in_range(None, ime_key); @@ -210,6 +226,12 @@ impl WaylandWindowState { } } } + + pub fn set_focused(&self, focus: bool) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change { + fun(focus); + } + } } #[derive(Clone)] @@ -228,13 +250,13 @@ impl HasDisplayHandle for WaylandWindow { } impl PlatformWindow for WaylandWindow { - //todo!(linux) + // todo(linux) fn bounds(&self) -> WindowBounds { WindowBounds::Maximized } fn content_size(&self) -> Size { - let inner = self.0.inner.lock(); + let inner = self.0.inner.borrow_mut(); Size { width: Pixels(inner.bounds.size.width as f32), height: Pixels(inner.bounds.size.height as f32), @@ -242,48 +264,48 @@ impl PlatformWindow for WaylandWindow { } fn scale_factor(&self) -> f32 { - self.0.inner.lock().scale + self.0.inner.borrow_mut().scale } - //todo!(linux) + // todo(linux) fn titlebar_height(&self) -> Pixels { unimplemented!() } - // todo!(linux) + // todo(linux) fn appearance(&self) -> WindowAppearance { WindowAppearance::Light } - // todo!(linux) + // todo(linux) fn display(&self) -> Rc { Rc::new(WaylandDisplay {}) } - // todo!(linux) + // todo(linux) fn mouse_position(&self) -> Point { Point::default() } - //todo!(linux) + // todo(linux) fn modifiers(&self) -> Modifiers { crate::Modifiers::default() } - //todo!(linux) + // todo(linux) fn as_any_mut(&mut self) -> &mut dyn Any { unimplemented!() } fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.0.inner.lock().input_handler = Some(input_handler); + self.0.inner.borrow_mut().input_handler = Some(input_handler); } fn take_input_handler(&mut self) -> Option { - self.0.inner.lock().input_handler.take() + self.0.inner.borrow_mut().input_handler.take() } - //todo!(linux) + // todo(linux) fn prompt( &self, level: PromptLevel, @@ -295,7 +317,7 @@ impl PlatformWindow for WaylandWindow { } fn activate(&self) { - //todo!(linux) + // todo(linux) } fn set_title(&mut self, title: &str) { @@ -303,77 +325,82 @@ impl PlatformWindow for WaylandWindow { } fn set_edited(&mut self, edited: bool) { - //todo!(linux) + // todo(linux) } fn show_character_palette(&self) { - //todo!(linux) + // todo(linux) } fn minimize(&self) { - //todo!(linux) + // todo(linux) } fn zoom(&self) { - //todo!(linux) + // todo(linux) } fn toggle_full_screen(&self) { - //todo!(linux) + // todo(linux) } fn on_request_frame(&self, callback: Box) { - self.0.callbacks.lock().request_frame = Some(callback); + self.0.callbacks.borrow_mut().request_frame = Some(callback); } fn on_input(&self, callback: Box bool>) { - self.0.callbacks.lock().input = Some(callback); + self.0.callbacks.borrow_mut().input = Some(callback); } fn on_active_status_change(&self, callback: Box) { - //todo!(linux) + self.0.callbacks.borrow_mut().active_status_change = Some(callback); } fn on_resize(&self, callback: Box, f32)>) { - self.0.callbacks.lock().resize = Some(callback); + self.0.callbacks.borrow_mut().resize = Some(callback); } fn on_fullscreen(&self, callback: Box) { - //todo!(linux) + // todo(linux) } fn on_moved(&self, callback: Box) { - self.0.callbacks.lock().moved = Some(callback); + self.0.callbacks.borrow_mut().moved = Some(callback); } fn on_should_close(&self, callback: Box bool>) { - self.0.callbacks.lock().should_close = Some(callback); + self.0.callbacks.borrow_mut().should_close = Some(callback); } fn on_close(&self, callback: Box) { - self.0.callbacks.lock().close = Some(callback); + self.0.callbacks.borrow_mut().close = Some(callback); } fn on_appearance_changed(&self, callback: Box) { - //todo!(linux) + // todo(linux) } - // todo!(linux) + // todo(linux) fn is_topmost_for_position(&self, position: Point) -> bool { false } fn draw(&self, scene: &Scene) { - let mut inner = self.0.inner.lock(); + let mut inner = self.0.inner.borrow_mut(); inner.renderer.draw(scene); } fn sprite_atlas(&self) -> Arc { - let inner = self.0.inner.lock(); + let inner = self.0.inner.borrow_mut(); inner.renderer.sprite_atlas().clone() } - - fn set_graphics_profiler_enabled(&self, enabled: bool) { - //todo!(linux) - } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum WaylandDecorationState { + /// Decorations are to be provided by the client + Client, + + /// Decorations are provided by the server + Server, } diff --git a/crates/gpui/src/platform/linux/x11.rs b/crates/gpui/src/platform/linux/x11.rs index bcb2215b01..958da047d6 100644 --- a/crates/gpui/src/platform/linux/x11.rs +++ b/crates/gpui/src/platform/linux/x11.rs @@ -1,11 +1,9 @@ mod client; -mod client_dispatcher; mod display; mod event; mod window; pub(crate) use client::*; -pub(crate) use client_dispatcher::*; pub(crate) use display::*; pub(crate) use event::*; pub(crate) use window::*; diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index fca487fc37..53fbc8747f 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1,214 +1,284 @@ -use std::{rc::Rc, sync::Arc}; +use std::cell::RefCell; +use std::rc::Rc; +use std::time::Duration; -use parking_lot::Mutex; use xcb::{x, Xid as _}; use xkbcommon::xkb; use collections::HashMap; +use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext}; +use copypasta::ClipboardProvider; use crate::platform::linux::client::Client; -use crate::platform::{ - LinuxPlatformInner, PlatformWindow, X11Display, X11Window, X11WindowState, XcbAtoms, -}; +use crate::platform::{LinuxPlatformInner, PlatformWindow}; use crate::{ - AnyWindowHandle, Bounds, DisplayId, PlatformDisplay, PlatformInput, Point, Size, WindowOptions, + AnyWindowHandle, Bounds, CursorStyle, DisplayId, PlatformDisplay, PlatformInput, Point, + ScrollDelta, Size, TouchPhase, WindowOptions, }; -pub(crate) struct X11ClientState { - pub(crate) windows: HashMap>, +use super::{X11Display, X11Window, X11WindowState, XcbAtoms}; +use calloop::{ + generic::{FdWrapper, Generic}, + RegistrationToken, +}; + +struct WindowRef { + state: Rc, + refresh_event_token: RegistrationToken, +} + +struct X11ClientState { + windows: HashMap, xkb: xkbcommon::xkb::State, + clipboard: Rc>>, + primary: Rc>>, } pub(crate) struct X11Client { platform_inner: Rc, - xcb_connection: Arc, + xcb_connection: Rc, x_root_index: i32, atoms: XcbAtoms, - state: Mutex, + state: RefCell, } impl X11Client { - pub(crate) fn new( - inner: Rc, - xcb_connection: Arc, - x_root_index: i32, - atoms: XcbAtoms, - ) -> Self { - let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); - let xkb_device_id = xkb::x11::get_core_keyboard_device_id(&xcb_connection); - let xkb_keymap = xkb::x11::keymap_new_from_device( - &xkb_context, - &xcb_connection, - xkb_device_id, - xkb::KEYMAP_COMPILE_NO_FLAGS, - ); - let xkb_state = - xkb::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id); + pub(crate) fn new(inner: Rc) -> Rc { + let (xcb_connection, x_root_index) = xcb::Connection::connect_with_extensions( + None, + &[xcb::Extension::RandR, xcb::Extension::Xkb], + &[], + ) + .unwrap(); - Self { - platform_inner: inner, - xcb_connection, + let xkb_ver = xcb_connection + .wait_for_reply(xcb_connection.send_request(&xcb::xkb::UseExtension { + wanted_major: xcb::xkb::MAJOR_VERSION as u16, + wanted_minor: xcb::xkb::MINOR_VERSION as u16, + })) + .unwrap(); + assert!(xkb_ver.supported()); + + let atoms = XcbAtoms::intern_all(&xcb_connection).unwrap(); + let xcb_connection = Rc::new(xcb_connection); + + let xkb_state = { + let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + let xkb_device_id = xkb::x11::get_core_keyboard_device_id(&xcb_connection); + let xkb_keymap = xkb::x11::keymap_new_from_device( + &xkb_context, + &xcb_connection, + xkb_device_id, + xkb::KEYMAP_COMPILE_NO_FLAGS, + ); + xkb::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id) + }; + + let clipboard = X11ClipboardContext::::new().unwrap(); + let primary = X11ClipboardContext::::new().unwrap(); + + let client: Rc = Rc::new(Self { + platform_inner: inner.clone(), + xcb_connection: Rc::clone(&xcb_connection), x_root_index, atoms, - state: Mutex::new(X11ClientState { + state: RefCell::new(X11ClientState { windows: HashMap::default(), xkb: xkb_state, + clipboard: Rc::new(RefCell::new(clipboard)), + primary: Rc::new(RefCell::new(primary)), }), - } + }); + + // Safety: Safe if xcb::Connection always returns a valid fd + let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; + + inner + .loop_handle + .insert_source( + Generic::new_with_error::( + fd, + calloop::Interest::READ, + calloop::Mode::Level, + ), + { + let client = Rc::clone(&client); + move |_readiness, _, _| { + while let Some(event) = xcb_connection.poll_for_event()? { + client.handle_event(event); + } + Ok(calloop::PostAction::Continue) + } + }, + ) + .expect("Failed to initialize x11 event source"); + + client } - fn get_window(&self, win: x::Window) -> Rc { - let state = self.state.lock(); - Rc::clone(&state.windows[&win]) + fn get_window(&self, win: x::Window) -> Option> { + let state = self.state.borrow(); + state.windows.get(&win).map(|wr| Rc::clone(&wr.state)) + } + + fn handle_event(&self, event: xcb::Event) -> Option<()> { + match event { + xcb::Event::X(x::Event::ClientMessage(event)) => { + if let x::ClientMessageData::Data32([atom, ..]) = event.data() { + if atom == self.atoms.wm_del_window.resource_id() { + // window "x" button clicked by user, we gracefully exit + let window_ref = self + .state + .borrow_mut() + .windows + .remove(&event.window()) + .unwrap(); + + self.platform_inner + .loop_handle + .remove(window_ref.refresh_event_token); + window_ref.state.destroy(); + + if self.state.borrow().windows.is_empty() { + self.platform_inner.loop_signal.stop(); + } + } + } + } + xcb::Event::X(x::Event::ConfigureNotify(event)) => { + let bounds = Bounds { + origin: Point { + x: event.x().into(), + y: event.y().into(), + }, + size: Size { + width: event.width().into(), + height: event.height().into(), + }, + }; + let window = self.get_window(event.window())?; + window.configure(bounds); + } + xcb::Event::X(x::Event::Expose(event)) => { + let window = self.get_window(event.window())?; + window.refresh(); + } + xcb::Event::X(x::Event::FocusIn(event)) => { + let window = self.get_window(event.event())?; + window.set_focused(true); + } + xcb::Event::X(x::Event::FocusOut(event)) => { + let window = self.get_window(event.event())?; + window.set_focused(false); + } + xcb::Event::X(x::Event::KeyPress(event)) => { + let window = self.get_window(event.event())?; + let modifiers = super::modifiers_from_state(event.state()); + let keystroke = { + let code = event.detail().into(); + let mut state = self.state.borrow_mut(); + let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); + state.xkb.update_key(code, xkb::KeyDirection::Down); + keystroke + }; + + window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { + keystroke, + is_held: false, + })); + } + xcb::Event::X(x::Event::KeyRelease(event)) => { + let window = self.get_window(event.event())?; + let modifiers = super::modifiers_from_state(event.state()); + let keystroke = { + let code = event.detail().into(); + let mut state = self.state.borrow_mut(); + let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); + state.xkb.update_key(code, xkb::KeyDirection::Up); + keystroke + }; + + window.handle_input(PlatformInput::KeyUp(crate::KeyUpEvent { keystroke })); + } + xcb::Event::X(x::Event::ButtonPress(event)) => { + let window = self.get_window(event.event())?; + let modifiers = super::modifiers_from_state(event.state()); + let position = Point::new( + (event.event_x() as f32).into(), + (event.event_y() as f32).into(), + ); + if let Some(button) = super::button_of_key(event.detail()) { + window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent { + button, + position, + modifiers, + click_count: 1, + })); + } else if event.detail() >= 4 && event.detail() <= 5 { + // https://stackoverflow.com/questions/15510472/scrollwheel-event-in-x11 + let delta_x = if event.detail() == 4 { 1.0 } else { -1.0 }; + window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent { + position, + delta: ScrollDelta::Lines(Point::new(0.0, delta_x)), + modifiers, + touch_phase: TouchPhase::default(), + })); + } else { + log::warn!("Unknown button press: {event:?}"); + } + } + xcb::Event::X(x::Event::ButtonRelease(event)) => { + let window = self.get_window(event.event())?; + let modifiers = super::modifiers_from_state(event.state()); + let position = Point::new( + (event.event_x() as f32).into(), + (event.event_y() as f32).into(), + ); + if let Some(button) = super::button_of_key(event.detail()) { + window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent { + button, + position, + modifiers, + click_count: 1, + })); + } + } + xcb::Event::X(x::Event::MotionNotify(event)) => { + let window = self.get_window(event.event())?; + let pressed_button = super::button_from_state(event.state()); + let position = Point::new( + (event.event_x() as f32).into(), + (event.event_y() as f32).into(), + ); + let modifiers = super::modifiers_from_state(event.state()); + window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent { + pressed_button, + position, + modifiers, + })); + } + xcb::Event::X(x::Event::LeaveNotify(event)) => { + let window = self.get_window(event.event())?; + let pressed_button = super::button_from_state(event.state()); + let position = Point::new( + (event.event_x() as f32).into(), + (event.event_y() as f32).into(), + ); + let modifiers = super::modifiers_from_state(event.state()); + window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent { + pressed_button, + position, + modifiers, + })); + } + _ => {} + }; + + Some(()) } } impl Client for X11Client { - fn run(&self, on_finish_launching: Box) { - on_finish_launching(); - //Note: here and below, don't keep the lock() open when calling - // into window functions as they may invoke callbacks that need - // to immediately access the platform (self). - while !self.platform_inner.state.lock().quit_requested { - let event = self.xcb_connection.wait_for_event().unwrap(); - match event { - xcb::Event::X(x::Event::ClientMessage(ev)) => { - if let x::ClientMessageData::Data32([atom, ..]) = ev.data() { - if atom == self.atoms.wm_del_window.resource_id() { - // window "x" button clicked by user, we gracefully exit - let window = self.state.lock().windows.remove(&ev.window()).unwrap(); - window.destroy(); - let state = self.state.lock(); - self.platform_inner.state.lock().quit_requested |= - state.windows.is_empty(); - } - } - } - xcb::Event::X(x::Event::Expose(ev)) => { - self.get_window(ev.window()).refresh(); - } - xcb::Event::X(x::Event::ConfigureNotify(ev)) => { - let bounds = Bounds { - origin: Point { - x: ev.x().into(), - y: ev.y().into(), - }, - size: Size { - width: ev.width().into(), - height: ev.height().into(), - }, - }; - self.get_window(ev.window()).configure(bounds) - } - xcb::Event::Present(xcb::present::Event::CompleteNotify(ev)) => { - let window = self.get_window(ev.window()); - window.refresh(); - window.request_refresh(); - } - xcb::Event::Present(xcb::present::Event::IdleNotify(_ev)) => {} - xcb::Event::X(x::Event::KeyPress(ev)) => { - let window = self.get_window(ev.event()); - let modifiers = super::modifiers_from_state(ev.state()); - let key = { - let code = ev.detail().into(); - let mut state = self.state.lock(); - let key = state.xkb.key_get_utf8(code); - state.xkb.update_key(code, xkb::KeyDirection::Down); - key - }; - window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { - keystroke: crate::Keystroke { - modifiers, - key, - ime_key: None, - }, - is_held: false, - })); - } - xcb::Event::X(x::Event::KeyRelease(ev)) => { - let window = self.get_window(ev.event()); - let modifiers = super::modifiers_from_state(ev.state()); - let key = { - let code = ev.detail().into(); - let mut state = self.state.lock(); - let key = state.xkb.key_get_utf8(code); - state.xkb.update_key(code, xkb::KeyDirection::Up); - key - }; - window.handle_input(PlatformInput::KeyUp(crate::KeyUpEvent { - keystroke: crate::Keystroke { - modifiers, - key, - ime_key: None, - }, - })); - } - xcb::Event::X(x::Event::ButtonPress(ev)) => { - let window = self.get_window(ev.event()); - let modifiers = super::modifiers_from_state(ev.state()); - let position = - Point::new((ev.event_x() as f32).into(), (ev.event_y() as f32).into()); - if let Some(button) = super::button_of_key(ev.detail()) { - window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent { - button, - position, - modifiers, - click_count: 1, - })); - } else { - log::warn!("Unknown button press: {ev:?}"); - } - } - xcb::Event::X(x::Event::ButtonRelease(ev)) => { - let window = self.get_window(ev.event()); - let modifiers = super::modifiers_from_state(ev.state()); - let position = - Point::new((ev.event_x() as f32).into(), (ev.event_y() as f32).into()); - if let Some(button) = super::button_of_key(ev.detail()) { - window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent { - button, - position, - modifiers, - click_count: 1, - })); - } - } - xcb::Event::X(x::Event::MotionNotify(ev)) => { - let window = self.get_window(ev.event()); - let pressed_button = super::button_from_state(ev.state()); - let position = - Point::new((ev.event_x() as f32).into(), (ev.event_y() as f32).into()); - let modifiers = super::modifiers_from_state(ev.state()); - window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent { - pressed_button, - position, - modifiers, - })); - } - xcb::Event::X(x::Event::LeaveNotify(ev)) => { - let window = self.get_window(ev.event()); - let pressed_button = super::button_from_state(ev.state()); - let position = - Point::new((ev.event_x() as f32).into(), (ev.event_y() as f32).into()); - let modifiers = super::modifiers_from_state(ev.state()); - window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent { - pressed_button, - position, - modifiers, - })); - } - _ => {} - } - - if let Ok(runnable) = self.platform_inner.main_receiver.try_recv() { - runnable.run(); - } - } - - if let Some(ref mut fun) = self.platform_inner.callbacks.lock().quit { - fun(); - } - } fn displays(&self) -> Vec> { let setup = self.xcb_connection.get_setup(); setup @@ -220,6 +290,7 @@ impl Client for X11Client { }) .collect() } + fn display(&self, id: DisplayId) -> Option> { Some(Rc::new(X11Display::new(&self.xcb_connection, id.0 as i32))) } @@ -238,12 +309,75 @@ impl Client for X11Client { x_window, &self.atoms, )); - window_ptr.request_refresh(); - self.state - .lock() - .windows - .insert(x_window, Rc::clone(&window_ptr)); + let cookie = self + .xcb_connection + .send_request(&xcb::randr::GetScreenResourcesCurrent { window: x_window }); + let screen_resources = self.xcb_connection.wait_for_reply(cookie).expect("TODO"); + let crtc = screen_resources.crtcs().first().expect("TODO"); + + let cookie = self.xcb_connection.send_request(&xcb::randr::GetCrtcInfo { + crtc: crtc.to_owned(), + config_timestamp: xcb::x::Time::CurrentTime as u32, + }); + let crtc_info = self.xcb_connection.wait_for_reply(cookie).expect("TODO"); + + let mode_id = crtc_info.mode().resource_id(); + let mode = screen_resources + .modes() + .iter() + .find(|m| m.id == mode_id) + .expect("Missing screen mode for crtc specified mode id"); + + let refresh_event_token = self + .platform_inner + .loop_handle + .insert_source(calloop::timer::Timer::immediate(), { + let refresh_duration = mode_refresh_rate(mode); + let xcb_connection = Rc::clone(&self.xcb_connection); + move |mut instant, (), _| { + xcb_connection.send_request(&x::SendEvent { + propagate: false, + destination: x::SendEventDest::Window(x_window), + event_mask: x::EventMask::EXPOSURE, + event: &x::ExposeEvent::new(x_window, 0, 0, 0, 0, 1), + }); + let _ = xcb_connection.flush(); + // Take into account that some frames have been skipped + let now = time::Instant::now(); + while instant < now { + instant += refresh_duration; + } + calloop::timer::TimeoutAction::ToInstant(instant) + } + }) + .expect("Failed to initialize refresh timer"); + + let window_ref = WindowRef { + state: Rc::clone(&window_ptr), + refresh_event_token, + }; + self.state.borrow_mut().windows.insert(x_window, window_ref); Box::new(X11Window(window_ptr)) } + + //todo!(linux) + fn set_cursor_style(&self, _style: CursorStyle) {} + + fn get_clipboard(&self) -> Rc> { + self.state.borrow().clipboard.clone() + } + + fn get_primary(&self) -> Rc> { + self.state.borrow().primary.clone() + } +} + +// Adatpted from: +// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111 +pub fn mode_refresh_rate(mode: &xcb::randr::ModeInfo) -> Duration { + let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64); + let micros = 1_000_000_000 / millihertz; + log::info!("Refreshing at {} micros", micros); + Duration::from_micros(micros) } diff --git a/crates/gpui/src/platform/linux/x11/client_dispatcher.rs b/crates/gpui/src/platform/linux/x11/client_dispatcher.rs deleted file mode 100644 index 07f67af99f..0000000000 --- a/crates/gpui/src/platform/linux/x11/client_dispatcher.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::sync::Arc; - -use xcb::x; - -use crate::platform::linux::client_dispatcher::ClientDispatcher; - -pub(crate) struct X11ClientDispatcher { - xcb_connection: Arc, - x_listener_window: x::Window, -} - -impl X11ClientDispatcher { - pub fn new(xcb_connection: &Arc, x_root_index: i32) -> Self { - let x_listener_window = xcb_connection.generate_id(); - let screen = xcb_connection - .get_setup() - .roots() - .nth(x_root_index as usize) - .unwrap(); - xcb_connection.send_request(&x::CreateWindow { - depth: 0, - wid: x_listener_window, - parent: screen.root(), - x: 0, - y: 0, - width: 1, - height: 1, - border_width: 0, - class: x::WindowClass::InputOnly, - visual: screen.root_visual(), - value_list: &[], - }); - - Self { - xcb_connection: Arc::clone(xcb_connection), - x_listener_window, - } - } -} - -impl Drop for X11ClientDispatcher { - fn drop(&mut self) { - self.xcb_connection.send_request(&x::DestroyWindow { - window: self.x_listener_window, - }); - } -} - -impl ClientDispatcher for X11ClientDispatcher { - fn dispatch_on_main_thread(&self) { - // Send a message to the invisible window, forcing - // the main loop to wake up and dispatch the runnable. - self.xcb_connection.send_request(&x::SendEvent { - propagate: false, - destination: x::SendEventDest::Window(self.x_listener_window), - event_mask: x::EventMask::NO_EVENT, - event: &x::VisibilityNotifyEvent::new( - self.x_listener_window, - x::Visibility::Unobscured, - ), - }); - self.xcb_connection.flush().unwrap(); - } -} diff --git a/crates/gpui/src/platform/linux/x11/event.rs b/crates/gpui/src/platform/linux/x11/event.rs index f9860082da..b346c55ab8 100644 --- a/crates/gpui/src/platform/linux/x11/event.rs +++ b/crates/gpui/src/platform/linux/x11/event.rs @@ -1,12 +1,14 @@ use xcb::x; -use crate::{Modifiers, MouseButton}; +use crate::{Modifiers, MouseButton, NavigationDirection}; pub(crate) fn button_of_key(detail: x::Button) -> Option { Some(match detail { 1 => MouseButton::Left, 2 => MouseButton::Middle, 3 => MouseButton::Right, + 8 => MouseButton::Navigate(NavigationDirection::Back), + 9 => MouseButton::Navigate(NavigationDirection::Forward), _ => return None, }) } diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index b5904cd425..f60f7f19b9 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -1,10 +1,10 @@ -//todo!(linux): remove +// todo(linux): remove #![allow(unused)] use crate::{ - platform::blade::BladeRenderer, size, Bounds, GlobalPixels, Pixels, PlatformDisplay, - PlatformInput, PlatformInputHandler, PlatformWindow, Point, Size, WindowAppearance, - WindowBounds, WindowOptions, X11Display, + platform::blade::BladeRenderer, size, Bounds, GlobalPixels, Modifiers, Pixels, PlatformAtlas, + PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel, + Scene, Size, WindowAppearance, WindowBounds, WindowOptions, }; use blade_graphics as gpu; use parking_lot::Mutex; @@ -16,6 +16,7 @@ use xcb::{ }; use std::{ + cell::RefCell, ffi::c_void, mem, num::NonZeroU32, @@ -24,10 +25,12 @@ use std::{ sync::{self, Arc}, }; +use super::X11Display; + #[derive(Default)] struct Callbacks { request_frame: Option>, - input: Option bool>>, + input: Option bool>>, active_status_change: Option>, resize: Option, f32)>>, fullscreen: Option>, @@ -85,18 +88,18 @@ struct RawWindow { } pub(crate) struct X11WindowState { - xcb_connection: Arc, + xcb_connection: Rc, display: Rc, raw: RawWindow, x_window: x::Window, - callbacks: Mutex, - inner: Mutex, + callbacks: RefCell, + inner: RefCell, } #[derive(Clone)] pub(crate) struct X11Window(pub(crate) Rc); -//todo!(linux): Remove other RawWindowHandle implementation +// todo(linux): Remove other RawWindowHandle implementation unsafe impl blade_rwh::HasRawWindowHandle for RawWindow { fn raw_window_handle(&self) -> blade_rwh::RawWindowHandle { let mut wh = blade_rwh::XcbWindowHandle::empty(); @@ -136,7 +139,7 @@ impl rwh::HasDisplayHandle for X11Window { impl X11WindowState { pub fn new( options: WindowOptions, - xcb_connection: &Arc, + xcb_connection: &Rc, x_main_screen_index: i32, x_window: x::Window, atoms: &XcbAtoms, @@ -155,6 +158,9 @@ impl X11WindowState { x::Cw::EventMask( x::EventMask::EXPOSURE | x::EventMask::STRUCTURE_NOTIFY + | x::EventMask::ENTER_WINDOW + | x::EventMask::LEAVE_WINDOW + | x::EventMask::FOCUS_CHANGE | x::EventMask::KEY_PRESS | x::EventMask::KEY_RELEASE | x::EventMask::BUTTON_PRESS @@ -205,25 +211,13 @@ impl X11WindowState { }); } } - xcb_connection - .send_and_check_request(&x::ChangeProperty { - mode: x::PropMode::Replace, - window: x_window, - property: atoms.wm_protocols, - r#type: x::ATOM_ATOM, - data: &[atoms.wm_del_window], - }) - .unwrap(); - - let fake_id = xcb_connection.generate_id(); - xcb_connection - .send_and_check_request(&xcb::present::SelectInput { - eid: fake_id, - window: x_window, - //Note: also consider `IDLE_NOTIFY` - event_mask: xcb::present::EventMask::COMPLETE_NOTIFY, - }) - .unwrap(); + xcb_connection.send_request(&x::ChangeProperty { + mode: x::PropMode::Replace, + window: x_window, + property: atoms.wm_protocols, + r#type: x::ATOM_ATOM, + data: &[atoms.wm_del_window], + }); xcb_connection.send_request(&x::MapWindow { window: x_window }); xcb_connection.flush().unwrap(); @@ -241,7 +235,7 @@ impl X11WindowState { gpu::Context::init_windowed( &raw, gpu::ContextDesc { - validation: cfg!(debug_assertions), + validation: false, capture: false, }, ) @@ -254,12 +248,12 @@ impl X11WindowState { let gpu_extent = query_render_extent(xcb_connection, x_window); Self { - xcb_connection: Arc::clone(xcb_connection), + xcb_connection: xcb_connection.clone(), display: Rc::new(X11Display::new(xcb_connection, x_screen_index)), raw, x_window, - callbacks: Mutex::new(Callbacks::default()), - inner: Mutex::new(LinuxWindowInner { + callbacks: RefCell::new(Callbacks::default()), + inner: RefCell::new(LinuxWindowInner { bounds, scale_factor: 1.0, renderer: BladeRenderer::new(gpu, gpu_extent), @@ -269,21 +263,21 @@ impl X11WindowState { } pub fn destroy(&self) { - self.inner.lock().renderer.destroy(); + self.inner.borrow_mut().renderer.destroy(); self.xcb_connection.send_request(&x::UnmapWindow { window: self.x_window, }); self.xcb_connection.send_request(&x::DestroyWindow { window: self.x_window, }); - if let Some(fun) = self.callbacks.lock().close.take() { + if let Some(fun) = self.callbacks.borrow_mut().close.take() { fun(); } self.xcb_connection.flush().unwrap(); } pub fn refresh(&self) { - let mut cb = self.callbacks.lock(); + let mut cb = self.callbacks.borrow_mut(); if let Some(ref mut fun) = cb.request_frame { fun(); } @@ -293,10 +287,10 @@ impl X11WindowState { let mut resize_args = None; let do_move; { - let mut inner = self.inner.lock(); + let mut inner = self.inner.borrow_mut(); let old_bounds = mem::replace(&mut inner.bounds, bounds); do_move = old_bounds.origin != bounds.origin; - //todo!(linux): use normal GPUI types here, refactor out the double + // todo(linux): use normal GPUI types here, refactor out the double // viewport check and extra casts ( ) let gpu_size = query_render_extent(&self.xcb_connection, self.x_window); if inner.renderer.viewport_size() != gpu_size { @@ -307,7 +301,7 @@ impl X11WindowState { } } - let mut callbacks = self.callbacks.lock(); + let mut callbacks = self.callbacks.borrow_mut(); if let Some((content_size, scale_factor)) = resize_args { if let Some(ref mut fun) = callbacks.resize { fun(content_size, scale_factor) @@ -320,52 +314,54 @@ impl X11WindowState { } } - pub fn request_refresh(&self) { - self.xcb_connection - .send_and_check_request(&xcb::present::NotifyMsc { - window: self.x_window, - serial: 0, - target_msc: 0, - divisor: 1, - remainder: 0, - }) - .unwrap(); - } - pub fn handle_input(&self, input: PlatformInput) { - if let Some(ref mut fun) = self.callbacks.lock().input { + if let Some(ref mut fun) = self.callbacks.borrow_mut().input { if fun(input.clone()) { return; } } if let PlatformInput::KeyDown(event) = input { - let mut inner = self.inner.lock(); + let mut inner = self.inner.borrow_mut(); if let Some(ref mut input_handler) = inner.input_handler { - input_handler.replace_text_in_range(None, &event.keystroke.key); + if let Some(ime_key) = &event.keystroke.ime_key { + input_handler.replace_text_in_range(None, ime_key); + } } } } + + pub fn set_focused(&self, focus: bool) { + if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change { + fun(focus); + } + } } impl PlatformWindow for X11Window { fn bounds(&self) -> WindowBounds { - WindowBounds::Fixed(self.0.inner.lock().bounds.map(|v| GlobalPixels(v as f32))) + WindowBounds::Fixed( + self.0 + .inner + .borrow_mut() + .bounds + .map(|v| GlobalPixels(v as f32)), + ) } fn content_size(&self) -> Size { - self.0.inner.lock().content_size() + self.0.inner.borrow_mut().content_size() } fn scale_factor(&self) -> f32 { - self.0.inner.lock().scale_factor + self.0.inner.borrow_mut().scale_factor } - //todo!(linux) + // todo(linux) fn titlebar_height(&self) -> Pixels { unimplemented!() } - //todo!(linux) + // todo(linux) fn appearance(&self) -> WindowAppearance { WindowAppearance::Light } @@ -374,14 +370,20 @@ impl PlatformWindow for X11Window { Rc::clone(&self.0.display) } - //todo!(linux) fn mouse_position(&self) -> Point { - Point::default() + let cookie = self.0.xcb_connection.send_request(&x::QueryPointer { + window: self.0.x_window, + }); + let reply: x::QueryPointerReply = self.0.xcb_connection.wait_for_reply(cookie).unwrap(); + Point::new( + (reply.root_x() as u32).into(), + (reply.root_y() as u32).into(), + ) } - //todo!(linux) - fn modifiers(&self) -> crate::Modifiers { - crate::Modifiers::default() + // todo(linux) + fn modifiers(&self) -> Modifiers { + Modifiers::default() } fn as_any_mut(&mut self) -> &mut dyn std::any::Any { @@ -389,17 +391,17 @@ impl PlatformWindow for X11Window { } fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.0.inner.lock().input_handler = Some(input_handler); + self.0.inner.borrow_mut().input_handler = Some(input_handler); } fn take_input_handler(&mut self) -> Option { - self.0.inner.lock().input_handler.take() + self.0.inner.borrow_mut().input_handler.take() } - //todo!(linux) + // todo(linux) fn prompt( &self, - _level: crate::PromptLevel, + _level: PromptLevel, _msg: &str, _detail: Option<&str>, _answers: &[&str], @@ -424,10 +426,10 @@ impl PlatformWindow for X11Window { }); } - //todo!(linux) + // todo(linux) fn set_edited(&mut self, edited: bool) {} - //todo!(linux), this corresponds to `orderFrontCharacterPalette` on macOS, + // todo(linux), this corresponds to `orderFrontCharacterPalette` on macOS, // but it looks like the equivalent for Linux is GTK specific: // // https://docs.gtk.org/gtk3/signal.Entry.insert-emoji.html @@ -437,73 +439,69 @@ impl PlatformWindow for X11Window { unimplemented!() } - //todo!(linux) + // todo(linux) fn minimize(&self) { unimplemented!() } - //todo!(linux) + // todo(linux) fn zoom(&self) { unimplemented!() } - //todo!(linux) + // todo(linux) fn toggle_full_screen(&self) { unimplemented!() } fn on_request_frame(&self, callback: Box) { - self.0.callbacks.lock().request_frame = Some(callback); + self.0.callbacks.borrow_mut().request_frame = Some(callback); } - fn on_input(&self, callback: Box bool>) { - self.0.callbacks.lock().input = Some(callback); + fn on_input(&self, callback: Box bool>) { + self.0.callbacks.borrow_mut().input = Some(callback); } fn on_active_status_change(&self, callback: Box) { - self.0.callbacks.lock().active_status_change = Some(callback); + self.0.callbacks.borrow_mut().active_status_change = Some(callback); } fn on_resize(&self, callback: Box, f32)>) { - self.0.callbacks.lock().resize = Some(callback); + self.0.callbacks.borrow_mut().resize = Some(callback); } fn on_fullscreen(&self, callback: Box) { - self.0.callbacks.lock().fullscreen = Some(callback); + self.0.callbacks.borrow_mut().fullscreen = Some(callback); } fn on_moved(&self, callback: Box) { - self.0.callbacks.lock().moved = Some(callback); + self.0.callbacks.borrow_mut().moved = Some(callback); } fn on_should_close(&self, callback: Box bool>) { - self.0.callbacks.lock().should_close = Some(callback); + self.0.callbacks.borrow_mut().should_close = Some(callback); } fn on_close(&self, callback: Box) { - self.0.callbacks.lock().close = Some(callback); + self.0.callbacks.borrow_mut().close = Some(callback); } fn on_appearance_changed(&self, callback: Box) { - self.0.callbacks.lock().appearance_changed = Some(callback); + self.0.callbacks.borrow_mut().appearance_changed = Some(callback); } - //todo!(linux) - fn is_topmost_for_position(&self, _position: crate::Point) -> bool { + // todo(linux) + fn is_topmost_for_position(&self, _position: Point) -> bool { unimplemented!() } - fn draw(&self, scene: &crate::Scene) { - let mut inner = self.0.inner.lock(); + fn draw(&self, scene: &Scene) { + let mut inner = self.0.inner.borrow_mut(); inner.renderer.draw(scene); } - fn sprite_atlas(&self) -> sync::Arc { - let inner = self.0.inner.lock(); + fn sprite_atlas(&self) -> sync::Arc { + let inner = self.0.inner.borrow_mut(); inner.renderer.sprite_atlas().clone() } - - fn set_graphics_profiler_enabled(&self, enabled: bool) { - unimplemented!("linux") - } } diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index d7a027e5cb..bc862c0996 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -26,8 +26,7 @@ pub(crate) type PointF = crate::Point; #[cfg(not(feature = "runtime_shaders"))] const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); #[cfg(feature = "runtime_shaders")] -const SHADERS_SOURCE_FILE: &'static str = - include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal")); +const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal")); const INSTANCE_BUFFER_SIZE: usize = 2 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...) pub type Context = Arc>>; diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 473fb5978a..d293f4cf40 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -5,7 +5,7 @@ use crate::{ MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowAppearance, WindowOptions, }; -use anyhow::anyhow; +use anyhow::{anyhow, bail}; use block::ConcreteBlock; use cocoa::{ appkit::{ @@ -496,11 +496,14 @@ impl Platform for MacPlatform { handle: AnyWindowHandle, options: WindowOptions, ) -> Box { + // Clippy thinks that this evaluates to `()`, for some reason. + #[allow(clippy::unit_arg, clippy::clone_on_copy)] + let renderer_context = self.0.lock().renderer_context.clone(); Box::new(MacWindow::open( handle, options, self.foreground_executor(), - self.0.lock().renderer_context.clone(), + renderer_context, )) } @@ -522,6 +525,49 @@ impl Platform for MacPlatform { } } + fn register_url_scheme(&self, scheme: &str) -> Task> { + // API only available post Monterey + // https://developer.apple.com/documentation/appkit/nsworkspace/3753004-setdefaultapplicationaturl + let (done_tx, done_rx) = oneshot::channel(); + if self.os_version().ok() < Some(SemanticVersion::new(12, 0, 0)) { + return Task::ready(Err(anyhow!( + "macOS 12.0 or later is required to register URL schemes" + ))); + } + + let bundle_id = unsafe { + let bundle: id = msg_send![class!(NSBundle), mainBundle]; + let bundle_id: id = msg_send![bundle, bundleIdentifier]; + if bundle_id == nil { + return Task::ready(Err(anyhow!("Can only register URL scheme in bundled apps"))); + } + bundle_id + }; + + unsafe { + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + let scheme: id = ns_string(scheme); + let app: id = msg_send![workspace, URLForApplicationWithBundleIdentifier: bundle_id]; + let done_tx = Cell::new(Some(done_tx)); + let block = ConcreteBlock::new(move |error: id| { + let result = if error == nil { + Ok(()) + } else { + let msg: id = msg_send![error, localizedDescription]; + Err(anyhow!("Failed to register: {:?}", msg)) + }; + + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(result); + } + }); + let _: () = msg_send![workspace, setDefaultApplicationAtURL: app toOpenURLsWithScheme: scheme completionHandler: block]; + } + + self.background_executor() + .spawn(async { crate::Flatten::flatten(done_rx.await.map_err(|e| anyhow!(e))) }) + } + fn on_open_urls(&self, callback: Box)>) { self.0.lock().open_urls = Some(callback); } @@ -685,6 +731,9 @@ impl Platform for MacPlatform { Err(anyhow!("app is not running inside a bundle")) } else { let version: id = msg_send![bundle, objectForInfoDictionaryKey: ns_string("CFBundleShortVersionString")]; + if version.is_null() { + bail!("bundle does not have version"); + } let len = msg_send![version, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; let bytes = version.UTF8String() as *const u8; let version = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap(); diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 0773b156bd..8e48cbef30 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -530,8 +530,27 @@ impl MacWindow { } } + let window_rect = match options.bounds { + WindowBounds::Fullscreen => { + // Set a temporary size as we will asynchronously resize the window + NSRect::new(NSPoint::new(0., 0.), NSSize::new(1024., 768.)) + } + WindowBounds::Maximized => { + let display_bounds = display.bounds(); + global_bounds_to_ns_rect(display_bounds) + } + WindowBounds::Fixed(bounds) => { + let display_bounds = display.bounds(); + if bounds.intersects(&display_bounds) { + global_bounds_to_ns_rect(bounds) + } else { + global_bounds_to_ns_rect(display_bounds) + } + } + }; + let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_( - NSRect::new(NSPoint::new(0., 0.), NSSize::new(1024., 768.)), + window_rect, style_mask, NSBackingStoreBuffered, NO, @@ -685,25 +704,10 @@ impl MacWindow { native_window.orderFront_(nil); } - let screen = native_window.screen(); - match options.bounds { - WindowBounds::Fullscreen => { - // We need to toggle full screen asynchronously as doing so may - // call back into the platform handlers. - window.toggle_full_screen() - } - WindowBounds::Maximized => { - native_window.setFrame_display_(screen.visibleFrame(), YES); - } - WindowBounds::Fixed(bounds) => { - let display_bounds = display.bounds(); - let frame = if bounds.intersects(&display_bounds) { - global_bounds_to_ns_rect(bounds) - } else { - global_bounds_to_ns_rect(display_bounds) - }; - native_window.setFrame_display_(frame, YES); - } + if options.bounds == WindowBounds::Fullscreen { + // We need to toggle full screen asynchronously as doing so may + // call back into the platform handlers. + window.toggle_full_screen(); } window.0.lock().move_traffic_light(); @@ -1060,27 +1064,6 @@ impl PlatformWindow for MacWindow { fn sprite_atlas(&self) -> Arc { self.0.lock().renderer.sprite_atlas().clone() } - - /// Enables or disables the Metal HUD for debugging purposes. Note that this only works - /// when the app is bundled and it has the `MetalHudEnabled` key set to true in Info.plist. - fn set_graphics_profiler_enabled(&self, enabled: bool) { - let this_lock = self.0.lock(); - let layer = this_lock.renderer.layer(); - - unsafe { - if enabled { - let hud_properties = NSDictionary::dictionaryWithObject_forKey_( - nil, - ns_string("default"), - ns_string("mode"), - ); - let _: () = msg_send![layer, setDeveloperHUDProperties: hud_properties]; - } else { - let _: () = - msg_send![layer, setDeveloperHUDProperties: NSDictionary::dictionary(nil)]; - } - } - } } impl HasWindowHandle for MacWindow { diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index fc73e49afb..5df416f9ab 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -125,6 +125,10 @@ impl Platform for TestPlatform { #[cfg(target_os = "macos")] return Arc::new(crate::platform::mac::MacTextSystem::new()); + + // todo("windows") + #[cfg(target_os = "windows")] + unimplemented!() } fn run(&self, _on_finish_launching: Box) { @@ -294,4 +298,8 @@ impl Platform for TestPlatform { fn double_click_interval(&self) -> std::time::Duration { Duration::from_millis(500) } + + fn register_url_scheme(&self, _: &str) -> Task> { + unimplemented!() + } } diff --git a/crates/gpui/src/platform/test/text_system.rs b/crates/gpui/src/platform/test/text_system.rs index 0e877aabbd..ca3a9e3a33 100644 --- a/crates/gpui/src/platform/test/text_system.rs +++ b/crates/gpui/src/platform/test/text_system.rs @@ -7,7 +7,7 @@ use std::borrow::Cow; pub(crate) struct TestTextSystem {} -//todo!(linux) +// todo(linux) #[allow(unused)] impl PlatformTextSystem for TestTextSystem { fn add_fonts(&self, fonts: Vec>) -> Result<()> { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 940a3e44c5..70dcce9033 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -1,7 +1,7 @@ use crate::{ - px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, KeyDownEvent, Keystroke, - Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, - Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions, + px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, Pixels, PlatformAtlas, + PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, Size, + TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions, }; use collections::HashMap; use parking_lot::Mutex; @@ -112,41 +112,6 @@ impl TestWindow { self.0.lock().input_callback = Some(callback); result } - - pub fn simulate_keystroke(&mut self, mut keystroke: Keystroke, is_held: bool) { - if keystroke.ime_key.is_none() - && !keystroke.modifiers.command - && !keystroke.modifiers.control - && !keystroke.modifiers.function - { - keystroke.ime_key = Some(if keystroke.modifiers.shift { - keystroke.key.to_ascii_uppercase().clone() - } else { - keystroke.key.clone() - }) - } - - if self.simulate_input(PlatformInput::KeyDown(KeyDownEvent { - keystroke: keystroke.clone(), - is_held, - })) { - return; - } - - let mut lock = self.0.lock(); - let Some(mut input_handler) = lock.input_handler.take() else { - panic!( - "simulate_keystroke {:?} input event was not handled and there was no active input", - &keystroke - ); - }; - drop(lock); - if let Some(text) = keystroke.ime_key.as_ref() { - input_handler.replace_text_in_range(None, &text); - } - - self.0.lock().input_handler = Some(input_handler); - } } impl PlatformWindow for TestWindow { @@ -286,8 +251,6 @@ impl PlatformWindow for TestWindow { self.0.lock().sprite_atlas.clone() } - fn set_graphics_profiler_enabled(&self, _enabled: bool) {} - fn as_test(&mut self) -> Option<&mut TestWindow> { Some(self) } diff --git a/crates/gpui/src/platform/windows.rs b/crates/gpui/src/platform/windows.rs new file mode 100644 index 0000000000..4347b9169a --- /dev/null +++ b/crates/gpui/src/platform/windows.rs @@ -0,0 +1,13 @@ +mod dispatcher; +mod display; +mod platform; +mod text_system; +mod util; +mod window; + +pub(crate) use dispatcher::*; +pub(crate) use display::*; +pub(crate) use platform::*; +pub(crate) use text_system::*; +pub(crate) use util::*; +pub(crate) use window::*; diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/crates/gpui/src/platform/windows/dispatcher.rs new file mode 100644 index 0000000000..16fdcd2b85 --- /dev/null +++ b/crates/gpui/src/platform/windows/dispatcher.rs @@ -0,0 +1,159 @@ +use std::{ + cmp::Ordering, + thread::{current, JoinHandle, ThreadId}, + time::{Duration, Instant}, +}; + +use async_task::Runnable; +use collections::BinaryHeap; +use flume::{RecvTimeoutError, Sender}; +use parking::Parker; +use parking_lot::Mutex; +use windows::Win32::{Foundation::HANDLE, System::Threading::SetEvent}; + +use crate::{PlatformDispatcher, TaskLabel}; + +pub(crate) struct WindowsDispatcher { + background_sender: Sender<(Runnable, Option)>, + main_sender: Sender, + timer_sender: Sender<(Runnable, Duration)>, + background_threads: Vec>, + timer_thread: JoinHandle<()>, + parker: Mutex, + main_thread_id: ThreadId, + event: HANDLE, +} + +impl WindowsDispatcher { + pub(crate) fn new(main_sender: Sender, event: HANDLE) -> Self { + let parker = Mutex::new(Parker::new()); + let (background_sender, background_receiver) = + flume::unbounded::<(Runnable, Option)>(); + let background_threads = (0..std::thread::available_parallelism() + .map(|i| i.get()) + .unwrap_or(1)) + .map(|_| { + let receiver = background_receiver.clone(); + std::thread::spawn(move || { + for (runnable, label) in receiver { + if let Some(label) = label { + log::debug!("TaskLabel: {label:?}"); + } + runnable.run(); + } + }) + }) + .collect::>(); + let (timer_sender, timer_receiver) = flume::unbounded::<(Runnable, Duration)>(); + let timer_thread = std::thread::spawn(move || { + let mut runnables = BinaryHeap::::new(); + let mut timeout_dur = None; + loop { + let recv = if let Some(dur) = timeout_dur { + match timer_receiver.recv_timeout(dur) { + Ok(recv) => Some(recv), + Err(RecvTimeoutError::Timeout) => None, + Err(RecvTimeoutError::Disconnected) => break, + } + } else if let Ok(recv) = timer_receiver.recv() { + Some(recv) + } else { + break; + }; + let now = Instant::now(); + if let Some((runnable, dur)) = recv { + runnables.push(RunnableAfter { + runnable, + instant: now + dur, + }); + while let Ok((runnable, dur)) = timer_receiver.try_recv() { + runnables.push(RunnableAfter { + runnable, + instant: now + dur, + }) + } + } + while runnables.peek().is_some_and(|entry| entry.instant <= now) { + runnables.pop().unwrap().runnable.run(); + } + timeout_dur = runnables.peek().map(|entry| entry.instant - now); + } + }); + let main_thread_id = current().id(); + Self { + background_sender, + main_sender, + timer_sender, + background_threads, + timer_thread, + parker, + main_thread_id, + event, + } + } +} + +impl PlatformDispatcher for WindowsDispatcher { + fn is_main_thread(&self) -> bool { + current().id() == self.main_thread_id + } + + fn dispatch(&self, runnable: Runnable, label: Option) { + self.background_sender + .send((runnable, label)) + .inspect_err(|e| log::error!("Dispatch failed: {e}")) + .ok(); + } + + fn dispatch_on_main_thread(&self, runnable: Runnable) { + self.main_sender + .send(runnable) + .inspect_err(|e| log::error!("Dispatch failed: {e}")) + .ok(); + unsafe { SetEvent(self.event) }.ok(); + } + + fn dispatch_after(&self, duration: std::time::Duration, runnable: Runnable) { + self.timer_sender + .send((runnable, duration)) + .inspect_err(|e| log::error!("Dispatch failed: {e}")) + .ok(); + } + + fn tick(&self, _background_only: bool) -> bool { + false + } + + fn park(&self) { + self.parker.lock().park(); + } + + fn unparker(&self) -> parking::Unparker { + self.parker.lock().unparker() + } +} + +struct RunnableAfter { + runnable: Runnable, + instant: Instant, +} + +impl PartialEq for RunnableAfter { + fn eq(&self, other: &Self) -> bool { + self.instant == other.instant + } +} + +impl Eq for RunnableAfter {} + +impl Ord for RunnableAfter { + fn cmp(&self, other: &Self) -> Ordering { + self.instant.cmp(&other.instant).reverse() + } +} + +impl PartialOrd for RunnableAfter { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/crates/gpui/src/platform/windows/display.rs b/crates/gpui/src/platform/windows/display.rs new file mode 100644 index 0000000000..448433b6fb --- /dev/null +++ b/crates/gpui/src/platform/windows/display.rs @@ -0,0 +1,36 @@ +use anyhow::{anyhow, Result}; +use uuid::Uuid; + +use crate::{Bounds, DisplayId, GlobalPixels, PlatformDisplay, Point, Size}; + +#[derive(Debug)] +pub(crate) struct WindowsDisplay; + +impl WindowsDisplay { + pub(crate) fn new() -> Self { + Self + } +} + +impl PlatformDisplay for WindowsDisplay { + // todo!("windows") + fn id(&self) -> DisplayId { + DisplayId(1) + } + + // todo!("windows") + fn uuid(&self) -> Result { + Err(anyhow!("not implemented yet.")) + } + + // todo!("windows") + fn bounds(&self) -> Bounds { + Bounds::new( + Point::new(0.0.into(), 0.0.into()), + Size { + width: 1920.0.into(), + height: 1280.0.into(), + }, + ) + } +} diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs new file mode 100644 index 0000000000..be3d789813 --- /dev/null +++ b/crates/gpui/src/platform/windows/platform.rs @@ -0,0 +1,321 @@ +// todo!("windows"): remove +#![allow(unused_variables)] + +use std::{ + cell::RefCell, + collections::HashSet, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, + time::Duration, +}; + +use anyhow::{anyhow, Result}; +use async_task::Runnable; +use futures::channel::oneshot::Receiver; +use parking_lot::Mutex; +use time::UtcOffset; +use util::SemanticVersion; +use windows::Win32::{ + Foundation::{CloseHandle, HANDLE, HWND}, + System::Threading::{CreateEventW, INFINITE}, + UI::WindowsAndMessaging::{ + DispatchMessageW, MsgWaitForMultipleObjects, PeekMessageW, PostQuitMessage, + TranslateMessage, MSG, PM_REMOVE, QS_ALLINPUT, WM_QUIT, + }, +}; + +use crate::{ + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, + Keymap, Menu, PathPromptOptions, Platform, PlatformDisplay, PlatformInput, PlatformTextSystem, + PlatformWindow, Task, WindowAppearance, WindowOptions, WindowsDispatcher, WindowsDisplay, + WindowsTextSystem, WindowsWindow, +}; + +pub(crate) struct WindowsPlatform { + inner: Rc, +} + +pub(crate) struct WindowsPlatformInner { + background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, + main_receiver: flume::Receiver, + text_system: Arc, + callbacks: Mutex, + pub(crate) window_handles: RefCell>, + pub(crate) event: HANDLE, +} + +impl Drop for WindowsPlatformInner { + fn drop(&mut self) { + unsafe { CloseHandle(self.event) }.ok(); + } +} + +#[derive(Default)] +struct Callbacks { + open_urls: Option)>>, + become_active: Option>, + resign_active: Option>, + quit: Option>, + reopen: Option>, + event: Option bool>>, + app_menu_action: Option>, + will_open_app_menu: Option>, + validate_app_menu_command: Option bool>>, +} + +impl WindowsPlatform { + pub(crate) fn new() -> Self { + let (main_sender, main_receiver) = flume::unbounded::(); + let event = unsafe { CreateEventW(None, false, false, None) }.unwrap(); + let dispatcher = Arc::new(WindowsDispatcher::new(main_sender, event)); + let background_executor = BackgroundExecutor::new(dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(dispatcher); + let text_system = Arc::new(WindowsTextSystem::new()); + let callbacks = Mutex::new(Callbacks::default()); + let window_handles = RefCell::new(HashSet::new()); + let inner = Rc::new(WindowsPlatformInner { + background_executor, + foreground_executor, + main_receiver, + text_system, + callbacks, + window_handles, + event, + }); + Self { inner } + } +} + +impl Platform for WindowsPlatform { + fn background_executor(&self) -> BackgroundExecutor { + self.inner.background_executor.clone() + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.inner.foreground_executor.clone() + } + + fn text_system(&self) -> Arc { + self.inner.text_system.clone() + } + + fn run(&self, on_finish_launching: Box) { + on_finish_launching(); + 'a: loop { + unsafe { + MsgWaitForMultipleObjects(Some(&[self.inner.event]), false, INFINITE, QS_ALLINPUT) + }; + let mut msg = MSG::default(); + while unsafe { PeekMessageW(&mut msg, HWND::default(), 0, 0, PM_REMOVE) }.as_bool() { + if msg.message == WM_QUIT { + break 'a; + } + unsafe { TranslateMessage(&msg) }; + unsafe { DispatchMessageW(&msg) }; + } + while let Ok(runnable) = self.inner.main_receiver.try_recv() { + runnable.run(); + } + } + let mut callbacks = self.inner.callbacks.lock(); + if let Some(callback) = callbacks.quit.as_mut() { + callback() + } + } + + fn quit(&self) { + self.foreground_executor() + .spawn(async { unsafe { PostQuitMessage(0) } }) + .detach(); + } + + // todo!("windows") + fn restart(&self) { + unimplemented!() + } + + // todo!("windows") + fn activate(&self, ignoring_other_apps: bool) {} + + // todo!("windows") + fn hide(&self) { + unimplemented!() + } + + // todo!("windows") + fn hide_other_apps(&self) { + unimplemented!() + } + + // todo!("windows") + fn unhide_other_apps(&self) { + unimplemented!() + } + + // todo!("windows") + fn displays(&self) -> Vec> { + vec![Rc::new(WindowsDisplay::new())] + } + + // todo!("windows") + fn display(&self, id: crate::DisplayId) -> Option> { + Some(Rc::new(WindowsDisplay::new())) + } + + // todo!("windows") + fn active_window(&self) -> Option { + unimplemented!() + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowOptions, + ) -> Box { + Box::new(WindowsWindow::new(self.inner.clone(), handle, options)) + } + + // todo!("windows") + fn window_appearance(&self) -> WindowAppearance { + WindowAppearance::Dark + } + + // todo!("windows") + fn open_url(&self, url: &str) { + // todo!("windows") + } + + // todo!("windows") + fn on_open_urls(&self, callback: Box)>) { + self.inner.callbacks.lock().open_urls = Some(callback); + } + + // todo!("windows") + fn prompt_for_paths(&self, options: PathPromptOptions) -> Receiver>> { + unimplemented!() + } + + // todo!("windows") + fn prompt_for_new_path(&self, directory: &Path) -> Receiver> { + unimplemented!() + } + + // todo!("windows") + fn reveal_path(&self, path: &Path) { + unimplemented!() + } + + fn on_become_active(&self, callback: Box) { + self.inner.callbacks.lock().become_active = Some(callback); + } + + fn on_resign_active(&self, callback: Box) { + self.inner.callbacks.lock().resign_active = Some(callback); + } + + fn on_quit(&self, callback: Box) { + self.inner.callbacks.lock().quit = Some(callback); + } + + fn on_reopen(&self, callback: Box) { + self.inner.callbacks.lock().reopen = Some(callback); + } + + fn on_event(&self, callback: Box bool>) { + self.inner.callbacks.lock().event = Some(callback); + } + + // todo!("windows") + fn set_menus(&self, menus: Vec, keymap: &Keymap) {} + + fn on_app_menu_action(&self, callback: Box) { + self.inner.callbacks.lock().app_menu_action = Some(callback); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.inner.callbacks.lock().will_open_app_menu = Some(callback); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.inner.callbacks.lock().validate_app_menu_command = Some(callback); + } + + fn os_name(&self) -> &'static str { + "Windows" + } + + fn os_version(&self) -> Result { + Ok(SemanticVersion { + major: 1, + minor: 0, + patch: 0, + }) + } + + fn app_version(&self) -> Result { + Ok(SemanticVersion { + major: 1, + minor: 0, + patch: 0, + }) + } + + // todo!("windows") + fn app_path(&self) -> Result { + Err(anyhow!("not yet implemented")) + } + + // todo!("windows") + fn local_timezone(&self) -> UtcOffset { + UtcOffset::from_hms(9, 0, 0).unwrap() + } + + // todo!("windows") + fn double_click_interval(&self) -> Duration { + Duration::from_millis(100) + } + + // todo!("windows") + fn path_for_auxiliary_executable(&self, name: &str) -> Result { + Err(anyhow!("not yet implemented")) + } + + // todo!("windows") + fn set_cursor_style(&self, style: CursorStyle) {} + + // todo!("windows") + fn should_auto_hide_scrollbars(&self) -> bool { + false + } + + // todo!("windows") + fn write_to_clipboard(&self, item: ClipboardItem) { + unimplemented!() + } + + // todo!("windows") + fn read_from_clipboard(&self) -> Option { + unimplemented!() + } + + // todo!("windows") + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task> { + Task::Ready(Some(Err(anyhow!("not implemented yet.")))) + } + + // todo!("windows") + fn read_credentials(&self, url: &str) -> Task)>>> { + Task::Ready(Some(Err(anyhow!("not implemented yet.")))) + } + + // todo!("windows") + fn delete_credentials(&self, url: &str) -> Task> { + Task::Ready(Some(Err(anyhow!("not implemented yet.")))) + } + + fn register_url_scheme(&self, _: &str) -> Task> { + Task::ready(Err(anyhow!("register_url_scheme unimplemented"))) + } +} diff --git a/crates/gpui/src/platform/windows/text_system.rs b/crates/gpui/src/platform/windows/text_system.rs new file mode 100644 index 0000000000..411e1a8d28 --- /dev/null +++ b/crates/gpui/src/platform/windows/text_system.rs @@ -0,0 +1,440 @@ +use crate::{ + point, size, Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, + FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, + ShapedGlyph, SharedString, Size, +}; +use anyhow::{anyhow, Context, Ok, Result}; +use collections::HashMap; +use cosmic_text::{ + fontdb::Query, Attrs, AttrsList, BufferLine, CacheKey, Family, Font as CosmicTextFont, + FontSystem, SwashCache, +}; +use parking_lot::{RwLock, RwLockUpgradableReadGuard}; +use pathfinder_geometry::{ + rect::{RectF, RectI}, + vector::{Vector2F, Vector2I}, +}; +use smallvec::SmallVec; +use std::{borrow::Cow, sync::Arc}; + +pub(crate) struct WindowsTextSystem(RwLock); + +struct WindowsTextSystemState { + swash_cache: SwashCache, + font_system: FontSystem, + fonts: Vec>, + font_selections: HashMap, + font_ids_by_family_name: HashMap>, + postscript_names_by_font_id: HashMap, +} + +impl WindowsTextSystem { + pub(crate) fn new() -> Self { + let mut font_system = FontSystem::new(); + + // todo!("windows") make font loading non-blocking + font_system.db_mut().load_system_fonts(); + + Self(RwLock::new(WindowsTextSystemState { + font_system, + swash_cache: SwashCache::new(), + fonts: Vec::new(), + font_selections: HashMap::default(), + // font_ids_by_postscript_name: HashMap::default(), + font_ids_by_family_name: HashMap::default(), + postscript_names_by_font_id: HashMap::default(), + })) + } +} + +impl Default for WindowsTextSystem { + fn default() -> Self { + Self::new() + } +} + +#[allow(unused)] +impl PlatformTextSystem for WindowsTextSystem { + fn add_fonts(&self, fonts: Vec>) -> Result<()> { + self.0.write().add_fonts(fonts) + } + + // todo!("windows") ensure that this integrates with platform font loading + // do we need to do more than call load_system_fonts()? + fn all_font_names(&self) -> Vec { + self.0 + .read() + .font_system + .db() + .faces() + .map(|face| face.post_script_name.clone()) + .collect() + } + + // todo!("windows") + fn all_font_families(&self) -> Vec { + Vec::new() + } + + fn font_id(&self, font: &Font) -> Result { + // todo!("windows"): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit? + let lock = self.0.upgradable_read(); + if let Some(font_id) = lock.font_selections.get(font) { + Ok(*font_id) + } else { + let mut lock = RwLockUpgradableReadGuard::upgrade(lock); + let candidates = if let Some(font_ids) = lock.font_ids_by_family_name.get(&font.family) + { + font_ids.as_slice() + } else { + let font_ids = lock.load_family(&font.family, font.features)?; + lock.font_ids_by_family_name + .insert(font.family.clone(), font_ids); + lock.font_ids_by_family_name[&font.family].as_ref() + }; + + let id = lock + .font_system + .db() + .query(&Query { + families: &[Family::Name(&font.family)], + weight: font.weight.into(), + style: font.style.into(), + stretch: Default::default(), + }) + .context("no font")?; + + let font_id = if let Some(font_id) = lock.fonts.iter().position(|font| font.id() == id) + { + FontId(font_id) + } else { + // Font isn't in fonts so add it there, this is because we query all the fonts in the db + // and maybe we haven't loaded it yet + let font_id = FontId(lock.fonts.len()); + let font = lock.font_system.get_font(id).unwrap(); + lock.fonts.push(font); + font_id + }; + + lock.font_selections.insert(font.clone(), font_id); + Ok(font_id) + } + } + + fn font_metrics(&self, font_id: FontId) -> FontMetrics { + let metrics = self.0.read().fonts[font_id.0].as_swash().metrics(&[]); + + FontMetrics { + units_per_em: metrics.units_per_em as u32, + ascent: metrics.ascent, + descent: -metrics.descent, // todo!("windows") confirm this is correct + line_gap: metrics.leading, + underline_position: metrics.underline_offset, + underline_thickness: metrics.stroke_size, + cap_height: metrics.cap_height, + x_height: metrics.x_height, + // todo!("windows"): Compute this correctly + bounding_box: Bounds { + origin: point(0.0, 0.0), + size: size(metrics.max_width, metrics.ascent + metrics.descent), + }, + } + } + + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + let lock = self.0.read(); + let metrics = lock.fonts[font_id.0].as_swash().metrics(&[]); + let glyph_metrics = lock.fonts[font_id.0].as_swash().glyph_metrics(&[]); + let glyph_id = glyph_id.0 as u16; + // todo!("windows"): Compute this correctly + // see https://github.com/servo/font-kit/blob/master/src/loaders/freetype.rs#L614-L620 + Ok(Bounds { + origin: point(0.0, 0.0), + size: size( + glyph_metrics.advance_width(glyph_id), + glyph_metrics.advance_height(glyph_id), + ), + }) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + self.0.read().advance(font_id, glyph_id) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + self.0.read().glyph_for_char(font_id, ch) + } + + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + self.0.write().raster_bounds(params) + } + + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)> { + self.0.write().rasterize_glyph(params, raster_bounds) + } + + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout { + self.0.write().layout_line(text, font_size, runs) + } + + // todo!("windows") Confirm that this has been superseded by the LineWrapper + fn wrap_line( + &self, + text: &str, + font_id: FontId, + font_size: Pixels, + width: Pixels, + ) -> Vec { + unimplemented!() + } +} + +impl WindowsTextSystemState { + #[profiling::function] + fn add_fonts(&mut self, fonts: Vec>) -> Result<()> { + let db = self.font_system.db_mut(); + for bytes in fonts { + match bytes { + Cow::Borrowed(embedded_font) => { + db.load_font_data(embedded_font.to_vec()); + } + Cow::Owned(bytes) => { + db.load_font_data(bytes); + } + } + } + Ok(()) + } + + #[profiling::function] + fn load_family( + &mut self, + name: &SharedString, + _features: FontFeatures, + ) -> Result> { + let mut font_ids = SmallVec::new(); + let family = self + .font_system + .get_font_matches(Attrs::new().family(cosmic_text::Family::Name(name))); + for font in family.as_ref() { + let font = self.font_system.get_font(*font).unwrap(); + if font.as_swash().charmap().map('m') == 0 { + self.font_system.db_mut().remove_face(font.id()); + continue; + }; + + let font_id = FontId(self.fonts.len()); + font_ids.push(font_id); + self.fonts.push(font); + } + Ok(font_ids) + } + + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { + let width = self.fonts[font_id.0] + .as_swash() + .glyph_metrics(&[]) + .advance_width(glyph_id.0 as u16); + let height = self.fonts[font_id.0] + .as_swash() + .glyph_metrics(&[]) + .advance_height(glyph_id.0 as u16); + Ok(Size { width, height }) + } + + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { + let glyph_id = self.fonts[font_id.0].as_swash().charmap().map(ch); + if glyph_id == 0 { + None + } else { + Some(GlyphId(glyph_id.into())) + } + } + + fn is_emoji(&self, font_id: FontId) -> bool { + // todo!("windows"): implement this correctly + self.postscript_names_by_font_id + .get(&font_id) + .map_or(false, |postscript_name| { + postscript_name == "AppleColorEmoji" + }) + } + + // todo!("windows") both raster functions have problems because I am not sure this is the correct mapping from cosmic text to gpui system + fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result> { + let font = &self.fonts[params.font_id.0]; + let font_system = &mut self.font_system; + let image = self + .swash_cache + .get_image( + font_system, + CacheKey::new( + font.id(), + params.glyph_id.0 as u16, + (params.font_size * params.scale_factor).into(), + (0.0, 0.0), + ) + .0, + ) + .clone() + .unwrap(); + Ok(Bounds { + origin: point(image.placement.left.into(), (-image.placement.top).into()), + size: size(image.placement.width.into(), image.placement.height.into()), + }) + } + + #[profiling::function] + fn rasterize_glyph( + &mut self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result<(Size, Vec)> { + if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { + Err(anyhow!("glyph bounds are empty")) + } else { + // todo!("windows") handle subpixel variants + let bitmap_size = glyph_bounds.size; + let font = &self.fonts[params.font_id.0]; + let font_system = &mut self.font_system; + let image = self + .swash_cache + .get_image( + font_system, + CacheKey::new( + font.id(), + params.glyph_id.0 as u16, + (params.font_size * params.scale_factor).into(), + (0.0, 0.0), + ) + .0, + ) + .clone() + .unwrap(); + + Ok((bitmap_size, image.data)) + } + } + + // todo!("windows") This is all a quick first pass, maybe we should be using cosmic_text::Buffer + #[profiling::function] + fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { + let mut attrs_list = AttrsList::new(Attrs::new()); + let mut offs = 0; + for run in font_runs { + // todo!("windows") We need to check we are doing utf properly + let font = &self.fonts[run.font_id.0]; + let font = self.font_system.db().face(font.id()).unwrap(); + attrs_list.add_span( + offs..offs + run.len, + Attrs::new() + .family(Family::Name(&font.families.first().unwrap().0)) + .stretch(font.stretch) + .style(font.style) + .weight(font.weight), + ); + offs += run.len; + } + let mut line = BufferLine::new(text, attrs_list, cosmic_text::Shaping::Advanced); + let layout = line.layout( + &mut self.font_system, + font_size.0, + f32::MAX, // todo!("windows") we don't have a width cause this should technically not be wrapped I believe + cosmic_text::Wrap::None, + ); + let mut runs = Vec::new(); + // todo!("windows") what I think can happen is layout returns possibly multiple lines which means we should be probably working with it higher up in the text rendering + let layout = layout.first().unwrap(); + for glyph in &layout.glyphs { + let font_id = glyph.font_id; + let font_id = FontId( + self.fonts + .iter() + .position(|font| font.id() == font_id) + .unwrap(), + ); + let mut glyphs = SmallVec::new(); + // todo!("windows") this is definitely wrong, each glyph in glyphs from cosmic-text is a cluster with one glyph, ShapedRun takes a run of glyphs with the same font and direction + glyphs.push(ShapedGlyph { + id: GlyphId(glyph.glyph_id as u32), + position: point((glyph.x).into(), glyph.y.into()), + index: glyph.start, + is_emoji: self.is_emoji(font_id), + }); + runs.push(crate::ShapedRun { font_id, glyphs }); + } + LineLayout { + font_size, + width: layout.w.into(), + ascent: layout.max_ascent.into(), + descent: layout.max_descent.into(), + runs, + len: text.len(), + } + } +} + +impl From for Bounds { + fn from(rect: RectF) -> Self { + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } + } +} + +impl From for Bounds { + fn from(rect: RectI) -> Self { + Bounds { + origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())), + size: size(DevicePixels(rect.width()), DevicePixels(rect.height())), + } + } +} + +impl From for Size { + fn from(value: Vector2I) -> Self { + size(value.x().into(), value.y().into()) + } +} + +impl From for Bounds { + fn from(rect: RectI) -> Self { + Bounds { + origin: point(rect.origin_x(), rect.origin_y()), + size: size(rect.width(), rect.height()), + } + } +} + +impl From> for Vector2I { + fn from(size: Point) -> Self { + Vector2I::new(size.x as i32, size.y as i32) + } +} + +impl From for Size { + fn from(vec: Vector2F) -> Self { + size(vec.x(), vec.y()) + } +} + +impl From for cosmic_text::Weight { + fn from(value: FontWeight) -> Self { + cosmic_text::Weight(value.0 as u16) + } +} + +impl From for cosmic_text::Style { + fn from(style: FontStyle) -> Self { + match style { + FontStyle::Normal => cosmic_text::Style::Normal, + FontStyle::Italic => cosmic_text::Style::Italic, + FontStyle::Oblique => cosmic_text::Style::Oblique, + } + } +} diff --git a/crates/gpui/src/platform/windows/util.rs b/crates/gpui/src/platform/windows/util.rs new file mode 100644 index 0000000000..bba6f132ab --- /dev/null +++ b/crates/gpui/src/platform/windows/util.rs @@ -0,0 +1,26 @@ +use windows::Win32::Foundation::{LPARAM, WPARAM}; + +pub(crate) trait HiLoWord { + fn hiword(&self) -> u16; + fn loword(&self) -> u16; +} + +impl HiLoWord for WPARAM { + fn hiword(&self) -> u16 { + ((self.0 >> 16) & 0xFFFF) as u16 + } + + fn loword(&self) -> u16 { + (self.0 & 0xFFFF) as u16 + } +} + +impl HiLoWord for LPARAM { + fn hiword(&self) -> u16 { + ((self.0 >> 16) & 0xFFFF) as u16 + } + + fn loword(&self) -> u16 { + (self.0 & 0xFFFF) as u16 + } +} diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs new file mode 100644 index 0000000000..f43ac7cec8 --- /dev/null +++ b/crates/gpui/src/platform/windows/window.rs @@ -0,0 +1,535 @@ +#![deny(unsafe_op_in_unsafe_fn)] +// todo!("windows"): remove +#![allow(unused_variables)] + +use std::{ + any::Any, + cell::{Cell, RefCell}, + ffi::c_void, + num::NonZeroIsize, + rc::{Rc, Weak}, + sync::{Arc, Once}, +}; + +use blade_graphics as gpu; +use futures::channel::oneshot::Receiver; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use windows::{ + core::{w, HSTRING, PCWSTR}, + Win32::{ + Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM}, + UI::WindowsAndMessaging::{ + CreateWindowExW, DefWindowProcW, GetWindowLongPtrW, LoadCursorW, PostQuitMessage, + RegisterClassW, SetWindowLongPtrW, SetWindowTextW, ShowWindow, CREATESTRUCTW, + CW_USEDEFAULT, GWLP_USERDATA, HMENU, IDC_ARROW, SW_MAXIMIZE, SW_SHOW, WINDOW_EX_STYLE, + WINDOW_LONG_PTR_INDEX, WM_CLOSE, WM_DESTROY, WM_MOVE, WM_NCCREATE, WM_NCDESTROY, + WM_PAINT, WM_SIZE, WNDCLASSW, WS_OVERLAPPEDWINDOW, WS_VISIBLE, + }, + }, +}; + +use crate::{ + platform::blade::BladeRenderer, AnyWindowHandle, Bounds, GlobalPixels, HiLoWord, Modifiers, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, + Point, PromptLevel, Scene, Size, WindowAppearance, WindowBounds, WindowOptions, WindowsDisplay, + WindowsPlatformInner, +}; + +struct WindowsWindowInner { + hwnd: HWND, + origin: Cell>, + size: Cell>, + mouse_position: Cell>, + input_handler: Cell>, + renderer: RefCell, + callbacks: RefCell, + platform_inner: Rc, + handle: AnyWindowHandle, +} + +impl WindowsWindowInner { + fn new( + hwnd: HWND, + cs: &CREATESTRUCTW, + platform_inner: Rc, + handle: AnyWindowHandle, + ) -> Self { + let origin = Cell::new(Point::new((cs.x as f64).into(), (cs.y as f64).into())); + let size = Cell::new(Size { + width: (cs.cx as f64).into(), + height: (cs.cy as f64).into(), + }); + let mouse_position = Cell::new(Point::default()); + let input_handler = Cell::new(None); + struct RawWindow { + hwnd: *mut c_void, + } + unsafe impl blade_rwh::HasRawWindowHandle for RawWindow { + fn raw_window_handle(&self) -> blade_rwh::RawWindowHandle { + let mut handle = blade_rwh::Win32WindowHandle::empty(); + handle.hwnd = self.hwnd; + handle.into() + } + } + unsafe impl blade_rwh::HasRawDisplayHandle for RawWindow { + fn raw_display_handle(&self) -> blade_rwh::RawDisplayHandle { + blade_rwh::WindowsDisplayHandle::empty().into() + } + } + let raw = RawWindow { hwnd: hwnd.0 as _ }; + let gpu = Arc::new( + unsafe { + gpu::Context::init_windowed( + &raw, + gpu::ContextDesc { + validation: false, + capture: false, + }, + ) + } + .unwrap(), + ); + let extent = gpu::Extent { + width: 1, + height: 1, + depth: 1, + }; + let renderer = RefCell::new(BladeRenderer::new(gpu, extent)); + let callbacks = RefCell::new(Callbacks::default()); + Self { + hwnd, + origin, + size, + mouse_position, + input_handler, + renderer, + callbacks, + platform_inner, + handle, + } + } + + fn handle_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { + log::debug!("msg: {msg}, wparam: {}, lparam: {}", wparam.0, lparam.0); + match msg { + WM_MOVE => { + let x = lparam.loword() as f64; + let y = lparam.hiword() as f64; + self.origin.set(Point::new(x.into(), y.into())); + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(callback) = callbacks.moved.as_mut() { + callback() + } + } + WM_SIZE => { + // todo!("windows"): handle maximized or minimized + let width = lparam.loword().max(1) as f64; + let height = lparam.hiword().max(1) as f64; + self.renderer + .borrow_mut() + .update_drawable_size(Size { width, height }); + let width = width.into(); + let height = height.into(); + self.size.set(Size { width, height }); + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(callback) = callbacks.resize.as_mut() { + callback( + Size { + width: Pixels(width.0), + height: Pixels(height.0), + }, + 1.0, + ) + } + } + WM_PAINT => { + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(callback) = callbacks.request_frame.as_mut() { + callback() + } + } + WM_CLOSE => { + let mut callbacks: std::cell::RefMut<'_, Callbacks> = self.callbacks.borrow_mut(); + if let Some(callback) = callbacks.should_close.as_mut() { + if callback() { + return LRESULT(0); + } + } + drop(callbacks); + return unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) }; + } + WM_DESTROY => { + let mut callbacks: std::cell::RefMut<'_, Callbacks> = self.callbacks.borrow_mut(); + if let Some(callback) = callbacks.close.take() { + callback() + } + let mut window_handles = self.platform_inner.window_handles.borrow_mut(); + window_handles.remove(&self.handle); + if window_handles.is_empty() { + self.platform_inner + .foreground_executor + .spawn(async { + unsafe { PostQuitMessage(0) }; + }) + .detach(); + } + return LRESULT(1); + } + _ => return unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) }, + } + LRESULT(0) + } +} + +#[derive(Default)] +struct Callbacks { + request_frame: Option>, + input: Option bool>>, + active_status_change: Option>, + resize: Option, f32)>>, + fullscreen: Option>, + moved: Option>, + should_close: Option bool>>, + close: Option>, + appearance_changed: Option>, +} + +pub(crate) struct WindowsWindow { + inner: Rc, +} + +struct WindowCreateContext { + inner: Option>, + platform_inner: Rc, + handle: AnyWindowHandle, +} + +impl WindowsWindow { + pub(crate) fn new( + platform_inner: Rc, + handle: AnyWindowHandle, + options: WindowOptions, + ) -> Self { + let dwexstyle = WINDOW_EX_STYLE::default(); + let classname = register_wnd_class(); + let windowname = HSTRING::from( + options + .titlebar + .as_ref() + .and_then(|titlebar| titlebar.title.as_ref()) + .map(|title| title.as_ref()) + .unwrap_or(""), + ); + let dwstyle = WS_OVERLAPPEDWINDOW & !WS_VISIBLE; + let mut x = CW_USEDEFAULT; + let mut y = CW_USEDEFAULT; + let mut nwidth = CW_USEDEFAULT; + let mut nheight = CW_USEDEFAULT; + match options.bounds { + WindowBounds::Fullscreen => {} + WindowBounds::Maximized => {} + WindowBounds::Fixed(bounds) => { + x = bounds.origin.x.0 as i32; + y = bounds.origin.y.0 as i32; + nwidth = bounds.size.width.0 as i32; + nheight = bounds.size.height.0 as i32; + } + }; + let hwndparent = HWND::default(); + let hmenu = HMENU::default(); + let hinstance = HINSTANCE::default(); + let mut context = WindowCreateContext { + inner: None, + platform_inner: platform_inner.clone(), + handle, + }; + let lpparam = Some(&context as *const _ as *const _); + unsafe { + CreateWindowExW( + dwexstyle, + classname, + &windowname, + dwstyle, + x, + y, + nwidth, + nheight, + hwndparent, + hmenu, + hinstance, + lpparam, + ) + }; + let wnd = Self { + inner: context.inner.unwrap(), + }; + platform_inner.window_handles.borrow_mut().insert(handle); + match options.bounds { + WindowBounds::Fullscreen => wnd.toggle_full_screen(), + WindowBounds::Maximized => wnd.maximize(), + WindowBounds::Fixed(_) => {} + } + unsafe { ShowWindow(wnd.inner.hwnd, SW_SHOW) }; + wnd + } + + fn maximize(&self) { + unsafe { ShowWindow(self.inner.hwnd, SW_MAXIMIZE) }; + } +} + +impl HasWindowHandle for WindowsWindow { + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + let raw = raw_window_handle::Win32WindowHandle::new(unsafe { + NonZeroIsize::new_unchecked(self.inner.hwnd.0) + }) + .into(); + Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(raw) }) + } +} + +// todo!("windows") +impl HasDisplayHandle for WindowsWindow { + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + unimplemented!() + } +} + +impl PlatformWindow for WindowsWindow { + fn bounds(&self) -> WindowBounds { + WindowBounds::Fixed(Bounds { + origin: self.inner.origin.get(), + size: self.inner.size.get(), + }) + } + + // todo!("windows") + fn content_size(&self) -> Size { + let size = self.inner.size.get(); + Size { + width: size.width.0.into(), + height: size.height.0.into(), + } + } + + // todo!("windows") + fn scale_factor(&self) -> f32 { + 1.0 + } + + // todo!("windows") + fn titlebar_height(&self) -> Pixels { + 20.0.into() + } + + // todo!("windows") + fn appearance(&self) -> WindowAppearance { + WindowAppearance::Dark + } + + // todo!("windows") + fn display(&self) -> Rc { + Rc::new(WindowsDisplay::new()) + } + + fn mouse_position(&self) -> Point { + self.inner.mouse_position.get() + } + + // todo!("windows") + fn modifiers(&self) -> Modifiers { + Modifiers::none() + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + // todo!("windows") + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.inner.input_handler.set(Some(input_handler)); + } + + // todo!("windows") + fn take_input_handler(&mut self) -> Option { + self.inner.input_handler.take() + } + + // todo!("windows") + fn prompt( + &self, + level: PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[&str], + ) -> Receiver { + unimplemented!() + } + + // todo!("windows") + fn activate(&self) {} + + // todo!("windows") + fn set_title(&mut self, title: &str) { + unsafe { SetWindowTextW(self.inner.hwnd, &HSTRING::from(title)) } + .inspect_err(|e| log::error!("Set title failed: {e}")) + .ok(); + } + + // todo!("windows") + fn set_edited(&mut self, edited: bool) {} + + // todo!("windows") + fn show_character_palette(&self) {} + + // todo!("windows") + fn minimize(&self) {} + + // todo!("windows") + fn zoom(&self) {} + + // todo!("windows") + fn toggle_full_screen(&self) {} + + // todo!("windows") + fn on_request_frame(&self, callback: Box) { + self.inner.callbacks.borrow_mut().request_frame = Some(callback); + } + + // todo!("windows") + fn on_input(&self, callback: Box bool>) { + self.inner.callbacks.borrow_mut().input = Some(callback); + } + + // todo!("windows") + fn on_active_status_change(&self, callback: Box) { + self.inner.callbacks.borrow_mut().active_status_change = Some(callback); + } + + // todo!("windows") + fn on_resize(&self, callback: Box, f32)>) { + self.inner.callbacks.borrow_mut().resize = Some(callback); + } + + // todo!("windows") + fn on_fullscreen(&self, callback: Box) { + self.inner.callbacks.borrow_mut().fullscreen = Some(callback); + } + + // todo!("windows") + fn on_moved(&self, callback: Box) { + self.inner.callbacks.borrow_mut().moved = Some(callback); + } + + // todo!("windows") + fn on_should_close(&self, callback: Box bool>) { + self.inner.callbacks.borrow_mut().should_close = Some(callback); + } + + // todo!("windows") + fn on_close(&self, callback: Box) { + self.inner.callbacks.borrow_mut().close = Some(callback); + } + + // todo!("windows") + fn on_appearance_changed(&self, callback: Box) { + self.inner.callbacks.borrow_mut().appearance_changed = Some(callback); + } + + // todo!("windows") + fn is_topmost_for_position(&self, position: Point) -> bool { + true + } + + // todo!("windows") + fn draw(&self, scene: &Scene) { + self.inner.renderer.borrow_mut().draw(scene) + } + + // todo!("windows") + fn sprite_atlas(&self) -> Arc { + self.inner.renderer.borrow().sprite_atlas().clone() + } +} + +fn register_wnd_class() -> PCWSTR { + const CLASS_NAME: PCWSTR = w!("Zed::Window"); + + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + let wc = WNDCLASSW { + lpfnWndProc: Some(wnd_proc), + hCursor: unsafe { LoadCursorW(None, IDC_ARROW).ok().unwrap() }, + lpszClassName: PCWSTR(CLASS_NAME.as_ptr()), + ..Default::default() + }; + unsafe { RegisterClassW(&wc) }; + }); + + CLASS_NAME +} + +unsafe extern "system" fn wnd_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + if msg == WM_NCCREATE { + let cs = lparam.0 as *const CREATESTRUCTW; + let cs = unsafe { &*cs }; + let ctx = cs.lpCreateParams as *mut WindowCreateContext; + let ctx = unsafe { &mut *ctx }; + let inner = Rc::new(WindowsWindowInner::new( + hwnd, + cs, + ctx.platform_inner.clone(), + ctx.handle, + )); + let weak = Box::new(Rc::downgrade(&inner)); + unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; + ctx.inner = Some(inner); + return LRESULT(1); + } + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + if ptr.is_null() { + return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; + } + let inner = unsafe { &*ptr }; + let r = if let Some(inner) = inner.upgrade() { + inner.handle_msg(msg, wparam, lparam) + } else { + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } + }; + if msg == WM_NCDESTROY { + unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) }; + unsafe { std::mem::drop(Box::from_raw(ptr)) }; + } + r +} + +unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize { + #[cfg(target_pointer_width = "64")] + unsafe { + GetWindowLongPtrW(hwnd, nindex) + } + #[cfg(target_pointer_width = "32")] + unsafe { + GetWindowLongW(hwnd, nindex) as isize + } +} + +unsafe fn set_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX, dwnewlong: isize) -> isize { + #[cfg(target_pointer_width = "64")] + unsafe { + SetWindowLongPtrW(hwnd, nindex, dwnewlong) + } + #[cfg(target_pointer_width = "32")] + unsafe { + SetWindowLongW(hwnd, nindex, dwnewlong as i32) as isize + } +} diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 17cb25a12f..1f7088ddfb 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -1,3 +1,6 @@ +// todo("windows"): remove +#![cfg_attr(windows, allow(dead_code))] + use crate::{ point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, EntityId, Hsla, Pixels, Point, ScaledPixels, StackingOrder, diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 71dccaf170..6c8fb400f2 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -122,7 +122,7 @@ pub struct Style { #[cfg(debug_assertions)] pub debug: bool, - /// Whether to draw a red debugging outline around this element and all of it's conforming children + /// Whether to draw a red debugging outline around this element and all of its conforming children #[cfg(debug_assertions)] pub debug_below: bool, } @@ -148,7 +148,7 @@ pub enum Visibility { pub struct BoxShadow { /// What color should the shadow have? pub color: Hsla, - /// How should it be offset from it's element? + /// How should it be offset from its element? pub offset: Point, /// How much should the shadow be blurred? pub blur_radius: Pixels, @@ -208,7 +208,7 @@ impl Default for TextStyle { fn default() -> Self { TextStyle { color: black(), - // todo!(linux) make this configurable or choose better default + // todo(linux) make this configurable or choose better default font_family: if cfg!(target_os = "linux") { "FreeMono".into() } else { @@ -333,7 +333,7 @@ impl Style { } /// Get the content mask for this element style, based on the given bounds. - /// If the element does not hide it's overflow, this will return `None`. + /// If the element does not hide its overflow, this will return `None`. pub fn overflow_mask( &self, bounds: Bounds, diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index d4014ffc2f..928f9f0a23 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -1,11 +1,11 @@ use crate::{ self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, - DefiniteLength, Fill, FlexDirection, FontWeight, Hsla, JustifyContent, Length, Position, - SharedString, StyleRefinement, Visibility, WhiteSpace, + DefiniteLength, Fill, FlexDirection, FlexWrap, FontWeight, Hsla, JustifyContent, Length, + Position, SharedString, StyleRefinement, Visibility, WhiteSpace, }; use crate::{BoxShadow, TextStyleRefinement}; use smallvec::{smallvec, SmallVec}; -use taffy::style::{Display, Overflow}; +use taffy::style::{AlignContent, Display, Overflow}; /// A trait for elements that can be styled. /// Use this to opt-in to a CSS-like styling API. @@ -333,6 +333,27 @@ pub trait Styled: Sized { self } + /// Sets the element to allow flex items to wrap. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#wrap-normally) + fn flex_wrap(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::Wrap); + self + } + + /// Sets the element wrap flex items in the reverse direction. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#wrap-reversed) + fn flex_wrap_reverse(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::WrapReverse); + self + } + + /// Sets the element to prevent flex items from wrapping, causing inflexible items to overflow the container if necessary. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#dont-wrap) + fn flex_nowrap(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::NoWrap); + self + } + /// Sets the element to align flex items to the start of the container's cross axis. /// [Docs](https://tailwindcss.com/docs/align-items#start) fn items_start(mut self) -> Self { @@ -391,6 +412,65 @@ pub trait Styled: Sized { self } + /// Sets the element to pack content items in their default position as if no align-content value was set. + /// [Docs](https://tailwindcss.com/docs/align-content#normal) + fn content_normal(mut self) -> Self { + self.style().align_content = None; + self + } + + /// Sets the element to pack content items in the center of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#center) + fn content_center(mut self) -> Self { + self.style().align_content = Some(AlignContent::Center); + self + } + + /// Sets the element to pack content items against the start of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#start) + fn content_start(mut self) -> Self { + self.style().align_content = Some(AlignContent::FlexStart); + self + } + + /// Sets the element to pack content items against the end of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#end) + fn content_end(mut self) -> Self { + self.style().align_content = Some(AlignContent::FlexEnd); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space between each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-between) + fn content_between(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceBetween); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space on each side of each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-around) + fn content_around(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceAround); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space between each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-evenly) + fn content_evenly(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceEvenly); + self + } + + /// Sets the element to allow content items to fill the available space along the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#stretch) + fn content_stretch(mut self) -> Self { + self.style().align_content = Some(AlignContent::Stretch); + self + } + /// Sets the background color of the element. fn bg(mut self, fill: F) -> Self where @@ -515,13 +595,13 @@ pub trait Styled: Sized { &mut style.text } - /// Set the text color of this element, this value cascades to it's child elements. + /// Set the text color of this element, this value cascades to its child elements. fn text_color(mut self, color: impl Into) -> Self { self.text_style().get_or_insert_with(Default::default).color = Some(color.into()); self } - /// Set the font weight of this element, this value cascades to it's child elements. + /// Set the font weight of this element, this value cascades to its child elements. fn font_weight(mut self, weight: FontWeight) -> Self { self.text_style() .get_or_insert_with(Default::default) @@ -529,7 +609,7 @@ pub trait Styled: Sized { self } - /// Set the background color of this element, this value cascades to it's child elements. + /// Set the background color of this element, this value cascades to its child elements. fn text_bg(mut self, bg: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) @@ -537,7 +617,7 @@ pub trait Styled: Sized { self } - /// Set the text size of this element, this value cascades to it's child elements. + /// Set the text size of this element, this value cascades to its child elements. fn text_size(mut self, size: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) @@ -563,7 +643,7 @@ pub trait Styled: Sized { self } - /// Reset the text styling for this element and it's children. + /// Reset the text styling for this element and its children. fn text_base(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) @@ -607,7 +687,7 @@ pub trait Styled: Sized { self } - /// Remove the text decoration on this element, this value cascades to it's child elements. + /// Remove the text decoration on this element, this value cascades to its child elements. fn text_decoration_none(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) @@ -679,7 +759,7 @@ pub trait Styled: Sized { self } - /// Change the font on this element and it's children. + /// Change the font on this element and its children. fn font(mut self, family_name: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) @@ -687,7 +767,7 @@ pub trait Styled: Sized { self } - /// Set the line height on this element and it's children. + /// Set the line height on this element and its children. fn line_height(mut self, line_height: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) diff --git a/crates/gpui/src/test.rs b/crates/gpui/src/test.rs index 77540704f9..eab11e7b4c 100644 --- a/crates/gpui/src/test.rs +++ b/crates/gpui/src/test.rs @@ -72,7 +72,9 @@ pub fn run_test( if is_randomized { eprintln!("failing seed: {}", seed); } - on_fail_fn.map(|f| f()); + if let Some(f) = on_fail_fn { + f() + } panic::resume_unwind(error); } } diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 014c2bb1ec..ac27705801 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -40,7 +40,7 @@ pub struct ShapedGlyph { /// The ID for this glyph, as determined by the text system. pub id: GlyphId, - /// The position of this glyph in it's containing line. + /// The position of this glyph in its containing line. pub position: Point, /// The index of this glyph in the original text. diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index c6951ff7a3..f615b192ee 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -280,7 +280,6 @@ pub struct Window { pub(crate) focus: Option, focus_enabled: bool, pending_input: Option, - graphics_profiler_enabled: bool, } #[derive(Default, Debug)] @@ -474,7 +473,6 @@ impl Window { focus: None, focus_enabled: true, pending_input: None, - graphics_profiler_enabled: false, } } fn new_focus_listener( @@ -950,7 +948,8 @@ impl<'a> WindowContext<'a> { /// Produces a new frame and assigns it to `rendered_frame`. To actually show /// the contents of the new [Scene], use [present]. - pub(crate) fn draw(&mut self) { + #[profiling::function] + pub fn draw(&mut self) { self.window.dirty.set(false); self.window.drawing = true; @@ -1022,13 +1021,10 @@ impl<'a> WindowContext<'a> { self.window.root_view = Some(root_view); // Set the cursor only if we're the active window. - let cursor_style = self - .window - .next_frame - .requested_cursor_style - .take() - .unwrap_or(CursorStyle::Arrow); + let cursor_style_request = self.window.next_frame.requested_cursor_style.take(); if self.is_window_active() { + let cursor_style = + cursor_style_request.map_or(CursorStyle::Arrow, |request| request.style); self.platform.set_cursor_style(cursor_style); } @@ -1092,14 +1088,55 @@ impl<'a> WindowContext<'a> { self.window.needs_present.set(true); } + #[profiling::function] fn present(&self) { self.window .platform_window .draw(&self.window.rendered_frame.scene); self.window.needs_present.set(false); + profiling::finish_frame!(); + } + + /// Dispatch a given keystroke as though the user had typed it. + /// You can create a keystroke with Keystroke::parse(""). + pub fn dispatch_keystroke(&mut self, keystroke: Keystroke) -> bool { + let keystroke = keystroke.with_simulated_ime(); + if self.dispatch_event(PlatformInput::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held: false, + })) { + return true; + } + + if let Some(input) = keystroke.ime_key { + if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { + input_handler.dispatch_input(&input, self); + self.window.platform_window.set_input_handler(input_handler); + return true; + } + } + + false + } + + /// Represent this action as a key binding string, to display in the UI. + pub fn keystroke_text_for(&self, action: &dyn Action) -> String { + self.bindings_for_action(action) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") + }) + .unwrap_or_else(|| action.name().to_string()) } /// Dispatch a mouse or keyboard event on the window. + #[profiling::function] pub fn dispatch_event(&mut self, event: PlatformInput) -> bool { self.window.last_input_timestamp.set(Instant::now()); // Handlers may set this to false by calling `stop_propagation`. @@ -1423,7 +1460,7 @@ impl<'a> WindowContext<'a> { if !input.is_empty() { if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { - input_handler.flush_pending_input(&input, self); + input_handler.dispatch_input(&input, self); self.window.platform_window.set_input_handler(input_handler) } } @@ -1480,14 +1517,6 @@ impl<'a> WindowContext<'a> { } } - /// Toggle the graphics profiler to debug your application's rendering performance. - pub fn toggle_graphics_profiler(&mut self) { - self.window.graphics_profiler_enabled = !self.window.graphics_profiler_enabled; - self.window - .platform_window - .set_graphics_profiler_enabled(self.window.graphics_profiler_enabled); - } - /// Register the given handler to be invoked whenever the global of the given type /// is updated. pub fn observe_global( @@ -2528,11 +2557,11 @@ impl WindowHandle { /// Check if this window is 'active'. /// - /// Will return `None` if the window is closed. - pub fn is_active(&self, cx: &AppContext) -> Option { - cx.windows - .get(self.id) - .and_then(|window| window.as_ref().map(|window| window.active.get())) + /// Will return `None` if the window is closed or currently + /// borrowed. + pub fn is_active(&self, cx: &mut AppContext) -> Option { + cx.update_window(self.any_handle, |_, cx| cx.is_window_active()) + .ok() } } diff --git a/crates/gpui/src/window/element_cx.rs b/crates/gpui/src/window/element_cx.rs index 22ab59a84c..46b5a21cf3 100644 --- a/crates/gpui/src/window/element_cx.rs +++ b/crates/gpui/src/window/element_cx.rs @@ -51,6 +51,12 @@ pub(crate) struct TooltipRequest { pub(crate) tooltip: AnyTooltip, } +#[derive(Clone)] +pub(crate) struct CursorStyleRequest { + pub(crate) style: CursorStyle, + stacking_order: StackingOrder, +} + pub(crate) struct Frame { pub(crate) focus: Option, pub(crate) window_active: bool, @@ -66,8 +72,8 @@ pub(crate) struct Frame { pub(crate) element_offset_stack: Vec>, pub(crate) requested_input_handler: Option, pub(crate) tooltip_request: Option, - pub(crate) cursor_styles: FxHashMap, - pub(crate) requested_cursor_style: Option, + pub(crate) cursor_styles: FxHashMap, + pub(crate) requested_cursor_style: Option, pub(crate) view_stack: Vec, pub(crate) reused_views: FxHashSet, @@ -346,9 +352,13 @@ impl<'a> ElementContext<'a> { } // Reuse the cursor styles previously requested during painting of the reused view. - if let Some(style) = self.window.rendered_frame.cursor_styles.remove(&view_id) { - self.window.next_frame.cursor_styles.insert(view_id, style); - self.window.next_frame.requested_cursor_style = Some(style); + if let Some(cursor_style_request) = + self.window.rendered_frame.cursor_styles.remove(&view_id) + { + self.set_cursor_style( + cursor_style_request.style, + cursor_style_request.stacking_order, + ); } } @@ -387,10 +397,27 @@ impl<'a> ElementContext<'a> { } /// Updates the cursor style at the platform level. - pub fn set_cursor_style(&mut self, style: CursorStyle) { + pub fn set_cursor_style(&mut self, style: CursorStyle, stacking_order: StackingOrder) { let view_id = self.parent_view_id(); - self.window.next_frame.cursor_styles.insert(view_id, style); - self.window.next_frame.requested_cursor_style = Some(style); + let style_request = CursorStyleRequest { + style, + stacking_order, + }; + if self + .window + .next_frame + .requested_cursor_style + .as_ref() + .map_or(true, |prev_style_request| { + style_request.stacking_order >= prev_style_request.stacking_order + }) + { + self.window.next_frame.requested_cursor_style = Some(style_request.clone()); + } + self.window + .next_frame + .cursor_styles + .insert(view_id, style_request); } /// Sets a tooltip to be rendered for the upcoming frame diff --git a/crates/gpui_macros/src/derive_render.rs b/crates/gpui_macros/src/derive_render.rs index e3fd17d4be..2b39248f80 100644 --- a/crates/gpui_macros/src/derive_render.rs +++ b/crates/gpui_macros/src/derive_render.rs @@ -12,7 +12,7 @@ pub fn derive_render(input: TokenStream) -> TokenStream { #where_clause { fn render(&mut self, _cx: &mut gpui::ViewContext) -> impl gpui::Element { - () + gpui::Empty } } }; diff --git a/crates/gpui_macros/src/style_helpers.rs b/crates/gpui_macros/src/style_helpers.rs index 00d3672033..6dc5d83ad3 100644 --- a/crates/gpui_macros/src/style_helpers.rs +++ b/crates/gpui_macros/src/style_helpers.rs @@ -399,6 +399,8 @@ fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> { ("72", quote! { rems(18.) }, "288px (18rem)"), ("80", quote! { rems(20.) }, "320px (20rem)"), ("96", quote! { rems(24.) }, "384px (24rem)"), + ("112", quote! { rems(28.) }, "448px (28rem)"), + ("128", quote! { rems(32.) }, "512px (32rem)"), ("auto", quote! { auto() }, "Auto"), ("px", quote! { px(1.) }, "1px"), ("full", quote! { relative(1.) }, "100%"), diff --git a/crates/install_cli/Cargo.toml b/crates/install_cli/Cargo.toml index 73bec50a16..8afccceaaf 100644 --- a/crates/install_cli/Cargo.toml +++ b/crates/install_cli/Cargo.toml @@ -14,6 +14,5 @@ test-support = [] [dependencies] anyhow.workspace = true gpui.workspace = true -log.workspace = true smol.workspace = true util.workspace = true diff --git a/crates/install_cli/src/install_cli.rs b/crates/install_cli/src/install_cli.rs index e1dff2c52b..506de309ef 100644 --- a/crates/install_cli/src/install_cli.rs +++ b/crates/install_cli/src/install_cli.rs @@ -1,29 +1,33 @@ use anyhow::{anyhow, Result}; use gpui::{actions, AsyncAppContext}; -use std::path::Path; +use std::path::{Path, PathBuf}; use util::ResultExt; -actions!(cli, [Install]); +actions!(cli, [Install, RegisterZedScheme]); -pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { +pub async fn install_cli(cx: &AsyncAppContext) -> Result { let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??; let link_path = Path::new("/usr/local/bin/zed"); let bin_dir_path = link_path.parent().unwrap(); // Don't re-create symlink if it points to the same CLI binary. if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) { - return Ok(()); + return Ok(link_path.into()); } // If the symlink is not there or is outdated, first try replacing it // without escalating. smol::fs::remove_file(link_path).await.log_err(); - if smol::fs::unix::symlink(&cli_path, link_path) - .await - .log_err() - .is_some() + // todo("windows") + #[cfg(not(windows))] { - return Ok(()); + if smol::fs::unix::symlink(&cli_path, link_path) + .await + .log_err() + .is_some() + { + return Ok(link_path.into()); + } } // The symlink could not be created, so use osascript with admin privileges @@ -47,7 +51,7 @@ pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { .await? .status; if status.success() { - Ok(()) + Ok(link_path.into()) } else { Err(anyhow!("error running osascript")) } diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml index 1b8fb44bc5..325e6b5297 100644 --- a/crates/journal/Cargo.toml +++ b/crates/journal/Cargo.toml @@ -11,16 +11,14 @@ doctest = false [dependencies] anyhow.workspace = true -chrono = "0.4" -dirs = "4.0" +chrono.workspace = true editor.workspace = true gpui.workspace = true log.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true -shellexpand = "2.1.0" -util.workspace = true +shellexpand.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 7d51ab2ea6..917e44ff68 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -12,7 +12,6 @@ doctest = false [features] test-support = [ "rand", - "client/test-support", "collections/test-support", "lsp/test-support", "text/test-support", @@ -24,7 +23,6 @@ test-support = [ [dependencies] anyhow.workspace = true -async-broadcast = "0.4" async-trait.workspace = true clock.workspace = true collections.workspace = true @@ -43,7 +41,6 @@ regex.workspace = true rpc.workspace = true schemars.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true settings.workspace = true similar = "1.3" @@ -52,7 +49,6 @@ smol.workspace = true sum_tree.workspace = true text.workspace = true theme.workspace = true -toml.workspace = true tree-sitter-rust = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } pulldown-cmark.workspace = true @@ -62,7 +58,6 @@ util.workspace = true seahash = "4.1.0" [dev-dependencies] -client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } ctor.workspace = true env_logger.workspace = true @@ -78,7 +73,6 @@ tree-sitter-heex.workspace = true tree-sitter-html.workspace = true tree-sitter-json.workspace = true tree-sitter-markdown.workspace = true -tree-sitter-python.workspace = true tree-sitter-ruby.workspace = true tree-sitter-rust.workspace = true tree-sitter-typescript.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 5b97c79e5c..85875845c9 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1325,7 +1325,7 @@ impl Buffer { self.edit(edits, None, cx); } - /// Create a minimal edit that will cause the the given row to be indented + /// Create a minimal edit that will cause the given row to be indented /// with the given size. After applying this edit, the length of the line /// will always be at least `new_size.len`. pub fn edit_for_indent_size_adjustment( @@ -2491,7 +2491,7 @@ impl BufferSnapshot { self.syntax.layers_for_range(0..self.len(), &self.text) } - fn syntax_layer_at(&self, position: D) -> Option { + pub fn syntax_layer_at(&self, position: D) -> Option { let offset = position.to_offset(self); self.syntax .layers_for_range(offset..offset, &self.text) @@ -2838,10 +2838,10 @@ impl BufferSnapshot { } /// Returns bracket range pairs overlapping or adjacent to `range` - pub fn bracket_ranges<'a, T: ToOffset>( - &'a self, + pub fn bracket_ranges( + &self, range: Range, - ) -> impl Iterator, Range)> + 'a { + ) -> impl Iterator, Range)> + '_ { // Find bracket pairs that *inclusively* contain the given range. let range = range.start.to_offset(self).saturating_sub(1) ..self.len().min(range.end.to_offset(self) + 1); @@ -2885,13 +2885,59 @@ impl BufferSnapshot { }) } + /// Returns enclosing bracket ranges containing the given range + pub fn enclosing_bracket_ranges( + &self, + range: Range, + ) -> impl Iterator, Range)> + '_ { + let range = range.start.to_offset(self)..range.end.to_offset(self); + + self.bracket_ranges(range.clone()) + .filter(move |(open, close)| open.start <= range.start && close.end >= range.end) + } + + /// Returns the smallest enclosing bracket ranges containing the given range or None if no brackets contain range + /// + /// Can optionally pass a range_filter to filter the ranges of brackets to consider + pub fn innermost_enclosing_bracket_ranges( + &self, + range: Range, + range_filter: Option<&dyn Fn(Range, Range) -> bool>, + ) -> Option<(Range, Range)> { + let range = range.start.to_offset(self)..range.end.to_offset(self); + + // Get the ranges of the innermost pair of brackets. + let mut result: Option<(Range, Range)> = None; + + for (open, close) in self.enclosing_bracket_ranges(range.clone()) { + if let Some(range_filter) = range_filter { + if !range_filter(open.clone(), close.clone()) { + continue; + } + } + + let len = close.end - open.start; + + if let Some((existing_open, existing_close)) = &result { + let existing_len = existing_close.end - existing_open.start; + if len > existing_len { + continue; + } + } + + result = Some((open, close)); + } + + result + } + /// Returns anchor ranges for any matches of the redaction query. /// The buffer can be associated with multiple languages, and the redaction query associated with each /// will be run on the relevant section of the buffer. - pub fn redacted_ranges<'a, T: ToOffset>( - &'a self, + pub fn redacted_ranges( + &self, range: Range, - ) -> impl Iterator> + 'a { + ) -> impl Iterator> + '_ { let offset_range = range.start.to_offset(self)..range.end.to_offset(self); let mut syntax_matches = self.syntax.matches(offset_range, self, |grammar| { grammar @@ -2968,28 +3014,28 @@ impl BufferSnapshot { /// Returns all the Git diff hunks intersecting the given /// row range. - pub fn git_diff_hunks_in_row_range<'a>( - &'a self, + pub fn git_diff_hunks_in_row_range( + &self, range: Range, - ) -> impl 'a + Iterator> { + ) -> impl '_ + Iterator> { self.git_diff.hunks_in_row_range(range, self) } /// Returns all the Git diff hunks intersecting the given /// range. - pub fn git_diff_hunks_intersecting_range<'a>( - &'a self, + pub fn git_diff_hunks_intersecting_range( + &self, range: Range, - ) -> impl 'a + Iterator> { + ) -> impl '_ + Iterator> { self.git_diff.hunks_intersecting_range(range, self) } /// Returns all the Git diff hunks intersecting the given /// range, in reverse order. - pub fn git_diff_hunks_intersecting_range_rev<'a>( - &'a self, + pub fn git_diff_hunks_intersecting_range_rev( + &self, range: Range, - ) -> impl 'a + Iterator> { + ) -> impl '_ + Iterator> { self.git_diff.hunks_intersecting_range_rev(range, self) } diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 6bdd259c83..bc3eb692fa 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -97,14 +97,14 @@ fn test_select_language() { // matching file extension assert_eq!( registry - .language_for_file("zed/lib.rs", None) + .language_for_file("zed/lib.rs".as_ref(), None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Rust".into()) ); assert_eq!( registry - .language_for_file("zed/lib.mk", None) + .language_for_file("zed/lib.mk".as_ref(), None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) @@ -113,7 +113,7 @@ fn test_select_language() { // matching filename assert_eq!( registry - .language_for_file("zed/Makefile", None) + .language_for_file("zed/Makefile".as_ref(), None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) @@ -122,21 +122,21 @@ fn test_select_language() { // matching suffix that is not the full file extension or filename assert_eq!( registry - .language_for_file("zed/cars", None) + .language_for_file("zed/cars".as_ref(), None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( registry - .language_for_file("zed/a.cars", None) + .language_for_file("zed/a.cars".as_ref(), None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( registry - .language_for_file("zed/sumk", None) + .language_for_file("zed/sumk".as_ref(), None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None @@ -1110,7 +1110,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC b(); | " - .replace("|", "") // marker to preserve trailing whitespace + .replace('|', "") // marker to preserve trailing whitespace .unindent(), ) .with_language(Arc::new(rust_lang()), cx); @@ -1787,7 +1787,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) { // In a JSX expression: use the default config. let expression_in_element_config = snapshot - .language_scope_at(text.find("{").unwrap() + 1) + .language_scope_at(text.find('{').unwrap() + 1) .unwrap(); assert_eq!( expression_in_element_config @@ -2321,7 +2321,7 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) { actual_ranges, expected_ranges, "wrong ranges for text lines:\n{:?}", - text.split("\n").collect::>() + text.split('\n').collect::>() ); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e9c1a83bed..ee137abe43 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -5,7 +5,7 @@ //! use Tree-sitter to provide syntax highlighting to the editor; note though that `language` doesn't perform the highlighting by itself. It only maps ranges in a buffer to colors. Treesitter is also used for buffer outlines (lists of symbols in a buffer) //! - Exposes [`LanguageConfig`] that describes how constructs (like brackets or line comments) should be handled by the editor for a source file of a particular language. //! -//! Notably we do *not* assign a single language to a single file; in real world a single file can consist of multiple programming languages - HTML is a good example of that - and `language` crate tends to reflect that status quo in it's API. +//! Notably we do *not* assign a single language to a single file; in real world a single file can consist of multiple programming languages - HTML is a good example of that - and `language` crate tends to reflect that status quo in its API. mod buffer; mod diagnostic_set; mod highlight_map; @@ -22,7 +22,8 @@ pub mod markdown; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use collections::{HashMap, HashSet}; -use gpui::{AppContext, AsyncAppContext, Task}; +use futures::Future; +use gpui::{AppContext, AsyncAppContext, Model, Task}; pub use highlight_map::HighlightMap; use lazy_static::lazy_static; use lsp::{CodeActionKind, LanguageServerBinary}; @@ -35,14 +36,17 @@ use schemars::{ }; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; +use smol::future::FutureExt as _; use std::{ any::Any, cell::RefCell, + ffi::OsString, fmt::Debug, hash::Hash, mem, ops::Range, path::{Path, PathBuf}, + pin::Pin, str, sync::{ atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst}, @@ -85,7 +89,9 @@ thread_local! { lazy_static! { static ref NEXT_LANGUAGE_ID: AtomicUsize = Default::default(); static ref NEXT_GRAMMAR_ID: AtomicUsize = Default::default(); - static ref WASM_ENGINE: wasmtime::Engine = wasmtime::Engine::default(); + static ref WASM_ENGINE: wasmtime::Engine = { + wasmtime::Engine::new(&wasmtime::Config::new()).unwrap() + }; /// A shared grammar for plain text, exposed for reuse by downstream crates. pub static ref PLAIN_TEXT: Arc = Arc::new(Language::new( @@ -105,54 +111,98 @@ pub trait ToLspPosition { } /// A name of a language server. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] pub struct LanguageServerName(pub Arc); +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Location { + pub buffer: Model, + pub range: Range, +} + +pub struct LanguageContext { + pub package: Option, + pub symbol: Option, +} + +pub trait LanguageContextProvider: Send + Sync { + fn build_context(&self, location: Location, cx: &mut AppContext) -> Result; +} + +/// A context provider that fills out LanguageContext without inspecting the contents. +pub struct DefaultContextProvider; + +impl LanguageContextProvider for DefaultContextProvider { + fn build_context( + &self, + location: Location, + cx: &mut AppContext, + ) -> gpui::Result { + let symbols = location + .buffer + .read(cx) + .snapshot() + .symbols_containing(location.range.start, None); + let symbol = symbols.and_then(|symbols| { + symbols.last().map(|symbol| { + let range = symbol + .name_ranges + .last() + .cloned() + .unwrap_or(0..symbol.text.len()); + symbol.text[range].to_string() + }) + }); + Ok(LanguageContext { + package: None, + symbol, + }) + } +} + /// Represents a Language Server, with certain cached sync properties. /// Uses [`LspAdapter`] under the hood, but calls all 'static' methods /// once at startup, and caches the results. pub struct CachedLspAdapter { pub name: LanguageServerName, - pub short_name: &'static str, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, pub language_ids: HashMap, pub adapter: Arc, pub reinstall_attempt_count: AtomicU64, + cached_binary: futures::lock::Mutex>, } impl CachedLspAdapter { - pub async fn new(adapter: Arc) -> Arc { + pub fn new(adapter: Arc) -> Arc { let name = adapter.name(); - let short_name = adapter.short_name(); let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources(); let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token(); let language_ids = adapter.language_ids(); Arc::new(CachedLspAdapter { name, - short_name, disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, language_ids, adapter, + cached_binary: Default::default(), reinstall_attempt_count: AtomicU64::new(0), }) } - pub async fn fetch_latest_server_version( - &self, - delegate: &dyn LspAdapterDelegate, - ) -> Result> { - self.adapter.fetch_latest_server_version(delegate).await - } - - pub fn will_fetch_server( - &self, - delegate: &Arc, + pub async fn get_language_server_command( + self: Arc, + language: Arc, + container_dir: Arc, + delegate: Arc, cx: &mut AsyncAppContext, - ) -> Option>> { - self.adapter.will_fetch_server(delegate, cx) + ) -> Result { + let cached_binary = self.cached_binary.lock().await; + self.adapter + .clone() + .get_language_server_command(language, container_dir, delegate, cached_binary, cx) + .await } pub fn will_start_server( @@ -163,27 +213,6 @@ impl CachedLspAdapter { self.adapter.will_start_server(delegate, cx) } - pub async fn fetch_server_binary( - &self, - version: Box, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Result { - self.adapter - .fetch_server_binary(version, container_dir, delegate) - .await - } - - pub async fn cached_server_binary( - &self, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Option { - self.adapter - .cached_server_binary(container_dir, delegate) - .await - } - pub fn can_be_reinstalled(&self) -> bool { self.adapter.can_be_reinstalled() } @@ -233,20 +262,126 @@ impl CachedLspAdapter { pub fn prettier_plugins(&self) -> &[&'static str] { self.adapter.prettier_plugins() } + + #[cfg(any(test, feature = "test-support"))] + fn as_fake(&self) -> Option<&FakeLspAdapter> { + self.adapter.as_fake() + } } /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application // e.g. to display a notification or fetch data from the web. +#[async_trait] pub trait LspAdapterDelegate: Send + Sync { fn show_notification(&self, message: &str, cx: &mut AppContext); fn http_client(&self) -> Arc; + fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus); + + async fn which_command(&self, command: OsString) -> Option<(PathBuf, HashMap)>; + async fn read_text_file(&self, path: PathBuf) -> Result; } #[async_trait] pub trait LspAdapter: 'static + Send + Sync { fn name(&self) -> LanguageServerName; - fn short_name(&self) -> &'static str; + fn get_language_server_command<'a>( + self: Arc, + language: Arc, + container_dir: Arc, + delegate: Arc, + mut cached_binary: futures::lock::MutexGuard<'a, Option>, + cx: &'a mut AsyncAppContext, + ) -> Pin>>> { + async move { + // First we check whether the adapter can give us a user-installed binary. + // If so, we do *not* want to cache that, because each worktree might give us a different + // binary: + // + // worktree 1: user-installed at `.bin/gopls` + // worktree 2: user-installed at `~/bin/gopls` + // worktree 3: no gopls found in PATH -> fallback to Zed installation + // + // We only want to cache when we fall back to the global one, + // because we don't want to download and overwrite our global one + // for each worktree we might have open. + if let Some(binary) = self.check_if_user_installed(delegate.as_ref()).await { + log::info!( + "found user-installed language server for {}. path: {:?}, arguments: {:?}", + language.name(), + binary.path, + binary.arguments + ); + return Ok(binary); + } + + if let Some(cached_binary) = cached_binary.as_ref() { + return Ok(cached_binary.clone()); + } + + if !container_dir.exists() { + smol::fs::create_dir_all(&container_dir) + .await + .context("failed to create container directory")?; + } + + if let Some(task) = self.will_fetch_server(&delegate, cx) { + task.await?; + } + + let name = self.name(); + log::info!("fetching latest version of language server {:?}", name.0); + delegate.update_status( + name.clone(), + LanguageServerBinaryStatus::CheckingForUpdate, + ); + let version_info = self.fetch_latest_server_version(delegate.as_ref()).await?; + + log::info!("downloading language server {:?}", name.0); + delegate.update_status(self.name(), LanguageServerBinaryStatus::Downloading); + let mut binary = self + .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate.as_ref()) + .await; + + delegate.update_status(name.clone(), LanguageServerBinaryStatus::Downloaded); + + if let Err(error) = binary.as_ref() { + if let Some(prev_downloaded_binary) = self + .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref()) + .await + { + delegate.update_status(name.clone(), LanguageServerBinaryStatus::Cached); + log::info!( + "failed to fetch newest version of language server {:?}. falling back to using {:?}", + name.clone(), + prev_downloaded_binary.path.display() + ); + binary = Ok(prev_downloaded_binary); + } else { + delegate.update_status( + name.clone(), + LanguageServerBinaryStatus::Failed { + error: format!("{:?}", error), + }, + ); + } + } + + if let Ok(binary) = &binary { + *cached_binary = Some(binary.clone()); + } + + binary + } + .boxed_local() + } + + async fn check_if_user_installed( + &self, + _: &dyn LspAdapterDelegate, + ) -> Option { + None + } async fn fetch_latest_server_version( &self, @@ -356,6 +491,11 @@ pub trait LspAdapter: 'static + Send + Sync { fn prettier_plugins(&self) -> &[&'static str] { &[] } + + #[cfg(any(test, feature = "test-support"))] + fn as_fake(&self) -> Option<&FakeLspAdapter> { + None + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -550,6 +690,7 @@ pub struct FakeLspAdapter { pub disk_based_diagnostics_progress_token: Option, pub disk_based_diagnostics_sources: Vec, pub prettier_plugins: Vec<&'static str>, + pub language_server_binary: LanguageServerBinary, } /// Configuration of handling bracket pairs for a given language. @@ -626,13 +767,7 @@ pub struct Language { pub(crate) id: LanguageId, pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, - pub(crate) adapters: Vec>, - - #[cfg(any(test, feature = "test-support"))] - fake_adapter: Option<( - futures::channel::mpsc::UnboundedSender, - Arc, - )>, + pub(crate) context_provider: Option>, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] @@ -747,15 +882,16 @@ impl Language { highlight_map: Default::default(), }) }), - adapters: Vec::new(), - - #[cfg(any(test, feature = "test-support"))] - fake_adapter: None, + context_provider: None, } } - pub fn lsp_adapters(&self) -> &[Arc] { - &self.adapters + pub fn with_context_provider( + mut self, + provider: Option>, + ) -> Self { + self.context_provider = provider; + self } pub fn with_queries(mut self, queries: LanguageQueries) -> Result { @@ -1049,74 +1185,12 @@ impl Language { Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap() } - pub async fn with_lsp_adapters(mut self, lsp_adapters: Vec>) -> Self { - for adapter in lsp_adapters { - self.adapters.push(CachedLspAdapter::new(adapter).await); - } - self - } - - #[cfg(any(test, feature = "test-support"))] - pub async fn set_fake_lsp_adapter( - &mut self, - fake_lsp_adapter: Arc, - ) -> futures::channel::mpsc::UnboundedReceiver { - let (servers_tx, servers_rx) = futures::channel::mpsc::unbounded(); - self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone())); - let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await; - self.adapters = vec![adapter]; - servers_rx - } - pub fn name(&self) -> Arc { self.config.name.clone() } - pub async fn disk_based_diagnostic_sources(&self) -> &[String] { - match self.adapters.first().as_ref() { - Some(adapter) => &adapter.disk_based_diagnostic_sources, - None => &[], - } - } - - pub async fn disk_based_diagnostics_progress_token(&self) -> Option<&str> { - for adapter in &self.adapters { - let token = adapter.disk_based_diagnostics_progress_token.as_deref(); - if token.is_some() { - return token; - } - } - - None - } - - pub async fn process_completion(self: &Arc, completion: &mut lsp::CompletionItem) { - for adapter in &self.adapters { - adapter.process_completion(completion).await; - } - } - - pub async fn label_for_completion( - self: &Arc, - completion: &lsp::CompletionItem, - ) -> Option { - self.adapters - .first() - .as_ref()? - .label_for_completion(completion, self) - .await - } - - pub async fn label_for_symbol( - self: &Arc, - name: &str, - kind: lsp::SymbolKind, - ) -> Option { - self.adapters - .first() - .as_ref()? - .label_for_symbol(name, kind, self) - .await + pub fn context_provider(&self) -> Option> { + self.context_provider.clone() } pub fn highlight_text<'a>( @@ -1376,19 +1450,31 @@ impl Default for FakeLspAdapter { initialization_options: None, disk_based_diagnostics_sources: Vec::new(), prettier_plugins: Vec::new(), + language_server_binary: LanguageServerBinary { + path: "/the/fake/lsp/path".into(), + arguments: vec![], + env: Default::default(), + }, } } } #[cfg(any(test, feature = "test-support"))] #[async_trait] -impl LspAdapter for Arc { +impl LspAdapter for FakeLspAdapter { fn name(&self) -> LanguageServerName { LanguageServerName(self.name.into()) } - fn short_name(&self) -> &'static str { - "FakeLspAdapter" + fn get_language_server_command<'a>( + self: Arc, + _: Arc, + _: Arc, + _: Arc, + _: futures::lock::MutexGuard<'a, Option>, + _: &'a mut AsyncAppContext, + ) -> Pin>>> { + async move { Ok(self.language_server_binary.clone()) }.boxed_local() } async fn fetch_latest_server_version( @@ -1436,6 +1522,10 @@ impl LspAdapter for Arc { fn prettier_plugins(&self) -> &[&'static str] { &self.prettier_plugins } + + fn as_fake(&self) -> Option<&FakeLspAdapter> { + Some(self) + } } fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option)]) { @@ -1494,16 +1584,16 @@ mod tests { }); languages - .language_for_file("the/script", None) + .language_for_file("the/script".as_ref(), None) .await .unwrap_err(); languages - .language_for_file("the/script", Some(&"nothing".into())) + .language_for_file("the/script".as_ref(), Some(&"nothing".into())) .await .unwrap_err(); assert_eq!( languages - .language_for_file("the/script", Some(&"#!/bin/env node".into())) + .language_for_file("the/script".as_ref(), Some(&"#!/bin/env node".into())) .await .unwrap() .name() diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index fbb72a21fb..d32b0f3346 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -1,15 +1,15 @@ use crate::{ - CachedLspAdapter, Language, LanguageConfig, LanguageId, LanguageMatcher, LanguageServerName, - LspAdapter, LspAdapterDelegate, PARSER, PLAIN_TEXT, + CachedLspAdapter, Language, LanguageConfig, LanguageContextProvider, LanguageId, + LanguageMatcher, LanguageServerName, LspAdapter, LspAdapterDelegate, PARSER, PLAIN_TEXT, }; use anyhow::{anyhow, Context as _, Result}; use collections::{hash_map, HashMap}; use futures::{ channel::{mpsc, oneshot}, future::Shared, - FutureExt as _, TryFutureExt as _, + Future, FutureExt as _, }; -use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task}; +use gpui::{AppContext, BackgroundExecutor, Task}; use lsp::{LanguageServerBinary, LanguageServerId}; use parking_lot::{Mutex, RwLock}; use postage::watch; @@ -24,7 +24,7 @@ use sum_tree::Bias; use text::{Point, Rope}; use theme::Theme; use unicase::UniCase; -use util::{paths::PathExt, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; +use util::{paths::PathExt, post_inc, ResultExt}; pub struct LanguageRegistry { state: RwLock, @@ -43,14 +43,19 @@ struct LanguageRegistryState { languages: Vec>, available_languages: Vec, grammars: HashMap, AvailableGrammar>, + lsp_adapters: HashMap, Vec>>, loading_languages: HashMap>>>>, subscription: (watch::Sender<()>, watch::Receiver<()>), theme: Option>, version: usize, reload_count: usize, + + #[cfg(any(test, feature = "test-support"))] + fake_server_txs: + HashMap, Vec>>, } -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum LanguageServerBinaryStatus { CheckingForUpdate, Downloading, @@ -72,13 +77,13 @@ struct AvailableLanguage { grammar: Option>, matcher: LanguageMatcher, load: Arc Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync>, - lsp_adapters: Vec>, loaded: bool, + context_provider: Option>, } enum AvailableGrammar { Native(tree_sitter::Language), - Loaded(PathBuf, tree_sitter::Language), + Loaded(#[allow(dead_code)] PathBuf, tree_sitter::Language), Loading(PathBuf, Vec>>), Unloaded(PathBuf), } @@ -112,7 +117,7 @@ pub struct LanguageQueries { #[derive(Clone, Default)] struct LspBinaryStatusSender { - txs: Arc, LanguageServerBinaryStatus)>>>>, + txs: Arc>>>, } impl LanguageRegistry { @@ -124,10 +129,14 @@ impl LanguageRegistry { available_languages: Default::default(), grammars: Default::default(), loading_languages: Default::default(), + lsp_adapters: Default::default(), subscription: watch::channel(), theme: Default::default(), version: 0, reload_count: 0, + + #[cfg(any(test, feature = "test-support"))] + fake_server_txs: Default::default(), }), language_server_download_dir: None, login_shell_env_loaded: login_shell_env_loaded.shared(), @@ -139,7 +148,9 @@ impl LanguageRegistry { #[cfg(any(test, feature = "test-support"))] pub fn test() -> Self { - Self::new(Task::ready(())) + let mut this = Self::new(Task::ready(())); + this.language_server_download_dir = Some(Path::new("/the-download-dir").into()); + this } pub fn set_executor(&mut self, executor: BackgroundExecutor) { @@ -162,24 +173,73 @@ impl LanguageRegistry { .remove_languages(languages_to_remove, grammars_to_remove) } + pub fn remove_lsp_adapter(&self, language_name: &str, name: &LanguageServerName) { + let mut state = self.state.write(); + if let Some(adapters) = state.lsp_adapters.get_mut(language_name) { + adapters.retain(|adapter| &adapter.name != name) + } + state.version += 1; + state.reload_count += 1; + *state.subscription.0.borrow_mut() = (); + } + #[cfg(any(feature = "test-support", test))] pub fn register_test_language(&self, config: LanguageConfig) { self.register_language( config.name.clone(), config.grammar.clone(), config.matcher.clone(), - vec![], + None, move || Ok((config.clone(), Default::default())), ) } + pub fn register_lsp_adapter(&self, language_name: Arc, adapter: Arc) { + self.state + .write() + .lsp_adapters + .entry(language_name) + .or_default() + .push(CachedLspAdapter::new(adapter)); + } + + #[cfg(any(feature = "test-support", test))] + pub fn register_fake_lsp_adapter( + &self, + language_name: &str, + adapter: crate::FakeLspAdapter, + ) -> futures::channel::mpsc::UnboundedReceiver { + self.state + .write() + .lsp_adapters + .entry(language_name.into()) + .or_default() + .push(CachedLspAdapter::new(Arc::new(adapter))); + self.fake_language_servers(language_name) + } + + #[cfg(any(feature = "test-support", test))] + pub fn fake_language_servers( + &self, + language_name: &str, + ) -> futures::channel::mpsc::UnboundedReceiver { + let (servers_tx, servers_rx) = futures::channel::mpsc::unbounded(); + self.state + .write() + .fake_server_txs + .entry(language_name.into()) + .or_default() + .push(servers_tx); + servers_rx + } + /// Adds a language to the registry, which can be loaded if needed. pub fn register_language( &self, name: Arc, grammar_name: Option>, matcher: LanguageMatcher, - lsp_adapters: Vec>, + context_provider: Option>, load: impl Fn() -> Result<(LanguageConfig, LanguageQueries)> + 'static + Send + Sync, ) { let load = Arc::new(load); @@ -189,7 +249,6 @@ impl LanguageRegistry { if existing_language.name == name { existing_language.grammar = grammar_name; existing_language.matcher = matcher; - existing_language.lsp_adapters = lsp_adapters; existing_language.load = load; return; } @@ -201,7 +260,8 @@ impl LanguageRegistry { grammar: grammar_name, matcher, load, - lsp_adapters, + + context_provider, loaded: false, }); state.version += 1; @@ -291,35 +351,36 @@ impl LanguageRegistry { pub fn language_for_name( self: &Arc, name: &str, - ) -> UnwrapFuture>>> { + ) -> impl Future>> { let name = UniCase::new(name); - self.get_or_load_language(|language_name, _| UniCase::new(language_name) == name) + let rx = self.get_or_load_language(|language_name, _| UniCase::new(language_name) == name); + async move { rx.await? } } pub fn language_for_name_or_extension( self: &Arc, string: &str, - ) -> UnwrapFuture>>> { + ) -> impl Future>> { let string = UniCase::new(string); - self.get_or_load_language(|name, config| { + let rx = self.get_or_load_language(|name, config| { UniCase::new(name) == string || config .path_suffixes .iter() .any(|suffix| UniCase::new(suffix) == string) - }) + }); + async move { rx.await? } } pub fn language_for_file( self: &Arc, - path: impl AsRef, + path: &Path, content: Option<&Rope>, - ) -> UnwrapFuture>>> { - let path = path.as_ref(); + ) -> impl Future>> { let filename = path.file_name().and_then(|name| name.to_str()); let extension = path.extension_or_hidden_file_name(); let path_suffixes = [extension, filename]; - self.get_or_load_language(|_, config| { + let rx = self.get_or_load_language(move |_, config| { let path_matches = config .path_suffixes .iter() @@ -334,13 +395,14 @@ impl LanguageRegistry { }, ); path_matches || content_matches - }) + }); + async move { rx.await? } } fn get_or_load_language( self: &Arc, callback: impl Fn(&str, &LanguageMatcher) -> bool, - ) -> UnwrapFuture>>> { + ) -> oneshot::Receiver>> { let (tx, rx) = oneshot::channel(); let mut state = self.state.write(); @@ -365,6 +427,7 @@ impl LanguageRegistry { .spawn(async move { let id = language.id; let name = language.name.clone(); + let provider = language.context_provider.clone(); let language = async { let (config, queries) = (language.load)()?; @@ -375,8 +438,7 @@ impl LanguageRegistry { }; Language::new_with_id(id, config, grammar) - .with_lsp_adapters(language.lsp_adapters) - .await + .with_context_provider(provider) .with_queries(queries) } .await; @@ -421,13 +483,13 @@ impl LanguageRegistry { let _ = tx.send(Err(anyhow!("executor does not exist"))); } - rx.unwrap() + rx } fn get_or_load_grammar( self: &Arc, name: Arc, - ) -> UnwrapFuture>> { + ) -> impl Future> { let (tx, rx) = oneshot::channel(); let mut state = self.state.write(); @@ -483,13 +545,30 @@ impl LanguageRegistry { tx.send(Err(anyhow!("no such grammar {}", name))).ok(); } - rx.unwrap() + async move { rx.await? } } pub fn to_vec(&self) -> Vec> { self.state.read().languages.iter().cloned().collect() } + pub fn lsp_adapters(&self, language: &Arc) -> Vec> { + self.state + .read() + .lsp_adapters + .get(&language.config.name) + .cloned() + .unwrap_or_default() + } + + pub fn update_lsp_status( + &self, + server_name: LanguageServerName, + status: LanguageServerBinaryStatus, + ) { + self.lsp_binary_status_tx.send(server_name, status); + } + pub fn create_pending_language_server( self: &Arc, stderr_capture: Arc>>, @@ -505,93 +584,85 @@ impl LanguageRegistry { adapter.name.0 ); - #[cfg(any(test, feature = "test-support"))] - if language.fake_adapter.is_some() { - let task = cx.spawn(|cx| async move { - let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap(); - let (server, mut fake_server) = lsp::FakeLanguageServer::new( - fake_adapter.name.to_string(), - fake_adapter.capabilities.clone(), - cx.clone(), - ); - - if let Some(initializer) = &fake_adapter.initializer { - initializer(&mut fake_server); - } - - let servers_tx = servers_tx.clone(); - cx.background_executor() - .spawn(async move { - if fake_server - .try_receive_notification::() - .await - .is_some() - { - servers_tx.unbounded_send(fake_server).ok(); - } - }) - .detach(); - - Ok(server) - }); - - return Some(PendingLanguageServer { - server_id, - task, - container_dir: None, - }); - } - let download_dir = self .language_server_download_dir .clone() .ok_or_else(|| anyhow!("language server download directory has not been assigned before starting server")) .log_err()?; - let this = self.clone(); let language = language.clone(); let container_dir: Arc = Arc::from(download_dir.join(adapter.name.0.as_ref())); let root_path = root_path.clone(); - let adapter = adapter.clone(); let login_shell_env_loaded = self.login_shell_env_loaded.clone(); - let lsp_binary_statuses = self.lsp_binary_status_tx.clone(); + let this = Arc::downgrade(self); - let task = { + let task = cx.spawn({ let container_dir = container_dir.clone(); - cx.spawn(move |mut cx| async move { + move |mut cx| async move { + // If we want to install a binary globally, we need to wait for + // the login shell to be set on our process. login_shell_env_loaded.await; - let entry = this - .lsp_binary_paths - .lock() - .entry(adapter.name.clone()) - .or_insert_with(|| { - let adapter = adapter.clone(); - let language = language.clone(); - let delegate = delegate.clone(); - cx.spawn(|cx| { - get_binary( - adapter, - language, - delegate, - container_dir, - lsp_binary_statuses, - cx, - ) - .map_err(Arc::new) - }) - .shared() - }) - .clone(); - - let binary = match entry.await { - Ok(binary) => binary, - Err(err) => anyhow::bail!("{err}"), - }; + let binary = adapter + .clone() + .get_language_server_command( + language.clone(), + container_dir, + delegate.clone(), + &mut cx, + ) + .await?; if let Some(task) = adapter.will_start_server(&delegate, &mut cx) { task.await?; } + #[cfg(any(test, feature = "test-support"))] + if true { + let capabilities = adapter + .as_fake() + .map(|fake_adapter| fake_adapter.capabilities.clone()) + .unwrap_or_default(); + + let (server, mut fake_server) = lsp::FakeLanguageServer::new( + binary, + adapter.name.0.to_string(), + capabilities, + cx.clone(), + ); + + if let Some(fake_adapter) = adapter.as_fake() { + if let Some(initializer) = &fake_adapter.initializer { + initializer(&mut fake_server); + } + } + + cx.background_executor() + .spawn(async move { + if fake_server + .try_receive_notification::() + .await + .is_some() + { + if let Some(this) = this.upgrade() { + if let Some(txs) = this + .state + .write() + .fake_server_txs + .get_mut(language.name().as_ref()) + { + for tx in txs { + tx.unbounded_send(fake_server.clone()).ok(); + } + } + } + } + }) + .detach(); + + return Ok(server); + } + + drop(this); lsp::LanguageServer::new( stderr_capture, server_id, @@ -600,8 +671,8 @@ impl LanguageRegistry { adapter.code_action_kinds(), cx, ) - }) - }; + } + }); Some(PendingLanguageServer { server_id, @@ -612,7 +683,7 @@ impl LanguageRegistry { pub fn language_server_binary_statuses( &self, - ) -> mpsc::UnboundedReceiver<(Arc, LanguageServerBinaryStatus)> { + ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> { self.lsp_binary_status_tx.subscribe() } @@ -709,88 +780,16 @@ impl LanguageRegistryState { } impl LspBinaryStatusSender { - fn subscribe(&self) -> mpsc::UnboundedReceiver<(Arc, LanguageServerBinaryStatus)> { + fn subscribe( + &self, + ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> { let (tx, rx) = mpsc::unbounded(); self.txs.lock().push(tx); rx } - fn send(&self, language: Arc, status: LanguageServerBinaryStatus) { + fn send(&self, name: LanguageServerName, status: LanguageServerBinaryStatus) { let mut txs = self.txs.lock(); - txs.retain(|tx| { - tx.unbounded_send((language.clone(), status.clone())) - .is_ok() - }); + txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok()); } } - -async fn get_binary( - adapter: Arc, - language: Arc, - delegate: Arc, - container_dir: Arc, - statuses: LspBinaryStatusSender, - mut cx: AsyncAppContext, -) -> Result { - if !container_dir.exists() { - smol::fs::create_dir_all(&container_dir) - .await - .context("failed to create container directory")?; - } - - if let Some(task) = adapter.will_fetch_server(&delegate, &mut cx) { - task.await?; - } - - let binary = fetch_latest_binary( - adapter.clone(), - language.clone(), - delegate.as_ref(), - &container_dir, - statuses.clone(), - ) - .await; - - if let Err(error) = binary.as_ref() { - if let Some(binary) = adapter - .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref()) - .await - { - statuses.send(language.clone(), LanguageServerBinaryStatus::Cached); - return Ok(binary); - } else { - statuses.send( - language.clone(), - LanguageServerBinaryStatus::Failed { - error: format!("{:?}", error), - }, - ); - } - } - - binary -} - -async fn fetch_latest_binary( - adapter: Arc, - language: Arc, - delegate: &dyn LspAdapterDelegate, - container_dir: &Path, - lsp_binary_statuses_tx: LspBinaryStatusSender, -) -> Result { - let container_dir: Arc = container_dir.into(); - lsp_binary_statuses_tx.send( - language.clone(), - LanguageServerBinaryStatus::CheckingForUpdate, - ); - - let version_info = adapter.fetch_latest_server_version(delegate).await?; - lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading); - - let binary = adapter - .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate) - .await?; - lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloaded); - - Ok(binary) -} diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 9de4c11aee..4babe6344c 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -327,12 +327,34 @@ pub struct InlayHintSettings { /// Default: true #[serde(default = "default_true")] pub show_other_hints: bool, + /// Whether or not to debounce inlay hints updates after buffer edits. + /// + /// Set to 0 to disable debouncing. + /// + /// Default: 700 + #[serde(default = "edit_debounce_ms")] + pub edit_debounce_ms: u64, + /// Whether or not to debounce inlay hints updates after buffer scrolls. + /// + /// Set to 0 to disable debouncing. + /// + /// Default: 50 + #[serde(default = "scroll_debounce_ms")] + pub scroll_debounce_ms: u64, } fn default_true() -> bool { true } +fn edit_debounce_ms() -> u64 { + 700 +} + +fn scroll_debounce_ms() -> u64 { + 50 +} + impl InlayHintSettings { /// Returns the kinds of inlay hints that are enabled based on the settings. pub fn enabled_inlay_hint_kinds(&self) -> HashSet> { diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index 0ff78e49a9..cc0c0becce 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -4,8 +4,8 @@ use std::sync::Arc; use std::{ops::Range, path::PathBuf}; use crate::{HighlightId, Language, LanguageRegistry}; -use gpui::{px, FontStyle, FontWeight, HighlightStyle, UnderlineStyle}; -use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; +use gpui::{px, FontStyle, FontWeight, HighlightStyle, StrikethroughStyle, UnderlineStyle}; +use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd}; /// Parsed Markdown content. #[derive(Debug, Clone)] @@ -47,6 +47,13 @@ impl MarkdownHighlight { }); } + if style.strikethrough { + highlight.strikethrough = Some(StrikethroughStyle { + thickness: px(1.), + ..Default::default() + }); + } + if style.weight != FontWeight::default() { highlight.font_weight = Some(style.weight); } @@ -66,6 +73,8 @@ pub struct MarkdownHighlightStyle { pub italic: bool, /// Whether the text should be underlined. pub underline: bool, + /// Whether the text should be struck through. + pub strikethrough: bool, /// The weight of the text. pub weight: FontWeight, } @@ -151,6 +160,7 @@ pub async fn parse_markdown_block( ) { let mut bold_depth = 0; let mut italic_depth = 0; + let mut strikethrough_depth = 0; let mut link_url = None; let mut current_language = None; let mut list_stack = Vec::new(); @@ -174,6 +184,10 @@ pub async fn parse_markdown_block( style.italic = true; } + if strikethrough_depth > 0 { + style.strikethrough = true; + } + if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) { region_ranges.push(prev_len..text.len()); regions.push(ParsedRegion { @@ -221,7 +235,12 @@ pub async fn parse_markdown_block( Event::Start(tag) => match tag { Tag::Paragraph => new_paragraph(text, &mut list_stack), - Tag::Heading(_, _, _) => { + Tag::Heading { + level: _, + id: _, + classes: _, + attrs: _, + } => { new_paragraph(text, &mut list_stack); bold_depth += 1; } @@ -242,7 +261,14 @@ pub async fn parse_markdown_block( Tag::Strong => bold_depth += 1, - Tag::Link(_, url, _) => link_url = Some(url.to_string()), + Tag::Strikethrough => strikethrough_depth += 1, + + Tag::Link { + link_type: _, + dest_url, + title: _, + id: _, + } => link_url = Some(dest_url.to_string()), Tag::List(number) => { list_stack.push((number, false)); @@ -272,12 +298,13 @@ pub async fn parse_markdown_block( }, Event::End(tag) => match tag { - Tag::Heading(_, _, _) => bold_depth -= 1, - Tag::CodeBlock(_) => current_language = None, - Tag::Emphasis => italic_depth -= 1, - Tag::Strong => bold_depth -= 1, - Tag::Link(_, _, _) => link_url = None, - Tag::List(_) => drop(list_stack.pop()), + TagEnd::Heading(_) => bold_depth -= 1, + TagEnd::CodeBlock => current_language = None, + TagEnd::Emphasis => italic_depth -= 1, + TagEnd::Strong => bold_depth -= 1, + TagEnd::Strikethrough => strikethrough_depth -= 1, + TagEnd::Link => link_url = None, + TagEnd::List(_) => drop(list_stack.pop()), _ => {} }, diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 014b32676a..685fd297fc 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -43,7 +43,7 @@ impl Outline { let candidate_text = item .name_ranges .iter() - .map(|range| &item.text[range.start as usize..range.end as usize]) + .map(|range| &item.text[range.start..range.end]) .collect::(); path_candidates.push(StringMatchCandidate::new(id, path_text.clone())); @@ -97,11 +97,11 @@ impl Outline { let mut name_range = name_ranges.next().unwrap(); let mut preceding_ranges_len = 0; for position in &mut string_match.positions { - while *position >= preceding_ranges_len + name_range.len() as usize { + while *position >= preceding_ranges_len + name_range.len() { preceding_ranges_len += name_range.len(); name_range = name_ranges.next().unwrap(); } - *position = name_range.start as usize + (*position - preceding_ranges_len); + *position = name_range.start + (*position - preceding_ranges_len); } } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 178d634c2b..ec26c87c7c 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -2,7 +2,7 @@ use crate::{ diagnostic_set::DiagnosticEntry, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, - Language, + Language, LanguageRegistry, }; use anyhow::{anyhow, Result}; use clock::ReplicaId; @@ -476,6 +476,7 @@ pub fn serialize_completion(completion: &Completion) -> proto::Completion { pub async fn deserialize_completion( completion: proto::Completion, language: Option>, + language_registry: &Arc, ) -> Result { let old_start = completion .old_start @@ -489,7 +490,11 @@ pub async fn deserialize_completion( let mut label = None; if let Some(language) = language { - label = language.label_for_completion(&lsp_completion).await; + if let Some(adapter) = language_registry.lsp_adapters(&language).first() { + label = adapter + .label_for_completion(&lsp_completion, &language) + .await; + } } Ok(Completion { diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 045e475f07..926c4a36ed 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -614,9 +614,12 @@ impl SyntaxSnapshot { Some(old_tree.clone()), ); changed_ranges = join_ranges( - invalidated_ranges.iter().cloned().filter(|range| { - range.start <= step_end_byte && range.end >= step_start_byte - }), + invalidated_ranges + .iter() + .filter(|&range| { + range.start <= step_end_byte && range.end >= step_start_byte + }) + .cloned(), old_tree.changed_ranges(&tree).map(|r| { step_start_byte + r.start_byte..step_start_byte + r.end_byte }), @@ -766,7 +769,7 @@ impl SyntaxSnapshot { SyntaxMapCaptures::new( range.clone(), buffer.as_rope(), - self.layers_for_range(range, buffer).into_iter(), + self.layers_for_range(range, buffer), query, ) } @@ -780,7 +783,7 @@ impl SyntaxSnapshot { SyntaxMapMatches::new( range.clone(), buffer.as_rope(), - self.layers_for_range(range, buffer).into_iter(), + self.layers_for_range(range, buffer), query, ) } @@ -1180,6 +1183,7 @@ fn parse_text( }) } +#[allow(clippy::too_many_arguments)] fn get_injections( config: &InjectionConfig, text: &BufferSnapshot, @@ -1412,7 +1416,7 @@ fn insert_newlines_between_ranges( continue; } - let range_b = ranges[ix].clone(); + let range_b = ranges[ix]; let range_a = &mut ranges[ix - 1]; if range_a.end_point.column == 0 { continue; @@ -1421,7 +1425,7 @@ fn insert_newlines_between_ranges( if range_a.end_point.row < range_b.start_point.row { let end_point = start_point + Point::from_ts_point(range_a.end_point); let line_end = Point::new(end_point.row, text.line_len(end_point.row)); - if end_point.column as u32 >= line_end.column { + if end_point.column >= line_end.column { range_a.end_byte += 1; range_a.end_point.row += 1; range_a.end_point.column = 0; diff --git a/crates/language_selector/Cargo.toml b/crates/language_selector/Cargo.toml index dc31046054..5365be61a9 100644 --- a/crates/language_selector/Cargo.toml +++ b/crates/language_selector/Cargo.toml @@ -17,8 +17,6 @@ gpui.workspace = true language.workspace = true picker.workspace = true project.workspace = true -settings.workspace = true -theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 0a3faffbee..6bdf5a67d0 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -120,7 +120,7 @@ impl LanguageSelectorDelegate { impl PickerDelegate for LanguageSelectorDelegate { type ListItem = ListItem; - fn placeholder_text(&self) -> Arc { + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Select a language...".into() } diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index 06884fe455..2afad658c8 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -18,7 +18,6 @@ gpui.workspace = true language.workspace = true lsp.workspace = true project.workspace = true -serde.workspace = true serde_json.workspace = true settings.workspace = true theme.workspace = true @@ -33,5 +32,4 @@ editor = { workspace = true, features = ["test-support"] } release_channel.workspace = true env_logger.workspace = true gpui = { workspace = true, features = ["test-support"] } -unindent.workspace = true util = { workspace = true, features = ["test-support"] } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index e7628ce3d1..f1590b6cb8 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -93,7 +93,7 @@ pub fn init(cx: &mut AppContext) { workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| { let project = workspace.project().read(cx); if project.is_local() { - workspace.add_item( + workspace.add_item_to_active_pane( Box::new(cx.new_view(|cx| { LspLogView::new(workspace.project().clone(), log_store.clone(), cx) })), @@ -756,8 +756,8 @@ impl Render for LspLogToolbarItemView { .trigger(Button::new( "language_server_menu_header", current_server - .and_then(|row| { - Some(Cow::Owned(format!( + .map(|row| { + Cow::Owned(format!( "{} ({}) - {}", row.server_name.0, row.worktree_root_name, @@ -766,7 +766,7 @@ impl Render for LspLogToolbarItemView { } else { SERVER_LOGS }, - ))) + )) }) .unwrap_or_else(|| "No server selected".into()), )) @@ -823,7 +823,7 @@ impl Render for LspLogToolbarItemView { selection, Selection::Selected ); - view.toggle_logging_for_server( + view.toggle_rpc_logging_for_server( row.server_id, enabled, cx, @@ -887,7 +887,7 @@ impl LspLogToolbarItemView { } } - fn toggle_logging_for_server( + fn toggle_rpc_logging_for_server( &mut self, id: LanguageServerId, enabled: bool, @@ -899,6 +899,9 @@ impl LspLogToolbarItemView { if !enabled && Some(id) == log_view.current_server_id { log_view.show_logs_for_server(id, cx); cx.notify(); + } else if enabled { + log_view.show_rpc_trace_for_server(id, cx); + cx.notify(); } cx.focus(&log_view.focus_handle); }); diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index b00d1bb79a..6058f454b5 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -20,24 +20,6 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { init_test(cx); - let mut rust_language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_rust_servers = rust_language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-rust-language-server", - ..Default::default() - })) - .await; - let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/the-root", @@ -47,10 +29,28 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { }), ) .await; + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages().add(Arc::new(rust_language)); - }); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ))); + let mut fake_rust_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: "the-rust-language-server", + ..Default::default() + }, + ); let log_store = cx.new_model(|cx| LogStore::new(cx)); log_store.update(cx, |store, cx| store.add_project(&project, cx)); diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 082e77fc36..bca193bc94 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -284,7 +284,7 @@ impl Render for SyntaxTreeView { move |this, range, cx| { let mut items = Vec::new(); let mut cursor = layer.node().walk(); - let mut descendant_ix = range.start as usize; + let mut descendant_ix = range.start; cursor.goto_descendant(descendant_ix); let mut depth = cursor.depth(); let mut visited_children = false; diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml new file mode 100644 index 0000000000..8dd4078abc --- /dev/null +++ b/crates/languages/Cargo.toml @@ -0,0 +1,87 @@ +[package] +name = "languages" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[dependencies] +anyhow.workspace = true +async-compression.workspace = true +async-tar.workspace = true +async-trait.workspace = true +collections.workspace = true +feature_flags.workspace = true +futures.workspace = true +gpui.workspace = true +language.workspace = true +lazy_static.workspace = true +log.workspace = true +lsp.workspace = true +node_runtime.workspace = true +parking_lot.workspace = true +project.workspace = true +regex.workspace = true +rope.workspace = true +rust-embed = "8.2.0" +schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +settings.workspace = true +shellexpand.workspace = true +smol.workspace = true +task.workspace = true +toml.workspace = true +tree-sitter-astro.workspace = true +tree-sitter-bash.workspace = true +tree-sitter-c-sharp.workspace = true +tree-sitter-c.workspace = true +tree-sitter-clojure.workspace = true +tree-sitter-cpp.workspace = true +tree-sitter-css.workspace = true +tree-sitter-dart.workspace = true +tree-sitter-dockerfile.workspace = true +tree-sitter-elixir.workspace = true +tree-sitter-elm.workspace = true +tree-sitter-embedded-template.workspace = true +tree-sitter-erlang.workspace = true +tree-sitter-gitcommit.workspace = true +tree-sitter-gleam.workspace = true +tree-sitter-glsl.workspace = true +tree-sitter-go.workspace = true +tree-sitter-gomod.workspace = true +tree-sitter-gowork.workspace = true +tree-sitter-haskell.workspace = true +tree-sitter-hcl.workspace = true +tree-sitter-heex.workspace = true +tree-sitter-html.workspace = true +tree-sitter-json.workspace = true +tree-sitter-lua.workspace = true +tree-sitter-markdown.workspace = true +tree-sitter-nix.workspace = true +tree-sitter-nu.workspace = true +tree-sitter-ocaml.workspace = true +tree-sitter-php.workspace = true +tree-sitter-prisma-io.workspace = true +tree-sitter-proto.workspace = true +tree-sitter-purescript.workspace = true +tree-sitter-python.workspace = true +tree-sitter-racket.workspace = true +tree-sitter-ruby.workspace = true +tree-sitter-rust.workspace = true +tree-sitter-scheme.workspace = true +tree-sitter-svelte.workspace = true +tree-sitter-toml.workspace = true +tree-sitter-typescript.workspace = true +tree-sitter-uiua.workspace = true +tree-sitter-vue.workspace = true +tree-sitter-yaml.workspace = true +tree-sitter-zig.workspace = true +tree-sitter.workspace = true +util.workspace = true + +[dev-dependencies] +text.workspace = true +theme.workspace = true +unindent.workspace = true diff --git a/crates/plugin_macros/LICENSE-GPL b/crates/languages/LICENSE-GPL similarity index 100% rename from crates/plugin_macros/LICENSE-GPL rename to crates/languages/LICENSE-GPL diff --git a/crates/zed/src/languages/astro.rs b/crates/languages/src/astro.rs similarity index 94% rename from crates/zed/src/languages/astro.rs rename to crates/languages/src/astro.rs index 88fd30950b..2ed3853e07 100644 --- a/crates/zed/src/languages/astro.rs +++ b/crates/languages/src/astro.rs @@ -12,9 +12,9 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::ResultExt; +use util::{async_maybe, ResultExt}; -const SERVER_PATH: &'static str = "node_modules/@astrojs/language-server/bin/nodeServer.js"; +const SERVER_PATH: &str = "node_modules/@astrojs/language-server/bin/nodeServer.js"; fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -36,10 +36,6 @@ impl LspAdapter for AstroLspAdapter { LanguageServerName("astro-language-server".into()) } - fn short_name(&self) -> &'static str { - "astro" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -71,6 +67,7 @@ impl LspAdapter for AstroLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -108,7 +105,7 @@ async fn get_cached_server_binary( container_dir: PathBuf, node: &dyn NodeRuntime, ) -> Option { - (|| async move { + async_maybe!({ let mut last_version_dir = None; let mut entries = fs::read_dir(&container_dir).await?; while let Some(entry) = entries.next().await { @@ -122,6 +119,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { @@ -130,7 +128,7 @@ async fn get_cached_server_binary( last_version_dir )) } - })() + }) .await .log_err() } diff --git a/crates/zed/src/languages/astro/brackets.scm b/crates/languages/src/astro/brackets.scm similarity index 100% rename from crates/zed/src/languages/astro/brackets.scm rename to crates/languages/src/astro/brackets.scm diff --git a/crates/zed/src/languages/astro/config.toml b/crates/languages/src/astro/config.toml similarity index 100% rename from crates/zed/src/languages/astro/config.toml rename to crates/languages/src/astro/config.toml diff --git a/crates/zed/src/languages/astro/highlights.scm b/crates/languages/src/astro/highlights.scm similarity index 100% rename from crates/zed/src/languages/astro/highlights.scm rename to crates/languages/src/astro/highlights.scm diff --git a/crates/zed/src/languages/astro/injections.scm b/crates/languages/src/astro/injections.scm similarity index 100% rename from crates/zed/src/languages/astro/injections.scm rename to crates/languages/src/astro/injections.scm diff --git a/crates/zed/src/languages/bash/brackets.scm b/crates/languages/src/bash/brackets.scm similarity index 100% rename from crates/zed/src/languages/bash/brackets.scm rename to crates/languages/src/bash/brackets.scm diff --git a/crates/zed/src/languages/bash/config.toml b/crates/languages/src/bash/config.toml similarity index 100% rename from crates/zed/src/languages/bash/config.toml rename to crates/languages/src/bash/config.toml diff --git a/crates/zed/src/languages/bash/highlights.scm b/crates/languages/src/bash/highlights.scm similarity index 100% rename from crates/zed/src/languages/bash/highlights.scm rename to crates/languages/src/bash/highlights.scm diff --git a/crates/zed/src/languages/bash/redactions.scm b/crates/languages/src/bash/redactions.scm similarity index 100% rename from crates/zed/src/languages/bash/redactions.scm rename to crates/languages/src/bash/redactions.scm diff --git a/crates/zed/src/languages/c.rs b/crates/languages/src/c.rs similarity index 96% rename from crates/zed/src/languages/c.rs rename to crates/languages/src/c.rs index 974a95766b..28ceedf7ad 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/languages/src/c.rs @@ -1,10 +1,10 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; use futures::StreamExt; pub use language::*; use lsp::LanguageServerBinary; use smol::fs::{self, File}; -use std::{any::Any, path::PathBuf, sync::Arc}; +use std::{any::Any, env::consts, path::PathBuf, sync::Arc}; use util::{ async_maybe, fs::remove_matching, @@ -20,17 +20,18 @@ impl super::LspAdapter for CLspAdapter { LanguageServerName("clangd".into()) } - fn short_name(&self) -> &'static str { - "clangd" - } - async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, ) -> Result> { let release = latest_github_release("clangd/clangd", true, false, delegate.http_client()).await?; - let asset_name = format!("clangd-mac-{}.zip", release.tag_name); + let os_suffix = match consts::OS { + "macos" => "mac", + "linux" => "linux", + other => bail!("Running on unsupported os: {other}"), + }; + let asset_name = format!("clangd-{}-{}.zip", os_suffix, release.tag_name); let asset = release .assets .iter() @@ -84,6 +85,7 @@ impl super::LspAdapter for CLspAdapter { Ok(LanguageServerBinary { path: binary_path, + env: None, arguments: vec![], }) } @@ -260,6 +262,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option &'static str { - "clojure" - } - async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, @@ -33,12 +29,18 @@ impl super::LspAdapter for ClojureLspAdapter { delegate.http_client(), ) .await?; + let os = match consts::OS { + "macos" => "macos", + "linux" => "linux", + "windows" => "windows", + other => bail!("Running on unsupported os: {other}"), + }; let platform = match consts::ARCH { "x86_64" => "amd64", "aarch64" => "aarch64", other => bail!("Running on unsupported platform: {other}"), }; - let asset_name = format!("clojure-lsp-native-macos-{platform}.zip"); + let asset_name = format!("clojure-lsp-native-{os}-{platform}.zip"); let asset = release .assets .iter() @@ -99,6 +101,7 @@ impl super::LspAdapter for ClojureLspAdapter { Ok(LanguageServerBinary { path: binary_path, + env: None, arguments: vec![], }) } @@ -112,6 +115,7 @@ impl super::LspAdapter for ClojureLspAdapter { if binary_path.exists() { Some(LanguageServerBinary { path: binary_path, + env: None, arguments: vec![], }) } else { @@ -127,6 +131,7 @@ impl super::LspAdapter for ClojureLspAdapter { if binary_path.exists() { Some(LanguageServerBinary { path: binary_path, + env: None, arguments: vec!["--version".into()], }) } else { diff --git a/crates/zed/src/languages/clojure/brackets.scm b/crates/languages/src/clojure/brackets.scm similarity index 100% rename from crates/zed/src/languages/clojure/brackets.scm rename to crates/languages/src/clojure/brackets.scm diff --git a/crates/zed/src/languages/clojure/config.toml b/crates/languages/src/clojure/config.toml similarity index 88% rename from crates/zed/src/languages/clojure/config.toml rename to crates/languages/src/clojure/config.toml index b773e88341..2fa4e5105e 100644 --- a/crates/zed/src/languages/clojure/config.toml +++ b/crates/languages/src/clojure/config.toml @@ -1,6 +1,6 @@ name = "Clojure" grammar = "clojure" -path_suffixes = ["clj", "cljs"] +path_suffixes = ["clj", "cljs", "cljc", "edn", "bb"] line_comments = [";; "] autoclose_before = "}])" brackets = [ diff --git a/crates/zed/src/languages/clojure/highlights.scm b/crates/languages/src/clojure/highlights.scm similarity index 100% rename from crates/zed/src/languages/clojure/highlights.scm rename to crates/languages/src/clojure/highlights.scm diff --git a/crates/zed/src/languages/clojure/indents.scm b/crates/languages/src/clojure/indents.scm similarity index 100% rename from crates/zed/src/languages/clojure/indents.scm rename to crates/languages/src/clojure/indents.scm diff --git a/crates/zed/src/languages/clojure/outline.scm b/crates/languages/src/clojure/outline.scm similarity index 100% rename from crates/zed/src/languages/clojure/outline.scm rename to crates/languages/src/clojure/outline.scm diff --git a/crates/zed/src/languages/cpp/brackets.scm b/crates/languages/src/cpp/brackets.scm similarity index 100% rename from crates/zed/src/languages/cpp/brackets.scm rename to crates/languages/src/cpp/brackets.scm diff --git a/crates/zed/src/languages/cpp/config.toml b/crates/languages/src/cpp/config.toml similarity index 100% rename from crates/zed/src/languages/cpp/config.toml rename to crates/languages/src/cpp/config.toml diff --git a/crates/zed/src/languages/cpp/embedding.scm b/crates/languages/src/cpp/embedding.scm similarity index 100% rename from crates/zed/src/languages/cpp/embedding.scm rename to crates/languages/src/cpp/embedding.scm diff --git a/crates/zed/src/languages/cpp/highlights.scm b/crates/languages/src/cpp/highlights.scm similarity index 100% rename from crates/zed/src/languages/cpp/highlights.scm rename to crates/languages/src/cpp/highlights.scm diff --git a/crates/zed/src/languages/cpp/indents.scm b/crates/languages/src/cpp/indents.scm similarity index 100% rename from crates/zed/src/languages/cpp/indents.scm rename to crates/languages/src/cpp/indents.scm diff --git a/crates/zed/src/languages/cpp/injections.scm b/crates/languages/src/cpp/injections.scm similarity index 100% rename from crates/zed/src/languages/cpp/injections.scm rename to crates/languages/src/cpp/injections.scm diff --git a/crates/zed/src/languages/cpp/outline.scm b/crates/languages/src/cpp/outline.scm similarity index 100% rename from crates/zed/src/languages/cpp/outline.scm rename to crates/languages/src/cpp/outline.scm diff --git a/crates/zed/src/languages/cpp/overrides.scm b/crates/languages/src/cpp/overrides.scm similarity index 100% rename from crates/zed/src/languages/cpp/overrides.scm rename to crates/languages/src/cpp/overrides.scm diff --git a/crates/zed/src/languages/csharp.rs b/crates/languages/src/csharp.rs similarity index 93% rename from crates/zed/src/languages/csharp.rs rename to crates/languages/src/csharp.rs index 475b7573d0..297e397cdd 100644 --- a/crates/zed/src/languages/csharp.rs +++ b/crates/languages/src/csharp.rs @@ -21,10 +21,6 @@ impl super::LspAdapter for OmniSharpAdapter { LanguageServerName("OmniSharp".into()) } - fn short_name(&self) -> &'static str { - "OmniSharp" - } - async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, @@ -81,13 +77,18 @@ impl super::LspAdapter for OmniSharpAdapter { archive.unpack(container_dir).await?; } - fs::set_permissions( - &binary_path, - ::from_mode(0o755), - ) - .await?; + // todo("windows") + #[cfg(not(windows))] + { + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + } Ok(LanguageServerBinary { path: binary_path, + env: None, arguments: server_binary_arguments(), }) } @@ -132,6 +133,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option Vec { @@ -37,10 +37,6 @@ impl LspAdapter for CssLspAdapter { LanguageServerName("vscode-css-language-server".into()) } - fn short_name(&self) -> &'static str { - "css" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -72,6 +68,7 @@ impl LspAdapter for CssLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -116,6 +113,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { diff --git a/crates/zed/src/languages/css/brackets.scm b/crates/languages/src/css/brackets.scm similarity index 100% rename from crates/zed/src/languages/css/brackets.scm rename to crates/languages/src/css/brackets.scm diff --git a/crates/zed/src/languages/css/config.toml b/crates/languages/src/css/config.toml similarity index 100% rename from crates/zed/src/languages/css/config.toml rename to crates/languages/src/css/config.toml diff --git a/crates/zed/src/languages/css/highlights.scm b/crates/languages/src/css/highlights.scm similarity index 100% rename from crates/zed/src/languages/css/highlights.scm rename to crates/languages/src/css/highlights.scm diff --git a/crates/zed/src/languages/css/indents.scm b/crates/languages/src/css/indents.scm similarity index 100% rename from crates/zed/src/languages/css/indents.scm rename to crates/languages/src/css/indents.scm diff --git a/crates/zed/src/languages/css/overrides.scm b/crates/languages/src/css/overrides.scm similarity index 100% rename from crates/zed/src/languages/css/overrides.scm rename to crates/languages/src/css/overrides.scm diff --git a/crates/languages/src/dart.rs b/crates/languages/src/dart.rs new file mode 100644 index 0000000000..5c0d0c21a1 --- /dev/null +++ b/crates/languages/src/dart.rs @@ -0,0 +1,69 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use gpui::AppContext; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use project::project_settings::ProjectSettings; +use serde_json::Value; +use settings::Settings; +use std::{ + any::Any, + path::{Path, PathBuf}, +}; + +pub struct DartLanguageServer; + +#[async_trait] +impl LspAdapter for DartLanguageServer { + fn name(&self) -> LanguageServerName { + LanguageServerName("dart".into()) + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(())) + } + + async fn fetch_server_binary( + &self, + _: Box, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + Err(anyhow!("dart must me installed from dart.dev/get-dart")) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + Some(LanguageServerBinary { + path: "dart".into(), + env: None, + arguments: vec!["language-server".into(), "--protocol=lsp".into()], + }) + } + + fn can_be_reinstalled(&self) -> bool { + false + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } + + fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value { + let settings = ProjectSettings::get_global(cx) + .lsp + .get("dart") + .and_then(|s| s.settings.clone()) + .unwrap_or_default(); + + serde_json::json!({ + "dart": settings + }) + } +} diff --git a/crates/languages/src/dart/brackets.scm b/crates/languages/src/dart/brackets.scm new file mode 100644 index 0000000000..8d96f95f86 --- /dev/null +++ b/crates/languages/src/dart/brackets.scm @@ -0,0 +1,6 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("<" @open ">" @close) +("\"" @open "\"" @close) +("'" @open "'" @close) diff --git a/crates/languages/src/dart/config.toml b/crates/languages/src/dart/config.toml new file mode 100644 index 0000000000..140e482289 --- /dev/null +++ b/crates/languages/src/dart/config.toml @@ -0,0 +1,13 @@ +name = "Dart" +grammar = "dart" +path_suffixes = ["dart"] +line_comments = ["// "] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, +] diff --git a/crates/languages/src/dart/highlights.scm b/crates/languages/src/dart/highlights.scm new file mode 100644 index 0000000000..270b40ec2d --- /dev/null +++ b/crates/languages/src/dart/highlights.scm @@ -0,0 +1,209 @@ +(dotted_identifier_list) @string + +; Methods +; -------------------- +(function_type + name: (identifier) @function) +(super) @function + +; Annotations +; -------------------- +(annotation + name: (identifier) @attribute) + +; Operators and Tokens +; -------------------- +(template_substitution + "$" @punctuation.special + "{" @punctuation.special + "}" @punctuation.special + ) @none + +(template_substitution + "$" @punctuation.special + (identifier_dollar_escaped) @variable + ) @none + +(escape_sequence) @string.escape + +[ + "@" + "=>" + ".." + "??" + "==" + "?" + ":" + "&&" + "%" + "<" + ">" + "=" + ">=" + "<=" + "||" + (increment_operator) + (is_operator) + (prefix_operator) + (equality_operator) + (additive_operator) + ] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<" + ">" + ] @punctuation.bracket + +; Delimiters +; -------------------- +[ + ";" + "." + "," + ] @punctuation.delimiter + +; Types +; -------------------- +(class_definition + name: (identifier) @type) +(constructor_signature + name: (identifier) @type) +(scoped_identifier + scope: (identifier) @type) +(function_signature + name: (identifier) @function) +(getter_signature + (identifier) @function) +(setter_signature + name: (identifier) @function) +(enum_declaration + name: (identifier) @type) +(enum_constant + name: (identifier) @type) +(type_identifier) @type +(void_type) @type + +((scoped_identifier + scope: (identifier) @type + name: (identifier) @type) + (#match? @type "^[a-zA-Z]")) + +(type_identifier) @type + +; Variables +; -------------------- +; var keyword +(inferred_type) @keyword + +(const_builtin) @constant.builtin +(final_builtin) @constant.builtin + +((identifier) @type + (#match? @type "^_?[A-Z]")) + +("Function" @type) + +; properties +; TODO: add method/call_expression to grammar and +; distinguish method call from variable access +(unconditional_assignable_selector + (identifier) @property) + +; assignments +(assignment_expression + left: (assignable_expression) @variable) + +(this) @variable.builtin + +; Literals +; -------------------- +[ + (hex_integer_literal) + (decimal_integer_literal) + (decimal_floating_point_literal) + ; TODO: inaccessible nodes + ; (octal_integer_literal) + ; (hex_floating_point_literal) + ] @number + +(symbol_literal) @symbol +(string_literal) @string +(true) @boolean +(false) @boolean +(null_literal) @constant.builtin + +(documentation_comment) @comment +(comment) @comment + +; Keywords +; -------------------- +["import" "library" "export"] @keyword.include + +; Reserved words (cannot be used as identifiers) +; TODO: "rethrow" @keyword +[ + ; "assert" + (case_builtin) + "extension" + "on" + "class" + "enum" + "extends" + "in" + "is" + "new" + "return" + "super" + "with" + ] @keyword + + +; Built in identifiers: +; alone these are marked as keywords +[ + "abstract" + "as" + "async" + "async*" + "yield" + "sync*" + "await" + "covariant" + "deferred" + "dynamic" + "external" + "factory" + "get" + "implements" + "interface" + "library" + "operator" + "mixin" + "part" + "set" + "show" + "static" + "typedef" + ] @keyword + +; when used as an identifier: +((identifier) @variable.builtin + (#vim-match? @variable.builtin "^(abstract|as|covariant|deferred|dynamic|export|external|factory|Function|get|implements|import|interface|library|operator|mixin|part|set|static|typedef)$")) + +["if" "else" "switch" "default"] @keyword + +[ + "try" + "throw" + "catch" + "finally" + (break_statement) + ] @keyword + +["do" "while" "continue" "for"] @keyword diff --git a/crates/languages/src/dart/indents.scm b/crates/languages/src/dart/indents.scm new file mode 100644 index 0000000000..3e8210957c --- /dev/null +++ b/crates/languages/src/dart/indents.scm @@ -0,0 +1,7 @@ +[ + (if_statement) + (for_statement) +] @indent + +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/languages/src/dart/outline.scm b/crates/languages/src/dart/outline.scm new file mode 100644 index 0000000000..4d6f8c1cb7 --- /dev/null +++ b/crates/languages/src/dart/outline.scm @@ -0,0 +1,18 @@ +(class_definition + "class" @context + name: (_) @name) @item + +(function_signature + name: (_) @name) @item + +(getter_signature + "get" @context + name: (_) @name) @item + +(setter_signature + "set" @context + name: (_) @name) @item + +(enum_declaration + "enum" @context + name: (_) @name) @item diff --git a/crates/zed/src/languages/deno.rs b/crates/languages/src/deno.rs similarity index 94% rename from crates/zed/src/languages/deno.rs rename to crates/languages/src/deno.rs index a06c6e42d5..abb9dccbfb 100644 --- a/crates/zed/src/languages/deno.rs +++ b/crates/languages/src/deno.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; use collections::HashMap; use futures::StreamExt; @@ -62,17 +62,19 @@ impl LspAdapter for DenoLspAdapter { LanguageServerName("deno-language-server".into()) } - fn short_name(&self) -> &'static str { - "deno-ts" - } - async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, ) -> Result> { let release = latest_github_release("denoland/deno", true, false, delegate.http_client()).await?; - let asset_name = format!("deno-{}-apple-darwin.zip", consts::ARCH); + let os = match consts::OS { + "macos" => "apple-darwin", + "linux" => "unknown-linux-gnu", + "windows" => "pc-windows-msvc", + other => bail!("Running on unsupported os: {other}"), + }; + let asset_name = format!("deno-{}-{os}.zip", consts::ARCH); let asset = release .assets .iter() @@ -128,6 +130,7 @@ impl LspAdapter for DenoLspAdapter { Ok(LanguageServerBinary { path: binary_path, + env: None, arguments: deno_server_binary_arguments(), }) } @@ -214,6 +217,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option Vec { vec![server_path.into(), "--stdio".into()] @@ -36,10 +35,6 @@ impl LspAdapter for DockerfileLspAdapter { LanguageServerName("docker-langserver".into()) } - fn short_name(&self) -> &'static str { - "dockerfile" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -71,6 +66,7 @@ impl LspAdapter for DockerfileLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -110,6 +106,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { diff --git a/crates/zed/src/languages/dockerfile/config.toml b/crates/languages/src/dockerfile/config.toml similarity index 100% rename from crates/zed/src/languages/dockerfile/config.toml rename to crates/languages/src/dockerfile/config.toml diff --git a/crates/zed/src/languages/dockerfile/highlights.scm b/crates/languages/src/dockerfile/highlights.scm similarity index 100% rename from crates/zed/src/languages/dockerfile/highlights.scm rename to crates/languages/src/dockerfile/highlights.scm diff --git a/crates/zed/src/languages/dockerfile/injections.scm b/crates/languages/src/dockerfile/injections.scm similarity index 100% rename from crates/zed/src/languages/dockerfile/injections.scm rename to crates/languages/src/dockerfile/injections.scm diff --git a/crates/zed/src/languages/elixir.rs b/crates/languages/src/elixir.rs similarity index 97% rename from crates/zed/src/languages/elixir.rs rename to crates/languages/src/elixir.rs index 39b9c0a100..471f466c84 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/languages/src/elixir.rs @@ -71,10 +71,6 @@ impl LspAdapter for ElixirLspAdapter { LanguageServerName("elixir-ls".into()) } - fn short_name(&self) -> &'static str { - "elixir-ls" - } - fn will_start_server( &self, delegate: &Arc, @@ -174,6 +170,7 @@ impl LspAdapter for ElixirLspAdapter { Ok(LanguageServerBinary { path: binary_path, + env: None, arguments: vec![], }) } @@ -284,6 +281,7 @@ async fn get_cached_server_binary_elixir_ls( if server_path.exists() { Some(LanguageServerBinary { path: server_path, + env: None, arguments: vec![], }) } else { @@ -300,10 +298,6 @@ impl LspAdapter for NextLspAdapter { LanguageServerName("next-ls".into()) } - fn short_name(&self) -> &'static str { - "next-ls" - } - async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, @@ -356,15 +350,20 @@ impl LspAdapter for NextLspAdapter { } futures::io::copy(response.body_mut(), &mut file).await?; - fs::set_permissions( - &binary_path, - ::from_mode(0o755), - ) - .await?; + // todo("windows") + #[cfg(not(windows))] + { + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + } } Ok(LanguageServerBinary { path: binary_path, + env: None, arguments: vec!["--stdio".into()], }) } @@ -431,6 +430,7 @@ async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option &'static str { - "local-ls" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -472,6 +468,7 @@ impl LspAdapter for LocalLspAdapter { let path = shellexpand::full(&self.path)?; Ok(LanguageServerBinary { path: PathBuf::from(path.deref()), + env: None, arguments: self.arguments.iter().map(|arg| arg.into()).collect(), }) } @@ -484,6 +481,7 @@ impl LspAdapter for LocalLspAdapter { let path = shellexpand::full(&self.path).ok()?; Some(LanguageServerBinary { path: PathBuf::from(path.deref()), + env: None, arguments: self.arguments.iter().map(|arg| arg.into()).collect(), }) } @@ -492,6 +490,7 @@ impl LspAdapter for LocalLspAdapter { let path = shellexpand::full(&self.path).ok()?; Some(LanguageServerBinary { path: PathBuf::from(path.deref()), + env: None, arguments: self.arguments.iter().map(|arg| arg.into()).collect(), }) } diff --git a/crates/zed/src/languages/elixir/brackets.scm b/crates/languages/src/elixir/brackets.scm similarity index 100% rename from crates/zed/src/languages/elixir/brackets.scm rename to crates/languages/src/elixir/brackets.scm diff --git a/crates/zed/src/languages/elixir/config.toml b/crates/languages/src/elixir/config.toml similarity index 100% rename from crates/zed/src/languages/elixir/config.toml rename to crates/languages/src/elixir/config.toml diff --git a/crates/zed/src/languages/elixir/embedding.scm b/crates/languages/src/elixir/embedding.scm similarity index 100% rename from crates/zed/src/languages/elixir/embedding.scm rename to crates/languages/src/elixir/embedding.scm diff --git a/crates/zed/src/languages/elixir/highlights.scm b/crates/languages/src/elixir/highlights.scm similarity index 100% rename from crates/zed/src/languages/elixir/highlights.scm rename to crates/languages/src/elixir/highlights.scm diff --git a/crates/zed/src/languages/elixir/indents.scm b/crates/languages/src/elixir/indents.scm similarity index 100% rename from crates/zed/src/languages/elixir/indents.scm rename to crates/languages/src/elixir/indents.scm diff --git a/crates/zed/src/languages/elixir/injections.scm b/crates/languages/src/elixir/injections.scm similarity index 100% rename from crates/zed/src/languages/elixir/injections.scm rename to crates/languages/src/elixir/injections.scm diff --git a/crates/zed/src/languages/elixir/outline.scm b/crates/languages/src/elixir/outline.scm similarity index 100% rename from crates/zed/src/languages/elixir/outline.scm rename to crates/languages/src/elixir/outline.scm diff --git a/crates/zed/src/languages/elixir/overrides.scm b/crates/languages/src/elixir/overrides.scm similarity index 100% rename from crates/zed/src/languages/elixir/overrides.scm rename to crates/languages/src/elixir/overrides.scm diff --git a/crates/zed/src/languages/elm.rs b/crates/languages/src/elm.rs similarity index 94% rename from crates/zed/src/languages/elm.rs rename to crates/languages/src/elm.rs index 9c50184498..37b156db91 100644 --- a/crates/zed/src/languages/elm.rs +++ b/crates/languages/src/elm.rs @@ -15,10 +15,10 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::ResultExt; +use util::{async_maybe, ResultExt}; -const SERVER_NAME: &'static str = "elm-language-server"; -const SERVER_PATH: &'static str = "node_modules/@elm-tooling/elm-language-server/out/node/index.js"; +const SERVER_NAME: &str = "elm-language-server"; +const SERVER_PATH: &str = "node_modules/@elm-tooling/elm-language-server/out/node/index.js"; fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -40,10 +40,6 @@ impl LspAdapter for ElmLspAdapter { LanguageServerName(SERVER_NAME.into()) } - fn short_name(&self) -> &'static str { - "elmLS" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -75,6 +71,7 @@ impl LspAdapter for ElmLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -120,7 +117,7 @@ async fn get_cached_server_binary( container_dir: PathBuf, node: &dyn NodeRuntime, ) -> Option { - (|| async move { + async_maybe!({ let mut last_version_dir = None; let mut entries = fs::read_dir(&container_dir).await?; while let Some(entry) = entries.next().await { @@ -134,6 +131,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { @@ -142,7 +140,7 @@ async fn get_cached_server_binary( last_version_dir )) } - })() + }) .await .log_err() } diff --git a/crates/zed/src/languages/elm/config.toml b/crates/languages/src/elm/config.toml similarity index 100% rename from crates/zed/src/languages/elm/config.toml rename to crates/languages/src/elm/config.toml diff --git a/crates/zed/src/languages/elm/highlights.scm b/crates/languages/src/elm/highlights.scm similarity index 100% rename from crates/zed/src/languages/elm/highlights.scm rename to crates/languages/src/elm/highlights.scm diff --git a/crates/zed/src/languages/elm/injections.scm b/crates/languages/src/elm/injections.scm similarity index 100% rename from crates/zed/src/languages/elm/injections.scm rename to crates/languages/src/elm/injections.scm diff --git a/crates/zed/src/languages/elm/outline.scm b/crates/languages/src/elm/outline.scm similarity index 100% rename from crates/zed/src/languages/elm/outline.scm rename to crates/languages/src/elm/outline.scm diff --git a/crates/zed/src/languages/erb/config.toml b/crates/languages/src/erb/config.toml similarity index 100% rename from crates/zed/src/languages/erb/config.toml rename to crates/languages/src/erb/config.toml diff --git a/crates/zed/src/languages/erb/highlights.scm b/crates/languages/src/erb/highlights.scm similarity index 100% rename from crates/zed/src/languages/erb/highlights.scm rename to crates/languages/src/erb/highlights.scm diff --git a/crates/zed/src/languages/erb/injections.scm b/crates/languages/src/erb/injections.scm similarity index 100% rename from crates/zed/src/languages/erb/injections.scm rename to crates/languages/src/erb/injections.scm diff --git a/crates/zed/src/languages/erlang.rs b/crates/languages/src/erlang.rs similarity index 95% rename from crates/zed/src/languages/erlang.rs rename to crates/languages/src/erlang.rs index b50b6e7564..2b6d33ed41 100644 --- a/crates/zed/src/languages/erlang.rs +++ b/crates/languages/src/erlang.rs @@ -12,10 +12,6 @@ impl LspAdapter for ErlangLspAdapter { LanguageServerName("erlang_ls".into()) } - fn short_name(&self) -> &'static str { - "erlang_ls" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -41,6 +37,7 @@ impl LspAdapter for ErlangLspAdapter { ) -> Option { Some(LanguageServerBinary { path: "erlang_ls".into(), + env: None, arguments: vec![], }) } @@ -52,6 +49,7 @@ impl LspAdapter for ErlangLspAdapter { async fn installation_test_binary(&self, _: PathBuf) -> Option { Some(LanguageServerBinary { path: "erlang_ls".into(), + env: None, arguments: vec!["--version".into()], }) } diff --git a/crates/zed/src/languages/erlang/brackets.scm b/crates/languages/src/erlang/brackets.scm similarity index 100% rename from crates/zed/src/languages/erlang/brackets.scm rename to crates/languages/src/erlang/brackets.scm diff --git a/crates/zed/src/languages/erlang/config.toml b/crates/languages/src/erlang/config.toml similarity index 100% rename from crates/zed/src/languages/erlang/config.toml rename to crates/languages/src/erlang/config.toml diff --git a/crates/zed/src/languages/erlang/folds.scm b/crates/languages/src/erlang/folds.scm similarity index 100% rename from crates/zed/src/languages/erlang/folds.scm rename to crates/languages/src/erlang/folds.scm diff --git a/crates/zed/src/languages/erlang/highlights.scm b/crates/languages/src/erlang/highlights.scm similarity index 100% rename from crates/zed/src/languages/erlang/highlights.scm rename to crates/languages/src/erlang/highlights.scm diff --git a/crates/zed/src/languages/erlang/indents.scm b/crates/languages/src/erlang/indents.scm similarity index 100% rename from crates/zed/src/languages/erlang/indents.scm rename to crates/languages/src/erlang/indents.scm diff --git a/crates/zed/src/languages/erlang/outline.scm b/crates/languages/src/erlang/outline.scm similarity index 100% rename from crates/zed/src/languages/erlang/outline.scm rename to crates/languages/src/erlang/outline.scm diff --git a/crates/zed/src/languages/gitcommit/config.toml b/crates/languages/src/gitcommit/config.toml similarity index 100% rename from crates/zed/src/languages/gitcommit/config.toml rename to crates/languages/src/gitcommit/config.toml diff --git a/crates/zed/src/languages/gitcommit/highlights.scm b/crates/languages/src/gitcommit/highlights.scm similarity index 100% rename from crates/zed/src/languages/gitcommit/highlights.scm rename to crates/languages/src/gitcommit/highlights.scm diff --git a/crates/zed/src/languages/gitcommit/injections.scm b/crates/languages/src/gitcommit/injections.scm similarity index 100% rename from crates/zed/src/languages/gitcommit/injections.scm rename to crates/languages/src/gitcommit/injections.scm diff --git a/crates/zed/src/languages/gleam.rs b/crates/languages/src/gleam.rs similarity index 88% rename from crates/zed/src/languages/gleam.rs rename to crates/languages/src/gleam.rs index 508956b099..9eb22c179e 100644 --- a/crates/zed/src/languages/gleam.rs +++ b/crates/languages/src/gleam.rs @@ -1,8 +1,9 @@ use std::any::Any; +use std::env::consts; use std::ffi::OsString; use std::path::PathBuf; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; @@ -26,21 +27,22 @@ impl LspAdapter for GleamLspAdapter { LanguageServerName("gleam".into()) } - fn short_name(&self) -> &'static str { - "gleam" - } - async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, ) -> Result> { let release = latest_github_release("gleam-lang/gleam", true, false, delegate.http_client()).await?; - let asset_name = format!( - "gleam-{version}-{arch}-apple-darwin.tar.gz", + "gleam-{version}-{arch}-{os}.tar.gz", version = release.tag_name, - arch = std::env::consts::ARCH + arch = std::env::consts::ARCH, + os = match consts::OS { + "macos" => "apple-darwin", + "linux" => "unknown-linux-musl", + "windows" => "pc-windows-msvc", + other => bail!("Running on unsupported os: {other}"), + }, ); let asset = release .assets @@ -75,6 +77,7 @@ impl LspAdapter for GleamLspAdapter { Ok(LanguageServerBinary { path: binary_path, + env: None, arguments: server_binary_arguments(), }) } @@ -110,6 +113,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option &'static str { - "gopls" - } - async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, @@ -58,6 +54,18 @@ impl super::LspAdapter for GoLspAdapter { Ok(Box::new(version) as Box<_>) } + async fn check_if_user_installed( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Option { + let (path, env) = delegate.which_command(OsString::from("gopls")).await?; + Some(LanguageServerBinary { + path, + arguments: server_binary_arguments(), + env: Some(env), + }) + } + fn will_fetch_server( &self, delegate: &Arc, @@ -107,6 +115,7 @@ impl super::LspAdapter for GoLspAdapter { return Ok(LanguageServerBinary { path: binary_path.to_path_buf(), arguments: server_binary_arguments(), + env: None, }); } } @@ -125,10 +134,16 @@ impl super::LspAdapter for GoLspAdapter { .args(["install", "golang.org/x/tools/gopls@latest"]) .output() .await?; - anyhow::ensure!( - install_output.status.success(), - "failed to install gopls. Is `go` installed and in the PATH?" - ); + + if !install_output.status.success() { + log::error!( + "failed to install gopls via `go install`. stdout: {:?}, stderr: {:?}", + String::from_utf8_lossy(&install_output.stdout), + String::from_utf8_lossy(&install_output.stderr) + ); + + return Err(anyhow!("failed to install gopls with `go install`. Is `go` installed and in the PATH? Check logs for more information.")); + } let installed_binary_path = gobin_dir.join("gopls"); let version_output = process::Command::new(&installed_binary_path) @@ -148,6 +163,7 @@ impl super::LspAdapter for GoLspAdapter { Ok(LanguageServerBinary { path: binary_path.to_path_buf(), arguments: server_binary_arguments(), + env: None, }) } @@ -366,6 +382,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option &'static str { - "hls" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -41,6 +37,7 @@ impl LspAdapter for HaskellLanguageServer { ) -> Option { Some(LanguageServerBinary { path: "haskell-language-server-wrapper".into(), + env: None, arguments: vec!["lsp".into()], }) } diff --git a/crates/zed/src/languages/haskell/brackets.scm b/crates/languages/src/haskell/brackets.scm similarity index 100% rename from crates/zed/src/languages/haskell/brackets.scm rename to crates/languages/src/haskell/brackets.scm diff --git a/crates/zed/src/languages/haskell/config.toml b/crates/languages/src/haskell/config.toml similarity index 100% rename from crates/zed/src/languages/haskell/config.toml rename to crates/languages/src/haskell/config.toml diff --git a/crates/zed/src/languages/haskell/highlights.scm b/crates/languages/src/haskell/highlights.scm similarity index 100% rename from crates/zed/src/languages/haskell/highlights.scm rename to crates/languages/src/haskell/highlights.scm diff --git a/crates/zed/src/languages/nu/indents.scm b/crates/languages/src/haskell/indents.scm similarity index 100% rename from crates/zed/src/languages/nu/indents.scm rename to crates/languages/src/haskell/indents.scm diff --git a/crates/zed/src/languages/haskell/outline.scm b/crates/languages/src/haskell/outline.scm similarity index 100% rename from crates/zed/src/languages/haskell/outline.scm rename to crates/languages/src/haskell/outline.scm diff --git a/crates/zed/src/languages/hcl/config.toml b/crates/languages/src/hcl/config.toml similarity index 100% rename from crates/zed/src/languages/hcl/config.toml rename to crates/languages/src/hcl/config.toml diff --git a/crates/zed/src/languages/hcl/highlights.scm b/crates/languages/src/hcl/highlights.scm similarity index 100% rename from crates/zed/src/languages/hcl/highlights.scm rename to crates/languages/src/hcl/highlights.scm diff --git a/crates/zed/src/languages/hcl/indents.scm b/crates/languages/src/hcl/indents.scm similarity index 100% rename from crates/zed/src/languages/hcl/indents.scm rename to crates/languages/src/hcl/indents.scm diff --git a/crates/zed/src/languages/hcl/injections.scm b/crates/languages/src/hcl/injections.scm similarity index 100% rename from crates/zed/src/languages/hcl/injections.scm rename to crates/languages/src/hcl/injections.scm diff --git a/crates/zed/src/languages/heex/config.toml b/crates/languages/src/heex/config.toml similarity index 100% rename from crates/zed/src/languages/heex/config.toml rename to crates/languages/src/heex/config.toml diff --git a/crates/zed/src/languages/heex/highlights.scm b/crates/languages/src/heex/highlights.scm similarity index 100% rename from crates/zed/src/languages/heex/highlights.scm rename to crates/languages/src/heex/highlights.scm diff --git a/crates/zed/src/languages/heex/injections.scm b/crates/languages/src/heex/injections.scm similarity index 100% rename from crates/zed/src/languages/heex/injections.scm rename to crates/languages/src/heex/injections.scm diff --git a/crates/zed/src/languages/heex/overrides.scm b/crates/languages/src/heex/overrides.scm similarity index 100% rename from crates/zed/src/languages/heex/overrides.scm rename to crates/languages/src/heex/overrides.scm diff --git a/crates/zed/src/languages/html.rs b/crates/languages/src/html.rs similarity index 97% rename from crates/zed/src/languages/html.rs rename to crates/languages/src/html.rs index 02c6fabaad..713a432de3 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/languages/src/html.rs @@ -14,7 +14,7 @@ use std::{ }; use util::{async_maybe, ResultExt}; -const SERVER_PATH: &'static str = +const SERVER_PATH: &str = "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server"; fn server_binary_arguments(server_path: &Path) -> Vec { @@ -37,10 +37,6 @@ impl LspAdapter for HtmlLspAdapter { LanguageServerName("vscode-html-language-server".into()) } - fn short_name(&self) -> &'static str { - "html" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -72,6 +68,7 @@ impl LspAdapter for HtmlLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -116,6 +113,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { diff --git a/crates/zed/src/languages/html/brackets.scm b/crates/languages/src/html/brackets.scm similarity index 100% rename from crates/zed/src/languages/html/brackets.scm rename to crates/languages/src/html/brackets.scm diff --git a/crates/zed/src/languages/html/config.toml b/crates/languages/src/html/config.toml similarity index 100% rename from crates/zed/src/languages/html/config.toml rename to crates/languages/src/html/config.toml diff --git a/crates/zed/src/languages/html/highlights.scm b/crates/languages/src/html/highlights.scm similarity index 100% rename from crates/zed/src/languages/html/highlights.scm rename to crates/languages/src/html/highlights.scm diff --git a/crates/zed/src/languages/html/indents.scm b/crates/languages/src/html/indents.scm similarity index 100% rename from crates/zed/src/languages/html/indents.scm rename to crates/languages/src/html/indents.scm diff --git a/crates/zed/src/languages/html/injections.scm b/crates/languages/src/html/injections.scm similarity index 100% rename from crates/zed/src/languages/html/injections.scm rename to crates/languages/src/html/injections.scm diff --git a/crates/zed/src/languages/html/outline.scm b/crates/languages/src/html/outline.scm similarity index 100% rename from crates/zed/src/languages/html/outline.scm rename to crates/languages/src/html/outline.scm diff --git a/crates/zed/src/languages/html/overrides.scm b/crates/languages/src/html/overrides.scm similarity index 100% rename from crates/zed/src/languages/html/overrides.scm rename to crates/languages/src/html/overrides.scm diff --git a/crates/zed/src/languages/javascript/brackets.scm b/crates/languages/src/javascript/brackets.scm similarity index 100% rename from crates/zed/src/languages/javascript/brackets.scm rename to crates/languages/src/javascript/brackets.scm diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/languages/src/javascript/config.toml similarity index 100% rename from crates/zed/src/languages/javascript/config.toml rename to crates/languages/src/javascript/config.toml diff --git a/crates/zed/src/languages/javascript/contexts.scm b/crates/languages/src/javascript/contexts.scm similarity index 100% rename from crates/zed/src/languages/javascript/contexts.scm rename to crates/languages/src/javascript/contexts.scm diff --git a/crates/zed/src/languages/javascript/embedding.scm b/crates/languages/src/javascript/embedding.scm similarity index 100% rename from crates/zed/src/languages/javascript/embedding.scm rename to crates/languages/src/javascript/embedding.scm diff --git a/crates/zed/src/languages/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm similarity index 100% rename from crates/zed/src/languages/javascript/highlights.scm rename to crates/languages/src/javascript/highlights.scm diff --git a/crates/zed/src/languages/javascript/indents.scm b/crates/languages/src/javascript/indents.scm similarity index 100% rename from crates/zed/src/languages/javascript/indents.scm rename to crates/languages/src/javascript/indents.scm diff --git a/crates/zed/src/languages/javascript/outline.scm b/crates/languages/src/javascript/outline.scm similarity index 100% rename from crates/zed/src/languages/javascript/outline.scm rename to crates/languages/src/javascript/outline.scm diff --git a/crates/zed/src/languages/javascript/overrides.scm b/crates/languages/src/javascript/overrides.scm similarity index 100% rename from crates/zed/src/languages/javascript/overrides.scm rename to crates/languages/src/javascript/overrides.scm diff --git a/crates/zed/src/languages/json.rs b/crates/languages/src/json.rs similarity index 92% rename from crates/zed/src/languages/json.rs rename to crates/languages/src/json.rs index eeb4d7b619..1813f4c270 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/languages/src/json.rs @@ -18,8 +18,7 @@ use std::{ }; use util::{async_maybe, paths, ResultExt}; -const SERVER_PATH: &'static str = - "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; +const SERVER_PATH: &str = "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -53,7 +52,7 @@ impl JsonLspAdapter { }, cx, ); - let runnables_schema = runnable::static_source::DefinitionProvider::generate_json_schema(); + let tasks_schema = task::static_source::DefinitionProvider::generate_json_schema(); serde_json::json!({ "json": { "format": { @@ -72,8 +71,11 @@ impl JsonLspAdapter { "schema": KeymapFile::generate_json_schema(&action_names), }, { - "fileMatch": [schema_file_match(&paths::RUNNABLES)], - "schema": runnables_schema, + "fileMatch": [ + schema_file_match(&paths::TASKS), + &*paths::LOCAL_TASKS_RELATIVE_PATH, + ], + "schema": tasks_schema, } ] } @@ -87,10 +89,6 @@ impl LspAdapter for JsonLspAdapter { LanguageServerName("json-language-server".into()) } - fn short_name(&self) -> &'static str { - "json" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -122,6 +120,7 @@ impl LspAdapter for JsonLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -177,6 +176,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { diff --git a/crates/zed/src/languages/json/brackets.scm b/crates/languages/src/json/brackets.scm similarity index 100% rename from crates/zed/src/languages/json/brackets.scm rename to crates/languages/src/json/brackets.scm diff --git a/crates/zed/src/languages/json/config.toml b/crates/languages/src/json/config.toml similarity index 100% rename from crates/zed/src/languages/json/config.toml rename to crates/languages/src/json/config.toml diff --git a/crates/zed/src/languages/json/embedding.scm b/crates/languages/src/json/embedding.scm similarity index 100% rename from crates/zed/src/languages/json/embedding.scm rename to crates/languages/src/json/embedding.scm diff --git a/crates/zed/src/languages/json/highlights.scm b/crates/languages/src/json/highlights.scm similarity index 100% rename from crates/zed/src/languages/json/highlights.scm rename to crates/languages/src/json/highlights.scm diff --git a/crates/zed/src/languages/json/indents.scm b/crates/languages/src/json/indents.scm similarity index 100% rename from crates/zed/src/languages/json/indents.scm rename to crates/languages/src/json/indents.scm diff --git a/crates/zed/src/languages/json/outline.scm b/crates/languages/src/json/outline.scm similarity index 100% rename from crates/zed/src/languages/json/outline.scm rename to crates/languages/src/json/outline.scm diff --git a/crates/zed/src/languages/json/overrides.scm b/crates/languages/src/json/overrides.scm similarity index 100% rename from crates/zed/src/languages/json/overrides.scm rename to crates/languages/src/json/overrides.scm diff --git a/crates/zed/src/languages/json/redactions.scm b/crates/languages/src/json/redactions.scm similarity index 100% rename from crates/zed/src/languages/json/redactions.scm rename to crates/languages/src/json/redactions.scm diff --git a/crates/zed/src/languages.rs b/crates/languages/src/lib.rs similarity index 65% rename from crates/zed/src/languages.rs rename to crates/languages/src/lib.rs index aa3f2166c7..6aef9f6d16 100644 --- a/crates/zed/src/languages.rs +++ b/crates/languages/src/lib.rs @@ -14,6 +14,7 @@ mod c; mod clojure; mod csharp; mod css; +mod dart; mod deno; mod dockerfile; mod elixir; @@ -24,8 +25,6 @@ mod go; mod haskell; mod html; mod json; -#[cfg(feature = "plugin_runtime")] -mod language_plugin; mod lua; mod nu; mod ocaml; @@ -37,6 +36,7 @@ mod ruby; mod rust; mod svelte; mod tailwind; +mod terraform; mod toml; mod typescript; mod uiua; @@ -54,7 +54,7 @@ mod zig; // 6. If the language has injections add an injections.scm query file #[derive(RustEmbed)] -#[folder = "src/languages"] +#[folder = "src/"] #[exclude = "*.rs"] struct LanguageDir; @@ -105,7 +105,6 @@ pub fn init( ("php", tree_sitter_php::language_php()), ("prisma", tree_sitter_prisma_io::language()), ("proto", tree_sitter_proto::language()), - #[cfg(not(target_os = "linux"))] ("purescript", tree_sitter_purescript::language()), ("python", tree_sitter_python::language()), ("racket", tree_sitter_racket::language()), @@ -120,224 +119,254 @@ pub fn init( ("vue", tree_sitter_vue::language()), ("yaml", tree_sitter_yaml::language()), ("zig", tree_sitter_zig::language()), + ("dart", tree_sitter_dart::language()), ]); - let language = |asset_dir_name: &'static str, adapters| { - let config = load_config(asset_dir_name); - languages.register_language( - config.name.clone(), - config.grammar.clone(), - config.matcher.clone(), - adapters, - move || Ok((config.clone(), load_queries(asset_dir_name))), - ) - }; - - language( + macro_rules! language { + ($name:literal) => { + let config = load_config($name); + languages.register_language( + config.name.clone(), + config.grammar.clone(), + config.matcher.clone(), + Some(Arc::new(language::DefaultContextProvider)), + move || Ok((config.clone(), load_queries($name))), + ); + }; + ($name:literal, $adapters:expr) => { + let config = load_config($name); + // typeck helper + let adapters: Vec> = $adapters; + for adapter in adapters { + languages.register_lsp_adapter(config.name.clone(), adapter); + } + languages.register_language( + config.name.clone(), + config.grammar.clone(), + config.matcher.clone(), + Some(Arc::new(language::DefaultContextProvider)), + move || Ok((config.clone(), load_queries($name))), + ); + }; + ($name:literal, $adapters:expr, $context_provider:expr) => { + let config = load_config($name); + // typeck helper + let adapters: Vec> = $adapters; + for adapter in $adapters { + languages.register_lsp_adapter(config.name.clone(), adapter); + } + languages.register_language( + config.name.clone(), + config.grammar.clone(), + config.matcher.clone(), + Some(Arc::new($context_provider)), + move || Ok((config.clone(), load_queries($name))), + ); + }; + } + language!( "astro", vec![ Arc::new(astro::AstroLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language("bash", vec![]); - language("c", vec![Arc::new(c::CLspAdapter) as Arc]); - language("clojure", vec![Arc::new(clojure::ClojureLspAdapter)]); - language("cpp", vec![Arc::new(c::CLspAdapter)]); - language("csharp", vec![Arc::new(csharp::OmniSharpAdapter {})]); - language( + language!("bash"); + language!("c", vec![Arc::new(c::CLspAdapter) as Arc]); + language!("clojure", vec![Arc::new(clojure::ClojureLspAdapter)]); + language!("cpp", vec![Arc::new(c::CLspAdapter)]); + language!("csharp", vec![Arc::new(csharp::OmniSharpAdapter {})]); + language!( "css", vec![ Arc::new(css::CssLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language( + language!( "dockerfile", vec![Arc::new(dockerfile::DockerfileLspAdapter::new( node_runtime.clone(), - ))], + ))] ); match &ElixirSettings::get(None, cx).lsp { - elixir::ElixirLspSetting::ElixirLs => language( - "elixir", - vec![ - Arc::new(elixir::ElixirLspAdapter), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], - ), - elixir::ElixirLspSetting::NextLs => { - language("elixir", vec![Arc::new(elixir::NextLspAdapter)]) + elixir::ElixirLspSetting::ElixirLs => { + language!( + "elixir", + vec![ + Arc::new(elixir::ElixirLspAdapter), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ] + ); + } + elixir::ElixirLspSetting::NextLs => { + language!("elixir", vec![Arc::new(elixir::NextLspAdapter)]); + } + elixir::ElixirLspSetting::Local { path, arguments } => { + language!( + "elixir", + vec![Arc::new(elixir::LocalLspAdapter { + path: path.clone(), + arguments: arguments.clone(), + })] + ); } - elixir::ElixirLspSetting::Local { path, arguments } => language( - "elixir", - vec![Arc::new(elixir::LocalLspAdapter { - path: path.clone(), - arguments: arguments.clone(), - })], - ), } - language("gitcommit", vec![]); - language("erlang", vec![Arc::new(erlang::ErlangLspAdapter)]); + language!("gitcommit"); + language!("erlang", vec![Arc::new(erlang::ErlangLspAdapter)]); - language("gleam", vec![Arc::new(gleam::GleamLspAdapter)]); - language("go", vec![Arc::new(go::GoLspAdapter)]); - language("gomod", vec![]); - language("gowork", vec![]); - language("zig", vec![Arc::new(zig::ZlsAdapter)]); - language( + language!("gleam", vec![Arc::new(gleam::GleamLspAdapter)]); + language!("go", vec![Arc::new(go::GoLspAdapter)]); + language!("gomod"); + language!("gowork"); + language!("zig", vec![Arc::new(zig::ZlsAdapter)]); + language!( "heex", vec![ Arc::new(elixir::ElixirLspAdapter), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language( + language!( "json", vec![Arc::new(json::JsonLspAdapter::new( node_runtime.clone(), languages.clone(), - ))], + ))] ); - language("markdown", vec![]); - language( + language!("markdown"); + language!( "python", vec![Arc::new(python::PythonLspAdapter::new( node_runtime.clone(), - ))], + ))] ); - language("rust", vec![Arc::new(rust::RustLspAdapter)]); - language("toml", vec![Arc::new(toml::TaploLspAdapter)]); + language!("rust", vec![Arc::new(rust::RustLspAdapter)]); + language!("toml", vec![Arc::new(toml::TaploLspAdapter)]); match &DenoSettings::get(None, cx).enable { true => { - language( + language!( "tsx", vec![ Arc::new(deno::DenoLspAdapter::new()), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language("typescript", vec![Arc::new(deno::DenoLspAdapter::new())]); - language( + language!("typescript", vec![Arc::new(deno::DenoLspAdapter::new())]); + language!( "javascript", vec![ Arc::new(deno::DenoLspAdapter::new()), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); } false => { - language( + language!( "tsx", vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language( + language!( "typescript", vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - ], + ] ); - language( + language!( "javascript", vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); } } - language("haskell", vec![Arc::new(haskell::HaskellLanguageServer {})]); - language( + language!("haskell", vec![Arc::new(haskell::HaskellLanguageServer {})]); + language!( "html", vec![ Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language("ruby", vec![Arc::new(ruby::RubyLanguageServer)]); - language( + language!("ruby", vec![Arc::new(ruby::RubyLanguageServer)]); + language!( "erb", vec![ Arc::new(ruby::RubyLanguageServer), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language("scheme", vec![]); - language("racket", vec![]); - language("lua", vec![Arc::new(lua::LuaLspAdapter)]); - language( + language!("scheme"); + language!("racket"); + language!("lua", vec![Arc::new(lua::LuaLspAdapter)]); + language!( "yaml", - vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))], + vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))] ); - language( + language!( "svelte", vec![ Arc::new(svelte::SvelteLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - language( + language!( "php", vec![ Arc::new(php::IntelephenseLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], + ] ); - - // Produces a link error on linux due to duplicated `state_new` symbol - // todo!(linux): Restore purescript - #[cfg(not(target_os = "linux"))] - language( + language!( "purescript", vec![Arc::new(purescript::PurescriptLspAdapter::new( node_runtime.clone(), - ))], + ))] ); - language( + language!( "elm", - vec![Arc::new(elm::ElmLspAdapter::new(node_runtime.clone()))], + vec![Arc::new(elm::ElmLspAdapter::new(node_runtime.clone()))] ); - language("glsl", vec![]); - language("nix", vec![]); - language("nu", vec![Arc::new(nu::NuLanguageServer {})]); - language("ocaml", vec![Arc::new(ocaml::OCamlLspAdapter)]); - language("ocaml-interface", vec![Arc::new(ocaml::OCamlLspAdapter)]); - language( + language!("glsl"); + language!("nix"); + language!("nu", vec![Arc::new(nu::NuLanguageServer {})]); + language!("ocaml", vec![Arc::new(ocaml::OCamlLspAdapter)]); + language!("ocaml-interface", vec![Arc::new(ocaml::OCamlLspAdapter)]); + language!( "vue", - vec![Arc::new(vue::VueLspAdapter::new(node_runtime.clone()))], + vec![Arc::new(vue::VueLspAdapter::new(node_runtime.clone()))] ); - language("uiua", vec![Arc::new(uiua::UiuaLanguageServer {})]); - language("proto", vec![]); - language("terraform", vec![]); - language("terraform-vars", vec![]); - language("hcl", vec![]); - language( + language!("uiua", vec![Arc::new(uiua::UiuaLanguageServer {})]); + language!("proto"); + language!("terraform", vec![Arc::new(terraform::TerraformLspAdapter)]); + language!( + "terraform-vars", + vec![Arc::new(terraform::TerraformLspAdapter)] + ); + language!("hcl", vec![]); + language!( "prisma", vec![Arc::new(prisma::PrismaLspAdapter::new( node_runtime.clone(), - ))], + ))] ); + language!("dart", vec![Arc::new(dart::DartLanguageServer {})]); } #[cfg(any(test, feature = "test-support"))] -pub async fn language( - name: &str, - grammar: tree_sitter::Language, - lsp_adapter: Option>, -) -> Arc { +pub fn language(name: &str, grammar: tree_sitter::Language) -> Arc { Arc::new( Language::new(load_config(name), Some(grammar)) - .with_lsp_adapters(lsp_adapter.into_iter().collect()) - .await .with_queries(load_queries(name)) .unwrap(), ) diff --git a/crates/zed/src/languages/lua.rs b/crates/languages/src/lua.rs similarity index 88% rename from crates/zed/src/languages/lua.rs rename to crates/languages/src/lua.rs index 7476ab37ea..cad9004480 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/languages/src/lua.rs @@ -22,14 +22,16 @@ impl super::LspAdapter for LuaLspAdapter { LanguageServerName("lua-language-server".into()) } - fn short_name(&self) -> &'static str { - "lua" - } - async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, ) -> Result> { + let os = match consts::OS { + "macos" => "darwin", + "linux" => "linux", + "windows" => "win32", + other => bail!("Running on unsupported os: {other}"), + }; let platform = match consts::ARCH { "x86_64" => "x64", "aarch64" => "arm64", @@ -43,7 +45,7 @@ impl super::LspAdapter for LuaLspAdapter { ) .await?; let version = &release.tag_name; - let asset_name = format!("lua-language-server-{version}-darwin-{platform}.tar.gz"); + let asset_name = format!("lua-language-server-{version}-{os}-{platform}.tar.gz"); let asset = release .assets .iter() @@ -77,13 +79,18 @@ impl super::LspAdapter for LuaLspAdapter { archive.unpack(container_dir).await?; } - fs::set_permissions( - &binary_path, - ::from_mode(0o755), - ) - .await?; + // todo("windows") + #[cfg(not(windows))] + { + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + } Ok(LanguageServerBinary { path: binary_path, + env: None, arguments: Vec::new(), }) } @@ -128,6 +135,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option &'static str { - "nu" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -41,6 +37,7 @@ impl LspAdapter for NuLanguageServer { ) -> Option { Some(LanguageServerBinary { path: "nu".into(), + env: None, arguments: vec!["--lsp".into()], }) } diff --git a/crates/zed/src/languages/nu/brackets.scm b/crates/languages/src/nu/brackets.scm similarity index 100% rename from crates/zed/src/languages/nu/brackets.scm rename to crates/languages/src/nu/brackets.scm diff --git a/crates/zed/src/languages/nu/config.toml b/crates/languages/src/nu/config.toml similarity index 100% rename from crates/zed/src/languages/nu/config.toml rename to crates/languages/src/nu/config.toml diff --git a/crates/zed/src/languages/nu/highlights.scm b/crates/languages/src/nu/highlights.scm similarity index 75% rename from crates/zed/src/languages/nu/highlights.scm rename to crates/languages/src/nu/highlights.scm index 97f46d3879..66a3058405 100644 --- a/crates/zed/src/languages/nu/highlights.scm +++ b/crates/languages/src/nu/highlights.scm @@ -2,7 +2,6 @@ ;;; keywords [ "def" - "def-env" "alias" "export-env" "export" @@ -46,22 +45,22 @@ "for" @keyword "in" @keyword ) -(overlay_list "list" @keyword) -(overlay_hide "hide" @keyword) -(overlay_new "new" @keyword) +(overlay_list "list" @keyword.storage.modifier) +(overlay_hide "hide" @keyword.storage.modifier) +(overlay_new "new" @keyword.storage.modifier) (overlay_use - "use" @keyword + "use" @keyword.storage.modifier "as" @keyword ) -(ctrl_error "make" @keyword) +(ctrl_error "make" @keyword.storage.modifier) ;;; --- ;;; literals -(val_number) @constant +(val_number) @constant.numeric (val_duration unit: [ "ns" "Β΅s" "us" "ms" "sec" "min" "hr" "day" "wk" - ] @variable + ] @variable.parameter ) (val_filesize unit: [ @@ -73,7 +72,6 @@ "tb" "tB" "Tb" "TB" "pb" "pB" "Pb" "PB" "eb" "eB" "Eb" "EB" - "zb" "zB" "Zb" "ZB" "kib" "kiB" "kIB" "kIb" "Kib" "KIb" "KIB" "mib" "miB" "mIB" "mIb" "Mib" "MIb" "MIB" @@ -81,28 +79,27 @@ "tib" "tiB" "tIB" "tIb" "Tib" "TIb" "TIB" "pib" "piB" "pIB" "pIb" "Pib" "PIb" "PIB" "eib" "eiB" "eIB" "eIb" "Eib" "EIb" "EIB" - "zib" "ziB" "zIB" "zIb" "Zib" "ZIb" "ZIB" - ] @variable + ] @variable.parameter ) (val_binary [ "0b" "0o" "0x" - ] @constant + ] @constant.numeric "[" @punctuation.bracket digit: [ "," @punctuation.delimiter - (hex_digit) @constant + (hex_digit) @constant.number ] "]" @punctuation.bracket -) @constant +) @constant.numeric (val_bool) @constant.builtin (val_nothing) @constant.builtin (val_string) @string -(val_date) @constant -(inter_escape_sequence) @constant -(escape_sequence) @constant +(val_date) @constant.number +(inter_escape_sequence) @constant.character.escape +(escape_sequence) @constant.character.escape (val_interpolated [ "$\"" "$\'" @@ -111,7 +108,7 @@ ] @string) (unescaped_interpolated_content) @string (escaped_interpolated_content) @string -(expr_interpolated ["(" ")"] @variable) +(expr_interpolated ["(" ")"] @variable.parameter) ;;; --- ;;; operators @@ -144,22 +141,7 @@ "not-in" "starts-with" "ends-with" -] @operator) - -(expr_binary opr: ([ - "and" - "or" - "xor" - "bit-or" - "bit-xor" - "bit-and" - "bit-shl" - "bit-shr" - "in" - "not-in" - "starts-with" - "ends-with" -]) @keyword) +] @operator ) (where_command [ "+" @@ -245,18 +227,18 @@ ;;; --- ;;; identifiers (param_rest - name: (_) @variable) + name: (_) @variable.parameter) (param_opt - name: (_) @variable) + name: (_) @variable.parameter) (parameter - param_name: (_) @variable) + param_name: (_) @variable.parameter) (param_cmd (cmd_identifier) @string) -(param_long_flag) @variable -(param_short_flag) @variable +(param_long_flag) @variable.parameter +(param_short_flag) @variable.parameter -(short_flag) @variable -(long_flag) @variable +(short_flag) @variable.parameter +(long_flag) @variable.parameter (scope_pattern [(wild_card) @function]) @@ -271,29 +253,29 @@ (path ["." "?"] @punctuation.delimiter -) @variable +) @variable.parameter -(val_variable - "$" @operator +(val_variable + "$" @variable.parameter [ - (identifier) @variable - "in" @type.builtin - "nu" @type.builtin - "env" @type.builtin - "nothing" @type.builtin - ] ; If we have a special styling, use it here + (identifier) @namespace + "in" + "nu" + "env" + "nothing" + ] @special ) ;;; --- ;;; types (flat_type) @type.builtin (list_type - "list" @type + "list" @type.enum ["<" ">"] @punctuation.bracket ) (collection_type - ["record" "table"] @type + ["record" "table"] @type.enum "<" @punctuation.bracket - key: (_) @variable + key: (_) @variable.parameter ["," ":"] @punctuation.delimiter ">" @punctuation.bracket ) diff --git a/crates/zed/src/languages/purescript/indents.scm b/crates/languages/src/nu/indents.scm similarity index 100% rename from crates/zed/src/languages/purescript/indents.scm rename to crates/languages/src/nu/indents.scm diff --git a/crates/zed/src/languages/ocaml-interface/brackets.scm b/crates/languages/src/ocaml-interface/brackets.scm similarity index 54% rename from crates/zed/src/languages/ocaml-interface/brackets.scm rename to crates/languages/src/ocaml-interface/brackets.scm index 0929a696fd..f05821c17c 100644 --- a/crates/zed/src/languages/ocaml-interface/brackets.scm +++ b/crates/languages/src/ocaml-interface/brackets.scm @@ -1,6 +1,3 @@ ("(" @open ")" @close) ("{" @open "}" @close) ("<" @open ">" @close) - -("sig" @open "end" @close) -("object" @open "end" @close) diff --git a/crates/zed/src/languages/ocaml-interface/config.toml b/crates/languages/src/ocaml-interface/config.toml similarity index 57% rename from crates/zed/src/languages/ocaml-interface/config.toml rename to crates/languages/src/ocaml-interface/config.toml index fdbf1aad81..4df8074953 100644 --- a/crates/zed/src/languages/ocaml-interface/config.toml +++ b/crates/languages/src/ocaml-interface/config.toml @@ -7,8 +7,5 @@ brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "<", end = ">", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, - { start = "(", end = ")", close = true, newline = true }, - { start = "sig", end = " end", close = true, newline = true }, - # HACK: For some reason `object` alone does not work - { start = "object ", end = "end", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true } ] diff --git a/crates/zed/src/languages/ocaml-interface/highlights.scm b/crates/languages/src/ocaml-interface/highlights.scm similarity index 100% rename from crates/zed/src/languages/ocaml-interface/highlights.scm rename to crates/languages/src/ocaml-interface/highlights.scm diff --git a/crates/zed/src/languages/ocaml-interface/indents.scm b/crates/languages/src/ocaml-interface/indents.scm similarity index 100% rename from crates/zed/src/languages/ocaml-interface/indents.scm rename to crates/languages/src/ocaml-interface/indents.scm diff --git a/crates/zed/src/languages/ocaml-interface/outline.scm b/crates/languages/src/ocaml-interface/outline.scm similarity index 100% rename from crates/zed/src/languages/ocaml-interface/outline.scm rename to crates/languages/src/ocaml-interface/outline.scm diff --git a/crates/zed/src/languages/ocaml.rs b/crates/languages/src/ocaml.rs similarity index 98% rename from crates/zed/src/languages/ocaml.rs rename to crates/languages/src/ocaml.rs index 9878b89e33..0dc4bfcd2b 100644 --- a/crates/zed/src/languages/ocaml.rs +++ b/crates/languages/src/ocaml.rs @@ -18,10 +18,6 @@ impl LspAdapter for OCamlLspAdapter { LanguageServerName("ocamllsp".into()) } - fn short_name(&self) -> &'static str { - "ocaml" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -47,6 +43,7 @@ impl LspAdapter for OCamlLspAdapter { ) -> Option { Some(LanguageServerBinary { path: "ocamllsp".into(), + env: None, arguments: vec![], }) } @@ -65,7 +62,7 @@ impl LspAdapter for OCamlLspAdapter { language: &Arc, ) -> Option { let name = &completion.label; - let detail = completion.detail.as_ref().map(|s| s.replace("\n", " ")); + let detail = completion.detail.as_ref().map(|s| s.replace('\n', " ")); match completion.kind.zip(detail) { // Error of 'b : ('a, 'b) result @@ -127,7 +124,7 @@ impl LspAdapter for OCamlLspAdapter { // version : string // NOTE: (~|?) are omitted as we don't use them in the fuzzy filtering Some((CompletionItemKind::FIELD, detail)) - if name.starts_with("~") || name.starts_with("?") => + if name.starts_with('~') || name.starts_with('?') => { let label = name.trim_start_matches(&['~', '?']); let text = format!("{} : {}", label, detail); @@ -143,7 +140,7 @@ impl LspAdapter for OCamlLspAdapter { } let mut label_highlight = vec![( - 0..0 + label.len(), + 0..label.len(), language.grammar()?.highlight_id_for_name("property")?, )]; diff --git a/crates/zed/src/languages/ocaml/brackets.scm b/crates/languages/src/ocaml/brackets.scm similarity index 50% rename from crates/zed/src/languages/ocaml/brackets.scm rename to crates/languages/src/ocaml/brackets.scm index 8aa7be2eaf..6afe4638fd 100644 --- a/crates/zed/src/languages/ocaml/brackets.scm +++ b/crates/languages/src/ocaml/brackets.scm @@ -5,8 +5,3 @@ ("<" @open ">" @close) ("\"" @open "\"" @close) -("begin" @open "end" @close) -("struct" @open "end" @close) -("sig" @open "end" @close) -("object" @open "end" @close) -("do" @open "done" @close) diff --git a/crates/zed/src/languages/ocaml/config.toml b/crates/languages/src/ocaml/config.toml similarity index 55% rename from crates/zed/src/languages/ocaml/config.toml rename to crates/languages/src/ocaml/config.toml index 313cbb46df..35e452ade0 100644 --- a/crates/zed/src/languages/ocaml/config.toml +++ b/crates/languages/src/ocaml/config.toml @@ -9,11 +9,5 @@ brackets = [ { start = "[", end = "]", close = true, newline = true }, { start = "[|", end = "|", close = true, newline = true, not_in = ["string"] }, { start = "(", end = ")", close = true, newline = true }, - { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, - { start = "begin", end = " end", close = true, newline = true }, - { start = "struct", end = " end", close = true, newline = true }, - { start = "sig", end = " end", close = true, newline = true }, - # HACK: For some reason `object` alone does not work - { start = "object ", end = "end", close = true, newline = true }, - { start = "do", end = " done", close = true, newline = true } + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] } ] diff --git a/crates/zed/src/languages/ocaml/highlights.scm b/crates/languages/src/ocaml/highlights.scm similarity index 96% rename from crates/zed/src/languages/ocaml/highlights.scm rename to crates/languages/src/ocaml/highlights.scm index e5125b912e..41db5a403e 100644 --- a/crates/zed/src/languages/ocaml/highlights.scm +++ b/crates/languages/src/ocaml/highlights.scm @@ -8,7 +8,8 @@ [(class_name) (class_type_name) (type_constructor)] @type -[(constructor_name) (tag)] @constructor +(tag) @variant ;; Polymorphic Variants +(constructor_name) @constructor ;; Exceptions, variants and the like ; Functions ;---------- diff --git a/crates/zed/src/languages/ocaml/indents.scm b/crates/languages/src/ocaml/indents.scm similarity index 95% rename from crates/zed/src/languages/ocaml/indents.scm rename to crates/languages/src/ocaml/indents.scm index 807495dae1..10995d15ab 100644 --- a/crates/zed/src/languages/ocaml/indents.scm +++ b/crates/languages/src/ocaml/indents.scm @@ -14,6 +14,8 @@ (field_declaration) (field_expression) + + (application_expression) ] @indent (_ "[" "]" @end) @indent diff --git a/crates/zed/src/languages/ocaml/outline.scm b/crates/languages/src/ocaml/outline.scm similarity index 100% rename from crates/zed/src/languages/ocaml/outline.scm rename to crates/languages/src/ocaml/outline.scm diff --git a/crates/zed/src/languages/php.rs b/crates/languages/src/php.rs similarity index 98% rename from crates/zed/src/languages/php.rs rename to crates/languages/src/php.rs index d952e4a2fb..1405614210 100644 --- a/crates/zed/src/languages/php.rs +++ b/crates/languages/src/php.rs @@ -40,10 +40,6 @@ impl LspAdapter for IntelephenseLspAdapter { LanguageServerName("intelephense".into()) } - fn short_name(&self) -> &'static str { - "php" - } - async fn fetch_latest_server_version( &self, _delegate: &dyn LspAdapterDelegate, @@ -69,6 +65,7 @@ impl LspAdapter for IntelephenseLspAdapter { } Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: intelephense_server_binary_arguments(&server_path), }) } @@ -126,6 +123,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: intelephense_server_binary_arguments(&server_path), }) } else { diff --git a/crates/zed/src/languages/php/config.toml b/crates/languages/src/php/config.toml similarity index 100% rename from crates/zed/src/languages/php/config.toml rename to crates/languages/src/php/config.toml diff --git a/crates/zed/src/languages/php/embedding.scm b/crates/languages/src/php/embedding.scm similarity index 100% rename from crates/zed/src/languages/php/embedding.scm rename to crates/languages/src/php/embedding.scm diff --git a/crates/zed/src/languages/php/highlights.scm b/crates/languages/src/php/highlights.scm similarity index 100% rename from crates/zed/src/languages/php/highlights.scm rename to crates/languages/src/php/highlights.scm diff --git a/crates/zed/src/languages/php/injections.scm b/crates/languages/src/php/injections.scm similarity index 100% rename from crates/zed/src/languages/php/injections.scm rename to crates/languages/src/php/injections.scm diff --git a/crates/zed/src/languages/php/outline.scm b/crates/languages/src/php/outline.scm similarity index 100% rename from crates/zed/src/languages/php/outline.scm rename to crates/languages/src/php/outline.scm diff --git a/crates/zed/src/languages/php/tags.scm b/crates/languages/src/php/tags.scm similarity index 100% rename from crates/zed/src/languages/php/tags.scm rename to crates/languages/src/php/tags.scm diff --git a/crates/zed/src/languages/prisma.rs b/crates/languages/src/prisma.rs similarity index 95% rename from crates/zed/src/languages/prisma.rs rename to crates/languages/src/prisma.rs index e84cf8d3e9..0731cc8d11 100644 --- a/crates/zed/src/languages/prisma.rs +++ b/crates/languages/src/prisma.rs @@ -13,7 +13,7 @@ use std::{ }; use util::{async_maybe, ResultExt}; -const SERVER_PATH: &'static str = "node_modules/.bin/prisma-language-server"; +const SERVER_PATH: &str = "node_modules/.bin/prisma-language-server"; fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -35,10 +35,6 @@ impl LspAdapter for PrismaLspAdapter { LanguageServerName("prisma-language-server".into()) } - fn short_name(&self) -> &'static str { - "prisma-language-server" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -70,6 +66,7 @@ impl LspAdapter for PrismaLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -112,6 +109,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { diff --git a/crates/zed/src/languages/prisma/config.toml b/crates/languages/src/prisma/config.toml similarity index 100% rename from crates/zed/src/languages/prisma/config.toml rename to crates/languages/src/prisma/config.toml diff --git a/crates/zed/src/languages/prisma/highlights.scm b/crates/languages/src/prisma/highlights.scm similarity index 100% rename from crates/zed/src/languages/prisma/highlights.scm rename to crates/languages/src/prisma/highlights.scm diff --git a/crates/zed/src/languages/proto/config.toml b/crates/languages/src/proto/config.toml similarity index 100% rename from crates/zed/src/languages/proto/config.toml rename to crates/languages/src/proto/config.toml diff --git a/crates/zed/src/languages/proto/highlights.scm b/crates/languages/src/proto/highlights.scm similarity index 100% rename from crates/zed/src/languages/proto/highlights.scm rename to crates/languages/src/proto/highlights.scm diff --git a/crates/zed/src/languages/proto/outline.scm b/crates/languages/src/proto/outline.scm similarity index 100% rename from crates/zed/src/languages/proto/outline.scm rename to crates/languages/src/proto/outline.scm diff --git a/crates/zed/src/languages/purescript.rs b/crates/languages/src/purescript.rs similarity index 95% rename from crates/zed/src/languages/purescript.rs rename to crates/languages/src/purescript.rs index 150f154633..df3cb5b5a9 100644 --- a/crates/zed/src/languages/purescript.rs +++ b/crates/languages/src/purescript.rs @@ -15,7 +15,7 @@ use std::{ }; use util::{async_maybe, ResultExt}; -const SERVER_PATH: &'static str = "node_modules/.bin/purescript-language-server"; +const SERVER_PATH: &str = "node_modules/.bin/purescript-language-server"; fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -26,7 +26,7 @@ pub struct PurescriptLspAdapter { } impl PurescriptLspAdapter { - // todo!(linux): remove + // todo(linux): remove #[cfg_attr(target_os = "linux", allow(dead_code))] pub fn new(node: Arc) -> Self { Self { node } @@ -39,10 +39,6 @@ impl LspAdapter for PurescriptLspAdapter { LanguageServerName("purescript-language-server".into()) } - fn short_name(&self) -> &'static str { - "purescript" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -74,6 +70,7 @@ impl LspAdapter for PurescriptLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -127,6 +124,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { diff --git a/crates/zed/src/languages/purescript/brackets.scm b/crates/languages/src/purescript/brackets.scm similarity index 100% rename from crates/zed/src/languages/purescript/brackets.scm rename to crates/languages/src/purescript/brackets.scm diff --git a/crates/zed/src/languages/purescript/config.toml b/crates/languages/src/purescript/config.toml similarity index 100% rename from crates/zed/src/languages/purescript/config.toml rename to crates/languages/src/purescript/config.toml diff --git a/crates/zed/src/languages/purescript/highlights.scm b/crates/languages/src/purescript/highlights.scm similarity index 100% rename from crates/zed/src/languages/purescript/highlights.scm rename to crates/languages/src/purescript/highlights.scm diff --git a/crates/zed/src/languages/python/indents.scm b/crates/languages/src/purescript/indents.scm similarity index 100% rename from crates/zed/src/languages/python/indents.scm rename to crates/languages/src/purescript/indents.scm diff --git a/crates/zed/src/languages/python.rs b/crates/languages/src/python.rs similarity index 96% rename from crates/zed/src/languages/python.rs rename to crates/languages/src/python.rs index 216957df66..bf1cae7d9d 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/languages/src/python.rs @@ -12,7 +12,7 @@ use std::{ }; use util::ResultExt; -const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js"; +const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js"; fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -34,10 +34,6 @@ impl LspAdapter for PythonLspAdapter { LanguageServerName("pyright".into()) } - fn short_name(&self) -> &'static str { - "pyright" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -62,6 +58,7 @@ impl LspAdapter for PythonLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -86,7 +83,7 @@ impl LspAdapter for PythonLspAdapter { // Where `XX` is the sorting category, `YYYY` is based on most recent usage, // and `name` is the symbol name itself. // - // Because the the symbol name is included, there generally are not ties when + // Because the symbol name is included, there generally are not ties when // sorting by the `sortText`, so the symbol's fuzzy match score is not taken // into account. Here, we remove the symbol name from the sortText in order // to allow our own fuzzy score to be used to break ties. @@ -167,6 +164,7 @@ async fn get_cached_server_binary( if server_path.exists() { Some(LanguageServerBinary { path: node.binary_path().await.log_err()?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { @@ -186,8 +184,7 @@ mod tests { #[gpui::test] async fn test_python_autoindent(cx: &mut TestAppContext) { cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX); - let language = - crate::languages::language("python", tree_sitter_python::language(), None).await; + let language = crate::language("python", tree_sitter_python::language()); cx.update(|cx| { let test_settings = SettingsStore::test(cx); cx.set_global(test_settings); diff --git a/crates/zed/src/languages/python/brackets.scm b/crates/languages/src/python/brackets.scm similarity index 100% rename from crates/zed/src/languages/python/brackets.scm rename to crates/languages/src/python/brackets.scm diff --git a/crates/zed/src/languages/python/config.toml b/crates/languages/src/python/config.toml similarity index 100% rename from crates/zed/src/languages/python/config.toml rename to crates/languages/src/python/config.toml diff --git a/crates/zed/src/languages/python/embedding.scm b/crates/languages/src/python/embedding.scm similarity index 100% rename from crates/zed/src/languages/python/embedding.scm rename to crates/languages/src/python/embedding.scm diff --git a/crates/zed/src/languages/python/highlights.scm b/crates/languages/src/python/highlights.scm similarity index 100% rename from crates/zed/src/languages/python/highlights.scm rename to crates/languages/src/python/highlights.scm diff --git a/crates/languages/src/python/indents.scm b/crates/languages/src/python/indents.scm new file mode 100644 index 0000000000..112b414aa4 --- /dev/null +++ b/crates/languages/src/python/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed/src/languages/python/outline.scm b/crates/languages/src/python/outline.scm similarity index 100% rename from crates/zed/src/languages/python/outline.scm rename to crates/languages/src/python/outline.scm diff --git a/crates/zed/src/languages/python/overrides.scm b/crates/languages/src/python/overrides.scm similarity index 100% rename from crates/zed/src/languages/python/overrides.scm rename to crates/languages/src/python/overrides.scm diff --git a/crates/zed/src/languages/racket/brackets.scm b/crates/languages/src/racket/brackets.scm similarity index 100% rename from crates/zed/src/languages/racket/brackets.scm rename to crates/languages/src/racket/brackets.scm diff --git a/crates/zed/src/languages/racket/config.toml b/crates/languages/src/racket/config.toml similarity index 100% rename from crates/zed/src/languages/racket/config.toml rename to crates/languages/src/racket/config.toml diff --git a/crates/zed/src/languages/racket/highlights.scm b/crates/languages/src/racket/highlights.scm similarity index 100% rename from crates/zed/src/languages/racket/highlights.scm rename to crates/languages/src/racket/highlights.scm diff --git a/crates/zed/src/languages/racket/indents.scm b/crates/languages/src/racket/indents.scm similarity index 100% rename from crates/zed/src/languages/racket/indents.scm rename to crates/languages/src/racket/indents.scm diff --git a/crates/zed/src/languages/racket/outline.scm b/crates/languages/src/racket/outline.scm similarity index 100% rename from crates/zed/src/languages/racket/outline.scm rename to crates/languages/src/racket/outline.scm diff --git a/crates/zed/src/languages/ruby.rs b/crates/languages/src/ruby.rs similarity index 98% rename from crates/zed/src/languages/ruby.rs rename to crates/languages/src/ruby.rs index 69294a5470..ced781bfbf 100644 --- a/crates/zed/src/languages/ruby.rs +++ b/crates/languages/src/ruby.rs @@ -12,10 +12,6 @@ impl LspAdapter for RubyLanguageServer { LanguageServerName("solargraph".into()) } - fn short_name(&self) -> &'static str { - "solargraph" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -39,6 +35,7 @@ impl LspAdapter for RubyLanguageServer { ) -> Option { Some(LanguageServerBinary { path: "solargraph".into(), + env: None, arguments: vec!["stdio".into()], }) } diff --git a/crates/zed/src/languages/ruby/brackets.scm b/crates/languages/src/ruby/brackets.scm similarity index 100% rename from crates/zed/src/languages/ruby/brackets.scm rename to crates/languages/src/ruby/brackets.scm diff --git a/crates/zed/src/languages/ruby/config.toml b/crates/languages/src/ruby/config.toml similarity index 100% rename from crates/zed/src/languages/ruby/config.toml rename to crates/languages/src/ruby/config.toml diff --git a/crates/zed/src/languages/ruby/embedding.scm b/crates/languages/src/ruby/embedding.scm similarity index 100% rename from crates/zed/src/languages/ruby/embedding.scm rename to crates/languages/src/ruby/embedding.scm diff --git a/crates/zed/src/languages/ruby/highlights.scm b/crates/languages/src/ruby/highlights.scm similarity index 84% rename from crates/zed/src/languages/ruby/highlights.scm rename to crates/languages/src/ruby/highlights.scm index 14703c4ff2..5178006cc4 100644 --- a/crates/zed/src/languages/ruby/highlights.scm +++ b/crates/languages/src/ruby/highlights.scm @@ -49,6 +49,11 @@ (setter (identifier) @function.method) (method name: [(identifier) (constant)] @function.method) (singleton_method name: [(identifier) (constant)] @function.method) +(method_parameters [ + (identifier) @variable.parameter + (optional_parameter name: (identifier) @variable.parameter) + (keyword_parameter [name: (identifier) (":")] @variable.parameter) + ]) ; Identifiers @@ -70,6 +75,18 @@ (constant) @type +(superclass + (constant) @type.super) + +(superclass + (scope_resolution + (constant) @type.super)) + +(superclass + (scope_resolution + (scope_resolution + (constant) @type.super))) + (self) @variable.special (super) @variable.special @@ -164,6 +181,7 @@ "," ";" "." + "::" ] @punctuation.delimiter [ diff --git a/crates/zed/src/languages/ruby/indents.scm b/crates/languages/src/ruby/indents.scm similarity index 100% rename from crates/zed/src/languages/ruby/indents.scm rename to crates/languages/src/ruby/indents.scm diff --git a/crates/zed/src/languages/ruby/outline.scm b/crates/languages/src/ruby/outline.scm similarity index 76% rename from crates/zed/src/languages/ruby/outline.scm rename to crates/languages/src/ruby/outline.scm index 0b36dabadb..544257ac0c 100644 --- a/crates/zed/src/languages/ruby/outline.scm +++ b/crates/languages/src/ruby/outline.scm @@ -2,6 +2,9 @@ "class" @context name: (_) @name) @item +((identifier) @context + (#match? @context "^(private|protected|public)$")) @item + (method "def" @context name: (_) @name) @item diff --git a/crates/zed/src/languages/ruby/overrides.scm b/crates/languages/src/ruby/overrides.scm similarity index 100% rename from crates/zed/src/languages/ruby/overrides.scm rename to crates/languages/src/ruby/overrides.scm diff --git a/crates/zed/src/languages/rust.rs b/crates/languages/src/rust.rs similarity index 87% rename from crates/zed/src/languages/rust.rs rename to crates/languages/src/rust.rs index 7a95e26d9b..2d3925e7d6 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/languages/src/rust.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; use futures::{io::BufReader, StreamExt}; @@ -23,10 +23,6 @@ impl LspAdapter for RustLspAdapter { LanguageServerName("rust-analyzer".into()) } - fn short_name(&self) -> &'static str { - "rust" - } - async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, @@ -38,7 +34,13 @@ impl LspAdapter for RustLspAdapter { delegate.http_client(), ) .await?; - let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); + let os = match consts::OS { + "macos" => "apple-darwin", + "linux" => "unknown-linux-gnu", + "windows" => "pc-windows-msvc", + other => bail!("Running on unsupported os: {other}"), + }; + let asset_name = format!("rust-analyzer-{}-{os}.gz", consts::ARCH); let asset = release .assets .iter() @@ -68,17 +70,22 @@ impl LspAdapter for RustLspAdapter { let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); let mut file = File::create(&destination_path).await?; futures::io::copy(decompressed_bytes, &mut file).await?; - fs::set_permissions( - &destination_path, - ::from_mode(0o755), - ) - .await?; + // todo("windows") + #[cfg(not(windows))] + { + fs::set_permissions( + &destination_path, + ::from_mode(0o755), + ) + .await?; + } remove_matching(&container_dir, |entry| entry != destination_path).await; } Ok(LanguageServerBinary { path: destination_path, + env: None, arguments: Default::default(), }) } @@ -286,6 +293,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option) -> Vec".to_string()), - ..Default::default() - }) + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }, + &language + ) .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec".to_string(), @@ -392,13 +399,16 @@ mod tests { }) ); assert_eq!( - language - .label_for_completion(&lsp::CompletionItem { - kind: Some(lsp::CompletionItemKind::FUNCTION), - label: "hello(…)".to_string(), - detail: Some("async fn(&mut Option) -> Vec".to_string()), - ..Default::default() - }) + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("async fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }, + &language + ) .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec".to_string(), @@ -414,13 +424,16 @@ mod tests { }) ); assert_eq!( - language - .label_for_completion(&lsp::CompletionItem { - kind: Some(lsp::CompletionItemKind::FIELD), - label: "len".to_string(), - detail: Some("usize".to_string()), - ..Default::default() - }) + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FIELD), + label: "len".to_string(), + detail: Some("usize".to_string()), + ..Default::default() + }, + &language + ) .await, Some(CodeLabel { text: "len: usize".to_string(), @@ -430,13 +443,16 @@ mod tests { ); assert_eq!( - language - .label_for_completion(&lsp::CompletionItem { - kind: Some(lsp::CompletionItemKind::FUNCTION), - label: "hello(…)".to_string(), - detail: Some("fn(&mut Option) -> Vec".to_string()), - ..Default::default() - }) + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }, + &language + ) .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec".to_string(), @@ -455,12 +471,8 @@ mod tests { #[gpui::test] async fn test_rust_label_for_symbol() { - let language = language( - "rust", - tree_sitter_rust::language(), - Some(Arc::new(RustLspAdapter)), - ) - .await; + let adapter = Arc::new(RustLspAdapter); + let language = language("rust", tree_sitter_rust::language()); let grammar = language.grammar().unwrap(); let theme = SyntaxTheme::new_test([ ("type", Hsla::default()), @@ -476,8 +488,8 @@ mod tests { let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap(); assert_eq!( - language - .label_for_symbol("hello", lsp::SymbolKind::FUNCTION) + adapter + .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language) .await, Some(CodeLabel { text: "fn hello".to_string(), @@ -487,8 +499,8 @@ mod tests { ); assert_eq!( - language - .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER) + adapter + .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language) .await, Some(CodeLabel { text: "type World".to_string(), @@ -512,7 +524,7 @@ mod tests { }); }); - let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await; + let language = crate::language("rust", tree_sitter_rust::language()); cx.new_model(|cx| { let mut buffer = Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "") diff --git a/crates/zed/src/languages/rust/brackets.scm b/crates/languages/src/rust/brackets.scm similarity index 100% rename from crates/zed/src/languages/rust/brackets.scm rename to crates/languages/src/rust/brackets.scm diff --git a/crates/zed/src/languages/rust/config.toml b/crates/languages/src/rust/config.toml similarity index 100% rename from crates/zed/src/languages/rust/config.toml rename to crates/languages/src/rust/config.toml diff --git a/crates/zed/src/languages/rust/embedding.scm b/crates/languages/src/rust/embedding.scm similarity index 100% rename from crates/zed/src/languages/rust/embedding.scm rename to crates/languages/src/rust/embedding.scm diff --git a/crates/zed/src/languages/rust/highlights.scm b/crates/languages/src/rust/highlights.scm similarity index 100% rename from crates/zed/src/languages/rust/highlights.scm rename to crates/languages/src/rust/highlights.scm diff --git a/crates/zed/src/languages/rust/indents.scm b/crates/languages/src/rust/indents.scm similarity index 100% rename from crates/zed/src/languages/rust/indents.scm rename to crates/languages/src/rust/indents.scm diff --git a/crates/zed/src/languages/rust/injections.scm b/crates/languages/src/rust/injections.scm similarity index 100% rename from crates/zed/src/languages/rust/injections.scm rename to crates/languages/src/rust/injections.scm diff --git a/crates/zed/src/languages/rust/outline.scm b/crates/languages/src/rust/outline.scm similarity index 100% rename from crates/zed/src/languages/rust/outline.scm rename to crates/languages/src/rust/outline.scm diff --git a/crates/zed/src/languages/rust/overrides.scm b/crates/languages/src/rust/overrides.scm similarity index 100% rename from crates/zed/src/languages/rust/overrides.scm rename to crates/languages/src/rust/overrides.scm diff --git a/crates/zed/src/languages/scheme/brackets.scm b/crates/languages/src/scheme/brackets.scm similarity index 100% rename from crates/zed/src/languages/scheme/brackets.scm rename to crates/languages/src/scheme/brackets.scm diff --git a/crates/zed/src/languages/scheme/config.toml b/crates/languages/src/scheme/config.toml similarity index 100% rename from crates/zed/src/languages/scheme/config.toml rename to crates/languages/src/scheme/config.toml diff --git a/crates/zed/src/languages/scheme/highlights.scm b/crates/languages/src/scheme/highlights.scm similarity index 100% rename from crates/zed/src/languages/scheme/highlights.scm rename to crates/languages/src/scheme/highlights.scm diff --git a/crates/zed/src/languages/scheme/indents.scm b/crates/languages/src/scheme/indents.scm similarity index 100% rename from crates/zed/src/languages/scheme/indents.scm rename to crates/languages/src/scheme/indents.scm diff --git a/crates/zed/src/languages/scheme/outline.scm b/crates/languages/src/scheme/outline.scm similarity index 100% rename from crates/zed/src/languages/scheme/outline.scm rename to crates/languages/src/scheme/outline.scm diff --git a/crates/zed/src/languages/scheme/overrides.scm b/crates/languages/src/scheme/overrides.scm similarity index 100% rename from crates/zed/src/languages/scheme/overrides.scm rename to crates/languages/src/scheme/overrides.scm diff --git a/crates/zed/src/languages/svelte.rs b/crates/languages/src/svelte.rs similarity index 96% rename from crates/zed/src/languages/svelte.rs rename to crates/languages/src/svelte.rs index 45fd1b0457..aff9a6db7d 100644 --- a/crates/zed/src/languages/svelte.rs +++ b/crates/languages/src/svelte.rs @@ -14,7 +14,7 @@ use std::{ }; use util::{async_maybe, ResultExt}; -const SERVER_PATH: &'static str = "node_modules/svelte-language-server/bin/server.js"; +const SERVER_PATH: &str = "node_modules/svelte-language-server/bin/server.js"; fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -36,10 +36,6 @@ impl LspAdapter for SvelteLspAdapter { LanguageServerName("svelte-language-server".into()) } - fn short_name(&self) -> &'static str { - "svelte" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -71,6 +67,7 @@ impl LspAdapter for SvelteLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -148,6 +145,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { diff --git a/crates/zed/src/languages/svelte/config.toml b/crates/languages/src/svelte/config.toml similarity index 100% rename from crates/zed/src/languages/svelte/config.toml rename to crates/languages/src/svelte/config.toml diff --git a/crates/zed/src/languages/svelte/folds.scm b/crates/languages/src/svelte/folds.scm similarity index 100% rename from crates/zed/src/languages/svelte/folds.scm rename to crates/languages/src/svelte/folds.scm diff --git a/crates/zed/src/languages/svelte/highlights.scm b/crates/languages/src/svelte/highlights.scm similarity index 100% rename from crates/zed/src/languages/svelte/highlights.scm rename to crates/languages/src/svelte/highlights.scm diff --git a/crates/zed/src/languages/svelte/indents.scm b/crates/languages/src/svelte/indents.scm similarity index 100% rename from crates/zed/src/languages/svelte/indents.scm rename to crates/languages/src/svelte/indents.scm diff --git a/crates/zed/src/languages/svelte/injections.scm b/crates/languages/src/svelte/injections.scm similarity index 79% rename from crates/zed/src/languages/svelte/injections.scm rename to crates/languages/src/svelte/injections.scm index 8c1ac9fcd0..1cbf02cfa2 100755 --- a/crates/zed/src/languages/svelte/injections.scm +++ b/crates/languages/src/svelte/injections.scm @@ -1,7 +1,11 @@ ; injections.scm ; -------------- -(script_element - (raw_text) @content +((script_element + (start_tag + (attribute + (quoted_attribute_value (attribute_value) @_language))?) + (raw_text) @content) + (#eq? @_language "") (#set! "language" "javascript")) ((script_element diff --git a/crates/zed/src/languages/svelte/overrides.scm b/crates/languages/src/svelte/overrides.scm similarity index 100% rename from crates/zed/src/languages/svelte/overrides.scm rename to crates/languages/src/svelte/overrides.scm diff --git a/crates/zed/src/languages/tailwind.rs b/crates/languages/src/tailwind.rs similarity index 96% rename from crates/zed/src/languages/tailwind.rs rename to crates/languages/src/tailwind.rs index 206e390e42..69ac629c7c 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -16,7 +16,7 @@ use std::{ }; use util::{async_maybe, ResultExt}; -const SERVER_PATH: &'static str = "node_modules/.bin/tailwindcss-language-server"; +const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server"; fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -38,10 +38,6 @@ impl LspAdapter for TailwindLspAdapter { LanguageServerName("tailwindcss-language-server".into()) } - fn short_name(&self) -> &'static str { - "tailwind" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -73,6 +69,7 @@ impl LspAdapter for TailwindLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -150,6 +147,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { diff --git a/crates/zed/src/languages/terraform-vars/config.toml b/crates/languages/src/terraform-vars/config.toml similarity index 100% rename from crates/zed/src/languages/terraform-vars/config.toml rename to crates/languages/src/terraform-vars/config.toml diff --git a/crates/zed/src/languages/terraform-vars/highlights.scm b/crates/languages/src/terraform-vars/highlights.scm similarity index 100% rename from crates/zed/src/languages/terraform-vars/highlights.scm rename to crates/languages/src/terraform-vars/highlights.scm diff --git a/crates/zed/src/languages/terraform-vars/indents.scm b/crates/languages/src/terraform-vars/indents.scm similarity index 100% rename from crates/zed/src/languages/terraform-vars/indents.scm rename to crates/languages/src/terraform-vars/indents.scm diff --git a/crates/zed/src/languages/terraform-vars/injections.scm b/crates/languages/src/terraform-vars/injections.scm similarity index 100% rename from crates/zed/src/languages/terraform-vars/injections.scm rename to crates/languages/src/terraform-vars/injections.scm diff --git a/crates/languages/src/terraform.rs b/crates/languages/src/terraform.rs new file mode 100644 index 0000000000..d201b8aeff --- /dev/null +++ b/crates/languages/src/terraform.rs @@ -0,0 +1,182 @@ +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use collections::HashMap; +use futures::StreamExt; +pub use language::*; +use lsp::{CodeActionKind, LanguageServerBinary}; +use smol::fs::{self, File}; +use std::{any::Any, ffi::OsString, path::PathBuf}; +use util::{ + async_maybe, + fs::remove_matching, + github::{latest_github_release, GitHubLspBinaryVersion}, + ResultExt, +}; + +fn terraform_ls_binary_arguments() -> Vec { + vec!["serve".into()] +} + +pub struct TerraformLspAdapter; + +#[async_trait] +impl LspAdapter for TerraformLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("terraform-ls".into()) + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + // TODO: maybe use release API instead + // https://api.releases.hashicorp.com/v1/releases/terraform-ls?limit=1 + let release = latest_github_release( + "hashicorp/terraform-ls", + false, + false, + delegate.http_client(), + ) + .await?; + + Ok(Box::new(GitHubLspBinaryVersion { + name: release.tag_name, + url: Default::default(), + })) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let zip_path = container_dir.join(format!("terraform-ls_{}.zip", version.name)); + let version_dir = container_dir.join(format!("terraform-ls_{}", version.name)); + let binary_path = version_dir.join("terraform-ls"); + let url = build_download_url(version.name)?; + + if fs::metadata(&binary_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&url, Default::default(), true) + .await + .context("error downloading release")?; + let mut file = File::create(&zip_path).await?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response.body_mut(), &mut file).await?; + + let unzip_status = smol::process::Command::new("unzip") + .current_dir(&container_dir) + .arg(&zip_path) + .arg("-d") + .arg(&version_dir) + .output() + .await? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip Terraform LS archive"))?; + } + + remove_matching(&container_dir, |entry| entry != version_dir).await; + } + + Ok(LanguageServerBinary { + path: binary_path, + env: None, + arguments: terraform_ls_binary_arguments(), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["version".into()]; + binary + }) + } + + fn code_action_kinds(&self) -> Option> { + // TODO: file issue for server supported code actions + // TODO: reenable default actions / delete override + Some(vec![]) + } + + fn language_ids(&self) -> HashMap { + HashMap::from_iter([ + ("Terraform".into(), "terraform".into()), + ("Terraform Vars".into(), "terraform-vars".into()), + ]) + } +} + +fn build_download_url(version: String) -> Result { + let v = version.strip_prefix('v').unwrap_or(&version); + let os = match std::env::consts::OS { + "linux" => "linux", + "macos" => "darwin", + "win" => "windows", + _ => Err(anyhow!("unsupported OS {}", std::env::consts::OS))?, + } + .to_string(); + let arch = match std::env::consts::ARCH { + "x86" => "386", + "x86_64" => "amd64", + "arm" => "arm", + "aarch64" => "arm64", + _ => Err(anyhow!("unsupported ARCH {}", std::env::consts::ARCH))?, + } + .to_string(); + + let url = format!( + "https://releases.hashicorp.com/terraform-ls/{v}/terraform-ls_{v}_{os}_{arch}.zip", + ); + + Ok(url) +} + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + async_maybe!({ + let mut last = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + last = Some(entry?.path()); + } + + match last { + Some(path) if path.is_dir() => { + let binary = path.join("terraform-ls"); + if fs::metadata(&binary).await.is_ok() { + return Ok(LanguageServerBinary { + path: binary, + env: None, + arguments: terraform_ls_binary_arguments(), + }); + } + } + _ => {} + } + + Err(anyhow!("no cached binary")) + }) + .await + .log_err() +} diff --git a/crates/zed/src/languages/terraform/config.toml b/crates/languages/src/terraform/config.toml similarity index 100% rename from crates/zed/src/languages/terraform/config.toml rename to crates/languages/src/terraform/config.toml diff --git a/crates/zed/src/languages/terraform/highlights.scm b/crates/languages/src/terraform/highlights.scm similarity index 100% rename from crates/zed/src/languages/terraform/highlights.scm rename to crates/languages/src/terraform/highlights.scm diff --git a/crates/zed/src/languages/terraform/indents.scm b/crates/languages/src/terraform/indents.scm similarity index 100% rename from crates/zed/src/languages/terraform/indents.scm rename to crates/languages/src/terraform/indents.scm diff --git a/crates/zed/src/languages/terraform/injections.scm b/crates/languages/src/terraform/injections.scm similarity index 100% rename from crates/zed/src/languages/terraform/injections.scm rename to crates/languages/src/terraform/injections.scm diff --git a/crates/zed/src/languages/toml.rs b/crates/languages/src/toml.rs similarity index 81% rename from crates/zed/src/languages/toml.rs rename to crates/languages/src/toml.rs index 9393fa691e..1ca6bb8d1d 100644 --- a/crates/zed/src/languages/toml.rs +++ b/crates/languages/src/toml.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; use futures::{io::BufReader, StreamExt}; @@ -18,17 +18,22 @@ impl LspAdapter for TaploLspAdapter { LanguageServerName("taplo-ls".into()) } - fn short_name(&self) -> &'static str { - "taplo-ls" - } - async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, ) -> Result> { let release = latest_github_release("tamasfe/taplo", true, false, delegate.http_client()).await?; - let asset_name = format!("taplo-full-darwin-{arch}.gz", arch = std::env::consts::ARCH); + let asset_name = format!( + "taplo-full-{os}-{arch}.gz", + os = match std::env::consts::OS { + "macos" => "darwin", + "linux" => "linux", + "windows" => "windows", + other => bail!("Running on unsupported os: {other}"), + }, + arch = std::env::consts::ARCH + ); let asset = release .assets @@ -63,15 +68,20 @@ impl LspAdapter for TaploLspAdapter { futures::io::copy(decompressed_bytes, &mut file).await?; - fs::set_permissions( - &binary_path, - ::from_mode(0o755), - ) - .await?; + // todo("windows") + #[cfg(not(windows))] + { + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + } } Ok(LanguageServerBinary { path: binary_path, + env: None, arguments: vec!["lsp".into(), "stdio".into()], }) } @@ -107,6 +117,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option &'static str { - "tsserver" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -97,6 +95,7 @@ impl LspAdapter for TypeScriptLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: typescript_server_binary_arguments(&server_path), }) } @@ -192,11 +191,13 @@ async fn get_cached_ts_server_binary( if new_server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: typescript_server_binary_arguments(&new_server_path), }) } else if old_server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: typescript_server_binary_arguments(&old_server_path), }) } else { @@ -216,6 +217,7 @@ pub struct EsLintLspAdapter { impl EsLintLspAdapter { const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js"; + const SERVER_NAME: &'static str = "eslint"; pub fn new(node: Arc) -> Self { EsLintLspAdapter { node } @@ -224,7 +226,34 @@ impl EsLintLspAdapter { #[async_trait] impl LspAdapter for EsLintLspAdapter { - fn workspace_configuration(&self, workspace_root: &Path, _: &mut AppContext) -> Value { + fn workspace_configuration(&self, workspace_root: &Path, cx: &mut AppContext) -> Value { + let eslint_user_settings = ProjectSettings::get_global(cx) + .lsp + .get(Self::SERVER_NAME) + .and_then(|s| s.settings.clone()) + .unwrap_or_default(); + + let mut code_action_on_save = json!({ + // We enable this, but without also configuring `code_actions_on_format` + // in the Zed configuration, it doesn't have an effect. + "enable": true, + }); + + if let Some(code_action_settings) = eslint_user_settings + .get("codeActionOnSave") + .and_then(|settings| settings.as_object()) + { + if let Some(enable) = code_action_settings.get("enable") { + code_action_on_save["enable"] = enable.clone(); + } + if let Some(mode) = code_action_settings.get("mode") { + code_action_on_save["mode"] = mode.clone(); + } + if let Some(rules) = code_action_settings.get("rules") { + code_action_on_save["rules"] = rules.clone(); + } + } + json!({ "": { "validate": "on", @@ -237,16 +266,17 @@ impl LspAdapter for EsLintLspAdapter { "name": workspace_root.file_name() .unwrap_or_else(|| workspace_root.as_os_str()), }, + "problems": {}, + "codeActionOnSave": code_action_on_save, + "experimental": { + "useFlatConfig": workspace_root.join("eslint.config.js").is_file(), + }, } }) } fn name(&self) -> LanguageServerName { - LanguageServerName("eslint".into()) - } - - fn short_name(&self) -> &'static str { - "eslint" + LanguageServerName(Self::SERVER_NAME.into()) } async fn fetch_latest_server_version( @@ -259,7 +289,7 @@ impl LspAdapter for EsLintLspAdapter { let release = latest_github_release( "microsoft/vscode-eslint", false, - false, + true, delegate.http_client(), ) .await?; @@ -307,6 +337,7 @@ impl LspAdapter for EsLintLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: eslint_server_binary_arguments(&server_path), }) } @@ -354,6 +385,7 @@ async fn get_cached_eslint_server_binary( Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: eslint_server_binary_arguments(&server_path), }) }) @@ -369,12 +401,7 @@ mod tests { #[gpui::test] async fn test_outline(cx: &mut TestAppContext) { - let language = crate::languages::language( - "typescript", - tree_sitter_typescript::language_typescript(), - None, - ) - .await; + let language = crate::language("typescript", tree_sitter_typescript::language_typescript()); let text = r#" function a() { diff --git a/crates/zed/src/languages/typescript/brackets.scm b/crates/languages/src/typescript/brackets.scm similarity index 100% rename from crates/zed/src/languages/typescript/brackets.scm rename to crates/languages/src/typescript/brackets.scm diff --git a/crates/zed/src/languages/typescript/config.toml b/crates/languages/src/typescript/config.toml similarity index 100% rename from crates/zed/src/languages/typescript/config.toml rename to crates/languages/src/typescript/config.toml diff --git a/crates/zed/src/languages/typescript/embedding.scm b/crates/languages/src/typescript/embedding.scm similarity index 100% rename from crates/zed/src/languages/typescript/embedding.scm rename to crates/languages/src/typescript/embedding.scm diff --git a/crates/zed/src/languages/typescript/highlights.scm b/crates/languages/src/typescript/highlights.scm similarity index 100% rename from crates/zed/src/languages/typescript/highlights.scm rename to crates/languages/src/typescript/highlights.scm diff --git a/crates/zed/src/languages/typescript/indents.scm b/crates/languages/src/typescript/indents.scm similarity index 100% rename from crates/zed/src/languages/typescript/indents.scm rename to crates/languages/src/typescript/indents.scm diff --git a/crates/zed/src/languages/typescript/outline.scm b/crates/languages/src/typescript/outline.scm similarity index 100% rename from crates/zed/src/languages/typescript/outline.scm rename to crates/languages/src/typescript/outline.scm diff --git a/crates/zed/src/languages/typescript/overrides.scm b/crates/languages/src/typescript/overrides.scm similarity index 100% rename from crates/zed/src/languages/typescript/overrides.scm rename to crates/languages/src/typescript/overrides.scm diff --git a/crates/zed/src/languages/uiua.rs b/crates/languages/src/uiua.rs similarity index 95% rename from crates/zed/src/languages/uiua.rs rename to crates/languages/src/uiua.rs index 50101b6651..229c0804f5 100644 --- a/crates/zed/src/languages/uiua.rs +++ b/crates/languages/src/uiua.rs @@ -12,10 +12,6 @@ impl LspAdapter for UiuaLanguageServer { LanguageServerName("uiua".into()) } - fn short_name(&self) -> &'static str { - "uiua" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -41,6 +37,7 @@ impl LspAdapter for UiuaLanguageServer { ) -> Option { Some(LanguageServerBinary { path: "uiua".into(), + env: None, arguments: vec!["lsp".into()], }) } diff --git a/crates/zed/src/languages/uiua/config.toml b/crates/languages/src/uiua/config.toml similarity index 100% rename from crates/zed/src/languages/uiua/config.toml rename to crates/languages/src/uiua/config.toml diff --git a/crates/zed/src/languages/uiua/highlights.scm b/crates/languages/src/uiua/highlights.scm similarity index 100% rename from crates/zed/src/languages/uiua/highlights.scm rename to crates/languages/src/uiua/highlights.scm diff --git a/crates/zed/src/languages/uiua/indents.scm b/crates/languages/src/uiua/indents.scm similarity index 100% rename from crates/zed/src/languages/uiua/indents.scm rename to crates/languages/src/uiua/indents.scm diff --git a/crates/zed/src/languages/vue.rs b/crates/languages/src/vue.rs similarity index 98% rename from crates/zed/src/languages/vue.rs rename to crates/languages/src/vue.rs index fa86d68eaa..f90364c66b 100644 --- a/crates/zed/src/languages/vue.rs +++ b/crates/languages/src/vue.rs @@ -44,10 +44,6 @@ impl super::LspAdapter for VueLspAdapter { LanguageServerName("vue-language-server".into()) } - fn short_name(&self) -> &'static str { - "vue-language-server" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -118,6 +114,7 @@ impl super::LspAdapter for VueLspAdapter { *self.typescript_install_path.lock() = Some(ts_path); Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: vue_server_binary_arguments(&server_path), }) } @@ -204,6 +201,7 @@ async fn get_cached_server_binary( Ok(( LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: vue_server_binary_arguments(&server_path), }, typescript_path, diff --git a/crates/zed/src/languages/vue/brackets.scm b/crates/languages/src/vue/brackets.scm similarity index 100% rename from crates/zed/src/languages/vue/brackets.scm rename to crates/languages/src/vue/brackets.scm diff --git a/crates/zed/src/languages/vue/config.toml b/crates/languages/src/vue/config.toml similarity index 100% rename from crates/zed/src/languages/vue/config.toml rename to crates/languages/src/vue/config.toml diff --git a/crates/zed/src/languages/vue/highlights.scm b/crates/languages/src/vue/highlights.scm similarity index 100% rename from crates/zed/src/languages/vue/highlights.scm rename to crates/languages/src/vue/highlights.scm diff --git a/crates/zed/src/languages/vue/injections.scm b/crates/languages/src/vue/injections.scm similarity index 100% rename from crates/zed/src/languages/vue/injections.scm rename to crates/languages/src/vue/injections.scm diff --git a/crates/zed/src/languages/yaml.rs b/crates/languages/src/yaml.rs similarity index 95% rename from crates/zed/src/languages/yaml.rs rename to crates/languages/src/yaml.rs index 633e5d7da9..fe115b09b3 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -17,7 +17,7 @@ use std::{ }; use util::{async_maybe, ResultExt}; -const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server"; +const SERVER_PATH: &str = "node_modules/yaml-language-server/bin/yaml-language-server"; fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -39,10 +39,6 @@ impl LspAdapter for YamlLspAdapter { LanguageServerName("yaml-language-server".into()) } - fn short_name(&self) -> &'static str { - "yaml" - } - async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -74,6 +70,7 @@ impl LspAdapter for YamlLspAdapter { Ok(LanguageServerBinary { path: self.node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } @@ -124,6 +121,7 @@ async fn get_cached_server_binary( if server_path.exists() { Ok(LanguageServerBinary { path: node.binary_path().await?, + env: None, arguments: server_binary_arguments(&server_path), }) } else { diff --git a/crates/zed/src/languages/yaml/brackets.scm b/crates/languages/src/yaml/brackets.scm similarity index 100% rename from crates/zed/src/languages/yaml/brackets.scm rename to crates/languages/src/yaml/brackets.scm diff --git a/crates/zed/src/languages/yaml/config.toml b/crates/languages/src/yaml/config.toml similarity index 100% rename from crates/zed/src/languages/yaml/config.toml rename to crates/languages/src/yaml/config.toml diff --git a/crates/zed/src/languages/yaml/highlights.scm b/crates/languages/src/yaml/highlights.scm similarity index 100% rename from crates/zed/src/languages/yaml/highlights.scm rename to crates/languages/src/yaml/highlights.scm diff --git a/crates/zed/src/languages/yaml/outline.scm b/crates/languages/src/yaml/outline.scm similarity index 100% rename from crates/zed/src/languages/yaml/outline.scm rename to crates/languages/src/yaml/outline.scm diff --git a/crates/zed/src/languages/yaml/redactions.scm b/crates/languages/src/yaml/redactions.scm similarity index 100% rename from crates/zed/src/languages/yaml/redactions.scm rename to crates/languages/src/yaml/redactions.scm diff --git a/crates/zed/src/languages/zig.rs b/crates/languages/src/zig.rs similarity index 82% rename from crates/zed/src/languages/zig.rs rename to crates/languages/src/zig.rs index 12268c2e14..d3ad22aa8b 100644 --- a/crates/zed/src/languages/zig.rs +++ b/crates/languages/src/zig.rs @@ -6,7 +6,8 @@ use futures::{io::BufReader, StreamExt}; use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use smol::fs; -use std::env::consts::ARCH; +use std::env::consts::{ARCH, OS}; +use std::ffi::OsString; use std::{any::Any, path::PathBuf}; use util::async_maybe; use util::github::latest_github_release; @@ -20,17 +21,13 @@ impl LspAdapter for ZlsAdapter { LanguageServerName("zls".into()) } - fn short_name(&self) -> &'static str { - "zls" - } - async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, ) -> Result> { let release = latest_github_release("zigtools/zls", true, false, delegate.http_client()).await?; - let asset_name = format!("zls-{ARCH}-macos.tar.gz"); + let asset_name = format!("zls-{ARCH}-{OS}.tar.gz"); let asset = release .assets .iter() @@ -44,6 +41,18 @@ impl LspAdapter for ZlsAdapter { Ok(Box::new(version) as Box<_>) } + async fn check_if_user_installed( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Option { + let (path, env) = delegate.which_command(OsString::from("zls")).await?; + Some(LanguageServerBinary { + path, + arguments: vec![], + env: Some(env), + }) + } + async fn fetch_server_binary( &self, version: Box, @@ -64,13 +73,18 @@ impl LspAdapter for ZlsAdapter { archive.unpack(container_dir).await?; } - fs::set_permissions( - &binary_path, - ::from_mode(0o755), - ) - .await?; + // todo("windows") + #[cfg(not(windows))] + { + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + } Ok(LanguageServerBinary { path: binary_path, + env: None, arguments: vec![], }) } @@ -115,6 +129,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option Result<()> { - //todo!(linux): Remove this once the cross-platform LiveKit implementation is merged + // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; let mut server_rooms = self.rooms.lock(); @@ -87,7 +87,7 @@ impl TestServer { async fn delete_room(&self, room: String) -> Result<()> { // TODO: clear state associated with all `Room`s. - //todo!(linux): Remove this once the cross-platform LiveKit implementation is merged + // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; let mut server_rooms = self.rooms.lock(); @@ -98,7 +98,7 @@ impl TestServer { } async fn join_room(&self, token: String, client_room: Arc) -> Result<()> { - //todo!(linux): Remove this once the cross-platform LiveKit implementation is merged + // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; @@ -147,7 +147,7 @@ impl TestServer { } async fn leave_room(&self, token: String) -> Result<()> { - //todo!(linux): Remove this once the cross-platform LiveKit implementation is merged + // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; let claims = live_kit_server::token::validate(&token, &self.secret_key)?; @@ -169,7 +169,7 @@ impl TestServer { async fn remove_participant(&self, room_name: String, identity: String) -> Result<()> { // TODO: clear state associated with the `Room`. - //todo!(linux): Remove this once the cross-platform LiveKit implementation is merged + // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; @@ -193,7 +193,7 @@ impl TestServer { identity: String, permission: proto::ParticipantPermission, ) -> Result<()> { - //todo!(linux): Remove this once the cross-platform LiveKit implementation is merged + // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; let mut server_rooms = self.rooms.lock(); @@ -205,7 +205,7 @@ impl TestServer { } pub async fn disconnect_client(&self, client_identity: String) { - //todo!(linux): Remove this once the cross-platform LiveKit implementation is merged + // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; let mut server_rooms = self.rooms.lock(); @@ -221,7 +221,7 @@ impl TestServer { token: String, local_track: LocalVideoTrack, ) -> Result { - //todo!(linux): Remove this once the cross-platform LiveKit implementation is merged + // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; let claims = live_kit_server::token::validate(&token, &self.secret_key)?; @@ -276,7 +276,7 @@ impl TestServer { token: String, _local_track: &LocalAudioTrack, ) -> Result { - //todo!(linux): Remove this once the cross-platform LiveKit implementation is merged + // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; @@ -559,7 +559,7 @@ impl Room { pub fn display_sources(self: &Arc) -> impl Future>> { let this = self.clone(); async move { - //todo!(linux): Remove this once the cross-platform LiveKit implementation is merged + // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] { let server = this.test_server(); diff --git a/crates/live_kit_server/Cargo.toml b/crates/live_kit_server/Cargo.toml index 63b4fe1066..3c742111c4 100644 --- a/crates/live_kit_server/Cargo.toml +++ b/crates/live_kit_server/Cargo.toml @@ -13,16 +13,17 @@ doctest = false [dependencies] anyhow.workspace = true async-trait.workspace = true -futures.workspace = true hmac = "0.12" jwt = "0.16" log.workspace = true -prost = "0.8" +prost.workspace = true prost-types = "0.8" reqwest = "0.11" serde.workspace = true -serde_derive.workspace = true -sha2 = "0.10" +sha2.workspace = true [build-dependencies] prost-build = "0.9" + +[package.metadata.cargo-machete] +ignored = ["prost-types"] diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 5f218aea6c..902105341b 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -23,7 +23,6 @@ lsp-types = { git = "https://github.com/zed-industries/lsp-types", branch = "upd parking_lot.workspace = true postage.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true smol.workspace = true util.workspace = true @@ -34,5 +33,4 @@ async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "8 ctor.workspace = true env_logger.workspace = true gpui = { workspace = true, features = ["test-support"] } -unindent.workspace = true util = { workspace = true, features = ["test-support"] } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 93a6d00e99..1652e34044 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -4,7 +4,7 @@ pub use lsp_types::*; use anyhow::{anyhow, Context, Result}; use collections::HashMap; -use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt}; +use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, FutureExt}; use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task}; use parking_lot::Mutex; use postage::{barrier, prelude::Stream}; @@ -35,6 +35,7 @@ const HEADER_DELIMITER: &'static [u8; 4] = b"\r\n\r\n"; const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); +const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); type NotificationHandler = Box, &str, AsyncAppContext)>; type ResponseHandler = Box)>; @@ -54,6 +55,7 @@ pub enum IoKind { pub struct LanguageServerBinary { pub path: PathBuf, pub arguments: Vec, + pub env: Option>, } /// A running language server process. @@ -61,7 +63,7 @@ pub struct LanguageServer { server_id: LanguageServerId, next_id: AtomicI32, outbound_tx: channel::Sender, - name: String, + name: Arc, capabilities: ServerCapabilities, code_action_kinds: Option>, notification_handlers: Arc>>, @@ -72,7 +74,7 @@ pub struct LanguageServer { io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, root_path: PathBuf, - _server: Option>, + server: Arc>>, } /// Identifies a running language server. @@ -162,6 +164,34 @@ struct Error { message: String, } +/// Experimental: Informs the end user about the state of the server +/// +/// [Rust Analyzer Specification](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#server-status) +#[derive(Debug)] +pub enum ServerStatus {} + +/// Other(String) variant to handle unknown values due to this still being experimental +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub enum ServerHealthStatus { + Ok, + Warning, + Error, + Other(String), +} + +#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ServerStatusParams { + pub health: ServerHealthStatus, + pub message: Option, +} + +impl lsp_types::notification::Notification for ServerStatus { + type Params = ServerStatusParams; + const METHOD: &'static str = "experimental/serverStatus"; +} + impl LanguageServer { /// Starts a language server process. pub fn new( @@ -178,9 +208,17 @@ impl LanguageServer { root_path.parent().unwrap_or_else(|| Path::new("/")) }; + log::info!( + "starting language server. binary path: {:?}, working directory: {:?}, args: {:?}", + binary.path, + working_dir, + &binary.arguments + ); + let mut server = process::Command::new(&binary.path) .current_dir(working_dir) .args(binary.arguments) + .envs(binary.env.unwrap_or_default()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -191,7 +229,7 @@ impl LanguageServer { let stdout = server.stdout.take().unwrap(); let stderr = server.stderr.take().unwrap(); let mut server = Self::new_internal( - server_id.clone(), + server_id, stdin, stdout, Some(stderr), @@ -202,7 +240,7 @@ impl LanguageServer { cx, move |notification| { log::info!( - "{} unhandled notification {}:\n{}", + "Language server with id {} sent unhandled notification {}:\n{}", server_id, notification.method, serde_json::to_string_pretty( @@ -217,12 +255,13 @@ impl LanguageServer { ); if let Some(name) = binary.path.file_name() { - server.name = name.to_string_lossy().to_string(); + server.name = name.to_string_lossy().into(); } Ok(server) } + #[allow(clippy::too_many_arguments)] fn new_internal( server_id: LanguageServerId, stdin: Stdin, @@ -293,7 +332,7 @@ impl LanguageServer { notification_handlers, response_handlers, io_handlers, - name: Default::default(), + name: "".into(), capabilities: Default::default(), code_action_kinds, next_id: Default::default(), @@ -302,7 +341,7 @@ impl LanguageServer { io_tasks: Mutex::new(Some((input_task, output_task))), output_done_rx: Mutex::new(Some(output_done_rx)), root_path: root_path.to_path_buf(), - _server: server.map(|server| Mutex::new(server)), + server: Arc::new(Mutex::new(server)), } } @@ -339,7 +378,7 @@ impl LanguageServer { let headers = std::str::from_utf8(&buffer)?; let message_len = headers - .split("\n") + .split('\n') .find(|line| line.starts_with(CONTENT_LEN_HEADER)) .and_then(|line| line.strip_prefix(CONTENT_LEN_HEADER)) .ok_or_else(|| anyhow!("invalid LSP message header {headers:?}"))? @@ -606,11 +645,11 @@ impl LanguageServer { uri: root_uri, name: Default::default(), }]), - client_info: Some(ClientInfo { - name: release_channel::ReleaseChannel::global(cx) - .display_name() - .to_string(), - version: Some(release_channel::AppVersion::global(cx).to_string()), + client_info: release_channel::ReleaseChannel::try_global(cx).map(|release_channel| { + ClientInfo { + name: release_channel.display_name().to_string(), + version: Some(release_channel::AppVersion::global(cx).to_string()), + } }), locale: None, }; @@ -618,7 +657,7 @@ impl LanguageServer { cx.spawn(|_| async move { let response = self.request::(params).await?; if let Some(info) = response.server_info { - self.name = info.name; + self.name = info.name.into(); } self.capabilities = response.capabilities; @@ -644,14 +683,30 @@ impl LanguageServer { ); let exit = Self::notify_internal::(&outbound_tx, ()); outbound_tx.close(); + + let server = self.server.clone(); + let name = self.name.clone(); + let mut timer = self.executor.timer(SERVER_SHUTDOWN_TIMEOUT).fuse(); Some( async move { log::debug!("language server shutdown started"); - shutdown_request.await?; + + select! { + request_result = shutdown_request.fuse() => { + request_result?; + } + + _ = timer => { + log::info!("timeout waiting for language server {name} to shutdown"); + }, + } + response_handlers.lock().take(); exit?; output_done.recv().await; + server.lock().take().map(|mut child| child.kill()); log::debug!("language server shutdown finished"); + drop(tasks); anyhow::Ok(()) } @@ -891,8 +946,13 @@ impl LanguageServer { executor .spawn(async move { let response = match result { - Ok(response) => serde_json::from_str(&response) - .context("failed to deserialize response"), + Ok(response) => match serde_json::from_str(&response) { + Ok(deserialized) => Ok(deserialized), + Err(error) => { + log::error!("failed to deserialize response from language server: {}. response from language server: {:?}", error, response); + Err(error).context("failed to deserialize response") + } + } Err(error) => Err(anyhow!("{}", error.message)), }; _ = tx.send(response); @@ -918,7 +978,7 @@ impl LanguageServer { Self::notify_internal::( &outbound_tx, CancelParams { - id: NumberOrString::Number(id as i32), + id: NumberOrString::Number(id), }, ) .log_err(); @@ -926,7 +986,7 @@ impl LanguageServer { }); let method = T::METHOD; - futures::select! { + select! { response = rx.fuse() => { let elapsed = started.elapsed(); log::trace!("Took {elapsed:?} to receive response to {method:?} id {id}"); @@ -1024,6 +1084,7 @@ impl Drop for Subscription { #[cfg(any(test, feature = "test-support"))] #[derive(Clone)] pub struct FakeLanguageServer { + pub binary: LanguageServerBinary, pub server: Arc, notifications_rx: channel::Receiver<(String, String)>, } @@ -1032,6 +1093,7 @@ pub struct FakeLanguageServer { impl FakeLanguageServer { /// Construct a fake language server. pub fn new( + binary: LanguageServerBinary, name: String, capabilities: ServerCapabilities, cx: AsyncAppContext, @@ -1053,6 +1115,7 @@ impl FakeLanguageServer { |_| {}, ); let fake = FakeLanguageServer { + binary, server: Arc::new(LanguageServer::new_internal( LanguageServerId(0), stdout_writer, @@ -1107,6 +1170,7 @@ impl LanguageServer { document_formatting_provider: Some(OneOf::Left(true)), document_range_formatting_provider: Some(OneOf::Left(true)), definition_provider: Some(OneOf::Left(true)), + implementation_provider: Some(ImplementationProviderCapability::Simple(true)), type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), ..Default::default() } @@ -1270,8 +1334,16 @@ mod tests { cx.update(|cx| { release_channel::init("0.0.0", cx); }); - let (server, mut fake) = - FakeLanguageServer::new("the-lsp".to_string(), Default::default(), cx.to_async()); + let (server, mut fake) = FakeLanguageServer::new( + LanguageServerBinary { + path: "path/to/language-server".into(), + arguments: vec![], + env: None, + }, + "the-lsp".to_string(), + Default::default(), + cx.to_async(), + ); let (message_tx, message_rx) = channel::unbounded(); let (diagnostics_tx, diagnostics_rx) = channel::unbounded(); diff --git a/crates/markdown_preview/Cargo.toml b/crates/markdown_preview/Cargo.toml index f07447ef3f..5ac5ae3cb5 100644 --- a/crates/markdown_preview/Cargo.toml +++ b/crates/markdown_preview/Cargo.toml @@ -12,19 +12,13 @@ path = "src/markdown_preview.rs" test-support = [] [dependencies] -anyhow.workspace = true editor.workspace = true gpui.workspace = true language.workspace = true -lazy_static.workspace = true -log.workspace = true -menu.workspace = true -project.workspace = true pretty_assertions.workspace = true pulldown-cmark.workspace = true theme.workspace = true ui.workspace = true -util.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index 54cd01f8cd..61f4459d62 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -1,4 +1,6 @@ -use gpui::{px, FontStyle, FontWeight, HighlightStyle, SharedString, UnderlineStyle}; +use gpui::{ + px, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle, UnderlineStyle, +}; use language::HighlightId; use std::{ops::Range, path::PathBuf}; @@ -170,6 +172,13 @@ impl MarkdownHighlight { }); } + if style.strikethrough { + highlight.strikethrough = Some(StrikethroughStyle { + thickness: px(1.), + ..Default::default() + }); + } + if style.weight != FontWeight::default() { highlight.font_weight = Some(style.weight); } @@ -189,6 +198,8 @@ pub struct MarkdownHighlightStyle { pub italic: bool, /// Whether the text should be underlined. pub underline: bool, + /// Whether the text should be struck through. + pub strikethrough: bool, /// The weight of the text. pub weight: FontWeight, } diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index f860e211e2..87e3266a22 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -1,6 +1,6 @@ use crate::markdown_elements::*; use gpui::FontWeight; -use pulldown_cmark::{Alignment, Event, Options, Parser, Tag}; +use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd}; use std::{ops::Range, path::PathBuf}; pub fn parse_markdown( @@ -70,11 +70,11 @@ impl<'a> MarkdownParser<'a> { | Event::Code(_) | Event::Html(_) | Event::FootnoteReference(_) - | Event::Start(Tag::Link(_, _, _)) + | Event::Start(Tag::Link { link_type: _, dest_url: _, title: _, id: _ }) | Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong) | Event::Start(Tag::Strikethrough) - | Event::Start(Tag::Image(_, _, _)) => { + | Event::Start(Tag::Image { link_type: _, dest_url: _, title: _, id: _ }) => { return true; } _ => return false, @@ -99,19 +99,25 @@ impl<'a> MarkdownParser<'a> { let text = self.parse_text(false); Some(ParsedMarkdownElement::Paragraph(text)) } - Tag::Heading(level, _, _) => { - let level = level.clone(); + Tag::Heading { + level, + id: _, + classes: _, + attrs: _, + } => { + let level = *level; self.cursor += 1; let heading = self.parse_heading(level); Some(ParsedMarkdownElement::Heading(heading)) } - Tag::Table(_) => { + Tag::Table(alignment) => { + let alignment = alignment.clone(); self.cursor += 1; - let table = self.parse_table(); + let table = self.parse_table(alignment); Some(ParsedMarkdownElement::Table(table)) } Tag::List(order) => { - let order = order.clone(); + let order = *order; self.cursor += 1; let list = self.parse_list(1, order); Some(ParsedMarkdownElement::List(list)) @@ -162,6 +168,7 @@ impl<'a> MarkdownParser<'a> { let mut text = String::new(); let mut bold_depth = 0; let mut italic_depth = 0; + let mut strikethrough_depth = 0; let mut link: Option = None; let mut region_ranges: Vec> = vec![]; let mut regions: Vec = vec![]; @@ -201,6 +208,10 @@ impl<'a> MarkdownParser<'a> { style.italic = true; } + if strikethrough_depth > 0 { + style.strikethrough = true; + } + if let Some(link) = link.clone() { region_ranges.push(prev_len..text.len()); regions.push(ParsedRegion { @@ -248,39 +259,40 @@ impl<'a> MarkdownParser<'a> { }); } - Event::Start(tag) => { - match tag { - Tag::Emphasis => italic_depth += 1, - Tag::Strong => bold_depth += 1, - Tag::Link(_type, url, _title) => { - link = Link::identify( - self.file_location_directory.clone(), - url.to_string(), - ); - } - Tag::Strikethrough => { - // TODO: Confirm that gpui currently doesn't support strikethroughs - } - _ => { - break; - } + Event::Start(tag) => match tag { + Tag::Emphasis => italic_depth += 1, + Tag::Strong => bold_depth += 1, + Tag::Strikethrough => strikethrough_depth += 1, + Tag::Link { + link_type: _, + dest_url, + title: _, + id: _, + } => { + link = Link::identify( + self.file_location_directory.clone(), + dest_url.to_string(), + ); } - } + _ => { + break; + } + }, Event::End(tag) => match tag { - Tag::Emphasis => { + TagEnd::Emphasis => { italic_depth -= 1; } - Tag::Strong => { + TagEnd::Strong => { bold_depth -= 1; } - Tag::Link(_, _, _) => { + TagEnd::Strikethrough => { + strikethrough_depth -= 1; + } + TagEnd::Link => { link = None; } - Tag::Strikethrough => { - // TODO: Confirm that gpui currently doesn't support strikethroughs - } - Tag::Paragraph => { + TagEnd::Paragraph => { self.cursor += 1; break; } @@ -328,14 +340,17 @@ impl<'a> MarkdownParser<'a> { } } - fn parse_table(&mut self) -> ParsedMarkdownTable { + fn parse_table(&mut self, alignment: Vec) -> ParsedMarkdownTable { let (_event, source_range) = self.previous().unwrap(); let source_range = source_range.clone(); let mut header = ParsedMarkdownTableRow::new(); let mut body = vec![]; let mut current_row = vec![]; let mut in_header = true; - let mut alignment: Vec = vec![]; + let column_alignments = alignment + .iter() + .map(|a| Self::convert_alignment(a)) + .collect(); loop { if self.eof() { @@ -346,7 +361,7 @@ impl<'a> MarkdownParser<'a> { match current { Event::Start(Tag::TableHead) | Event::Start(Tag::TableRow) - | Event::End(Tag::TableCell) => { + | Event::End(TagEnd::TableCell) => { self.cursor += 1; } Event::Start(Tag::TableCell) => { @@ -354,7 +369,7 @@ impl<'a> MarkdownParser<'a> { let cell_contents = self.parse_text(false); current_row.push(cell_contents); } - Event::End(Tag::TableHead) | Event::End(Tag::TableRow) => { + Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => { self.cursor += 1; let new_row = std::mem::replace(&mut current_row, vec![]); if in_header { @@ -365,11 +380,7 @@ impl<'a> MarkdownParser<'a> { body.push(row); } } - Event::End(Tag::Table(table_alignment)) => { - alignment = table_alignment - .iter() - .map(|a| Self::convert_alignment(a)) - .collect(); + Event::End(TagEnd::Table) => { self.cursor += 1; break; } @@ -383,7 +394,7 @@ impl<'a> MarkdownParser<'a> { source_range, header, body, - column_alignments: alignment, + column_alignments, } } @@ -410,14 +421,14 @@ impl<'a> MarkdownParser<'a> { let (current, _source_range) = self.current().unwrap(); match current { Event::Start(Tag::List(order)) => { - let order = order.clone(); + let order = *order; self.cursor += 1; let inner_list = self.parse_list(depth + 1, order); let block = ParsedMarkdownElement::List(inner_list); current_list_items.push(Box::new(block)); } - Event::End(Tag::List(_)) => { + Event::End(TagEnd::List(_)) => { self.cursor += 1; break; } @@ -451,12 +462,12 @@ impl<'a> MarkdownParser<'a> { } } } - Event::End(Tag::Item) => { + Event::End(TagEnd::Item) => { self.cursor += 1; let item_type = if let Some(checked) = task_item { ParsedMarkdownListItemType::Task(checked) - } else if let Some(order) = order.clone() { + } else if let Some(order) = order { ParsedMarkdownListItemType::Ordered(order) } else { ParsedMarkdownListItemType::Unordered @@ -525,7 +536,7 @@ impl<'a> MarkdownParser<'a> { Event::Start(Tag::BlockQuote) => { nested_depth += 1; } - Event::End(Tag::BlockQuote) => { + Event::End(TagEnd::BlockQuote) => { nested_depth -= 1; if nested_depth == 0 { self.cursor += 1; @@ -554,7 +565,7 @@ impl<'a> MarkdownParser<'a> { code.push_str(&text); self.cursor += 1; } - Event::End(Tag::CodeBlock(_)) => { + Event::End(TagEnd::CodeBlock) => { self.cursor += 1; break; } @@ -642,6 +653,56 @@ mod tests { ); } + #[test] + fn test_nested_bold_strikethrough_text() { + let parsed = parse("Some **bo~~strikethrough~~ld** text"); + + assert_eq!(parsed.children.len(), 1); + assert_eq!( + parsed.children[0], + ParsedMarkdownElement::Paragraph(ParsedMarkdownText { + source_range: 0..35, + contents: "Some bostrikethroughld text".to_string(), + highlights: Vec::new(), + region_ranges: Vec::new(), + regions: Vec::new(), + }) + ); + + let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { + text + } else { + panic!("Expected a paragraph"); + }; + assert_eq!( + paragraph.highlights, + vec![ + ( + 5..7, + MarkdownHighlight::Style(MarkdownHighlightStyle { + weight: FontWeight::BOLD, + ..Default::default() + }), + ), + ( + 7..20, + MarkdownHighlight::Style(MarkdownHighlightStyle { + weight: FontWeight::BOLD, + strikethrough: true, + ..Default::default() + }), + ), + ( + 20..22, + MarkdownHighlight::Style(MarkdownHighlightStyle { + weight: FontWeight::BOLD, + ..Default::default() + }), + ), + ] + ); + } + #[test] fn test_header_only_table() { let markdown = "\ diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index fad1a3f0e0..50eb958987 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -6,7 +6,7 @@ use gpui::{ IntoElement, ListState, ParentElement, Render, Styled, View, ViewContext, WeakView, }; use ui::prelude::*; -use workspace::item::Item; +use workspace::item::{Item, ItemHandle}; use workspace::Workspace; use crate::{ @@ -22,6 +22,7 @@ pub struct MarkdownPreviewView { contents: ParsedMarkdown, selected_block: usize, list_state: ListState, + tab_description: String, } impl MarkdownPreviewView { @@ -34,8 +35,9 @@ impl MarkdownPreviewView { if let Some(editor) = workspace.active_item_as::(cx) { let workspace_handle = workspace.weak_handle(); + let tab_description = editor.tab_description(0, cx); let view: View = - MarkdownPreviewView::new(editor, workspace_handle, cx); + MarkdownPreviewView::new(editor, workspace_handle, tab_description, cx); workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx); cx.notify(); } @@ -45,6 +47,7 @@ impl MarkdownPreviewView { pub fn new( active_editor: View, workspace: WeakView, + tab_description: Option, cx: &mut ViewContext, ) -> View { cx.new_view(|cx: &mut ViewContext| { @@ -119,12 +122,17 @@ impl MarkdownPreviewView { }, ); + let tab_description = tab_description + .map(|tab_description| format!("Preview {}", tab_description)) + .unwrap_or("Markdown preview".to_string()); + Self { selected_block: 0, focus_handle: cx.focus_handle(), workspace, contents, list_state, + tab_description: tab_description, } }) } @@ -188,11 +196,13 @@ impl Item for MarkdownPreviewView { } else { Color::Muted })) - .child(Label::new("Markdown preview").color(if selected { - Color::Default - } else { - Color::Muted - })) + .child( + Label::new(self.tab_description.to_string()).color(if selected { + Color::Default + } else { + Color::Muted + }), + ) .into_any() } diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 18e6cba18d..a4f5eeb711 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -8,7 +8,10 @@ use gpui::{ HighlightStyle, Hsla, InteractiveText, IntoElement, ParentElement, SharedString, Styled, StyledText, TextStyle, WeakView, WindowContext, }; -use std::{ops::Range, sync::Arc}; +use std::{ + ops::{Mul, Range}, + sync::Arc, +}; use theme::{ActiveTheme, SyntaxTheme}; use ui::{h_flex, v_flex, Label}; use workspace::Workspace; @@ -115,7 +118,7 @@ fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContex _ => cx.text_color, }; - let line_height = DefiniteLength::from(rems(1.25)); + let line_height = DefiniteLength::from(size.mul(1.25)); div() .line_height(line_height) @@ -124,6 +127,7 @@ fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContex .pt(rems(0.15)) .pb_1() .child(render_markdown_text(&parsed.contents, cx)) + .whitespace_normal() .into_any() } @@ -150,7 +154,7 @@ fn render_markdown_list(parsed: &ParsedMarkdownList, cx: &mut RenderContext) -> let item = h_flex() .pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding))) .items_start() - .children(vec![bullet, div().children(contents).pr_2().w_full()]); + .children(vec![bullet, div().children(contents).pr_4().w_full()]); items.push(item); } diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index b15190765d..784139f129 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -11,11 +11,9 @@ doctest = false [dependencies] anyhow.workspace = true -block = "0.1" -bytes = "1.2" [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = "0.9.3" +core-foundation.workspace = true foreign-types = "0.5" metal = "0.25" objc = "0.2" diff --git a/crates/menu/src/menu.rs b/crates/menu/src/menu.rs index a0e8abfabd..69bd0860db 100644 --- a/crates/menu/src/menu.rs +++ b/crates/menu/src/menu.rs @@ -19,6 +19,7 @@ actions!( SelectNext, SelectFirst, SelectLast, - ShowContextMenu + ShowContextMenu, + UseSelectedQuery, ] ); diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml index ed3993400a..322ff38a5a 100644 --- a/crates/multi_buffer/Cargo.toml +++ b/crates/multi_buffer/Cargo.toml @@ -11,66 +11,33 @@ doctest = false [features] test-support = [ - "copilot/test-support", "text/test-support", "language/test-support", "gpui/test-support", "util/test-support", - "tree-sitter-rust", - "tree-sitter-typescript" ] [dependencies] -aho-corasick = "1.1" anyhow.workspace = true -client.workspace = true clock.workspace = true collections.workspace = true -convert_case = "0.6.0" futures.workspace = true git.workspace = true gpui.workspace = true -indoc = "1.0.4" -itertools = "0.10" language.workspace = true -lazy_static.workspace = true log.workspace = true -lsp.workspace = true -ordered-float.workspace = true parking_lot.workspace = true -postage.workspace = true -pulldown-cmark.workspace = true rand.workspace = true -rich_text.workspace = true -schemars.workspace = true -serde.workspace = true -serde_derive.workspace = true settings.workspace = true -smallvec.workspace = true -smol.workspace = true -snippet.workspace = true sum_tree.workspace = true text.workspace = true theme.workspace = true -tree-sitter-html = { workspace = true, optional = true } -tree-sitter-rust = { workspace = true, optional = true } -tree-sitter-typescript = { workspace = true, optional = true } util.workspace = true [dev-dependencies] -copilot = { workspace = true, features = ["test-support"] } -ctor.workspace = true -env_logger.workspace = true gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } -lsp = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } rand.workspace = true settings = { workspace = true, features = ["test-support"] } text = { workspace = true, features = ["test-support"] } -tree-sitter-html.workspace = true -tree-sitter-rust.workspace = true -tree-sitter-typescript.workspace = true -tree-sitter.workspace = true -unindent.workspace = true util = { workspace = true, features = ["test-support"] } diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index 877f74c21b..ee0862b957 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -55,12 +55,12 @@ impl Anchor { if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { return Self { buffer_id: self.buffer_id, - excerpt_id: self.excerpt_id.clone(), + excerpt_id: self.excerpt_id, text_anchor: self.text_anchor.bias_left(&excerpt.buffer), }; } } - self.clone() + *self } pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor { @@ -68,12 +68,12 @@ impl Anchor { if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { return Self { buffer_id: self.buffer_id, - excerpt_id: self.excerpt_id.clone(), + excerpt_id: self.excerpt_id, text_anchor: self.text_anchor.bias_right(&excerpt.buffer), }; } } - self.clone() + *self } pub fn summary(&self, snapshot: &MultiBufferSnapshot) -> D diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f6aaded23e..60b8af4a53 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -191,6 +191,16 @@ struct Excerpt { has_trailing_newline: bool, } +/// A public view into an [`Excerpt`] in a [`MultiBuffer`]. +/// +/// Contains methods for getting the [`Buffer`] of the excerpt, +/// as well as mapping offsets to/from buffer and multibuffer coordinates. +#[derive(Clone)] +pub struct MultiBufferExcerpt<'a> { + excerpt: &'a Excerpt, + excerpt_offset: usize, +} + #[derive(Clone, Debug)] struct ExcerptIdMapping { id: ExcerptId, @@ -940,12 +950,12 @@ impl MultiBuffer { for range in ranges.by_ref().take(range_count) { let start = Anchor { buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), + excerpt_id: excerpt_id, text_anchor: range.start, }; let end = Anchor { buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), + excerpt_id: excerpt_id, text_anchor: range.end, }; if tx.send(start..end).await.is_err() { @@ -995,12 +1005,12 @@ impl MultiBuffer { anchor_ranges.extend(ranges.by_ref().take(range_count).map(|range| { let start = Anchor { buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), + excerpt_id: excerpt_id, text_anchor: buffer_snapshot.anchor_after(range.start), }; let end = Anchor { buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), + excerpt_id: excerpt_id, text_anchor: buffer_snapshot.anchor_after(range.end), }; start..end @@ -1196,7 +1206,7 @@ impl MultiBuffer { cursor.seek_forward(&Some(locator), Bias::Left, &()); if let Some(excerpt) = cursor.item() { if excerpt.locator == *locator { - excerpts.push((excerpt.id.clone(), excerpt.range.clone())); + excerpts.push((excerpt.id, excerpt.range.clone())); } } } @@ -1228,7 +1238,7 @@ impl MultiBuffer { .or_else(|| snapshot.excerpts.last()) .map(|excerpt| { ( - excerpt.id.clone(), + excerpt.id, self.buffers .borrow() .get(&excerpt.buffer_id) @@ -1515,11 +1525,7 @@ impl MultiBuffer { .unwrap_or(false) } - pub fn language_at<'a, T: ToOffset>( - &self, - point: T, - cx: &'a AppContext, - ) -> Option> { + pub fn language_at(&self, point: T, cx: &AppContext) -> Option> { self.point_to_buffer_offset(point, cx) .and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset)) } @@ -1849,7 +1855,7 @@ impl MultiBuffer { .cloned() .collect::>(); let snapshot = self.snapshot.borrow(); - excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &*snapshot)); + excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot)); drop(snapshot); log::info!("Removing excerpts {:?}", excerpts_to_remove); self.remove_excerpts(excerpts_to_remove, cx); @@ -1910,7 +1916,7 @@ impl MultiBuffer { for (ix, entry) in excerpt_ids.iter().enumerate() { if ix == 0 { - if entry.id.cmp(&ExcerptId::min(), &*snapshot).is_le() { + if entry.id.cmp(&ExcerptId::min(), &snapshot).is_le() { panic!("invalid first excerpt id {:?}", entry.id); } } else { @@ -2679,7 +2685,7 @@ impl MultiBufferSnapshot { if !kept_position { for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) { if excerpt.contains(&anchor) { - anchor.excerpt_id = excerpt.id.clone(); + anchor.excerpt_id = excerpt.id; kept_position = true; break; } @@ -2703,7 +2709,7 @@ impl MultiBufferSnapshot { } Anchor { buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), + excerpt_id: excerpt.id, text_anchor, } } else if let Some(excerpt) = prev_excerpt { @@ -2720,7 +2726,7 @@ impl MultiBufferSnapshot { } Anchor { buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), + excerpt_id: excerpt.id, text_anchor, } } else if anchor.text_anchor.bias == Bias::Left { @@ -2750,7 +2756,7 @@ impl MultiBufferSnapshot { if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { return Anchor { buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), + excerpt_id: *excerpt_id, text_anchor: buffer.anchor_at(offset, bias), }; } @@ -2772,7 +2778,7 @@ impl MultiBufferSnapshot { excerpt.clip_anchor(excerpt.buffer.anchor_at(buffer_start + overshoot, bias)); Anchor { buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), + excerpt_id: excerpt.id, text_anchor, } } else if offset == 0 && bias == Bias::Left { @@ -2818,10 +2824,10 @@ impl MultiBufferSnapshot { .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone())) } - fn excerpts_for_range<'a, T: ToOffset>( - &'a self, + fn excerpts_for_range( + &self, range: Range, - ) -> impl Iterator + 'a { + ) -> impl Iterator + '_ { let range = range.start.to_offset(self)..range.end.to_offset(self); let mut cursor = self.excerpts.cursor::(); @@ -2885,7 +2891,7 @@ impl MultiBufferSnapshot { let excerpt = cursor.item()?; let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id; let boundary = ExcerptBoundary { - id: excerpt.id.clone(), + id: excerpt.id, row: cursor.start().1.row, buffer: excerpt.buffer.clone(), range: excerpt.range.clone(), @@ -2912,88 +2918,89 @@ impl MultiBufferSnapshot { /// Returns the smallest enclosing bracket ranges containing the given range or /// None if no brackets contain range or the range is not contained in a single /// excerpt + /// + /// Can optionally pass a range_filter to filter the ranges of brackets to consider pub fn innermost_enclosing_bracket_ranges( &self, range: Range, + range_filter: Option<&dyn Fn(Range, Range) -> bool>, ) -> Option<(Range, Range)> { let range = range.start.to_offset(self)..range.end.to_offset(self); + let excerpt = self.excerpt_containing(range.clone())?; - // Get the ranges of the innermost pair of brackets. - let mut result: Option<(Range, Range)> = None; - - let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { - return None; + // Filter to ranges contained in the excerpt + let range_filter = |open: Range, close: Range| -> bool { + excerpt.contains_buffer_range(open.start..close.end) + && range_filter.map_or(true, |filter| { + filter( + excerpt.map_range_from_buffer(open), + excerpt.map_range_from_buffer(close), + ) + }) }; - for (open, close) in enclosing_bracket_ranges { - let len = close.end - open.start; + let (open, close) = excerpt.buffer().innermost_enclosing_bracket_ranges( + excerpt.map_range_to_buffer(range), + Some(&range_filter), + )?; - if let Some((existing_open, existing_close)) = &result { - let existing_len = existing_close.end - existing_open.start; - if len > existing_len { - continue; - } - } - - result = Some((open, close)); - } - - result + Some(( + excerpt.map_range_from_buffer(open), + excerpt.map_range_from_buffer(close), + )) } /// Returns enclosing bracket ranges containing the given range or returns None if the range is /// not contained in a single excerpt - pub fn enclosing_bracket_ranges<'a, T: ToOffset>( - &'a self, + pub fn enclosing_bracket_ranges( + &self, range: Range, - ) -> Option, Range)> + 'a> { + ) -> Option, Range)> + '_> { let range = range.start.to_offset(self)..range.end.to_offset(self); + let excerpt = self.excerpt_containing(range.clone())?; - self.bracket_ranges(range.clone()).map(|range_pairs| { - range_pairs - .filter(move |(open, close)| open.start <= range.start && close.end >= range.end) - }) + Some( + excerpt + .buffer() + .enclosing_bracket_ranges(excerpt.map_range_to_buffer(range)) + .filter_map(move |(open, close)| { + if excerpt.contains_buffer_range(open.start..close.end) { + Some(( + excerpt.map_range_from_buffer(open), + excerpt.map_range_from_buffer(close), + )) + } else { + None + } + }), + ) } /// Returns bracket range pairs overlapping the given `range` or returns None if the `range` is /// not contained in a single excerpt - pub fn bracket_ranges<'a, T: ToOffset>( - &'a self, + pub fn bracket_ranges( + &self, range: Range, - ) -> Option, Range)> + 'a> { + ) -> Option, Range)> + '_> { let range = range.start.to_offset(self)..range.end.to_offset(self); - let excerpt = self.excerpt_containing(range.clone()); - excerpt.map(|(excerpt, excerpt_offset)| { - let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len; - - let start_in_buffer = excerpt_buffer_start + range.start.saturating_sub(excerpt_offset); - let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset); + let excerpt = self.excerpt_containing(range.clone())?; + Some( excerpt - .buffer - .bracket_ranges(start_in_buffer..end_in_buffer) - .filter_map(move |(start_bracket_range, end_bracket_range)| { - if start_bracket_range.start < excerpt_buffer_start - || end_bracket_range.end > excerpt_buffer_end - { - return None; + .buffer() + .bracket_ranges(excerpt.map_range_to_buffer(range)) + .filter_map(move |(start_bracket_range, close_bracket_range)| { + let buffer_range = start_bracket_range.start..close_bracket_range.end; + if excerpt.contains_buffer_range(buffer_range) { + Some(( + excerpt.map_range_from_buffer(start_bracket_range), + excerpt.map_range_from_buffer(close_bracket_range), + )) + } else { + None } - - let mut start_bracket_range = start_bracket_range.clone(); - start_bracket_range.start = - excerpt_offset + (start_bracket_range.start - excerpt_buffer_start); - start_bracket_range.end = - excerpt_offset + (start_bracket_range.end - excerpt_buffer_start); - - let mut end_bracket_range = end_bracket_range.clone(); - end_bracket_range.start = - excerpt_offset + (end_bracket_range.start - excerpt_buffer_start); - end_bracket_range.end = - excerpt_offset + (end_bracket_range.end - excerpt_buffer_start); - Some((start_bracket_range, end_bracket_range)) - }) - }) + }), + ) } pub fn redacted_ranges<'a, T: ToOffset>( @@ -3039,12 +3046,12 @@ impl MultiBufferSnapshot { self.trailing_excerpt_update_count } - pub fn file_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { + pub fn file_at(&self, point: T) -> Option<&Arc> { self.point_to_buffer_offset(point) .and_then(|(buffer, _)| buffer.file()) } - pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { + pub fn language_at(&self, point: T) -> Option<&Arc> { self.point_to_buffer_offset(point) .and_then(|(buffer, offset)| buffer.language_at(offset)) } @@ -3063,7 +3070,7 @@ impl MultiBufferSnapshot { language_settings(language, file, cx) } - pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option { + pub fn language_scope_at(&self, point: T) -> Option { self.point_to_buffer_offset(point) .and_then(|(buffer, offset)| buffer.language_scope_at(offset)) } @@ -3131,10 +3138,10 @@ impl MultiBufferSnapshot { false } - pub fn git_diff_hunks_in_range_rev<'a>( - &'a self, + pub fn git_diff_hunks_in_range_rev( + &self, row_range: Range, - ) -> impl 'a + Iterator> { + ) -> impl Iterator> + '_ { let mut cursor = self.excerpts.cursor::(); cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &()); @@ -3170,7 +3177,7 @@ impl MultiBufferSnapshot { let buffer_hunks = excerpt .buffer .git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end) - .filter_map(move |hunk| { + .map(move |hunk| { let start = multibuffer_start.row + hunk .buffer_range @@ -3183,10 +3190,10 @@ impl MultiBufferSnapshot { .min(excerpt_end_point.row + 1) .saturating_sub(excerpt_start_point.row); - Some(DiffHunk { + DiffHunk { buffer_range: start..end, diff_base_byte_range: hunk.diff_base_byte_range.clone(), - }) + } }); cursor.prev(&()); @@ -3196,10 +3203,10 @@ impl MultiBufferSnapshot { .flatten() } - pub fn git_diff_hunks_in_range<'a>( - &'a self, + pub fn git_diff_hunks_in_range( + &self, row_range: Range, - ) -> impl 'a + Iterator> { + ) -> impl Iterator> + '_ { let mut cursor = self.excerpts.cursor::(); cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &()); @@ -3232,7 +3239,7 @@ impl MultiBufferSnapshot { let buffer_hunks = excerpt .buffer .git_diff_hunks_intersecting_range(buffer_start..buffer_end) - .filter_map(move |hunk| { + .map(move |hunk| { let start = multibuffer_start.row + hunk .buffer_range @@ -3245,10 +3252,10 @@ impl MultiBufferSnapshot { .min(excerpt_end_point.row + 1) .saturating_sub(excerpt_start_point.row); - Some(DiffHunk { + DiffHunk { buffer_range: start..end, diff_base_byte_range: hunk.diff_base_byte_range.clone(), - }) + } }); cursor.next(&()); @@ -3260,26 +3267,13 @@ impl MultiBufferSnapshot { pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { let range = range.start.to_offset(self)..range.end.to_offset(self); + let excerpt = self.excerpt_containing(range.clone())?; - self.excerpt_containing(range.clone()) - .and_then(|(excerpt, excerpt_offset)| { - let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len; + let ancestor_buffer_range = excerpt + .buffer() + .range_for_syntax_ancestor(excerpt.map_range_to_buffer(range))?; - let start_in_buffer = - excerpt_buffer_start + range.start.saturating_sub(excerpt_offset); - let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset); - let mut ancestor_buffer_range = excerpt - .buffer - .range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?; - ancestor_buffer_range.start = - cmp::max(ancestor_buffer_range.start, excerpt_buffer_start); - ancestor_buffer_range.end = cmp::min(ancestor_buffer_range.end, excerpt_buffer_end); - - let start = excerpt_offset + (ancestor_buffer_range.start - excerpt_buffer_start); - let end = excerpt_offset + (ancestor_buffer_range.end - excerpt_buffer_start); - Some(start..end) - }) + Some(excerpt.map_range_from_buffer(ancestor_buffer_range)) } pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { @@ -3291,8 +3285,8 @@ impl MultiBufferSnapshot { .into_iter() .map(|item| OutlineItem { depth: item.depth, - range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start) - ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), + range: self.anchor_in_excerpt(*excerpt_id, item.range.start) + ..self.anchor_in_excerpt(*excerpt_id, item.range.end), text: item.text, highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, @@ -3328,7 +3322,7 @@ impl MultiBufferSnapshot { )) } - fn excerpt_locator_for_id<'a>(&'a self, id: ExcerptId) -> &'a Locator { + fn excerpt_locator_for_id(&self, id: ExcerptId) -> &Locator { if id == ExcerptId::min() { Locator::min_ref() } else if id == ExcerptId::max() { @@ -3353,7 +3347,7 @@ impl MultiBufferSnapshot { Some(&self.excerpt(excerpt_id)?.buffer) } - fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> { + fn excerpt(&self, excerpt_id: ExcerptId) -> Option<&Excerpt> { let mut cursor = self.excerpts.cursor::>(); let locator = self.excerpt_locator_for_id(excerpt_id); cursor.seek(&Some(locator), Bias::Left, &()); @@ -3366,32 +3360,25 @@ impl MultiBufferSnapshot { } /// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts - fn excerpt_containing<'a, T: ToOffset>( - &'a self, - range: Range, - ) -> Option<(&'a Excerpt, usize)> { + pub fn excerpt_containing(&self, range: Range) -> Option { let range = range.start.to_offset(self)..range.end.to_offset(self); let mut cursor = self.excerpts.cursor::(); cursor.seek(&range.start, Bias::Right, &()); - let start_excerpt = cursor.item(); + let start_excerpt = cursor.item()?; if range.start == range.end { - return start_excerpt.map(|excerpt| (excerpt, *cursor.start())); + return Some(MultiBufferExcerpt::new(start_excerpt, *cursor.start())); } cursor.seek(&range.end, Bias::Right, &()); - let end_excerpt = cursor.item(); + let end_excerpt = cursor.item()?; - start_excerpt - .zip(end_excerpt) - .and_then(|(start_excerpt, end_excerpt)| { - if start_excerpt.id != end_excerpt.id { - return None; - } - - Some((start_excerpt, *cursor.start())) - }) + if start_excerpt.id != end_excerpt.id { + None + } else { + Some(MultiBufferExcerpt::new(start_excerpt, *cursor.start())) + } } pub fn remote_selections_in_range<'a>( @@ -3420,19 +3407,19 @@ impl MultiBufferSnapshot { selections.map(move |selection| { let mut start = Anchor { buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), + excerpt_id: excerpt.id, text_anchor: selection.start, }; let mut end = Anchor { buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), + excerpt_id: excerpt.id, text_anchor: selection.end, }; if range.start.cmp(&start, self).is_gt() { - start = range.start.clone(); + start = range.start; } if range.end.cmp(&end, self).is_lt() { - end = range.end.clone(); + end = range.end; } ( @@ -3768,6 +3755,61 @@ impl Excerpt { .cmp(&anchor.text_anchor, &self.buffer) .is_ge() } + + /// The [`Excerpt`]'s start offset in its [`Buffer`] + fn buffer_start_offset(&self) -> usize { + self.range.context.start.to_offset(&self.buffer) + } + + /// The [`Excerpt`]'s end offset in its [`Buffer`] + fn buffer_end_offset(&self) -> usize { + self.buffer_start_offset() + self.text_summary.len + } +} + +impl<'a> MultiBufferExcerpt<'a> { + fn new(excerpt: &'a Excerpt, excerpt_offset: usize) -> Self { + MultiBufferExcerpt { + excerpt, + excerpt_offset, + } + } + + pub fn buffer(&self) -> &'a BufferSnapshot { + &self.excerpt.buffer + } + + /// Maps an offset within the [`MultiBuffer`] to an offset within the [`Buffer`] + pub fn map_offset_to_buffer(&self, offset: usize) -> usize { + self.excerpt.buffer_start_offset() + offset.saturating_sub(self.excerpt_offset) + } + + /// Maps a range within the [`MultiBuffer`] to a range within the [`Buffer`] + pub fn map_range_to_buffer(&self, range: Range) -> Range { + self.map_offset_to_buffer(range.start)..self.map_offset_to_buffer(range.end) + } + + /// Map an offset within the [`Buffer`] to an offset within the [`MultiBuffer`] + pub fn map_offset_from_buffer(&self, buffer_offset: usize) -> usize { + let mut buffer_offset_in_excerpt = + buffer_offset.saturating_sub(self.excerpt.buffer_start_offset()); + buffer_offset_in_excerpt = + cmp::min(buffer_offset_in_excerpt, self.excerpt.text_summary.len); + + self.excerpt_offset + buffer_offset_in_excerpt + } + + /// Map a range within the [`Buffer`] to a range within the [`MultiBuffer`] + pub fn map_range_from_buffer(&self, buffer_range: Range) -> Range { + self.map_offset_from_buffer(buffer_range.start) + ..self.map_offset_from_buffer(buffer_range.end) + } + + /// Returns true if the entirety of the given range is in the buffer's excerpt + pub fn contains_buffer_range(&self, range: Range) -> bool { + range.start >= self.excerpt.buffer_start_offset() + && range.end <= self.excerpt.buffer_end_offset() + } } impl ExcerptId { diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml index 891dcd9e48..1c608b703f 100644 --- a/crates/node_runtime/Cargo.toml +++ b/crates/node_runtime/Cargo.toml @@ -12,13 +12,11 @@ doctest = false [dependencies] anyhow.workspace = true async-compression.workspace = true -async-tar = "0.4.2" +async-tar.workspace = true async-trait.workspace = true futures.workspace = true log.workspace = true -parking_lot.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true smol.workspace = true util.workspace = true diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index e40f83fae0..7317635dd1 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -60,13 +60,20 @@ impl RealNodeRuntime { let _lock = self.installation_lock.lock().await; log::info!("Node runtime install_if_needed"); + let os = match consts::OS { + "macos" => "darwin", + "linux" => "linux", + "windows" => "win", + other => bail!("Running on unsupported os: {other}"), + }; + let arch = match consts::ARCH { "x86_64" => "x64", "aarch64" => "arm64", - other => bail!("Running on unsupported platform: {other}"), + other => bail!("Running on unsupported architecture: {other}"), }; - let folder_name = format!("node-{VERSION}-darwin-{arch}"); + let folder_name = format!("node-{VERSION}-{os}-{arch}"); let node_containing_dir = util::paths::SUPPORT_DIR.join("node"); let node_dir = node_containing_dir.join(folder_name); let node_binary = node_dir.join("bin/node"); @@ -92,7 +99,7 @@ impl RealNodeRuntime { .await .context("error creating node containing dir")?; - let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz"); + let file_name = format!("node-{VERSION}-{os}-{arch}.tar.gz"); let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}"); let mut response = self .http diff --git a/crates/notifications/Cargo.toml b/crates/notifications/Cargo.toml index 269766dcce..40d61087fa 100644 --- a/crates/notifications/Cargo.toml +++ b/crates/notifications/Cargo.toml @@ -21,15 +21,11 @@ test-support = [ anyhow.workspace = true channel.workspace = true client.workspace = true -clock.workspace = true collections.workspace = true db.workspace = true -feature_flags.workspace = true gpui.workspace = true rpc.workspace = true -settings.workspace = true sum_tree.workspace = true -text.workspace = true time.workspace = true util.workspace = true diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index a01a1e59d8..67a1ec487a 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use channel::{ChannelMessage, ChannelMessageId, ChannelStore}; -use client::{Client, UserStore}; +use client::{ChannelId, Client, UserStore}; use collections::HashMap; use db::smol::stream::StreamExt; use gpui::{ @@ -413,7 +413,7 @@ impl NotificationStore { Notification::ChannelInvitation { channel_id, .. } => { self.channel_store .update(cx, |store, cx| { - store.respond_to_channel_invite(channel_id, response, cx) + store.respond_to_channel_invite(ChannelId(channel_id), response, cx) }) .detach(); } diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index a67371847a..28dc5d6a1b 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -16,10 +16,8 @@ gpui.workspace = true language.workspace = true ordered-float.workspace = true picker.workspace = true -postage.workspace = true settings.workspace = true smol.workspace = true -text.workspace = true theme.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index a078ad6a7c..0a8b9a08ec 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -157,7 +157,7 @@ impl OutlineViewDelegate { impl PickerDelegate for OutlineViewDelegate { type ListItem = ListItem; - fn placeholder_text(&self) -> Arc { + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Search buffer symbols...".into() } @@ -279,7 +279,7 @@ impl PickerDelegate for OutlineViewDelegate { font_size: settings.buffer_font_size(cx).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, - line_height: relative(1.).into(), + line_height: relative(1.), background_color: None, underline: None, strikethrough: None, diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml index 63c408fb2f..510ce14a59 100644 --- a/crates/picker/Cargo.toml +++ b/crates/picker/Cargo.toml @@ -13,11 +13,7 @@ doctest = false editor.workspace = true gpui.workspace = true menu.workspace = true -parking_lot.workspace = true -settings.workspace = true -theme.workspace = true ui.workspace = true -util.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/picker/src/highlighted_match_with_paths.rs b/crates/picker/src/highlighted_match_with_paths.rs new file mode 100644 index 0000000000..02994c87a7 --- /dev/null +++ b/crates/picker/src/highlighted_match_with_paths.rs @@ -0,0 +1,70 @@ +use ui::{prelude::*, HighlightedLabel}; + +#[derive(Clone)] +pub struct HighlightedMatchWithPaths { + pub match_label: HighlightedText, + pub paths: Vec, +} + +#[derive(Debug, Clone, IntoElement)] +pub struct HighlightedText { + pub text: String, + pub highlight_positions: Vec, + pub char_count: usize, +} + +impl HighlightedText { + pub fn join(components: impl Iterator, separator: &str) -> Self { + let mut char_count = 0; + let separator_char_count = separator.chars().count(); + let mut text = String::new(); + let mut highlight_positions = Vec::new(); + for component in components { + if char_count != 0 { + text.push_str(separator); + char_count += separator_char_count; + } + + highlight_positions.extend( + component + .highlight_positions + .iter() + .map(|position| position + char_count), + ); + text.push_str(&component.text); + char_count += component.text.chars().count(); + } + + Self { + text, + highlight_positions, + char_count, + } + } +} + +impl RenderOnce for HighlightedText { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + HighlightedLabel::new(self.text, self.highlight_positions) + } +} + +impl HighlightedMatchWithPaths { + pub fn render_paths_children(&mut self, element: Div) -> Div { + element.children(self.paths.clone().into_iter().map(|path| { + HighlightedLabel::new(path.text, path.highlight_positions) + .size(LabelSize::Small) + .color(Color::Muted) + })) + } +} + +impl RenderOnce for HighlightedMatchWithPaths { + fn render(mut self, _: &mut WindowContext) -> impl IntoElement { + v_flex() + .child(self.match_label.clone()) + .when(!self.paths.is_empty(), |this| { + self.render_paths_children(this) + }) + } +} diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 09019a8434..11c2458ee1 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -1,13 +1,15 @@ use editor::Editor; use gpui::{ - div, list, prelude::*, uniform_list, AnyElement, AppContext, DismissEvent, EventEmitter, - FocusHandle, FocusableView, Length, ListState, MouseButton, MouseDownEvent, Render, Task, + div, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent, DismissEvent, + EventEmitter, FocusHandle, FocusableView, Length, ListState, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, }; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing}; use workspace::ModalView; +pub mod highlighted_match_with_paths; + enum ElementContainer { List(ListState), UniformList(UniformListScrollHandle), @@ -30,6 +32,7 @@ pub struct Picker { pub trait PickerDelegate: Sized + 'static { type ListItem: IntoElement; + fn match_count(&self) -> usize; fn selected_index(&self) -> usize; fn separators_after_indices(&self) -> Vec { @@ -37,11 +40,27 @@ pub trait PickerDelegate: Sized + 'static { } fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); - fn placeholder_text(&self) -> Arc; + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc; fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; + // Delegates that support this method (e.g. the CommandPalette) can chose to block on any background + // work for up to `duration` to try and get a result synchronously. + // This avoids a flash of an empty command-palette on cmd-shift-p, and lets workspace::SendKeystrokes + // mostly work when dismissing a palette. + fn finalize_update_matches( + &mut self, + _query: String, + _duration: Duration, + _cx: &mut ViewContext>, + ) -> bool { + false + } + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); fn dismissed(&mut self, cx: &mut ViewContext>); + fn selected_as_query(&self) -> Option { + None + } fn render_match( &self, @@ -85,12 +104,12 @@ impl Picker { } fn new(delegate: D, cx: &mut ViewContext, is_uniform: bool) -> Self { - let editor = create_editor(delegate.placeholder_text(), cx); + let editor = create_editor(delegate.placeholder_text(cx), cx); cx.subscribe(&editor, Self::on_input_editor_event).detach(); let mut this = Self { delegate, editor, - element_container: Self::crate_element_container(is_uniform, cx), + element_container: Self::create_element_container(is_uniform, cx), pending_update_matches: None, confirm_on_update: None, width: None, @@ -98,10 +117,13 @@ impl Picker { is_modal: true, }; this.update_matches("".to_string(), cx); + // give the delegate 4ms to renderthe first set of suggestions. + this.delegate + .finalize_update_matches("".to_string(), Duration::from_millis(4), cx); this } - fn crate_element_container(is_uniform: bool, cx: &mut ViewContext) -> ElementContainer { + fn create_element_container(is_uniform: bool, cx: &mut ViewContext) -> ElementContainer { if is_uniform { ElementContainer::UniformList(UniformListScrollHandle::new()) } else { @@ -197,21 +219,37 @@ impl Picker { } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - if self.pending_update_matches.is_some() { + if self.pending_update_matches.is_some() + && !self + .delegate + .finalize_update_matches(self.query(cx), Duration::from_millis(16), cx) + { self.confirm_on_update = Some(false) } else { + self.pending_update_matches.take(); self.delegate.confirm(false, cx); } } fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { - if self.pending_update_matches.is_some() { + if self.pending_update_matches.is_some() + && !self + .delegate + .finalize_update_matches(self.query(cx), Duration::from_millis(16), cx) + { self.confirm_on_update = Some(true) } else { self.delegate.confirm(true, cx); } } + fn use_selected_query(&mut self, _: &menu::UseSelectedQuery, cx: &mut ViewContext) { + if let Some(new_query) = self.delegate.selected_as_query() { + self.set_query(new_query, cx); + cx.stop_propagation(); + } + } + fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext) { cx.stop_propagation(); cx.prevent_default(); @@ -286,12 +324,11 @@ impl Picker { fn render_element(&self, cx: &mut ViewContext, ix: usize) -> impl IntoElement { div() - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, event: &MouseDownEvent, cx| { - this.handle_click(ix, event.modifiers.command, cx) - }), - ) + .id(("item", ix)) + .cursor_pointer() + .on_click(cx.listener(move |this, event: &ClickEvent, cx| { + this.handle_click(ix, event.down.modifiers.command, cx) + })) .children( self.delegate .render_match(ix, ix == self.delegate.selected_index(), cx), @@ -359,6 +396,7 @@ impl Render for Picker { .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::secondary_confirm)) + .on_action(cx.listener(Self::use_selected_query)) .child(picker_editor) .child(Divider::horizontal()) .when(self.delegate.match_count() > 0, |el| { diff --git a/crates/plugin/src/lib.rs b/crates/plugin/src/lib.rs deleted file mode 100644 index 079b96fba8..0000000000 --- a/crates/plugin/src/lib.rs +++ /dev/null @@ -1,61 +0,0 @@ -pub use bincode; -pub use serde; - -/// This is the buffer that is used Wasm side. -/// Note that it mirrors the functionality of -/// the `WasiBuffer` found in `plugin_runtime/src/plugin.rs`, -/// But has a few different methods. -pub struct __Buffer { - pub ptr: u32, // *const u8, - pub len: u32, // usize, -} - -impl __Buffer { - pub fn into_u64(self) -> u64 { - ((self.ptr as u64) << 32) | (self.len as u64) - } - - pub fn from_u64(packed: u64) -> Self { - __Buffer { - ptr: (packed >> 32) as u32, - len: packed as u32, - } - } -} - -/// Allocates a buffer with an exact size. -/// We don't return the size because it has to be passed in anyway. -#[no_mangle] -pub extern "C" fn __alloc_buffer(len: u32) -> u32 { - let vec = vec![0; len as usize]; - let buffer = unsafe { __Buffer::from_vec(vec) }; - buffer.ptr -} - -/// Frees a given buffer, requires the size. -#[no_mangle] -pub extern "C" fn __free_buffer(buffer: u64) { - let vec = unsafe { __Buffer::from_u64(buffer).to_vec() }; - std::mem::drop(vec); -} - -impl __Buffer { - #[inline(always)] - pub unsafe fn to_vec(&self) -> Vec { - core::slice::from_raw_parts(self.ptr as *const u8, self.len as usize).to_vec() - } - - #[inline(always)] - pub unsafe fn from_vec(mut vec: Vec) -> __Buffer { - vec.shrink_to(0); - let ptr = vec.as_ptr() as u32; - let len = vec.len() as u32; - std::mem::forget(vec); - __Buffer { ptr, len } - } -} - -pub mod prelude { - pub use super::{__Buffer, __alloc_buffer}; - pub use plugin_macros::{export, import}; -} diff --git a/crates/plugin_macros/Cargo.toml b/crates/plugin_macros/Cargo.toml deleted file mode 100644 index 9d940f2423..0000000000 --- a/crates/plugin_macros/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "plugin_macros" -version = "0.1.0" -edition = "2021" -publish = false -license = "GPL-3.0-or-later" - -[lib] -proc-macro = true - -[dependencies] -bincode = "1.3" -proc-macro2 = "1.0" -quote = "1.0" -serde.workspace = true -serde_derive.workspace = true -syn = { version = "1.0", features = ["full", "extra-traits"] } diff --git a/crates/plugin_macros/src/lib.rs b/crates/plugin_macros/src/lib.rs deleted file mode 100644 index 2fe8b31b61..0000000000 --- a/crates/plugin_macros/src/lib.rs +++ /dev/null @@ -1,168 +0,0 @@ -use core::panic; - -use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{parse_macro_input, Block, FnArg, ForeignItemFn, Ident, ItemFn, Pat, Type, Visibility}; - -/// Attribute macro to be used guest-side within a plugin. -/// ```ignore -/// #[export] -/// pub fn say_hello() -> String { -/// "Hello from Wasm".into() -/// } -/// ``` -/// This macro makes a function defined guest-side available host-side. -/// Note that all arguments and return types must be `serde`. -#[proc_macro_attribute] -pub fn export(args: TokenStream, function: TokenStream) -> TokenStream { - if !args.is_empty() { - panic!("The export attribute does not take any arguments"); - } - - let inner_fn = parse_macro_input!(function as ItemFn); - - if !inner_fn.sig.generics.params.is_empty() { - panic!("Exported functions can not take generic parameters"); - } - - if let Visibility::Public(_) = inner_fn.vis { - } else { - panic!("The export attribute only works for public functions"); - } - - let inner_fn_name = format_ident!("{}", inner_fn.sig.ident); - let outer_fn_name = format_ident!("__{}", inner_fn_name); - - let variadic = inner_fn.sig.inputs.len(); - let i = (0..variadic).map(syn::Index::from); - let t: Vec = inner_fn - .sig - .inputs - .iter() - .map(|x| match x { - FnArg::Receiver(_) => { - panic!("All arguments must have specified types, no `self` allowed") - } - FnArg::Typed(item) => *item.ty.clone(), - }) - .collect(); - - // this is cursed... - let (args, ty) = if variadic != 1 { - ( - quote! { - #( data.#i ),* - }, - quote! { - ( #( #t ),* ) - }, - ) - } else { - let ty = &t[0]; - (quote! { data }, quote! { #ty }) - }; - - TokenStream::from(quote! { - #[no_mangle] - #inner_fn - - #[no_mangle] - pub extern "C" fn #outer_fn_name(packed_buffer: u64) -> u64 { - // setup - let data = unsafe { ::plugin::__Buffer::from_u64(packed_buffer).to_vec() }; - - // operation - let data: #ty = match ::plugin::bincode::deserialize(&data) { - Ok(d) => d, - Err(e) => panic!("Data passed to function not deserializable."), - }; - let result = #inner_fn_name(#args); - let new_data: Result, _> = ::plugin::bincode::serialize(&result); - let new_data = new_data.unwrap(); - - // teardown - let new_buffer = unsafe { ::plugin::__Buffer::from_vec(new_data) }.into_u64(); - return new_buffer; - } - }) -} - -/// Attribute macro to be used guest-side within a plugin. -/// ```ignore -/// #[import] -/// pub fn operating_system_name() -> String; -/// ``` -/// This macro makes a function defined host-side available guest-side. -/// Note that all arguments and return types must be `serde`. -/// All that's provided is a signature, as the function is implemented host-side. -#[proc_macro_attribute] -pub fn import(args: TokenStream, function: TokenStream) -> TokenStream { - if !args.is_empty() { - panic!("The import attribute does not take any arguments"); - } - - let fn_declare = parse_macro_input!(function as ForeignItemFn); - - if !fn_declare.sig.generics.params.is_empty() { - panic!("Exported functions can not take generic parameters"); - } - - // let inner_fn_name = format_ident!("{}", fn_declare.sig.ident); - let extern_fn_name = format_ident!("__{}", fn_declare.sig.ident); - - let (args, tys): (Vec, Vec) = fn_declare - .sig - .inputs - .clone() - .into_iter() - .map(|x| match x { - FnArg::Receiver(_) => { - panic!("All arguments must have specified types, no `self` allowed") - } - FnArg::Typed(t) => { - if let Pat::Ident(i) = *t.pat { - (i.ident, *t.ty) - } else { - panic!("All function arguments must be identifiers"); - } - } - }) - .unzip(); - - let body = TokenStream::from(quote! { - { - // setup - let data: (#( #tys ),*) = (#( #args ),*); - let data = ::plugin::bincode::serialize(&data).unwrap(); - let buffer = unsafe { ::plugin::__Buffer::from_vec(data) }; - - // operation - let new_buffer = unsafe { #extern_fn_name(buffer.into_u64()) }; - let new_data = unsafe { ::plugin::__Buffer::from_u64(new_buffer).to_vec() }; - - // teardown - match ::plugin::bincode::deserialize(&new_data) { - Ok(d) => d, - Err(e) => panic!("Data returned from function not deserializable."), - } - } - }); - - let block = parse_macro_input!(body as Block); - - let inner_fn = ItemFn { - attrs: fn_declare.attrs, - vis: fn_declare.vis, - sig: fn_declare.sig, - block: Box::new(block), - }; - - TokenStream::from(quote! { - extern "C" { - fn #extern_fn_name(buffer: u64) -> u64; - } - - #[no_mangle] - #inner_fn - }) -} diff --git a/crates/plugin_runtime/Cargo.toml b/crates/plugin_runtime/Cargo.toml deleted file mode 100644 index 9df33c05b5..0000000000 --- a/crates/plugin_runtime/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "plugin_runtime" -version = "0.1.0" -edition = "2021" -publish = false -license = "GPL-3.0-or-later" - -[dependencies] -anyhow.workspace = true -bincode = "1.3" -pollster = "0.2.5" -serde.workspace = true -serde_derive.workspace = true -serde_json.workspace = true -smol.workspace = true -wasi-common = "2.0" -wasmtime = "2.0" -wasmtime-wasi = "2.0" - -[build-dependencies] -wasmtime = { version = "2.0", features = ["all-arch"] } diff --git a/crates/plugin_runtime/OPAQUE.md b/crates/plugin_runtime/OPAQUE.md deleted file mode 100644 index 4d38409ec2..0000000000 --- a/crates/plugin_runtime/OPAQUE.md +++ /dev/null @@ -1,188 +0,0 @@ -# Opaque handles to resources - -Currently, Zed's plugin system only supports moving *data* (e.g. things you can serialize) across the boundary between guest-side plugin and host-side runtime. Resources, things you can't just copy, have been set aside for now. Given how important this is to Zed, I think it's about time we address this. - -Managing resources is very important to Zed, because a lot of what Zed does is exactly thatβ€”managing resources. Each open buffer you're editing is a resource, as is the language server you're querying, or the collaboration session you're currently in. Therefore, writing a plugin system with deep integration with Zed requires some mechanism to manage resources. - -The reason resources are problematic is because, unlike data, we can't pass resources across the ABI boundary. Wasm can't take references to host memory (and even if it could, that doesn't mean that it's a good idea). To add support for resources to plugins, we'd need three things: - -1. Some sort of way for the host-side runtime to hang onto **references** to a resource. If the plugin requests to modify a resource, but we don't even know where that resource is, that's kinda bad, isn't it? - -2. Some sort of way for the guest-side runtime to hang onto **handles** to a resource. We can't reference the resource directly from a plugin, but if a resource *has* been registered with the runtime, we can at least take a runtime-provided handle to that resource so that we may request that the runtime modify it in the future. - -3. Some sort of way to **modify the resources** we're holding onto. This requires two things: some way for a plugin to request a modification, and some for the runtime to apply that modification. Here I'm using 'modification' in the most general sense, which includes, e.g. reading or writing to the resource, i.e. calling a method on it. - -Luckily for us, managing resources across boundaries is a problem that languages have had to deal with for eons. File descriptors referencing resources managed by the kernel quintessentially defines of resource management, but this pattern is oft repeated in games, scripting languages, or surprise surprise, when writing plugins. - -To see what managing resources in plugins could look like in Rust, we need look no further than Rhai. Rhai is a scripting language powered by a tree-walk interpreter written in Rust. It's pretty neat, but what we care about is not the language itself, but how it interfaces with Rust types. - -In its [guide](https://rhai.rs/book/rust/custom-types.html), Rhai claims the following: - -> Rhai works seamlessly with any Rust type, as long as it implements `Clone` as this allows the `Engine` to pass by value. - -This doesn't mean that the underlying resources themselves need to be copied: - -> \[Because Rhai works with types implementing `Clone`\] it is extremely easy to use Rhai with data types such as `Rc<...>`, `Arc<...>`, `Rc>`, `Arc>` etc. - -Given that we have to register a resource with our plugin runtime before we use it, requiring the resource to be behind a shared reference makes sense, so I think the `Clone` bound is reasonable. So how does `Rhai` represent types under the hood? - -> A custom type is stored in Rhai as a Rust trait object (specifically, a `dyn rhai::Variant`), with no restrictions other than being `Clone` (plus `Send + Sync` under the `sync` feature). - -I'd be interested to know how Rhai disambiguates between different types if everything's a trait object under the hood. - -Rhai actually exposes a pretty nice interface for working with native Rust types. We can register a type using `Engine::register_type::()`. Internally, this just grabs the string name of the type for future reference. - -> **Note**: Rhai uses strings, but I wonder if you could get away with something more compact using `TypeIds`. Maybe not, given that `TypeId`s are not deterministic across builds, and we'd need matching IDs both host-side and guest side. - -In Rhai, we can alternatively use the method `Engine::register_type_with_name::(name: &str)` if we have a different type name host-side (in Rust) and guest-side (in Rhai). - -With respect to Wasm plugins, I think an interface like this is fairly important, because we don't know whether the original plugin was written in Rust. (This may not be true now, because we write all the plugins Zed uses, but once we allow packaging and shipping plugins, it's important to maintain a consistent interface, because even Rust changes over time.) - -Once we've registered a type, we can begin using this type in functions. We can add new function using the standard `Engine::register_fn` function, which has the following signature: - -```rust -pub fn register_fn(&mut self, name: N, func: F) -> &mut Self -where - N: AsRef + Into, - F: RegisterNativeFunction, -``` - -This is quite complex, but under the hood it's fairly similar to our own `PluginBuilder::host_function` async method. Looking at `RegisterNativeFunction`, it seems as though this trait essentially provides methods that expose the `TypeID`s and type/param names of the arguments and return types of the function. - -So once we register a function, what happens when we call it? Well, let me introduce you to my friend `Engine::call_native_fn`, whose type signature is too complex to list here. - -> **Note**: Finding this function took like 7 levels of indirection from `eval`. It's surprising how much shuffling of data Rhai does under the hood, I bet you could probably make it a lot faster. - -This takes and returns, like everything else in Rhai, an object of type `Dynamic`. We know that we can use native Rust types, so how does Rhai perform the conversion to and from `Dynamic`? - -The secret lies in `Dynamic::try_cast::(self) -> Option`. Like most dynamic scripting languages, Rhai uses a tagged `Union` to represent types. Remember `dyn Variant` from earlier? Rhai's `Union` has a variant, `Variant`, to hold the dynamic native types: - -```rust -/// Any type as a trait object. -#[allow(clippy::redundant_allocation)] -Variant(Box>, Tag, AccessMode), -``` - -Redundant allocations aside, To `try_cast` a `Dynamic` type to `T: Any`thing, we pattern match on `Union`. In the case of variant, we: - -```rust -Union::Variant(v, ..) => (*v).as_boxed_any().downcast().ok().map(|x| *x), -``` - -Now Rhai can do this because it's implemented in Rust. In other words, unlike Wasm, Rhai scripts can, indirectly, hold references to places in host memory. For us to implement something like this for Wasm plugins, we'd have to keep track of a "`ResourcePool`"β€”alive for the duration of each function callβ€”that we can check rust types into and out of. - - I think I've got a handle on how Rhai works now, so let's stop talking about Rhai and discuss what this opaque object system would look like if we implemented it in Rust. - - # Design Sketch - -First things first, we'd have to generalize the arguments we can pass to and return from functions host-side. Currently, we support anything that's `serde`able. We'd have to create a new trait, say `Value`, that has blanket implementations for both `serde` and `Clone` (or something like this; if a type is both `serde` and `clone`, we'd have to figure out a way to disambiguate). - - We'd also create a `ResourcePool` struct that essentially is a `Vec` of `Box`. When calling a function, all `Value` arguments that are resources (e.g. `Clone` instead of `serde`) would be typecasted to `dyn Any` and stored in the `ResourcePool`. - - We'd probably also need a `Resource` trait that defines an associated handle for a resource. Something like this: - - ```rust - pub trait Resource { - type Handle: Serialize + DeserializeOwned; - fn handle(index: u32) -> Self; - fn index(handle: Self) -> u32; - } - ``` - - Where a handle is just a dead-simple wrapper around a `u32`: - - ```rust - #[derive(Serialize, Deserialize)] - pub struct CoolHandle(u32); - ``` - - It's important that this handle be accessible *both* host-side and plugin side. I don't know if this means that we have another crate, like `plugin_handles`, that contains a bunch of u32 wrappers, or something else. Because a `Resource::Handle` is just a u32, it's trivially `serde`, and can cross the ABI boundary. - - So when we add each `T: Resource` to the `ResourcePool`, the resource pool typecasts it to `Any`, appends it to the `Vec`, and returns the associated `Resource::Handle`. This handle is what we pass through to Wasm. - - ```rust - // Implementations and attributes omitted - pub struct Rope { ... }; - pub struct RopeHandle(u32); - impl Resource for Arc> { ... } - - let builder: PluginBuilder = ...; - let builder = builder - .host_fn_async( - "append", - |(rope, string): (Arc>, &str)| async move { - rope.write().await.append(Rope::from(string)) - } - ) - // ... -``` - -He're we're providing a host function, `append` that can be called from Wasm. To import this function into a plugin, we'd do something like the following: - -```rust -use plugin::prelude::*; -use plugin_handles::RopeHandle; - -#[import] -pub fn append(rope: RopeHandle, string: &str); -``` - -This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only acquire resources to handles we're given, so we'd need to expose a function that takes a handle. - -To illustrate that point, here's an example. First, we'd define a plugin-side function as follows: - -```rust -// same file as above ... - -#[export] -pub fn append_newline(rope: RopeHandle){ - append(rope, "\n"); -} -``` - -Host-side, we'd treat this function like any other: - -```rust -pub struct NewlineAppenderPlugin { - append_newline: WasiFn>, ()>, - runtime: Arc>, -} -``` - -To call this function, we'd do the following: - -```rust -let plugin: NewlineAppenderPlugin = ...; -let rope = Arc::new(RwLock::new(Rope::from("Hello World"))); - -plugin.lock().await.call( - &plugin.append_newline, - rope.clone(), -).await?; - -// `rope` is now "Hello World\n" -``` - -So here's what calling `append_newline` would do, from the top: - -1. First, we'd create a new `ResourcePool`, and insert the `Arc>`, creating a `RopeHandle` in the process. (We could also reuse a resource pool across calls, but the idea is that the pool only keeps track of resources for the duration of the call). - -2. Then, we'd call the Wasm plugin function `append_newline`, passing in the `RopeHandle` we created, which easily crosses the ABI boundary. - -3. Next, in Wasm, we call the native imported function `append`. This sends the `RopeHandle` back over the boundary, to Rust. - -4. Looking in the `Plugin`'s `ResourcePool`, we'd convert the handle into an index, grab and downcast the `dyn Any` back into the type we need, and then call the async Rust callback with an `Arc>`. - -5. The Rust async callback actually acquires a lock and appends the newline. - -6. And from here on out we return up the callstack, through Wasm, to Rust all the way back to where we started. Right before we return, we clear out the `ResourcePool`, so that we're no longer holding onto the underlying resource. - -Throughout this entire chain of calls, the resource remain host-side. By temporarily checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource. - -## Final Notes - -Using this approach, it should be possible to add fairly good support for resources to Wasm. I've only done a little rough prototyping, so we're bound to run into some issues along the way, but I think this should be a good first approximation. - -This next week, I'll try to get a production-ready version of this working, using the `Language` resource required by some Language Server Adapters. - -Hope this guide made sense! diff --git a/crates/plugin_runtime/README.md b/crates/plugin_runtime/README.md deleted file mode 100644 index 25524dd272..0000000000 --- a/crates/plugin_runtime/README.md +++ /dev/null @@ -1,320 +0,0 @@ -# Zed's Plugin Runner -This is a short guide that aims to answer the following questions: - -- How do plugins work in Zed? -- How can I create a new plugin? -- How can I integrate plugins into a part of Zed? - -### Nomenclature - -- Host-side: The native Rust runtime managing plugins, e.g. Zed. -- Guest-side: The wasm-based runtime that plugins use. - -## How plugins work -Zed's plugins are WebAssembly (Wasm) based, and have access to the WebAssembly System Interface (WASI), which allows for permissions-based access to subsets of system resources, like the filesystem. - -To execute plugins, Zed's plugin system uses the sandboxed [`wasmtime`](https://wasmtime.dev/) runtime, which is Open Source and developed by the [Bytecode Alliance](https://bytecodealliance.org/). Wasmtime uses the [Cranelift](https://docs.rs/cranelift/latest/cranelift/) codegen library to compile plugins to native code. - -Zed has three `plugin` crates that implement different things: - -1. `plugin_runtime` is a host-side library that loads and runs compiled `Wasm` plugins, in addition to setting up system bindings. This crate should be used host-side - -2. `plugin` contains a prelude for guest-side plugins to depend on. It re-exports some required crates (e.g. `serde`, `bincode`) and provides some necessary macros for generating bindings that `plugin_runtime` can hook into. - -3. `plugin_macros` implements the proc macros required by `plugin`, like the `#[import]` and `#[export]` attribute macros, and should also be used guest-side. - -### ABI -The interface between the host Rust runtime ('Runtime') and plugins implemented in Wasm ('Plugin') is pretty simple. - -When calling a guest-side function, all arguments are serialized to bytes and passed through `Buffer`s. We currently use `serde` + [`bincode`](https://docs.rs/bincode/latest/bincode/) to do this serialization. This means that any type that can be serialized using serde can be passed across the ABI boundary. For types that represent resources that cannot pass the ABI boundary (e.g. `Rope`), we are working on an opaque callback-based system. - -> **Note**: It's important to note that there is a draft ABI standard for Wasm called WebAssembly Interface Types (often abbreviated `WITX`). This standard is currently not stable and only experimentally supported in some runtimes. Once this proposal becomes stable, it would be a good idea to transition towards using WITX as the ABI, rather than the rather rudimentary `bincode` ABI we have now. - -All `Buffer`s are stored in Wasm linear memory (Wasm memory). A `Buffer` is a pointer, length pair to a byte array somewhere in Wasm memory. A `Buffer` itself is represented as a pair of two 4-byte (`u32`) fields: - -```rust -struct Buffer { - ptr: u32, - len: u32, -} -``` - -Which we encode as a single `u64` when crossing the ABI boundary: - -``` -+-------+-------+ -| ptr | len | -+-------+-------+ - | -~ ~ ~ ~ | ~ ~ ~ ~ spOoky ABI boundary O.o - V -+---------------+ -| u64 | -+---------------+ -``` - -All functions that a plugin exports or imports have the following properties: - -- A function signature of `fn(u64) -> u64`, where both the argument (input) and return type (output) are a `Buffer`: - - - The input `Buffer` will contain the input arguments serialized to `bincode`. - - The output `Buffer` will contain the output arguments serialized to `bincode`. - -- Have a name starting with two underscores. - -Luckily for us, we don't have to worry about mangling names or writing serialization code. The `plugin::prelude::*` defines a couple of macrosβ€”aptly named `#[import]` and `#[export]`β€”that generate all serialization code and perform all mangling of names requisite for crossing the ABI boundary. - -There are also a couple important things every plugin must have: - -- `__alloc_buffer` function that, given a `u32` length, returns a `u32` pointer to a buffer of that length. -- `__free_buffer` function that, given a buffer encoded as a `u64`, frees the buffer at the given location, and does not return anything. - -Luckily enough for us yet again, the `plugin` prelude defines two ready-made versions of these functions, so you don't have to worry about implementing them yourselves. - -So, what does importing and exporting functions from a plugin look like in practice? I'm glad you asked... - -## Creating new plugins -Since Zed's plugin system uses Wasm + WASI, in theory any language that compiles to Wasm can be used to write plugins. In practice, and out of practicality, however, we currently only really support plugins written in Rust. - -A plugin is just a rust crate like any other. All plugins embedded in Zed are located in the `plugins` folder in the root. These plugins will automatically be compiled, optimized, and recompiled on change, so it's recommended that when creating a new plugin you create it there. - -As plugins are compiled to Wasm + WASI, you need to have the `wasm32-wasi` toolchain installed on your system. If you don't have it already, a little rustup magick will do the trick: - -```bash -rustup target add wasm32-wasi -``` - -### Configuring a plugin -After you've created a new plugin in `plugins` using `cargo new --lib`, edit your `Cargo.toml` to ensure that it looks something like this: - -```toml -[package] -name = "my_very_cool_incredible_plugin_with_a_short_name_of_course" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -plugin = { path = "../../crates/plugin" } - -[profile.release] -opt-level = "z" -lto = true -``` - -Here's a quick explainer of what we're doing: - -- `crate-type = ["cdylib"]` is used because a plugin essentially acts a *library*, exposing functions with specific signatures that perform certain tasks. This key ensures that the library is generated in a reproducible manner with a layout `plugin_runtime` knows how to hook into. - -- `plugin = { path = "../../crates/plugin" }` is used so we have access to the prelude, which has a few useful functions and can automatically generate serialization glue code for us. - -- `[profile.release]` these options wholistically optimize for size, which will become increasingly important as we add more plugins. - -### Importing and Exporting functions -To import or export a function, all you need are two things: - -1. Make sure that you've imported `plugin::prelude::*` -2. Annotate your function or signature with `#[export]` or `#[import]` respectively. - -Here's an example plugin that doubles the value of every float in a `Vec` passed into it: - -```rust -use plugin::prelude::*; - -#[export] -pub fn double(mut x: Vec) -> Vec { - x.into_iter().map(|x| x * 2.0).collect() -} -``` - -All the serialization code is automatically generated by `#[export]`. - -You can specify functions that must be defined host-side by using the `#[import]` attribute. This attribute must be attached to a function signature: - -```rust -use plugin::prelude::*; - -#[import] -fn run(command: String) -> Vec; -``` - -The `#[import]` macro will generate a function body that performs the proper serialization/deserialization needed to call out to the host rust runtime. Note that the same `serde` + `bincode` + `Buffer` ABI is used for both `#[import]` and `#[export]`. - -> **Note**: If you'd like to see an example of importing and exporting functions, check out the `test_plugin`, which can be found in the `plugins` directory. - -## Integrating plugins into Zed -Currently, plugins are used to add support for language servers to Zed. Plugins should be fairly simple to integrate for library-like applications. Here's a quick overview of how plugins work: - -### Normal vs Precompiled plugins -Plugins in the `plugins` directory are automatically recompiled and serialized to disk when compiling Zed. The resulting artifacts can be found in the `plugins/bin` directory. For each `plugin`, you should see two files: - -- `plugin.wasm` is the plugin compiled to Wasm. As a baseline, this should be about 4MB for debug builds and 2MB for release builds, but it depends on the specific plugin being built. - -- `plugin.wasm.pre` is the plugin compiled to Wasm *and additionally* precompiled to host-platform-specific native code, determined by the `TARGET` cargo exposes at compile-time. This should be about 700KB for debug builds and 500KB in release builds. Each plugin takes about 1 or 2 seconds to compile to native code using cranelift, so precompiling plugins drastically reduces the startup time required to begin to run a plugin. - -For all intents and purposes, it is *highly recommended* that you use precompiled plugins where possible, as they are much more lightweight and take much less time to instantiate. - -### Instantiating a plugin -So you have something you'd like to add a plugin for. What now? The general pattern for adding support for plugins is as follows: - -#### 1. Create a struct to hold the plugin -To call the functions that a plugin exports host-side, you need to have 'handles' to those functions. Each handle is typed and stored in `WasiFn` where `A: Serialize` and `R: DeserializeOwned`. - -For example, let's suppose we're creating a plugin that: - -1. formats a message -2. processes a list of numbers somehow - -We could create a struct for this plugin as follows: - -```rust -use plugin_runtime::{WasiFn, Plugin}; - -pub struct CoolPlugin { - format: WasiFn, - process: WasiFn, f64>, - runtime: Plugin, -} -``` - -Note that this plugin also holds an owned reference to the runtime, which is stored in the `Plugin` type. In asynchronous or multithreaded contexts, it may be required to put `Plugin` behind an `Arc>`. Although plugins expose an asynchronous interface, the underlying Wasm engine can only execute a single function at a time. - -> **Note**: This is a limitation of the WebAssembly standard itself. In the future, to work around this, we've been considering starting a pool of plugins, or instantiating a new plugin per call (this isn't as bad as it sounds, as instantiating a new plugin only takes about 30Β΅s). - -In the following steps, we're going to build a plugin and extract handles to fill this struct we've created. - -#### 2. Bind all imported functions -While a plugin can export functions, it can also import them. We'll refer to the host-side functions that a plugin imports as 'native' functions. Native functions are represented using callbacks, and both synchronous and asynchronous callbacks are supported. - -To bind imported functions, the first thing we need to do is create a new plugin using `PluginBuilder`. `PluginBuilder` uses the builder pattern to configure a new plugin, after which calling the `init` method will instantiate the `Plugin`. - -You can create a new plugin builder as follows: - -```rust -let builder = PluginBuilder::new_with_default_ctx(); -``` - -This creates a plugin with a sensible default set of WASI permissions, namely the ability to write to `stdout` and `stderr` (note that, by default, plugins do not have access to `stdin`). For more control, you can use `PluginBuilder::new` and pass in a `WasiCtx` manually. - -##### Synchronous Functions -To add a sync native function to a plugin, use the `.host_function` method: - -```rust -let builder = builder.host_function( - "add_f64", - |(a, b): (f64, f64)| a + b, -).unwrap(); -``` - -The `.host_function` method takes two arguments: the name of the function, and a sync callback that implements it. Note that this name must match the name of the function declared in the plugin exactly. For example, to use the `add_f64` from a plugin, you must include the following `#[import]` signature: - -```rust -use plugin::prelude::*; - -#[import] -fn add_f64(a: f64, b: f64) -> f64; -``` - -Note that the specific names of the arguments do not matter, as long as they are unique. Once a function has been imported, it may be used in the plugin as any other Rust function. - -##### Asynchronous Functions -To add an async native function to a plugin, use the `.host_function_async` method: - -```rust -let builder = builder.host_function_async( - "half", - |n: f64| async move { n / 2.0 }, -).unwrap(); -``` - -This method works exactly the same as the `.host_function` method, but requires a callback that returns an async future. On the plugin side, there is no distinction made between sync and async functions (as Wasm has no built-in notion of sync vs. async), so the required import signature should *not* use the `async` keyword: - -```rust -use plugin::prelude::*; - -#[import] -fn half(n: f64) -> f64; -``` - -All functions declared by the builder must be imported by the Wasm plugin, otherwise an error will be raised. - -#### 3. Get the compiled plugin -Once all imports are marked, we can instantiate the plugin. To instantiate the plugin, simply call the `.init` method on a `PluginBuilder`: - -```rust -let plugin = builder - .init( - PluginBinary::Precompiled(bytes), - ) - .await - .unwrap(); -``` - -The `.init` method takes a single argument containing the plugin binary. - -1. If not precompiled, use `PluginBinary::Wasm(bytes)`. This supports both the WebAssembly Textual format (`.wat`) and the WebAssembly Binary format (`.wasm`). - -2. If precompiled, use `PluginBinary::Precompiled(bytes)`. This supports precompiled plugins ending in `.wasm.pre`. You need to be extra-careful when using precompiled plugins to ensure that the plugin target matches the target of the binary you are compiling. - -The `.init` method is asynchronous, and must be `.await`ed upon. If the plugin is malformed or doesn't import the right functions, an error will be raised. - -#### 4. Get handles to all exported functions -Once the plugin has been compiled, it's time to start filling in the plugin struct defined earlier. In the case of `CoolPlugin` from earlier, this can be done as follows: - -```rust -let mut cool_plugin = CoolPlugin { - format: plugin.function("format").unwrap(), - process: plugin.function("process").unwrap(), - runtime: plugin, -}; -``` - -Because the struct definition defines the types of functions we're grabbing handles to, it's not required to specify the types of the functions here. - -Note that, yet again, the names of guest-side functions we import must match exactly. Here's an example of what that implementation might look like: - -```rust -use plugin::prelude::*; - -#[export] -pub fn format(message: String) -> String { - format!("Cool Plugin says... '{}!'", message) -} - -#[export] -pub fn process(numbers: Vec) -> f64 { - // Process by calculating the average - let mut total = 0.0; - for number in numbers.into_iter() { - total += number; - } - total / numbers.len() -} -``` - -That's it! Now you have a struct that holds an instance of a plugin. The last thing you need to know is how to call out the plugin you've defined... - -### Using a plugin -To call a plugin function, use the async `.call` method on `Plugin`: - -```rust -let average = cool_plugin.runtime - .call( - &cool_plugin.process, - vec![1.0, 2.0, 3.0], - ) - .await - .unwrap(); -``` - -The `.call` method takes two arguments: - -1. A reference to the handle of the function we want to call. -2. The input argument to this function. - -This method is async, and must be `.await`ed. If something goes wrong (e.g. the plugin panics, or there is a type mismatch between the plugin and `WasiFn`), then this method will return an error. - -## Last Notes -This has been a brief overview of how the plugin system currently works in Zed. We hope to implement higher-level affordances as time goes on, to make writing plugins easier, and providing tooling so that users of Zed may also write plugins to extend their own editors. diff --git a/crates/plugin_runtime/build.rs b/crates/plugin_runtime/build.rs deleted file mode 100644 index d253064103..0000000000 --- a/crates/plugin_runtime/build.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::{io::Write, path::Path}; -use wasmtime::{Config, Engine}; - -fn main() { - let base = Path::new("../../plugins"); - - // Find all files and folders that don't change when rebuilt - let crates = std::fs::read_dir(base).expect("Could not find plugin directory"); - for dir in crates { - let path = dir.unwrap().path(); - let name = path.file_name().and_then(|x| x.to_str()); - let is_dir = path.is_dir(); - if is_dir && name != Some("target") && name != Some("bin") { - println!("cargo:rerun-if-changed={}", path.display()); - } - } - - // Clear out and recreate the plugin bin directory - let _ = std::fs::remove_dir_all(base.join("bin")); - std::fs::create_dir_all(base.join("bin")).expect("Could not make plugins bin directory"); - - // Compile the plugins using the same profile as the current Zed build - let (profile_flags, profile_target) = match std::env::var("PROFILE").unwrap().as_str() { - "debug" => (&[][..], "debug"), - "release" => (&["--release"][..], "release"), - unknown => panic!("unknown profile `{}`", unknown), - }; - // Invoke cargo to build the plugins - let build_successful = std::process::Command::new("cargo") - .args([ - "build", - "--target", - "wasm32-wasi", - "--manifest-path", - base.join("Cargo.toml").to_str().unwrap(), - ]) - .args(profile_flags) - .status() - .expect("Could not build plugins") - .success(); - assert!(build_successful); - - // Get the target architecture for pre-cross-compilation of plugins - // and create and engine with the appropriate config - let target_triple = std::env::var("TARGET").unwrap(); - println!("cargo:rerun-if-env-changed=TARGET"); - let engine = create_default_engine(&target_triple); - - // Find all compiled binaries - let binaries = std::fs::read_dir(base.join("target/wasm32-wasi").join(profile_target)) - .expect("Could not find compiled plugins in target"); - - // Copy and precompile all compiled plugins we can find - for file in binaries { - let is_wasm = || { - let path = file.ok()?.path(); - if path.extension()? == "wasm" { - Some(path) - } else { - None - } - }; - - if let Some(path) = is_wasm() { - let out_path = base.join("bin").join(path.file_name().unwrap()); - std::fs::copy(&path, &out_path).expect("Could not copy compiled plugin to bin"); - precompile(&out_path, &engine); - } - } -} - -/// Creates an engine with the default configuration. -/// N.B. This must create an engine with the same config as the one -/// in `plugin_runtime/src/plugin.rs`. -fn create_default_engine(target_triple: &str) -> Engine { - let mut config = Config::default(); - config - .target(target_triple) - .unwrap_or_else(|_| panic!("Could not set target to `{}`", target_triple)); - config.async_support(true); - config.consume_fuel(true); - Engine::new(&config).expect("Could not create precompilation engine") -} - -fn precompile(path: &Path, engine: &Engine) { - let bytes = std::fs::read(path).expect("Could not read wasm module"); - let compiled = engine - .precompile_module(&bytes) - .expect("Could not precompile module"); - let out_path = path.parent().unwrap().join(&format!( - "{}.pre", - path.file_name().unwrap().to_string_lossy(), - )); - let mut out_file = std::fs::File::create(out_path) - .expect("Could not create output file for precompiled module"); - out_file.write_all(&compiled).unwrap(); -} diff --git a/crates/plugin_runtime/src/lib.rs b/crates/plugin_runtime/src/lib.rs deleted file mode 100644 index 177092a529..0000000000 --- a/crates/plugin_runtime/src/lib.rs +++ /dev/null @@ -1,92 +0,0 @@ -pub mod plugin; -pub use plugin::*; - -#[cfg(test)] -mod tests { - use super::*; - use pollster::FutureExt as _; - - #[test] - pub fn test_plugin() { - pub struct TestPlugin { - noop: WasiFn<(), ()>, - constant: WasiFn<(), u32>, - identity: WasiFn, - add: WasiFn<(u32, u32), u32>, - swap: WasiFn<(u32, u32), (u32, u32)>, - sort: WasiFn, Vec>, - print: WasiFn, - and_back: WasiFn, - imports: WasiFn, - half_async: WasiFn, - echo_async: WasiFn, - } - - async { - let mut runtime = PluginBuilder::new_default() - .unwrap() - .host_function("mystery_number", |input: u32| input + 7) - .unwrap() - .host_function("import_noop", |_: ()| ()) - .unwrap() - .host_function("import_identity", |input: u32| input) - .unwrap() - .host_function("import_swap", |(a, b): (u32, u32)| (b, a)) - .unwrap() - .host_function_async("import_half", |a: u32| async move { a / 2 }) - .unwrap() - .host_function_async("command_async", |command: String| async move { - let mut args = command.split(' '); - let command = args.next().unwrap(); - smol::process::Command::new(command) - .args(args) - .output() - .await - .ok() - .map(|output| output.stdout) - }) - .unwrap() - .init(PluginBinary::Wasm( - include_bytes!("../../../plugins/bin/test_plugin.wasm").as_ref(), - )) - .await - .unwrap(); - - let plugin = TestPlugin { - noop: runtime.function("noop").unwrap(), - constant: runtime.function("constant").unwrap(), - identity: runtime.function("identity").unwrap(), - add: runtime.function("add").unwrap(), - swap: runtime.function("swap").unwrap(), - sort: runtime.function("sort").unwrap(), - print: runtime.function("print").unwrap(), - and_back: runtime.function("and_back").unwrap(), - imports: runtime.function("imports").unwrap(), - half_async: runtime.function("half_async").unwrap(), - echo_async: runtime.function("echo_async").unwrap(), - }; - - let unsorted = vec![1, 3, 4, 2, 5]; - let sorted = vec![1, 2, 3, 4, 5]; - - runtime.call(&plugin.noop, ()).await.unwrap(); - assert_eq!(runtime.call(&plugin.constant, ()).await.unwrap(), 27); - assert_eq!(runtime.call(&plugin.identity, 58).await.unwrap(), 58); - assert_eq!(runtime.call(&plugin.add, (3, 4)).await.unwrap(), 7); - assert_eq!(runtime.call(&plugin.swap, (1, 2)).await.unwrap(), (2, 1)); - assert_eq!(runtime.call(&plugin.sort, unsorted).await.unwrap(), sorted); - runtime.call(&plugin.print, "Hi!".into()).await.unwrap(); - assert_eq!(runtime.call(&plugin.and_back, 1).await.unwrap(), 8); - assert_eq!(runtime.call(&plugin.imports, 1).await.unwrap(), 8); - assert_eq!(runtime.call(&plugin.half_async, 4).await.unwrap(), 2); - assert_eq!( - runtime - .call(&plugin.echo_async, "eko".into()) - .await - .unwrap(), - "eko\n" - ); - } - .block_on() - } -} diff --git a/crates/plugin_runtime/src/plugin.rs b/crates/plugin_runtime/src/plugin.rs deleted file mode 100644 index 78ec85a0f2..0000000000 --- a/crates/plugin_runtime/src/plugin.rs +++ /dev/null @@ -1,584 +0,0 @@ -use std::future::Future; - -use std::{fs::File, marker::PhantomData, path::Path}; - -use anyhow::{anyhow, Error}; -use serde::{de::DeserializeOwned, Serialize}; - -use wasi_common::{dir, file}; -use wasmtime::Memory; -use wasmtime::{ - AsContext, AsContextMut, Caller, Config, Engine, Extern, Instance, Linker, Module, Store, Trap, - TypedFunc, -}; -use wasmtime_wasi::{Dir, WasiCtx, WasiCtxBuilder}; - -/// Represents a resource currently managed by the plugin, like a file descriptor. -pub struct PluginResource(u32); - -/// This is the buffer that is used Host side. -/// Note that it mirrors the functionality of -/// the `__Buffer` found in the `plugin/src/lib.rs` prelude. -struct WasiBuffer { - ptr: u32, - len: u32, -} - -impl WasiBuffer { - pub fn into_u64(self) -> u64 { - ((self.ptr as u64) << 32) | (self.len as u64) - } - - pub fn from_u64(packed: u64) -> Self { - WasiBuffer { - ptr: (packed >> 32) as u32, - len: packed as u32, - } - } -} - -/// Represents a typed WebAssembly function. -pub struct WasiFn { - function: TypedFunc, - _function_type: PhantomData R>, -} - -impl Copy for WasiFn {} - -impl Clone for WasiFn { - fn clone(&self) -> Self { - Self { - function: self.function, - _function_type: PhantomData, - } - } -} - -pub struct Metering { - initial: u64, - refill: u64, -} - -impl Default for Metering { - fn default() -> Self { - Metering { - initial: 1000, - refill: 1000, - } - } -} - -/// This struct is used to build a new [`Plugin`], using the builder pattern. -/// Creates a new default plugin with `PluginBuilder::new_with_default_ctx`, -/// and add host-side exported functions using `host_function` and `host_function_async`. -/// Finalize the plugin by calling [`init`]. -pub struct PluginBuilder { - wasi_ctx: WasiCtx, - engine: Engine, - linker: Linker, - metering: Metering, -} - -/// Creates an engine with the default configuration. -/// N.B. This must create an engine with the same config as the one -/// in `plugin_runtime/build.rs`. -fn create_default_engine() -> Result { - let mut config = Config::default(); - config.async_support(true); - config.consume_fuel(true); - Engine::new(&config) -} - -impl PluginBuilder { - /// Creates a new [`PluginBuilder`] with the given WASI context. - /// Using the default context is a safe bet, see [`new_with_default_context`]. - /// This plugin will yield after a configurable amount of fuel is consumed. - pub fn new(wasi_ctx: WasiCtx, metering: Metering) -> Result { - let engine = create_default_engine()?; - let linker = Linker::new(&engine); - - Ok(PluginBuilder { - wasi_ctx, - engine, - linker, - metering, - }) - } - - /// Creates a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]). - /// This plugin will yield after a configurable amount of fuel is consumed. - pub fn new_default() -> Result { - let default_ctx = WasiCtxBuilder::new() - .inherit_stdout() - .inherit_stderr() - .build(); - let metering = Metering::default(); - Self::new(default_ctx, metering) - } - - /// Add an `async` host function. See [`host_function`] for details. - pub fn host_function_async( - mut self, - name: &str, - function: F, - ) -> Result - where - F: Fn(A) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, - A: DeserializeOwned + Send + 'static, - R: Serialize + Send + Sync + 'static, - { - self.linker.func_wrap1_async( - "env", - &format!("__{}", name), - move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| { - // TODO: use try block once available - let result: Result<(WasiBuffer, Memory, _), Trap> = (|| { - // grab a handle to the memory - let plugin_memory = match caller.get_export("memory") { - Some(Extern::Memory(mem)) => mem, - _ => return Err(Trap::new("Could not grab slice of plugin memory"))?, - }; - - let buffer = WasiBuffer::from_u64(packed_buffer); - - // get the args passed from Guest - let args = - Plugin::buffer_to_bytes(&plugin_memory, caller.as_context(), &buffer)?; - - let args: A = Plugin::deserialize_to_type(args)?; - - // Call the Host-side function - let result = function(args); - - Ok((buffer, plugin_memory, result)) - })(); - - Box::new(async move { - let (buffer, mut plugin_memory, future) = result?; - - let result: R = future.await; - let result: Result, Error> = Plugin::serialize_to_bytes(result) - .map_err(|_| { - Trap::new("Could not serialize value returned from function").into() - }); - let result = result?; - - Plugin::buffer_to_free(caller.data().free_buffer(), &mut caller, buffer) - .await?; - - let buffer = Plugin::bytes_to_buffer( - caller.data().alloc_buffer(), - &mut plugin_memory, - &mut caller, - result, - ) - .await?; - - Ok(buffer.into_u64()) - }) - }, - )?; - Ok(self) - } - - /// Add a new host function to the given `PluginBuilder`. - /// A host function is a function defined host-side, in Rust, - /// that is accessible guest-side, in WebAssembly. - /// You can specify host-side functions to import using - /// the `#[input]` macro attribute: - /// ```ignore - /// #[input] - /// fn total(counts: Vec) -> f64; - /// ``` - /// When loading a plugin, you need to provide all host functions the plugin imports: - /// ```ignore - /// let plugin = PluginBuilder::new_with_default_context() - /// .host_function("total", |counts| counts.iter().fold(0.0, |tot, n| tot + n)) - /// // and so on... - /// ``` - /// And that's a wrap! - pub fn host_function( - mut self, - name: &str, - function: impl Fn(A) -> R + Send + Sync + 'static, - ) -> Result - where - A: DeserializeOwned + Send, - R: Serialize + Send + Sync, - { - self.linker.func_wrap1_async( - "env", - &format!("__{}", name), - move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| { - // TODO: use try block once available - let result: Result<(WasiBuffer, Memory, Vec), Trap> = (|| { - // grab a handle to the memory - let plugin_memory = match caller.get_export("memory") { - Some(Extern::Memory(mem)) => mem, - _ => return Err(Trap::new("Could not grab slice of plugin memory"))?, - }; - - let buffer = WasiBuffer::from_u64(packed_buffer); - - // get the args passed from Guest - let args = Plugin::buffer_to_type(&plugin_memory, &mut caller, &buffer)?; - - // Call the Host-side function - let result: R = function(args); - - // Serialize the result back to guest - let result = Plugin::serialize_to_bytes(result).map_err(|_| { - Trap::new("Could not serialize value returned from function") - })?; - - Ok((buffer, plugin_memory, result)) - })(); - - Box::new(async move { - let (buffer, mut plugin_memory, result) = result?; - - Plugin::buffer_to_free(caller.data().free_buffer(), &mut caller, buffer) - .await?; - - let buffer = Plugin::bytes_to_buffer( - caller.data().alloc_buffer(), - &mut plugin_memory, - &mut caller, - result, - ) - .await?; - - Ok(buffer.into_u64()) - }) - }, - )?; - Ok(self) - } - - /// Initializes a [`Plugin`] from a given compiled Wasm module. - /// Both binary (`.wasm`) and text (`.wat`) module formats are supported. - pub async fn init(self, binary: PluginBinary<'_>) -> Result { - Plugin::init(binary, self).await - } -} - -#[derive(Copy, Clone)] -struct WasiAlloc { - alloc_buffer: TypedFunc, - free_buffer: TypedFunc, -} - -struct WasiCtxAlloc { - wasi_ctx: WasiCtx, - alloc: Option, -} - -impl WasiCtxAlloc { - fn alloc_buffer(&self) -> TypedFunc { - self.alloc - .expect("allocator has been not initialized, cannot allocate buffer!") - .alloc_buffer - } - - fn free_buffer(&self) -> TypedFunc { - self.alloc - .expect("allocator has been not initialized, cannot free buffer!") - .free_buffer - } - - fn init_alloc(&mut self, alloc: WasiAlloc) { - self.alloc = Some(alloc) - } -} - -pub enum PluginBinary<'a> { - Wasm(&'a [u8]), - Precompiled(&'a [u8]), -} - -/// Represents a WebAssembly plugin, with access to the WebAssembly System Interface. -/// Build a new plugin using [`PluginBuilder`]. -pub struct Plugin { - store: Store, - instance: Instance, -} - -impl Plugin { - /// Dumps the *entirety* of Wasm linear memory to `stdout`. - /// Don't call this unless you're debugging a memory issue! - pub fn dump_memory(data: &[u8]) { - for (i, byte) in data.iter().enumerate() { - if i % 32 == 0 { - println!(); - } - if i % 4 == 0 { - print!("|"); - } - if *byte == 0 { - print!("__") - } else { - print!("{:02x}", byte); - } - } - println!(); - } - - async fn init(binary: PluginBinary<'_>, plugin: PluginBuilder) -> Result { - // initialize the WebAssembly System Interface context - let engine = plugin.engine; - let mut linker = plugin.linker; - wasmtime_wasi::add_to_linker(&mut linker, |s| &mut s.wasi_ctx)?; - - // create a store, note that we can't initialize the allocator, - // because we can't grab the functions until initialized. - let mut store: Store = Store::new( - &engine, - WasiCtxAlloc { - wasi_ctx: plugin.wasi_ctx, - alloc: None, - }, - ); - - let module = match binary { - PluginBinary::Precompiled(bytes) => unsafe { Module::deserialize(&engine, bytes)? }, - PluginBinary::Wasm(bytes) => Module::new(&engine, bytes)?, - }; - - // set up automatic yielding based on configuration - store.add_fuel(plugin.metering.initial).unwrap(); - store.out_of_fuel_async_yield(u64::MAX, plugin.metering.refill); - - // load the provided module into the asynchronous runtime - linker.module_async(&mut store, "", &module).await?; - let instance = linker.instantiate_async(&mut store, &module).await?; - - // now that the module is initialized, - // we can initialize the store's allocator - let alloc_buffer = instance.get_typed_func(&mut store, "__alloc_buffer")?; - let free_buffer = instance.get_typed_func(&mut store, "__free_buffer")?; - store.data_mut().init_alloc(WasiAlloc { - alloc_buffer, - free_buffer, - }); - - Ok(Plugin { store, instance }) - } - - /// Attaches a file or directory the the given system path to the runtime. - /// Note that the resource must be freed by calling `remove_resource` afterwards. - pub fn attach_path>(&mut self, path: T) -> Result { - // grab the WASI context - let ctx = self.store.data_mut(); - - // open the file we want, and convert it into the right type - // this is a footgun and a half - let file = File::open(&path).unwrap(); - let dir = Dir::from_std_file(file); - let dir = Box::new(wasmtime_wasi::dir::Dir::from_cap_std(dir)); - - // grab an empty file descriptor, specify capabilities - let fd = ctx.wasi_ctx.table().push(Box::new(()))?; - let caps = dir::DirCaps::all(); - let file_caps = file::FileCaps::all(); - - // insert the directory at the given fd, - // return a handle to the resource - ctx.wasi_ctx - .insert_dir(fd, dir, caps, file_caps, path.as_ref().to_path_buf()); - Ok(PluginResource(fd)) - } - - /// Returns `true` if the resource existed and was removed. - /// Currently the only resource we support is adding scoped paths (e.g. folders and files) - /// to plugins using [`attach_path`]. - pub fn remove_resource(&mut self, resource: PluginResource) -> Result<(), Error> { - self.store - .data_mut() - .wasi_ctx - .table() - .delete(resource.0) - .ok_or_else(|| anyhow!("Resource did not exist, but a valid handle was passed in"))?; - Ok(()) - } - - // So this call function is kinda a dance, I figured it'd be a good idea to document it. - // the high level is we take a serde type, serialize it to a byte array, - // (we're doing this using bincode for now) - // then toss that byte array into webassembly. - // webassembly grabs that byte array, does some magic, - // and serializes the result into yet another byte array. - // we then grab *that* result byte array and deserialize it into a result. - // - // phew... - // - // now the problem is, webassembly doesn't support buffers. - // only really like i32s, that's it (yeah, it's sad. Not even unsigned!) - // (ok, I'm exaggerating a bit). - // - // the Wasm function that this calls must have a very specific signature: - // - // fn(pointer to byte array: i32, length of byte array: i32) - // -> pointer to ( - // pointer to byte_array: i32, - // length of byte array: i32, - // ): i32 - // - // This pair `(pointer to byte array, length of byte array)` is called a `Buffer` - // and can be found in the cargo_test plugin. - // - // so on the wasm side, we grab the two parameters to the function, - // stuff them into a `Buffer`, - // and then pray to the `unsafe` Rust gods above that a valid byte array pops out. - // - // On the flip side, when returning from a wasm function, - // we convert whatever serialized result we get into byte array, - // which we stuff into a Buffer and allocate on the heap, - // which pointer to we then return. - // Note the double indirection! - // - // So when returning from a function, we actually leak memory *twice*: - // - // 1) once when we leak the byte array - // 2) again when we leak the allocated `Buffer` - // - // This isn't a problem because Wasm stops executing after the function returns, - // so the heap is still valid for our inspection when we want to pull things out. - - /// Serializes a given type to bytes. - fn serialize_to_bytes(item: A) -> Result, Error> { - // serialize the argument using bincode - let bytes = bincode::serialize(&item)?; - Ok(bytes) - } - - /// Deserializes a given type from bytes. - fn deserialize_to_type(bytes: &[u8]) -> Result { - // serialize the argument using bincode - let bytes = bincode::deserialize(bytes)?; - Ok(bytes) - } - - // fn deserialize( - // plugin_memory: &mut Memory, - // mut store: impl AsContextMut, - // buffer: WasiBuffer, - // ) -> Result { - // let buffer_start = buffer.ptr as usize; - // let buffer_end = buffer_start + buffer.len as usize; - - // // read the buffer at this point into a byte array - // // deserialize the byte array into the provided serde type - // let item = &plugin_memory.data(store.as_context())[buffer_start..buffer_end]; - // let item = bincode::deserialize(bytes)?; - // Ok(item) - // } - - /// Takes an item, allocates a buffer, serializes the argument to that buffer, - /// and returns a (ptr, len) pair to that buffer. - async fn bytes_to_buffer( - alloc_buffer: TypedFunc, - plugin_memory: &mut Memory, - mut store: impl AsContextMut, - item: Vec, - ) -> Result { - // allocate a buffer and write the argument to that buffer - let len = item.len() as u32; - let ptr = alloc_buffer.call_async(&mut store, len).await?; - plugin_memory.write(&mut store, ptr as usize, &item)?; - Ok(WasiBuffer { ptr, len }) - } - - /// Takes a `(ptr, len)` pair and returns the corresponding deserialized buffer. - fn buffer_to_type( - plugin_memory: &Memory, - store: impl AsContext, - buffer: &WasiBuffer, - ) -> Result { - let buffer_start = buffer.ptr as usize; - let buffer_end = buffer_start + buffer.len as usize; - - // read the buffer at this point into a byte array - // deserialize the byte array into the provided serde type - let result = &plugin_memory.data(store.as_context())[buffer_start..buffer_end]; - let result = bincode::deserialize(result)?; - - Ok(result) - } - - /// Takes a `(ptr, len)` pair and returns the corresponding deserialized buffer. - fn buffer_to_bytes<'a>( - plugin_memory: &'a Memory, - store: wasmtime::StoreContext<'a, WasiCtxAlloc>, - buffer: &'a WasiBuffer, - ) -> Result<&'a [u8], Error> { - let buffer_start = buffer.ptr as usize; - let buffer_end = buffer_start + buffer.len as usize; - - // read the buffer at this point into a byte array - // deserialize the byte array into the provided serde type - let result = &plugin_memory.data(store)[buffer_start..buffer_end]; - Ok(result) - } - - async fn buffer_to_free( - free_buffer: TypedFunc, - mut store: impl AsContextMut, - buffer: WasiBuffer, - ) -> Result<(), Error> { - // deallocate the argument buffer - Ok(free_buffer - .call_async(&mut store, buffer.into_u64()) - .await?) - } - - /// Retrieves the handle to a function of a given type. - pub fn function>( - &mut self, - name: T, - ) -> Result, Error> { - let fun_name = format!("__{}", name.as_ref()); - let fun = self - .instance - .get_typed_func::(&mut self.store, &fun_name)?; - Ok(WasiFn { - function: fun, - _function_type: PhantomData, - }) - } - - /// Asynchronously calls a function defined Guest-side. - pub async fn call( - &mut self, - handle: &WasiFn, - arg: A, - ) -> Result { - let mut plugin_memory = self - .instance - .get_memory(&mut self.store, "memory") - .ok_or_else(|| anyhow!("Could not grab slice of plugin memory"))?; - - // write the argument to linear memory - // this returns a (ptr, length) pair - let arg_buffer = Self::bytes_to_buffer( - self.store.data().alloc_buffer(), - &mut plugin_memory, - &mut self.store, - Self::serialize_to_bytes(arg)?, - ) - .await?; - - // call the function, passing in the buffer and its length - // this returns a ptr to a (ptr, length) pair - let result_buffer = handle - .function - .call_async(&mut self.store, arg_buffer.into_u64()) - .await?; - - Self::buffer_to_type( - &plugin_memory, - &mut self.store, - &WasiBuffer::from_u64(result_buffer), - ) - } -} diff --git a/crates/prettier/Cargo.toml b/crates/prettier/Cargo.toml index 451ef78306..242a65ba7b 100644 --- a/crates/prettier/Cargo.toml +++ b/crates/prettier/Cargo.toml @@ -14,10 +14,8 @@ test-support = [] [dependencies] anyhow.workspace = true -client.workspace = true collections.workspace = true fs.workspace = true -futures.workspace = true gpui.workspace = true language.workspace = true log.workspace = true @@ -25,7 +23,6 @@ lsp.workspace = true node_runtime.workspace = true parking_lot.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true util.workspace = true diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 119901cf07..676ed6d1ac 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -2,7 +2,7 @@ use anyhow::Context; use collections::{HashMap, HashSet}; use fs::Fs; use gpui::{AsyncAppContext, Model}; -use language::{language_settings::language_settings, Buffer, Diff}; +use language::{language_settings::language_settings, Buffer, Diff, LanguageRegistry}; use lsp::{LanguageServer, LanguageServerId}; use node_runtime::NodeRuntime; use serde::{Deserialize, Serialize}; @@ -25,6 +25,7 @@ pub struct RealPrettier { default: bool, prettier_dir: PathBuf, server: Arc, + language_registry: Arc, } #[cfg(any(test, feature = "test-support"))] @@ -155,6 +156,7 @@ impl Prettier { _: LanguageServerId, prettier_dir: PathBuf, _: Arc, + _: Arc, _: AsyncAppContext, ) -> anyhow::Result { Ok(Self::Test(TestPrettier { @@ -168,6 +170,7 @@ impl Prettier { server_id: LanguageServerId, prettier_dir: PathBuf, node: Arc, + language_registry: Arc, cx: AsyncAppContext, ) -> anyhow::Result { use lsp::LanguageServerBinary; @@ -192,8 +195,9 @@ impl Prettier { LanguageServerBinary { path: node_path, arguments: vec![prettier_server.into(), prettier_dir.as_path().into()], + env: None, }, - Path::new("/"), + &prettier_dir, None, cx.clone(), ) @@ -205,6 +209,7 @@ impl Prettier { Ok(Self::Real(RealPrettier { server, default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(), + language_registry, prettier_dir, })) } @@ -222,10 +227,12 @@ impl Prettier { let buffer_language = buffer.language(); let parser_with_plugins = buffer_language.and_then(|l| { let prettier_parser = l.prettier_parser_name()?; - let mut prettier_plugins = l - .lsp_adapters() + let mut prettier_plugins = local + .language_registry + .lsp_adapters(l) .iter() .flat_map(|adapter| adapter.prettier_plugins()) + .copied() .collect::>(); prettier_plugins.dedup(); Some((prettier_parser, prettier_plugins)) @@ -238,7 +245,7 @@ impl Prettier { ); let plugin_name_into_path = |plugin_name: &str| { let prettier_plugin_dir = prettier_node_modules.join(plugin_name); - for possible_plugin_path in [ + [ prettier_plugin_dir.join("dist").join("index.mjs"), prettier_plugin_dir.join("dist").join("index.js"), prettier_plugin_dir.join("dist").join("plugin.js"), @@ -248,12 +255,9 @@ impl Prettier { // this one is for @prettier/plugin-php prettier_plugin_dir.join("standalone.js"), prettier_plugin_dir, - ] { - if possible_plugin_path.is_file() { - return Some(possible_plugin_path); - } - } - None + ] + .into_iter() + .find(|possible_plugin_path| possible_plugin_path.is_file()) }; let (parser, located_plugins) = match parser_with_plugins { Some((parser, plugins)) => { @@ -263,7 +267,7 @@ impl Prettier { let mut plugins = plugins .into_iter() - .filter(|&&plugin_name| { + .filter(|&plugin_name| { if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME { add_tailwind_back = true; false @@ -331,9 +335,9 @@ impl Prettier { .collect(); log::debug!( "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}", + buffer.file().map(|f| f.full_path(cx)), plugins, prettier_options, - buffer.file().map(|f| f.full_path(cx)) ); anyhow::Ok(FormatParams { diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index c11cdb6874..c76424144f 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -12,11 +12,11 @@ doctest = false [features] test-support = [ "client/test-support", - "db/test-support", "language/test-support", "settings/test-support", "text/test-support", "prettier/test-support", + "project_core/test-support", "gpui/test-support", ] @@ -24,53 +24,42 @@ test-support = [ aho-corasick = "1.1" anyhow.workspace = true async-trait.workspace = true -backtrace = "0.3" client.workspace = true clock.workspace = true collections.workspace = true copilot.workspace = true -db.workspace = true fs.workspace = true -fsevent.workspace = true futures.workspace = true fuzzy.workspace = true -git.workspace = true globset.workspace = true gpui.workspace = true -ignore = "0.4" -itertools = "0.10" +itertools.workspace = true language.workspace = true -lazy_static.workspace = true log.workspace = true lsp.workspace = true node_runtime.workspace = true parking_lot.workspace = true postage.workspace = true prettier.workspace = true +project_core.workspace = true rand.workspace = true regex.workspace = true rpc.workspace = true -runnable.workspace = true -schemars.workspace = true +task.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true settings.workspace = true -sha2 = "0.10" +sha2.workspace = true similar = "1.3" smol.workspace = true -sum_tree.workspace = true terminal.workspace = true text.workspace = true -thiserror.workspace = true -toml.workspace = true util.workspace = true +which.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } -ctor.workspace = true -db = { workspace = true, features = ["test-support"] } env_logger.workspace = true fs = { workspace = true, features = ["test-support"] } git2.workspace = true @@ -80,8 +69,8 @@ release_channel.workspace = true lsp = { workspace = true, features = ["test-support"] } prettier = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true +project_core = { workspace = true, features = ["test-support"] } rpc = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } -tempfile.workspace = true unindent.workspace = true util = { workspace = true, features = ["test-support"] } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 202da1e973..2f8e409afa 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -105,6 +105,10 @@ pub(crate) struct GetTypeDefinition { pub position: PointUtf16, } +pub(crate) struct GetImplementation { + pub position: PointUtf16, +} + pub(crate) struct GetReferences { pub position: PointUtf16, } @@ -492,6 +496,99 @@ impl LspCommand for GetDefinition { } } +#[async_trait(?Send)] +impl LspCommand for GetImplementation { + type Response = Vec; + type LspRequest = lsp::request::GotoImplementation; + type ProtoRequest = proto::GetImplementation; + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp::GotoImplementationParams { + lsp::GotoImplementationParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option, + project: Model, + buffer: Model, + server_id: LanguageServerId, + cx: AsyncAppContext, + ) -> Result> { + location_links_from_lsp(message, project, buffer, server_id, cx).await + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetImplementation { + proto::GetImplementation { + project_id, + buffer_id: buffer.remote_id().into(), + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetImplementation, + _: Model, + buffer: Model, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + })? + .await?; + Ok(Self { + position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + }) + } + + fn response_to_proto( + response: Vec, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &mut AppContext, + ) -> proto::GetImplementationResponse { + let links = location_links_to_proto(response, project, peer_id, cx); + proto::GetImplementationResponse { links } + } + + async fn response_from_proto( + self, + message: proto::GetImplementationResponse, + project: Model, + _: Model, + cx: AsyncAppContext, + ) -> Result> { + location_links_from_proto(message.links, project, cx).await + } + + fn buffer_id_from_proto(message: &proto::GetImplementation) -> Result { + BufferId::new(message.buffer_id) + } +} + #[async_trait(?Send)] impl LspCommand for GetTypeDefinition { type Response = Vec; @@ -1308,6 +1405,13 @@ impl LspCommand for GetHover { } else { None }; + if let Some(range) = range.as_ref() { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_anchors([range.start, range.end]) + })? + .await?; + } Ok(Some(Hover { contents, @@ -1368,6 +1472,12 @@ impl LspCommand for GetCompletions { Default::default() }; + let language_server_adapter = project + .update(&mut cx, |project, _cx| { + project.language_server_adapter_for_id(server_id) + })? + .ok_or_else(|| anyhow!("no such language server"))?; + let completions = buffer.update(&mut cx, |buffer, cx| { let language_registry = project.read(cx).languages().clone(); let language = buffer.language().cloned(); @@ -1413,7 +1523,7 @@ impl LspCommand for GetCompletions { }); let range = if let Some(range) = default_edit_range { - let range = range_from_lsp(range.clone()); + let range = range_from_lsp(*range); let start = snapshot.clip_point_utf16(range.start, Bias::Left); let end = snapshot.clip_point_utf16(range.end, Bias::Left); if start != range.start.0 || end != range.end.0 { @@ -1455,12 +1565,17 @@ impl LspCommand for GetCompletions { let language_registry = language_registry.clone(); let language = language.clone(); + let language_server_adapter = language_server_adapter.clone(); LineEnding::normalize(&mut new_text); Some(async move { let mut label = None; - if let Some(language) = language.as_ref() { - language.process_completion(&mut lsp_completion).await; - label = language.label_for_completion(&lsp_completion).await; + if let Some(language) = &language { + language_server_adapter + .process_completion(&mut lsp_completion) + .await; + label = language_server_adapter + .label_for_completion(&lsp_completion, language) + .await; } let documentation = if let Some(lsp_docs) = &lsp_completion.documentation { @@ -1547,7 +1662,7 @@ impl LspCommand for GetCompletions { async fn response_from_proto( self, message: proto::GetCompletionsResponse, - _: Model, + project: Model, buffer: Model, mut cx: AsyncAppContext, ) -> Result> { @@ -1558,8 +1673,13 @@ impl LspCommand for GetCompletions { .await?; let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?; + let language_registry = project.update(&mut cx, |project, _| project.languages.clone())?; let completions = message.completions.into_iter().map(|completion| { - language::proto::deserialize_completion(completion, language.clone()) + language::proto::deserialize_completion( + completion, + language.clone(), + &language_registry, + ) }); future::try_join_all(completions).await } diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index 098fe2b39a..48a21f9158 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -14,7 +14,7 @@ use futures::{ use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel}; use language::{ language_settings::{Formatter, LanguageSettings}, - Buffer, Language, LanguageServerName, LocalFile, + Buffer, Language, LanguageRegistry, LanguageServerName, LocalFile, }; use lsp::{LanguageServer, LanguageServerId}; use node_runtime::NodeRuntime; @@ -26,7 +26,8 @@ use crate::{ }; pub fn prettier_plugins_for_language( - language: &Language, + language_registry: &Arc, + language: &Arc, language_settings: &LanguageSettings, ) -> Option> { match &language_settings.formatter { @@ -38,8 +39,8 @@ pub fn prettier_plugins_for_language( prettier_plugins .get_or_insert_with(|| HashSet::default()) .extend( - language - .lsp_adapters() + language_registry + .lsp_adapters(language) .iter() .flat_map(|adapter| adapter.prettier_plugins()), ) @@ -70,9 +71,14 @@ pub(super) async fn format_with_prettier( match prettier.format(buffer, buffer_path, cx).await { Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)), Err(e) => { - log::error!( - "Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}" - ); + match prettier_path { + Some(prettier_path) => log::error!( + "Prettier instance from path {prettier_path:?} failed to format a buffer: {e:#}" + ), + None => log::error!( + "Default prettier instance failed to format a buffer: {e:#}" + ), + } } } } @@ -298,15 +304,20 @@ fn start_prettier( ) -> PrettierTask { cx.spawn(|project, mut cx| async move { log::info!("Starting prettier at path {prettier_dir:?}"); - let new_server_id = project.update(&mut cx, |project, _| { - project.languages.next_language_server_id() - })?; + let language_registry = project.update(&mut cx, |project, _| project.languages.clone())?; + let new_server_id = language_registry.next_language_server_id(); - let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) - .await - .context("default prettier spawn") - .map(Arc::new) - .map_err(Arc::new)?; + let new_prettier = Prettier::start( + new_server_id, + prettier_dir, + node, + language_registry, + cx.clone(), + ) + .await + .context("default prettier spawn") + .map(Arc::new) + .map_err(Arc::new)?; register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); Ok(new_prettier) }) @@ -366,6 +377,7 @@ fn register_new_prettier( } async fn install_prettier_packages( + fs: &dyn Fs, plugins_to_install: HashSet<&'static str>, node: Arc, ) -> anyhow::Result<()> { @@ -385,18 +397,32 @@ async fn install_prettier_packages( .await .context("fetching latest npm versions")?; - log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); + let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path(); + match fs.metadata(default_prettier_dir).await.with_context(|| { + format!("fetching FS metadata for default prettier dir {default_prettier_dir:?}") + })? { + Some(prettier_dir_metadata) => anyhow::ensure!( + prettier_dir_metadata.is_dir, + "default prettier dir {default_prettier_dir:?} is not a directory" + ), + None => fs + .create_dir(default_prettier_dir) + .await + .with_context(|| format!("creating default prettier dir {default_prettier_dir:?}"))?, + } + + log::info!("Installing default prettier and plugins: {packages_to_versions:?}"); let borrowed_packages = packages_to_versions .iter() .map(|(package, version)| (package.as_str(), version.as_str())) .collect::>(); - node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages) + node.npm_install_packages(default_prettier_dir, &borrowed_packages) .await .context("fetching formatter packages")?; anyhow::Ok(()) } -async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> { +async fn save_prettier_server_file(fs: &dyn Fs) -> anyhow::Result<()> { let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); fs.save( &prettier_wrapper_path, @@ -413,6 +439,17 @@ async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> { Ok(()) } +async fn should_write_prettier_server_file(fs: &dyn Fs) -> bool { + let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); + if !fs.is_file(&prettier_wrapper_path).await { + return true; + } + let Ok(prettier_server_file_contents) = fs.load(&prettier_wrapper_path).await else { + return true; + }; + prettier_server_file_contents != prettier::PRETTIER_SERVER_JS +} + impl Project { pub fn update_prettier_settings( &self, @@ -491,6 +528,9 @@ impl Project { buffer: &Model, cx: &mut ModelContext, ) -> Task, PrettierTask)>> { + if !self.is_local() { + return Task::ready(None); + } let buffer = buffer.read(cx); let buffer_file = buffer.file(); let Some(buffer_language) = buffer.language() else { @@ -499,119 +539,105 @@ impl Project { if buffer_language.prettier_parser_name().is_none() { return Task::ready(None); } - - if self.is_local() { - let Some(node) = self.node.as_ref().map(Arc::clone) else { - return Task::ready(None); - }; - match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) - { - Some((worktree_id, buffer_path)) => { - let fs = Arc::clone(&self.fs); - let installed_prettiers = self.prettier_instances.keys().cloned().collect(); - return cx.spawn(|project, mut cx| async move { - match cx - .background_executor() - .spawn(async move { - Prettier::locate_prettier_installation( - fs.as_ref(), - &installed_prettiers, - &buffer_path, - ) - .await - }) - .await - { - Ok(ControlFlow::Break(())) => { - return None; - } - Ok(ControlFlow::Continue(None)) => { - let default_instance = project - .update(&mut cx, |project, cx| { - project - .prettiers_per_worktree - .entry(worktree_id) - .or_default() - .insert(None); - project.default_prettier.prettier_task( - &node, - Some(worktree_id), - cx, - ) - }) - .ok()?; - Some((None, default_instance?.log_err().await?)) - } - Ok(ControlFlow::Continue(Some(prettier_dir))) => { - project - .update(&mut cx, |project, _| { - project - .prettiers_per_worktree - .entry(worktree_id) - .or_default() - .insert(Some(prettier_dir.clone())) - }) - .ok()?; - if let Some(prettier_task) = project - .update(&mut cx, |project, cx| { - project.prettier_instances.get_mut(&prettier_dir).map( - |existing_instance| { - existing_instance.prettier_task( - &node, - Some(&prettier_dir), - Some(worktree_id), - cx, - ) - }, - ) - }) - .ok()? - { - log::debug!( - "Found already started prettier in {prettier_dir:?}" - ); - return Some(( - Some(prettier_dir), - prettier_task?.await.log_err()?, - )); - } - - log::info!("Found prettier in {prettier_dir:?}, starting."); - let new_prettier_task = project - .update(&mut cx, |project, cx| { - let new_prettier_task = start_prettier( - node, - prettier_dir.clone(), - Some(worktree_id), - cx, - ); - project.prettier_instances.insert( - prettier_dir.clone(), - PrettierInstance { - attempt: 0, - prettier: Some(new_prettier_task.clone()), - }, - ); - new_prettier_task - }) - .ok()?; - Some((Some(prettier_dir), new_prettier_task)) - } - Err(e) => { - log::error!("Failed to determine prettier path for buffer: {e:#}"); - return None; - } - } - }); - } - None => { - let new_task = self.default_prettier.prettier_task(&node, None, cx); - return cx - .spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) }); - } - } - } else { + let Some(node) = self.node.as_ref().map(Arc::clone) else { return Task::ready(None); + }; + match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) { + Some((worktree_id, buffer_path)) => { + let fs = Arc::clone(&self.fs); + let installed_prettiers = self.prettier_instances.keys().cloned().collect(); + cx.spawn(|project, mut cx| async move { + match cx + .background_executor() + .spawn(async move { + Prettier::locate_prettier_installation( + fs.as_ref(), + &installed_prettiers, + &buffer_path, + ) + .await + }) + .await + { + Ok(ControlFlow::Break(())) => None, + Ok(ControlFlow::Continue(None)) => { + let default_instance = project + .update(&mut cx, |project, cx| { + project + .prettiers_per_worktree + .entry(worktree_id) + .or_default() + .insert(None); + project.default_prettier.prettier_task( + &node, + Some(worktree_id), + cx, + ) + }) + .ok()?; + Some((None, default_instance?.log_err().await?)) + } + Ok(ControlFlow::Continue(Some(prettier_dir))) => { + project + .update(&mut cx, |project, _| { + project + .prettiers_per_worktree + .entry(worktree_id) + .or_default() + .insert(Some(prettier_dir.clone())) + }) + .ok()?; + if let Some(prettier_task) = project + .update(&mut cx, |project, cx| { + project.prettier_instances.get_mut(&prettier_dir).map( + |existing_instance| { + existing_instance.prettier_task( + &node, + Some(&prettier_dir), + Some(worktree_id), + cx, + ) + }, + ) + }) + .ok()? + { + log::debug!("Found already started prettier in {prettier_dir:?}"); + return Some((Some(prettier_dir), prettier_task?.await.log_err()?)); + } + + log::info!("Found prettier in {prettier_dir:?}, starting."); + let new_prettier_task = project + .update(&mut cx, |project, cx| { + let new_prettier_task = start_prettier( + node, + prettier_dir.clone(), + Some(worktree_id), + cx, + ); + project.prettier_instances.insert( + prettier_dir.clone(), + PrettierInstance { + attempt: 0, + prettier: Some(new_prettier_task.clone()), + }, + ); + new_prettier_task + }) + .ok()?; + Some((Some(prettier_dir), new_prettier_task)) + } + Err(e) => { + log::error!("Failed to determine prettier path for buffer: {e:#}"); + None + } + } + }) + } + None => { + let new_task = self.default_prettier.prettier_task(&node, None, cx); + cx.spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) }) + } } } @@ -623,6 +649,7 @@ impl Project { _cx: &mut ModelContext, ) { // suppress unused code warnings + let _ = should_write_prettier_server_file; let _ = install_prettier_packages; let _ = save_prettier_server_file; @@ -643,7 +670,6 @@ impl Project { let Some(node) = self.node.as_ref().cloned() else { return; }; - log::info!("Initializing default prettier with plugins {new_plugins:?}"); let fs = Arc::clone(&self.fs); let locate_prettier_installation = match worktree.and_then(|worktree_id| { self.worktree_for_id(worktree_id, cx) @@ -689,6 +715,7 @@ impl Project { } }; + log::info!("Initializing default prettier with plugins {new_plugins:?}"); let plugins_to_install = new_plugins.clone(); let fs = Arc::clone(&self.fs); let new_installation_task = cx @@ -703,7 +730,7 @@ impl Project { if prettier_path.is_some() { new_plugins.clear(); } - let mut needs_install = false; + let mut needs_install = should_write_prettier_server_file(fs.as_ref()).await; if let Some(previous_installation_task) = previous_installation_task { if let Err(e) = previous_installation_task.await { log::error!("Failed to install default prettier: {e:#}"); @@ -744,8 +771,10 @@ impl Project { let installed_plugins = new_plugins.clone(); cx.background_executor() .spawn(async move { + install_prettier_packages(fs.as_ref(), new_plugins, node).await?; + // Save the server file last, so the reinstall need could be determined by the absence of the file. save_prettier_server_file(fs.as_ref()).await?; - install_prettier_packages(new_plugins, node).await + anyhow::Ok(()) }) .await .context("prettier & plugins install") diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2845228001..479fbf4a17 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,21 +1,17 @@ pub mod debounced_delay; -mod ignore; pub mod lsp_command; pub mod lsp_ext_command; mod prettier_support; -pub mod project_settings; -mod runnable_inventory; pub mod search; +mod task_inventory; pub mod terminals; -pub mod worktree; #[cfg(test)] mod project_tests; -#[cfg(test)] -mod worktree_tests; use anyhow::{anyhow, bail, Context as _, Result}; -use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; +use async_trait::async_trait; +use client::{proto, Client, Collaborator, HostedProjectId, TypedEnvelope, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use copilot::Copilot; @@ -23,6 +19,7 @@ use debounced_delay::DebouncedDelay; use futures::{ channel::mpsc::{self, UnboundedReceiver}, future::{try_join_all, Shared}, + select, stream::FuturesUnordered, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, }; @@ -49,20 +46,21 @@ use log::error; use lsp::{ DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, - MessageActionItem, OneOf, + MessageActionItem, OneOf, ServerHealthStatus, ServerStatus, }; use lsp_command::*; use node_runtime::NodeRuntime; use parking_lot::{Mutex, RwLock}; use postage::watch; use prettier_support::{DefaultPrettier, PrettierInstance}; -use project_settings::{LspSettings, ProjectSettings}; +use project_core::project_settings::{LspSettings, ProjectSettings}; +pub use project_core::{DiagnosticSummary, ProjectEntryId}; use rand::prelude::*; use rpc::{ErrorCode, ErrorExt as _}; use search::SearchQuery; use serde::Serialize; -use settings::{Settings, SettingsStore}; +use settings::{watch_config_file, Settings, SettingsStore}; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; use smol::channel::{Receiver, Sender}; @@ -70,6 +68,8 @@ use smol::lock::Semaphore; use std::{ cmp::{self, Ordering}, convert::TryInto, + env, + ffi::OsString, hash::Hash, mem, num::NonZeroU32, @@ -83,20 +83,30 @@ use std::{ }, time::{Duration, Instant}, }; +use task::static_source::StaticSource; use terminals::Terminals; use text::{Anchor, BufferId}; use util::{ - debug_panic, defer, http::HttpClient, merge_json_value_into, - paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, + debug_panic, defer, + http::HttpClient, + merge_json_value_into, + paths::{LOCAL_SETTINGS_RELATIVE_PATH, LOCAL_TASKS_RELATIVE_PATH}, + post_inc, ResultExt, TryFutureExt as _, }; pub use fs::*; +pub use language::Location; #[cfg(any(test, feature = "test-support"))] pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; -pub use runnable_inventory::Inventory; -pub use worktree::*; +pub use project_core::project_settings; +pub use project_core::worktree::{self, *}; +#[cfg(feature = "test-support")] +pub use task_inventory::test_inventory::*; +pub use task_inventory::{Inventory, TaskSourceKind}; const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4; +const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); +const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); pub trait Item { fn entry_id(&self, cx: &AppContext) -> Option; @@ -156,7 +166,8 @@ pub struct Project { default_prettier: DefaultPrettier, prettiers_per_worktree: HashMap>>, prettier_instances: HashMap, - runnables: Model, + tasks: Model, + hosted_project_id: Option, } pub enum LanguageServerToQuery { @@ -224,6 +235,7 @@ pub struct LanguageServerPromptRequest { pub level: PromptLevel, pub message: String, pub actions: Vec, + pub lsp_name: String, response_channel: Sender, } @@ -312,18 +324,6 @@ pub struct ProjectPath { pub path: Arc, } -#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)] -pub struct DiagnosticSummary { - pub error_count: usize, - pub warning_count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Location { - pub buffer: Model, - pub range: Range, -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { pub position: language::Anchor, @@ -435,78 +435,12 @@ impl Hover { #[derive(Default)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); -impl DiagnosticSummary { - fn new<'a, T: 'a>(diagnostics: impl IntoIterator>) -> Self { - let mut this = Self { - error_count: 0, - warning_count: 0, - }; - - for entry in diagnostics { - if entry.diagnostic.is_primary { - match entry.diagnostic.severity { - DiagnosticSeverity::ERROR => this.error_count += 1, - DiagnosticSeverity::WARNING => this.warning_count += 1, - _ => {} - } - } - } - - this - } - - pub fn is_empty(&self) -> bool { - self.error_count == 0 && self.warning_count == 0 - } - - pub fn to_proto( - &self, - language_server_id: LanguageServerId, - path: &Path, - ) -> proto::DiagnosticSummary { - proto::DiagnosticSummary { - path: path.to_string_lossy().to_string(), - language_server_id: language_server_id.0 as u64, - error_count: self.error_count as u32, - warning_count: self.warning_count as u32, - } - } -} - -#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct ProjectEntryId(usize); - -impl ProjectEntryId { - pub const MAX: Self = Self(usize::MAX); - - pub fn new(counter: &AtomicUsize) -> Self { - Self(counter.fetch_add(1, SeqCst)) - } - - pub fn from_proto(id: u64) -> Self { - Self(id as usize) - } - - pub fn to_proto(&self) -> u64 { - self.0 as u64 - } - - pub fn to_usize(&self) -> usize { - self.0 - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FormatTrigger { Save, Manual, } -struct ProjectLspAdapterDelegate { - project: Model, - http_client: Arc, -} - // Currently, formatting operations are represented differently depending on // whether they come from a language server or an external command. enum FormatOperation { @@ -619,7 +553,7 @@ impl Project { .detach(); let copilot_lsp_subscription = Copilot::global(cx).map(|copilot| subscribe_for_copilot_events(&copilot, cx)); - let runnables = Inventory::new(cx); + let tasks = Inventory::new(cx); Self { worktrees: Vec::new(), @@ -671,7 +605,8 @@ impl Project { default_prettier: DefaultPrettier::default(), prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), - runnables, + tasks, + hosted_project_id: None, } }) } @@ -682,20 +617,33 @@ impl Project { user_store: Model, languages: Arc, fs: Arc, - role: proto::ChannelRole, - mut cx: AsyncAppContext, + cx: AsyncAppContext, ) -> Result> { client.authenticate_and_connect(true, &cx).await?; - let subscription = client.subscribe_to_entity(remote_id)?; let response = client .request_envelope(proto::JoinProject { project_id: remote_id, }) .await?; + Self::from_join_project_response(response, None, client, user_store, languages, fs, cx) + .await + } + async fn from_join_project_response( + response: TypedEnvelope, + hosted_project_id: Option, + client: Arc, + user_store: Model, + languages: Arc, + fs: Arc, + mut cx: AsyncAppContext, + ) -> Result> { + let remote_id = response.payload.project_id; + let role = response.payload.role(); + let subscription = client.subscribe_to_entity(remote_id)?; let this = cx.new_model(|cx| { let replica_id = response.payload.replica_id as ReplicaId; - let runnables = Inventory::new(cx); + let tasks = Inventory::new(cx); // BIG CAUTION NOTE: The order in which we initialize fields here matters and it should match what's done in Self::local. // Otherwise, you might run into issues where worktree id on remote is different than what's on local host. // That's because Worktree's identifier is entity id, which should probably be changed. @@ -780,7 +728,8 @@ impl Project { default_prettier: DefaultPrettier::default(), prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), - runnables, + tasks, + hosted_project_id, }; this.set_role(role, cx); for worktree in worktrees { @@ -809,6 +758,31 @@ impl Project { Ok(this) } + pub async fn hosted( + hosted_project_id: HostedProjectId, + user_store: Model, + client: Arc, + languages: Arc, + fs: Arc, + cx: AsyncAppContext, + ) -> Result> { + let response = client + .request_envelope(proto::JoinHostedProject { + id: hosted_project_id.0, + }) + .await?; + Self::from_join_project_response( + response, + Some(hosted_project_id), + client, + user_store, + languages, + fs, + cx, + ) + .await + } + fn release(&mut self, cx: &mut AppContext) { match &self.client_state { ProjectClientState::Local => {} @@ -851,10 +825,13 @@ impl Project { root_paths: impl IntoIterator, cx: &mut gpui::TestAppContext, ) -> Model { + use clock::FakeSystemClock; + let mut languages = LanguageRegistry::test(); languages.set_executor(cx.executor()); + let clock = Arc::new(FakeSystemClock::default()); let http_client = util::http::FakeHttpClient::with_404_response(); - let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); + let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx)); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); let project = cx.update(|cx| { Project::local( @@ -912,10 +889,12 @@ impl Project { let current_lsp_settings = &self.current_lsp_settings; for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { let language = languages.iter().find_map(|l| { - let adapter = l - .lsp_adapters() + let adapter = self + .languages + .lsp_adapters(l) .iter() - .find(|adapter| &adapter.name == started_lsp_name)?; + .find(|adapter| &adapter.name == started_lsp_name)? + .clone(); Some((l, adapter)) }); if let Some((language, adapter)) = language { @@ -954,9 +933,11 @@ impl Project { let mut prettier_plugins_by_worktree = HashMap::default(); for (worktree, language, settings) in language_formatters_to_check { - if let Some(plugins) = - prettier_support::prettier_plugins_for_language(&language, &settings) - { + if let Some(plugins) = prettier_support::prettier_plugins_for_language( + &self.languages, + &language, + &settings, + ) { prettier_plugins_by_worktree .entry(worktree) .or_insert_with(|| HashSet::default()) @@ -1047,6 +1028,10 @@ impl Project { } } + pub fn hosted_project_id(&self) -> Option { + self.hosted_project_id + } + pub fn replica_id(&self) -> ReplicaId { match self.client_state { ProjectClientState::Remote { replica_id, .. } => replica_id, @@ -1063,8 +1048,8 @@ impl Project { cx.notify(); } - pub fn runnable_inventory(&self) -> &Model { - &self.runnables + pub fn task_inventory(&self) -> &Model { + &self.tasks } pub fn collaborators(&self) -> &HashMap { @@ -1076,7 +1061,7 @@ impl Project { } /// Collect all worktrees, including ones that don't appear in the project panel - pub fn worktrees<'a>(&'a self) -> impl 'a + DoubleEndedIterator> { + pub fn worktrees(&self) -> impl '_ + DoubleEndedIterator> { self.worktrees .iter() .filter_map(move |worktree| worktree.upgrade()) @@ -2112,7 +2097,7 @@ impl Project { } if let Some(language) = language { - for adapter in language.lsp_adapters() { + for adapter in self.languages.lsp_adapters(&language) { let language_id = adapter.language_ids.get(language.name().as_ref()).cloned(); let server = self .language_server_ids @@ -2183,10 +2168,12 @@ impl Project { let worktree_id = old_file.worktree_id(cx); let ids = &self.language_server_ids; - let language = buffer.language().cloned(); - let adapters = language.iter().flat_map(|language| language.lsp_adapters()); - for &server_id in adapters.flat_map(|a| ids.get(&(worktree_id, a.name.clone()))) { - buffer.update_diagnostics(server_id, Default::default(), cx); + if let Some(language) = buffer.language().cloned() { + for adapter in self.languages.lsp_adapters(&language) { + if let Some(server_id) = ids.get(&(worktree_id, adapter.name.clone())) { + buffer.update_diagnostics(*server_id, Default::default(), cx); + } + } } self.buffer_snapshots.remove(&buffer.remote_id()); @@ -2766,9 +2753,11 @@ impl Project { let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone(); let buffer_file = File::from_dyn(buffer_file.as_ref()); let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx)); - if let Some(prettier_plugins) = - prettier_support::prettier_plugins_for_language(&new_language, &settings) - { + if let Some(prettier_plugins) = prettier_support::prettier_plugins_for_language( + &self.languages, + &new_language, + &settings, + ) { self.install_default_prettier(worktree, prettier_plugins, cx); }; if let Some(file) = buffer_file { @@ -2791,14 +2780,14 @@ impl Project { return; } - for adapter in language.lsp_adapters() { + for adapter in self.languages.clone().lsp_adapters(&language) { self.start_language_server(worktree, adapter.clone(), language.clone(), cx); } } fn start_language_server( &mut self, - worktree: &Model, + worktree_handle: &Model, adapter: Arc, language: Arc, cx: &mut ModelContext, @@ -2807,7 +2796,7 @@ impl Project { return; } - let worktree = worktree.read(cx); + let worktree = worktree_handle.read(cx); let worktree_id = worktree.id(); let worktree_path = worktree.abs_path(); let key = (worktree_id, adapter.name.clone()); @@ -2821,7 +2810,7 @@ impl Project { language.clone(), adapter.clone(), Arc::clone(&worktree_path), - ProjectLspAdapterDelegate::new(self, cx), + ProjectLspAdapterDelegate::new(self, worktree_handle, cx), cx, ) { Some(pending_server) => pending_server, @@ -2831,7 +2820,7 @@ impl Project { let project_settings = ProjectSettings::get(Some((worktree_id.to_proto() as usize, Path::new(""))), cx); let lsp = project_settings.lsp.get(&adapter.name.0); - let override_options = lsp.map(|s| s.initialization_options.clone()).flatten(); + let override_options = lsp.and_then(|s| s.initialization_options.clone()); let server_id = pending_server.server_id; let container_dir = pending_server.container_dir.clone(); @@ -2875,6 +2864,14 @@ impl Project { return None; } + log::info!( + "retrying installation of language server {server_name:?} in {}s", + SERVER_REINSTALL_DEBOUNCE_TIMEOUT.as_secs() + ); + cx.background_executor() + .timer(SERVER_REINSTALL_DEBOUNCE_TIMEOUT) + .await; + let installation_test_binary = adapter .installation_test_binary(container_dir.to_path_buf()) .await; @@ -2957,6 +2954,7 @@ impl Project { })) } + #[allow(clippy::too_many_arguments)] async fn setup_and_insert_language_server( this: WeakModel, worktree_path: &Path, @@ -3011,6 +3009,7 @@ impl Project { cx.update(|cx| adapter.workspace_configuration(worktree_path, cx))?; let language_server = pending_server.task.await?; + let name = language_server.name(); language_server .on_notification::({ let adapter = adapter.clone(); @@ -3149,8 +3148,10 @@ impl Project { language_server .on_request::({ let this = this.clone(); + let name = name.to_string(); move |params, mut cx| { let this = this.clone(); + let name = name.to_string(); async move { if let Some(actions) = params.actions { let (tx, mut rx) = smol::channel::bounded(1); @@ -3163,6 +3164,7 @@ impl Project { message: params.message, actions, response_channel: tx, + lsp_name: name.clone(), }; if let Ok(_) = this.update(&mut cx, |_, cx| { @@ -3185,6 +3187,50 @@ impl Project { let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token.clone(); + language_server + .on_notification::({ + let this = this.clone(); + let name = name.to_string(); + move |params, mut cx| { + let this = this.clone(); + let name = name.to_string(); + if let Some(ref message) = params.message { + let message = message.trim(); + if !message.is_empty() { + let formatted_message = format!( + "Language server {name} (id {server_id}) status update: {message}" + ); + match params.health { + ServerHealthStatus::Ok => log::info!("{}", formatted_message), + ServerHealthStatus::Warning => log::warn!("{}", formatted_message), + ServerHealthStatus::Error => { + log::error!("{}", formatted_message); + let (tx, _rx) = smol::channel::bounded(1); + let request = LanguageServerPromptRequest { + level: PromptLevel::Critical, + message: params.message.unwrap_or_default(), + actions: Vec::new(), + response_channel: tx, + lsp_name: name.clone(), + }; + let _ = this + .update(&mut cx, |_, cx| { + cx.emit(Event::LanguageServerPrompt(request)); + }) + .ok(); + } + ServerHealthStatus::Other(status) => { + log::info!( + "Unknown server health: {status}\n{formatted_message}" + ) + } + } + } + } + } + }) + .detach(); + language_server .on_notification::(move |params, mut cx| { if let Some(this) = this.upgrade() { @@ -3200,6 +3246,7 @@ impl Project { } }) .detach(); + let mut initialization_options = adapter.adapter.initialization_options(); match (&mut initialization_options, override_options) { (Some(initialization_options), Some(override_options)) => { @@ -3292,7 +3339,11 @@ impl Project { }; if file.worktree.read(cx).id() != key.0 - || !language.lsp_adapters().iter().any(|a| a.name == key.1) + || !self + .languages + .lsp_adapters(&language) + .iter() + .any(|a| a.name == key.1) { continue; } @@ -3361,7 +3412,8 @@ impl Project { ) -> Task> { let key = (worktree_id, adapter_name); if let Some(server_id) = self.language_server_ids.remove(&key) { - log::info!("stopping language server {}", key.1 .0); + let name = key.1 .0; + log::info!("stopping language server {name}"); // Remove other entries for this language server as well let mut orphaned_worktrees = vec![worktree_id]; @@ -3395,27 +3447,8 @@ impl Project { let server_state = self.language_servers.remove(&server_id); cx.emit(Event::LanguageServerRemoved(server_id)); - cx.spawn(move |this, mut cx| async move { - let server = match server_state { - Some(LanguageServerState::Starting(task)) => task.await, - Some(LanguageServerState::Running { server, .. }) => Some(server), - None => None, - }; - - if let Some(server) = server { - if let Some(shutdown) = server.shutdown() { - shutdown.await; - } - } - - if let Some(this) = this.upgrade() { - this.update(&mut cx, |this, cx| { - this.language_server_statuses.remove(&server_id); - cx.notify(); - }) - .ok(); - } - + cx.spawn(move |this, cx| async move { + Self::shutdown_language_server(this, server_state, name, server_id, cx).await; orphaned_worktrees }) } else { @@ -3423,6 +3456,52 @@ impl Project { } } + async fn shutdown_language_server( + this: WeakModel, + server_state: Option, + name: Arc, + server_id: LanguageServerId, + mut cx: AsyncAppContext, + ) { + let server = match server_state { + Some(LanguageServerState::Starting(task)) => { + let mut timer = cx + .background_executor() + .timer(SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT) + .fuse(); + + select! { + server = task.fuse() => server, + _ = timer => { + log::info!( + "timeout waiting for language server {} to finish launching before stopping", + name + ); + None + }, + } + } + + Some(LanguageServerState::Running { server, .. }) => Some(server), + + None => None, + }; + + if let Some(server) = server { + if let Some(shutdown) = server.shutdown() { + shutdown.await; + } + } + + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| { + this.language_server_statuses.remove(&server_id); + cx.notify(); + }) + .ok(); + } + } + pub fn restart_language_servers_for_buffers( &mut self, buffers: impl IntoIterator>, @@ -3457,8 +3536,10 @@ impl Project { ) { let worktree_id = worktree.read(cx).id(); - let stop_tasks = language - .lsp_adapters() + let stop_tasks = self + .languages + .clone() + .lsp_adapters(&language) .iter() .map(|adapter| { let stop_task = self.stop_language_server(worktree_id, adapter.name.clone(), cx); @@ -3630,7 +3711,7 @@ impl Project { proto::LspWorkStart { token, message: report.message, - percentage: report.percentage.map(|p| p as u32), + percentage: report.percentage, }, ), }) @@ -3656,7 +3737,7 @@ impl Project { proto::LspWorkProgress { token, message: report.message, - percentage: report.percentage.map(|p| p as u32), + percentage: report.percentage, }, ), }) @@ -4159,17 +4240,13 @@ impl Project { cx: &mut ModelContext, ) -> Task> { if self.is_local() { - let mut buffers_with_paths_and_servers = buffers + let mut buffers_with_paths = buffers .into_iter() .filter_map(|buffer_handle| { let buffer = buffer_handle.read(cx); let file = File::from_dyn(buffer.file())?; let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx)); - let (adapter, server) = self - .primary_language_server_for_buffer(buffer, cx) - .map(|(a, s)| (Some(a.clone()), Some(s.clone()))) - .unwrap_or((None, None)); - Some((buffer_handle, buffer_abs_path, adapter, server)) + Some((buffer_handle, buffer_abs_path)) }) .collect::>(); @@ -4177,7 +4254,7 @@ impl Project { // Do not allow multiple concurrent formatting requests for the // same buffer. project.update(&mut cx, |this, cx| { - buffers_with_paths_and_servers.retain(|(buffer, _, _, _)| { + buffers_with_paths.retain(|(buffer, _)| { this.buffers_being_formatted .insert(buffer.read(cx).remote_id()) }); @@ -4186,10 +4263,10 @@ impl Project { let _cleanup = defer({ let this = project.clone(); let mut cx = cx.clone(); - let buffers = &buffers_with_paths_and_servers; + let buffers = &buffers_with_paths; move || { this.update(&mut cx, |this, cx| { - for (buffer, _, _, _) in buffers { + for (buffer, _) in buffers { this.buffers_being_formatted .remove(&buffer.read(cx).remote_id()); } @@ -4199,9 +4276,14 @@ impl Project { }); let mut project_transaction = ProjectTransaction::default(); - for (buffer, buffer_abs_path, lsp_adapter, language_server) in - &buffers_with_paths_and_servers - { + for (buffer, buffer_abs_path) in &buffers_with_paths { + let adapters_and_servers: Vec<_> = project.update(&mut cx, |project, cx| { + project + .language_servers_for_buffer(&buffer.read(cx), cx) + .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) + .collect() + })?; + let settings = buffer.update(&mut cx, |buffer, cx| { language_settings(buffer.language(), buffer.file(), cx).clone() })?; @@ -4232,9 +4314,7 @@ impl Project { buffer.end_transaction(cx) })?; - if let (Some(lsp_adapter), Some(language_server)) = - (lsp_adapter, language_server) - { + for (lsp_adapter, language_server) in adapters_and_servers.iter() { // Apply the code actions on let code_actions: Vec = settings .code_actions_on_format @@ -4248,6 +4328,7 @@ impl Project { }) .collect(); + #[allow(clippy::nonminimal_bool)] if !code_actions.is_empty() && !(trigger == FormatTrigger::Save && settings.format_on_save == FormatOnSave::Off) @@ -4271,6 +4352,7 @@ impl Project { if edit.changes.is_none() && edit.document_changes.is_none() { continue; } + let new = Self::deserialize_workspace_edit( project .upgrade() @@ -4314,17 +4396,23 @@ impl Project { } } - // Apply language-specific formatting using either a language server + // Apply language-specific formatting using either the primary language server // or external command. + let primary_language_server = adapters_and_servers + .first() + .cloned() + .map(|(_, lsp)| lsp.clone()); + let server_and_buffer = primary_language_server + .as_ref() + .zip(buffer_abs_path.as_ref()); + let mut format_operation = None; match (&settings.formatter, &settings.format_on_save) { (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {} (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off) | (_, FormatOnSave::LanguageServer) => { - if let Some((language_server, buffer_abs_path)) = - language_server.as_ref().zip(buffer_abs_path.as_ref()) - { + if let Some((language_server, buffer_abs_path)) = server_and_buffer { format_operation = Some(FormatOperation::Lsp( Self::format_via_lsp( &project, @@ -4368,7 +4456,7 @@ impl Project { { format_operation = Some(new_operation); } else if let Some((language_server, buffer_abs_path)) = - language_server.as_ref().zip(buffer_abs_path.as_ref()) + server_and_buffer { format_operation = Some(FormatOperation::Lsp( Self::format_via_lsp( @@ -4616,6 +4704,7 @@ impl Project { cx, ) } + pub fn type_definition( &self, buffer: &Model, @@ -4623,10 +4712,33 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.type_definition_impl(buffer, position, cx) } + fn implementation_impl( + &self, + buffer: &Model, + position: PointUtf16, + cx: &mut ModelContext, + ) -> Task>> { + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetImplementation { position }, + cx, + ) + } + + pub fn implementation( + &self, + buffer: &Model, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.implementation_impl(buffer, position, cx) + } + fn references_impl( &self, buffer: &Model, @@ -4779,14 +4891,15 @@ impl Project { .languages .language_for_file(&project_path.path, None) .unwrap_or_else(move |_| adapter_language); - let language_server_name = adapter.name.clone(); + let adapter = adapter.clone(); Some(async move { let language = language.await; - let label = - language.label_for_symbol(&symbol_name, symbol_kind).await; + let label = adapter + .label_for_symbol(&symbol_name, symbol_kind, &language) + .await; Symbol { - language_server_name, + language_server_name: adapter.name.clone(), source_worktree_id, path: project_path, label: label.unwrap_or_else(|| { @@ -5280,103 +5393,6 @@ impl Project { self.code_actions_impl(buffer_handle, range, cx) } - pub fn apply_code_actions_on_save( - &self, - buffers: HashSet>, - cx: &mut ModelContext, - ) -> Task> { - if !self.is_local() { - return Task::ready(Ok(Default::default())); - } - - let buffers_with_adapters_and_servers = buffers - .into_iter() - .filter_map(|buffer_handle| { - let buffer = buffer_handle.read(cx); - self.primary_language_server_for_buffer(buffer, cx) - .map(|(a, s)| (buffer_handle, a.clone(), s.clone())) - }) - .collect::>(); - - cx.spawn(move |this, mut cx| async move { - for (buffer_handle, lsp_adapter, lang_server) in buffers_with_adapters_and_servers { - let actions = this - .update(&mut cx, |this, cx| { - let buffer = buffer_handle.read(cx); - let kinds: Vec = - language_settings(buffer.language(), buffer.file(), cx) - .code_actions_on_format - .iter() - .flat_map(|(kind, enabled)| { - if *enabled { - Some(kind.clone().into()) - } else { - None - } - }) - .collect(); - if kinds.is_empty() { - return Task::ready(Ok(vec![])); - } - - this.request_lsp( - buffer_handle.clone(), - LanguageServerToQuery::Other(lang_server.server_id()), - GetCodeActions { - range: text::Anchor::MIN..text::Anchor::MAX, - kinds: Some(kinds), - }, - cx, - ) - })? - .await?; - - for action in actions { - if let Some(edit) = action.lsp_action.edit { - if edit.changes.is_some() || edit.document_changes.is_some() { - return Self::deserialize_workspace_edit( - this.upgrade().ok_or_else(|| anyhow!("no app present"))?, - edit, - true, - lsp_adapter.clone(), - lang_server.clone(), - &mut cx, - ) - .await; - } - } - - if let Some(command) = action.lsp_action.command { - this.update(&mut cx, |this, _| { - this.last_workspace_edits_by_language_server - .remove(&lang_server.server_id()); - })?; - - let result = lang_server - .request::(lsp::ExecuteCommandParams { - command: command.command, - arguments: command.arguments.unwrap_or_default(), - ..Default::default() - }) - .await; - - if let Err(err) = result { - // TODO: LSP ERROR - return Err(err); - } - - return Ok(this.update(&mut cx, |this, _| { - this.last_workspace_edits_by_language_server - .remove(&lang_server.server_id()) - .unwrap_or_default() - })?); - } - } - } - Ok(ProjectTransaction::default()) - }) - } - pub fn apply_code_action( &self, buffer_handle: Model, @@ -5453,11 +5469,11 @@ impl Project { return Err(err); } - return Ok(this.update(&mut cx, |this, _| { + return this.update(&mut cx, |this, _| { this.last_workspace_edits_by_language_server .remove(&lang_server.server_id()) .unwrap_or_default() - })?); + }); } Ok(ProjectTransaction::default()) @@ -5854,7 +5870,6 @@ impl Project { let range_start = range.start; let range_end = range.end; let buffer_id = buffer.remote_id().into(); - let buffer_version = buffer.version().clone(); let lsp_request = InlayHints { range }; if self.is_local() { @@ -5880,23 +5895,22 @@ impl Project { buffer_id, start: Some(serialize_anchor(&range_start)), end: Some(serialize_anchor(&range_end)), - version: serialize_version(&buffer_version), + version: serialize_version(&buffer_handle.read(cx).version()), }; cx.spawn(move |project, cx| async move { let response = client .request(request) .await .context("inlay hints proto request")?; - let hints_request_result = LspCommand::response_from_proto( + LspCommand::response_from_proto( lsp_request, response, project.upgrade().ok_or_else(|| anyhow!("No project"))?, buffer_handle.clone(), - cx, + cx.clone(), ) - .await; - - hints_request_result.context("inlay hints proto response conversion") + .await + .context("inlay hints proto response conversion") }) } else { Task::ready(Err(anyhow!("project does not have a remote id"))) @@ -6215,6 +6229,7 @@ impl Project { } /// Pick paths that might potentially contain a match of a given search query. + #[allow(clippy::too_many_arguments)] async fn background_search( unnamed_buffers: Vec>, opened_buffers: HashMap, (Model, BufferSnapshot)>, @@ -6714,6 +6729,10 @@ impl Project { }) .detach(); + self.task_inventory().update(cx, |inventory, _| { + inventory.remove_worktree_sources(id_to_remove); + }); + self.worktrees.retain(|worktree| { if let Some(worktree) = worktree.upgrade() { let id = worktree.read(cx).id(); @@ -7029,14 +7048,14 @@ impl Project { .spawn(async move { future_buffers .into_iter() - .filter_map(|e| e) + .flatten() .chain(current_buffers) .filter_map(|(buffer, path)| { let (work_directory, repo) = snapshot.repository_and_work_directory_for_path(&path)?; let repo = snapshot.get_local_repo(&repo)?; let relative_path = path.strip_prefix(&work_directory).ok()?; - let base_text = repo.repo_ptr.lock().load_index_text(relative_path); + let base_text = repo.load_index_text(relative_path); Some((buffer, base_text)) }) .collect::>() @@ -7071,32 +7090,66 @@ impl Project { changes: &UpdatedEntriesSet, cx: &mut ModelContext, ) { + if worktree.read(cx).as_local().is_none() { + return; + } let project_id = self.remote_id(); let worktree_id = worktree.entity_id(); - let worktree = worktree.read(cx).as_local().unwrap(); - let remote_worktree_id = worktree.id(); + let remote_worktree_id = worktree.read(cx).id(); let mut settings_contents = Vec::new(); for (path, _, change) in changes.iter() { - if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) { + let removed = change == &PathChange::Removed; + let abs_path = match worktree.read(cx).absolutize(path) { + Ok(abs_path) => abs_path, + Err(e) => { + log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}"); + continue; + } + }; + + if abs_path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) { let settings_dir = Arc::from( path.ancestors() .nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count()) .unwrap(), ); let fs = self.fs.clone(); - let removed = *change == PathChange::Removed; - let abs_path = worktree.absolutize(path); settings_contents.push(async move { ( settings_dir, if removed { None } else { - Some(async move { fs.load(&abs_path?).await }.await) + Some(async move { fs.load(&abs_path).await }.await) }, ) }); + } else if abs_path.ends_with(&*LOCAL_TASKS_RELATIVE_PATH) { + self.task_inventory().update(cx, |task_inventory, cx| { + if removed { + task_inventory.remove_local_static_source(&abs_path); + } else { + let fs = self.fs.clone(); + let task_abs_path = abs_path.clone(); + task_inventory.add_source( + TaskSourceKind::Worktree { + id: remote_worktree_id, + abs_path, + }, + |cx| { + let tasks_file_rx = + watch_config_file(&cx.background_executor(), fs, task_abs_path); + StaticSource::new( + format!("local_tasks_for_workspace_{remote_worktree_id}"), + tasks_file_rx, + cx, + ) + }, + cx, + ); + } + }) } } @@ -7116,7 +7169,7 @@ impl Project { .set_local_settings( worktree_id.as_u64() as usize, directory.clone(), - file_content.as_ref().map(String::as_str), + file_content.as_deref(), cx, ) .log_err(); @@ -7417,7 +7470,7 @@ impl Project { .set_local_settings( worktree.entity_id().as_u64() as usize, PathBuf::from(&envelope.payload.path).into(), - envelope.payload.content.as_ref().map(String::as_str), + envelope.payload.content.as_deref(), cx, ) .log_err(); @@ -7862,11 +7915,12 @@ impl Project { })?; this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))? .await?; - Ok(buffer.update(&mut cx, |buffer, _| proto::BufferSaved { + buffer.update(&mut cx, |buffer, _| proto::BufferSaved { project_id, buffer_id: buffer_id.into(), version: serialize_version(buffer.saved_version()), mtime: Some(buffer.saved_mtime().into()), + saved_undo_top, })?) } @@ -8033,6 +8087,7 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result { + let languages = this.update(&mut cx, |this, _| this.languages.clone())?; let (buffer, completion) = this.update(&mut cx, |this, cx| { let buffer_id = BufferId::new(envelope.payload.buffer_id)?; let buffer = this @@ -8047,6 +8102,7 @@ impl Project { .completion .ok_or_else(|| anyhow!("invalid completion"))?, language.cloned(), + &languages, ); Ok::<_, anyhow::Error>((buffer, completion)) })??; @@ -8178,20 +8234,12 @@ impl Project { .and_then(|buffer| buffer.upgrade()) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) })??; - let buffer_version = deserialize_version(&envelope.payload.version); - buffer .update(&mut cx, |buffer, _| { - buffer.wait_for_version(buffer_version.clone()) + buffer.wait_for_version(deserialize_version(&envelope.payload.version)) })? .await - .with_context(|| { - format!( - "waiting for version {:?} for buffer {}", - buffer_version, - buffer.entity_id() - ) - })?; + .with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?; let start = envelope .payload @@ -8205,14 +8253,20 @@ impl Project { .context("missing range end")?; let buffer_hints = this .update(&mut cx, |project, cx| { - project.inlay_hints(buffer, start..end, cx) + project.inlay_hints(buffer.clone(), start..end, cx) })? .await .context("inlay hints fetch")?; - Ok(this.update(&mut cx, |project, cx| { - InlayHints::response_to_proto(buffer_hints, project, sender_id, &buffer_version, cx) - })?) + this.update(&mut cx, |project, cx| { + InlayHints::response_to_proto( + buffer_hints, + project, + sender_id, + &buffer.read(cx).version(), + cx, + ) + }) } async fn handle_resolve_inlay_hint( @@ -8287,10 +8341,14 @@ impl Project { cx.clone(), ) .await?; - let buffer_version = buffer_handle.update(&mut cx, |buffer, _| buffer.version())?; let response = this .update(&mut cx, |this, cx| { - this.request_lsp(buffer_handle, LanguageServerToQuery::Primary, request, cx) + this.request_lsp( + buffer_handle.clone(), + LanguageServerToQuery::Primary, + request, + cx, + ) })? .await?; this.update(&mut cx, |this, cx| { @@ -8298,7 +8356,7 @@ impl Project { response, this, sender_id, - &buffer_version, + &buffer_handle.read(cx).version(), cx, )) })? @@ -8772,6 +8830,9 @@ impl Project { .language_for_file(&path.path, None) .await .log_err(); + let adapter = language + .as_ref() + .and_then(|language| languages.lsp_adapters(language).first().cloned()); Ok(Symbol { language_server_name: LanguageServerName( serialized_symbol.language_server_name.into(), @@ -8779,10 +8840,10 @@ impl Project { source_worktree_id, path, label: { - match language { - Some(language) => { - language - .label_for_symbol(&serialized_symbol.name, kind) + match language.as_ref().zip(adapter.as_ref()) { + Some((language, adapter)) => { + adapter + .label_for_symbol(&serialized_symbol.name, kind, language) .await } None => None, @@ -9032,6 +9093,17 @@ impl Project { self.supplementary_language_servers.iter() } + pub fn language_server_adapter_for_id( + &self, + id: LanguageServerId, + ) -> Option> { + if let Some(LanguageServerState::Running { adapter, .. }) = self.language_servers.get(&id) { + Some(adapter.clone()) + } else { + None + } + } + pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) { Some(server.clone()) @@ -9082,8 +9154,8 @@ impl Project { ) -> Vec { if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { let worktree_id = file.worktree_id(cx); - language - .lsp_adapters() + self.languages + .lsp_adapters(&language) .iter() .flat_map(|adapter| { let key = (worktree_id, adapter.name.clone()); @@ -9133,7 +9205,7 @@ fn subscribe_for_copilot_events( ) } -fn glob_literal_prefix<'a>(glob: &'a str) -> &'a str { +fn glob_literal_prefix(glob: &str) -> &str { let mut literal_end = 0; for (i, part) in glob.split(path::MAIN_SEPARATOR).enumerate() { if part.contains(&['*', '?', '{', '}']) { @@ -9245,15 +9317,27 @@ impl> From<(WorktreeId, P)> for ProjectPath { } } +struct ProjectLspAdapterDelegate { + project: Model, + worktree: worktree::Snapshot, + fs: Arc, + http_client: Arc, + language_registry: Arc, +} + impl ProjectLspAdapterDelegate { - fn new(project: &Project, cx: &ModelContext) -> Arc { + fn new(project: &Project, worktree: &Model, cx: &ModelContext) -> Arc { Arc::new(Self { project: cx.handle(), + worktree: worktree.read(cx).snapshot(), + fs: project.fs.clone(), http_client: project.client.http_client(), + language_registry: project.languages.clone(), }) } } +#[async_trait] impl LspAdapterDelegate for ProjectLspAdapterDelegate { fn show_notification(&self, message: &str, cx: &mut AppContext) { self.project @@ -9263,6 +9347,52 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate { fn http_client(&self) -> Arc { self.http_client.clone() } + + async fn which_command(&self, command: OsString) -> Option<(PathBuf, HashMap)> { + let worktree_abs_path = self.worktree.abs_path(); + + let shell_env = load_shell_environment(&worktree_abs_path) + .await + .with_context(|| { + format!("failed to determine load login shell environment in {worktree_abs_path:?}") + }) + .log_err(); + + if let Some(shell_env) = shell_env { + let shell_path = shell_env.get("PATH"); + match which::which_in(&command, shell_path, &worktree_abs_path) { + Ok(command_path) => Some((command_path, shell_env)), + Err(error) => { + log::warn!( + "failed to determine path for command {:?} in shell PATH {:?}: {error}", + command.to_string_lossy(), + shell_path.map(String::as_str).unwrap_or("") + ); + None + } + } + } else { + None + } + } + + fn update_status( + &self, + server_name: LanguageServerName, + status: language::LanguageServerBinaryStatus, + ) { + self.language_registry + .update_lsp_status(server_name, status); + } + + async fn read_text_file(&self, path: PathBuf) -> Result { + if self.worktree.entry_for_path(&path).is_none() { + return Err(anyhow!("no such path {path:?}")); + } + let path = self.worktree.absolutize(path.as_ref())?; + let content = self.fs.load(&path).await?; + Ok(content) + } } fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { @@ -9299,7 +9429,7 @@ fn relativize_path(base: &Path, path: &Path) -> PathBuf { } (None, _) => components.push(Component::ParentDir), (Some(a), Some(b)) if components.is_empty() && a == b => (), - (Some(a), Some(b)) if b == Component::CurDir => components.push(a), + (Some(a), Some(Component::CurDir)) => components.push(a), (Some(a), Some(_)) => { components.push(Component::ParentDir); for _ in base_components { @@ -9370,3 +9500,70 @@ fn include_text(server: &lsp::LanguageServer) -> bool { }) .unwrap_or(false) } + +async fn load_shell_environment(dir: &Path) -> Result> { + let marker = "ZED_SHELL_START"; + let shell = env::var("SHELL").context( + "SHELL environment variable is not assigned so we can't source login environment variables", + )?; + + // What we're doing here is to spawn a shell and then `cd` into + // the project directory to get the env in there as if the user + // `cd`'d into it. We do that because tools like direnv, asdf, ... + // hook into `cd` and only set up the env after that. + // + // In certain shells we need to execute additional_command in order to + // trigger the behavior of direnv, etc. + // + // + // The `exit 0` is the result of hours of debugging, trying to find out + // why running this command here, without `exit 0`, would mess + // up signal process for our process so that `ctrl-c` doesn't work + // anymore. + // + // We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would + // do that, but it does, and `exit 0` helps. + let additional_command = PathBuf::from(&shell) + .file_name() + .and_then(|f| f.to_str()) + .and_then(|shell| match shell { + "fish" => Some("emit fish_prompt;"), + _ => None, + }); + + let command = format!( + "cd {dir:?};{} echo {marker}; /usr/bin/env -0; exit 0;", + additional_command.unwrap_or("") + ); + + let output = smol::process::Command::new(&shell) + .args(["-i", "-c", &command]) + .output() + .await + .context("failed to spawn login shell to source login environment variables")?; + + anyhow::ensure!( + output.status.success(), + "login shell exited with error {:?}", + output.status + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + let env_output_start = stdout.find(marker).ok_or_else(|| { + anyhow!( + "failed to parse output of `env` command in login shell: {}", + stdout + ) + })?; + + let mut parsed_env = HashMap::default(); + let env_output = &stdout[env_output_start + marker.len()..]; + for line in env_output.split_terminator('\0') { + if let Some(separator_index) = line.find('=') { + let key = line[..separator_index].to_string(); + let value = line[separator_index + 1..].to_string(); + parsed_env.insert(key, value); + } + } + Ok(parsed_env) +} diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index fcef76273d..1af8fca291 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -45,6 +45,7 @@ async fn test_block_via_smol(cx: &mut gpui::TestAppContext) { task.await; } +#[cfg(not(windows))] #[gpui::test] async fn test_symlinks(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -94,14 +95,24 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) "/the-root", json!({ ".zed": { - "settings.json": r#"{ "tab_size": 8 }"# + "settings.json": r#"{ "tab_size": 8 }"#, + "tasks.json": r#"[{ + "label": "cargo check", + "command": "cargo", + "args": ["check", "--all"] + },]"#, }, "a": { "a.rs": "fn a() {\n A\n}" }, "b": { ".zed": { - "settings.json": r#"{ "tab_size": 2 }"# + "settings.json": r#"{ "tab_size": 2 }"#, + "tasks.json": r#"[{ + "label": "cargo check", + "command": "cargo", + "args": ["check"] + },]"#, }, "b.rs": "fn b() {\n B\n}" } @@ -139,6 +150,38 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) assert_eq!(settings_a.tab_size.get(), 8); assert_eq!(settings_b.tab_size.get(), 2); + + let workree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); + let all_tasks = project + .update(cx, |project, cx| { + project.task_inventory().update(cx, |inventory, cx| { + inventory.list_tasks(None, None, false, cx) + }) + }) + .into_iter() + .map(|(source_kind, task)| (source_kind, task.name().to_string())) + .collect::>(); + assert_eq!( + all_tasks, + vec![ + ( + TaskSourceKind::Worktree { + id: workree_id, + abs_path: PathBuf::from("/the-root/.zed/tasks.json") + }, + "cargo check".to_string() + ), + ( + TaskSourceKind::Worktree { + id: workree_id, + abs_path: PathBuf::from("/the-root/b/.zed/tasks.json") + }, + "cargo check".to_string() + ), + ] + ); }); } @@ -146,55 +189,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut rust_language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut json_language = Language::new( - LanguageConfig { - name: "JSON".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["json".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - ); - let mut fake_rust_servers = rust_language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-rust-language-server", - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), "::".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - let mut fake_json_servers = json_language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-json-language-server", - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/the-root", @@ -208,6 +202,36 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + + let mut fake_rust_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: "the-rust-language-server", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), "::".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }, + ); + let mut fake_json_servers = language_registry.register_fake_lsp_adapter( + "JSON", + FakeLspAdapter { + name: "the-json-language-server", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }, + ); // Open a buffer without an associated language server. let toml_buffer = project @@ -230,10 +254,8 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { // Now we add the languages to the project, and ensure they get assigned to all // the relevant open buffers. - project.update(cx, |project, _| { - project.languages.add(Arc::new(json_language)); - project.languages.add(Arc::new(rust_language)); - }); + language_registry.add(json_lang()); + language_registry.add(rust_lang()); cx.executor().run_until_parked(); rust_buffer.update(cx, |buffer, _| { assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into())); @@ -538,24 +560,6 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-language-server", - ..Default::default() - })) - .await; - let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/the-root", @@ -587,9 +591,16 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon .await; let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages.add(Arc::new(language)); - }); + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: "the-language-server", + ..Default::default() + }, + ); + cx.executor().run_until_parked(); // Start the language server by opening a buffer with a compatible file extension. @@ -976,24 +987,6 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { init_test(cx); let progress_token = "the-progress-token"; - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - disk_based_diagnostics_progress_token: Some(progress_token.into()), - disk_based_diagnostics_sources: vec!["disk".into()], - ..Default::default() - })) - .await; let fs = FakeFs::new(cx.executor()); fs.insert_tree( @@ -1006,7 +999,18 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + disk_based_diagnostics_progress_token: Some(progress_token.into()), + disk_based_diagnostics_sources: vec!["disk".into()], + ..Default::default() + }, + ); + let worktree_id = project.update(cx, |p, cx| p.worktrees().next().unwrap().read(cx).id()); // Cause worktree to start the fake language server @@ -1112,29 +1116,23 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC init_test(cx); let progress_token = "the-progress-token"; - let mut language = Language::new( - LanguageConfig { - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - disk_based_diagnostics_sources: vec!["disk".into()], - disk_based_diagnostics_progress_token: Some(progress_token.into()), - ..Default::default() - })) - .await; let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "" })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: "the-language-server", + disk_based_diagnostics_sources: vec!["disk".into()], + disk_based_diagnostics_progress_token: Some(progress_token.into()), + ..Default::default() + }, + ); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) @@ -1196,27 +1194,15 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut language = Language::new( - LanguageConfig { - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - ..Default::default() - })) - .await; - let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "x" })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = + language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default()); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) @@ -1288,28 +1274,15 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut language = Language::new( - LanguageConfig { - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-lsp", - ..Default::default() - })) - .await; - let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "" })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + + language_registry.add(rust_lang()); + let mut fake_servers = + language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default()); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) @@ -1340,50 +1313,29 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut rust = Language::new( - LanguageConfig { - name: Arc::from("Rust"), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - ); - let mut fake_rust_servers = rust - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "rust-lsp", - ..Default::default() - })) - .await; - let mut js = Language::new( - LanguageConfig { - name: Arc::from("JavaScript"), - matcher: LanguageMatcher { - path_suffixes: vec!["js".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - ); - let mut fake_js_servers = js - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "js-lsp", - ..Default::default() - })) - .await; - let fs = FakeFs::new(cx.executor()); fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" })) .await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages.add(Arc::new(rust)); - project.languages.add(Arc::new(js)); - }); + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + + let mut fake_rust_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: "rust-lsp", + ..Default::default() + }, + ); + let mut fake_js_servers = language_registry.register_fake_lsp_adapter( + "JavaScript", + FakeLspAdapter { + name: "js-lsp", + ..Default::default() + }, + ); + language_registry.add(rust_lang()); + language_registry.add(js_lang()); let _rs_buffer = project .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) @@ -1475,24 +1427,6 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - disk_based_diagnostics_sources: vec!["disk".into()], - ..Default::default() - })) - .await; - let text = " fn a() { A } fn b() { BB } @@ -1504,7 +1438,16 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { fs.insert_tree("/dir", json!({ "a.rs": text })).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + disk_based_diagnostics_sources: vec!["disk".into()], + ..Default::default() + }, + ); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) @@ -1889,19 +1832,6 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await; - let text = " fn a() { f1(); @@ -1925,7 +1855,12 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = + language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default()); + let buffer = project .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) .await @@ -2279,19 +2214,6 @@ fn chunks_with_diagnostics( async fn test_definition(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await; - let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", @@ -2303,7 +2225,11 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = + language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default()); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) @@ -2383,30 +2309,6 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut language = Language::new( - LanguageConfig { - name: "TypeScript".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["ts".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_typescript::language_typescript()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", @@ -2417,7 +2319,23 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(typescript_lang()); + let mut fake_language_servers = language_registry.register_fake_lsp_adapter( + "TypeScript", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }, + ); + let buffer = project .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) .await @@ -2483,30 +2401,6 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut language = Language::new( - LanguageConfig { - name: "TypeScript".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["ts".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_typescript::language_typescript()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", @@ -2517,7 +2411,23 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(typescript_lang()); + let mut fake_language_servers = language_registry.register_fake_lsp_adapter( + "TypeScript", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }, + ); + let buffer = project .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) .await @@ -2552,19 +2462,6 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut language = Language::new( - LanguageConfig { - name: "TypeScript".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["ts".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", @@ -2575,7 +2472,12 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(typescript_lang()); + let mut fake_language_servers = + language_registry.register_fake_lsp_adapter("TypeScript", Default::default()); + let buffer = project .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) .await @@ -2861,16 +2763,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) { let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let languages = project.update(cx, |project, _| project.languages().clone()); - languages.register_native_grammars([("rust", tree_sitter_rust::language())]); - languages.register_test_language(LanguageConfig { - name: "Rust".into(), - grammar: Some("rust".into()), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".into()], - ..Default::default() - }, - ..Default::default() - }); + languages.add(rust_lang()); let buffer = project.update(cx, |project, cx| { project.create_buffer("", None, cx).unwrap() @@ -3690,30 +3583,6 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { async fn test_rename(cx: &mut gpui::TestAppContext) { init_test(cx); - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { - prepare_provider: Some(true), - work_done_progress_options: Default::default(), - })), - ..Default::default() - }, - ..Default::default() - })) - .await; - let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", @@ -3725,7 +3594,23 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: Default::default(), + })), + ..Default::default() + }, + ..Default::default() + }, + ); + let buffer = project .update(cx, |project, cx| { project.open_local_buffer("/dir/one.rs", cx) @@ -4432,3 +4317,59 @@ fn init_test(cx: &mut gpui::TestAppContext) { Project::init_settings(cx); }); } + +fn json_lang() -> Arc { + Arc::new(Language::new( + LanguageConfig { + name: "JSON".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["json".to_string()], + ..Default::default() + }, + ..Default::default() + }, + None, + )) +} + +fn js_lang() -> Arc { + Arc::new(Language::new( + LanguageConfig { + name: Arc::from("JavaScript"), + matcher: LanguageMatcher { + path_suffixes: vec!["js".to_string()], + ..Default::default() + }, + ..Default::default() + }, + None, + )) +} + +fn rust_lang() -> Arc { + Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )) +} + +fn typescript_lang() -> Arc { + Arc::new(Language::new( + LanguageConfig { + name: "TypeScript".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_typescript::language_typescript()), + )) +} diff --git a/crates/project/src/runnable_inventory.rs b/crates/project/src/runnable_inventory.rs deleted file mode 100644 index 52867acc49..0000000000 --- a/crates/project/src/runnable_inventory.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Project-wide storage of the runnables available, capable of updating itself from the sources set. - -use std::{path::Path, sync::Arc}; - -use gpui::{AppContext, Context, Model, ModelContext, Subscription}; -use runnable::{Runnable, RunnableId, Source}; - -/// Inventory tracks available runnables for a given project. -pub struct Inventory { - sources: Vec, - pub last_scheduled_runnable: Option, -} - -struct SourceInInventory { - source: Model>, - _subscription: Subscription, -} - -impl Inventory { - pub(crate) fn new(cx: &mut AppContext) -> Model { - cx.new_model(|_| Self { - sources: Vec::new(), - last_scheduled_runnable: None, - }) - } - - /// Registers a new runnables source, that would be fetched for available runnables. - pub fn add_source(&mut self, source: Model>, cx: &mut ModelContext) { - let _subscription = cx.observe(&source, |_, _, cx| { - cx.notify(); - }); - let source = SourceInInventory { - source, - _subscription, - }; - self.sources.push(source); - cx.notify(); - } - - /// Pulls its sources to list runanbles for the path given (up to the source to decide what to return for no path). - pub fn list_runnables( - &self, - path: Option<&Path>, - cx: &mut AppContext, - ) -> Vec> { - let mut runnables = Vec::new(); - for source in &self.sources { - runnables.extend( - source - .source - .update(cx, |source, cx| source.runnables_for_path(path, cx)), - ); - } - runnables - } - - /// Returns the last scheduled runnable, if any of the sources contains one with the matching id. - pub fn last_scheduled_runnable(&self, cx: &mut AppContext) -> Option> { - self.last_scheduled_runnable.as_ref().and_then(|id| { - // TODO straighten the `Path` story to understand what has to be passed here: or it will break in the future. - self.list_runnables(None, cx) - .into_iter() - .find(|runnable| runnable.id() == id) - }) - } -} diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs new file mode 100644 index 0000000000..b51cdf2ba4 --- /dev/null +++ b/crates/project/src/task_inventory.rs @@ -0,0 +1,666 @@ +//! Project-wide storage of the tasks available, capable of updating itself from the sources set. + +use std::{ + any::TypeId, + path::{Path, PathBuf}, + sync::Arc, +}; + +use collections::{HashMap, VecDeque}; +use gpui::{AppContext, Context, Model, ModelContext, Subscription}; +use itertools::Itertools; +use project_core::worktree::WorktreeId; +use task::{Task, TaskContext, TaskId, TaskSource}; +use util::{post_inc, NumericPrefixWithSuffix}; + +/// Inventory tracks available tasks for a given project. +pub struct Inventory { + sources: Vec, + last_scheduled_tasks: VecDeque<(TaskId, TaskContext)>, +} + +struct SourceInInventory { + source: Model>, + _subscription: Subscription, + type_id: TypeId, + kind: TaskSourceKind, +} + +/// Kind of a source the tasks are fetched from, used to display more source information in the UI. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TaskSourceKind { + /// bash-like commands spawned by users, not associated with any path + UserInput, + /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path + AbsPath(PathBuf), + /// Worktree-specific task definitions, e.g. dynamic tasks from open worktree file, or tasks from the worktree's .zed/task.json + Worktree { id: WorktreeId, abs_path: PathBuf }, +} + +impl TaskSourceKind { + fn abs_path(&self) -> Option<&Path> { + match self { + Self::AbsPath(abs_path) | Self::Worktree { abs_path, .. } => Some(abs_path), + Self::UserInput => None, + } + } + + fn worktree(&self) -> Option { + match self { + Self::Worktree { id, .. } => Some(*id), + _ => None, + } + } +} + +impl Inventory { + pub fn new(cx: &mut AppContext) -> Model { + cx.new_model(|_| Self { + sources: Vec::new(), + last_scheduled_tasks: VecDeque::new(), + }) + } + + /// If the task with the same path was not added yet, + /// registers a new tasks source to fetch for available tasks later. + /// Unless a source is removed, ignores future additions for the same path. + pub fn add_source( + &mut self, + kind: TaskSourceKind, + create_source: impl FnOnce(&mut ModelContext) -> Model>, + cx: &mut ModelContext, + ) { + let abs_path = kind.abs_path(); + if abs_path.is_some() { + if let Some(a) = self.sources.iter().find(|s| s.kind.abs_path() == abs_path) { + log::debug!("Source for path {abs_path:?} already exists, not adding. Old kind: {OLD_KIND:?}, new kind: {kind:?}", OLD_KIND = a.kind); + return; + } + } + + let source = create_source(cx); + let type_id = source.read(cx).type_id(); + let source = SourceInInventory { + _subscription: cx.observe(&source, |_, _, cx| { + cx.notify(); + }), + source, + type_id, + kind, + }; + self.sources.push(source); + cx.notify(); + } + + /// If present, removes the local static source entry that has the given path, + /// making corresponding task definitions unavailable in the fetch results. + /// + /// Now, entry for this path can be re-added again. + pub fn remove_local_static_source(&mut self, abs_path: &Path) { + self.sources.retain(|s| s.kind.abs_path() != Some(abs_path)); + } + + /// If present, removes the worktree source entry that has the given worktree id, + /// making corresponding task definitions unavailable in the fetch results. + /// + /// Now, entry for this path can be re-added again. + pub fn remove_worktree_sources(&mut self, worktree: WorktreeId) { + self.sources.retain(|s| s.kind.worktree() != Some(worktree)); + } + + pub fn source(&self) -> Option>> { + let target_type_id = std::any::TypeId::of::(); + self.sources.iter().find_map( + |SourceInInventory { + type_id, source, .. + }| { + if &target_type_id == type_id { + Some(source.clone()) + } else { + None + } + }, + ) + } + + /// Pulls its sources to list runanbles for the path given (up to the source to decide what to return for no path). + pub fn list_tasks( + &self, + path: Option<&Path>, + worktree: Option, + lru: bool, + cx: &mut AppContext, + ) -> Vec<(TaskSourceKind, Arc)> { + let mut lru_score = 0_u32; + let tasks_by_usage = if lru { + self.last_scheduled_tasks.iter().rev().fold( + HashMap::default(), + |mut tasks, (id, context)| { + tasks + .entry(id) + .or_insert_with(|| (post_inc(&mut lru_score), Some(context))); + tasks + }, + ) + } else { + HashMap::default() + }; + let not_used_task_context = None; + let not_used_score = (post_inc(&mut lru_score), not_used_task_context); + self.sources + .iter() + .filter(|source| { + let source_worktree = source.kind.worktree(); + worktree.is_none() || source_worktree.is_none() || source_worktree == worktree + }) + .flat_map(|source| { + source + .source + .update(cx, |source, cx| source.tasks_for_path(path, cx)) + .into_iter() + .map(|task| (&source.kind, task)) + }) + .map(|task| { + let usages = if lru { + tasks_by_usage + .get(&task.1.id()) + .copied() + .unwrap_or(not_used_score) + } else { + not_used_score + }; + (task, usages) + }) + .sorted_unstable_by( + |((kind_a, task_a), usages_a), ((kind_b, task_b), usages_b)| { + usages_a + .0 + .cmp(&usages_b.0) + .then( + kind_a + .worktree() + .is_none() + .cmp(&kind_b.worktree().is_none()), + ) + .then(kind_a.worktree().cmp(&kind_b.worktree())) + .then( + kind_a + .abs_path() + .is_none() + .cmp(&kind_b.abs_path().is_none()), + ) + .then(kind_a.abs_path().cmp(&kind_b.abs_path())) + .then({ + NumericPrefixWithSuffix::from_numeric_prefixed_str(task_a.name()) + .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str( + task_b.name(), + )) + .then(task_a.name().cmp(task_b.name())) + }) + }, + ) + .map(|((kind, task), _)| (kind.clone(), task)) + .collect() + } + + /// Returns the last scheduled task, if any of the sources contains one with the matching id. + pub fn last_scheduled_task(&self, cx: &mut AppContext) -> Option<(Arc, TaskContext)> { + self.last_scheduled_tasks + .back() + .and_then(|(id, task_context)| { + // TODO straighten the `Path` story to understand what has to be passed here: or it will break in the future. + self.list_tasks(None, None, false, cx) + .into_iter() + .find(|(_, task)| task.id() == id) + .map(|(_, task)| (task, task_context.clone())) + }) + } + + /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks. + pub fn task_scheduled(&mut self, id: TaskId, task_context: TaskContext) { + self.last_scheduled_tasks.push_back((id, task_context)); + if self.last_scheduled_tasks.len() > 5_000 { + self.last_scheduled_tasks.pop_front(); + } + } +} + +#[cfg(any(test, feature = "test-support"))] +pub mod test_inventory { + use std::{path::Path, sync::Arc}; + + use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext}; + use project_core::worktree::WorktreeId; + use task::{Task, TaskContext, TaskId, TaskSource}; + + use crate::Inventory; + + use super::TaskSourceKind; + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct TestTask { + pub id: task::TaskId, + pub name: String, + } + + impl Task for TestTask { + fn id(&self) -> &TaskId { + &self.id + } + + fn name(&self) -> &str { + &self.name + } + + fn cwd(&self) -> Option<&str> { + None + } + + fn exec(&self, _cwd: TaskContext) -> Option { + None + } + } + + pub struct StaticTestSource { + pub tasks: Vec, + } + + impl StaticTestSource { + pub fn new( + task_names: impl IntoIterator, + cx: &mut AppContext, + ) -> Model> { + cx.new_model(|_| { + Box::new(Self { + tasks: task_names + .into_iter() + .enumerate() + .map(|(i, name)| TestTask { + id: TaskId(format!("task_{i}_{name}")), + name, + }) + .collect(), + }) as Box + }) + } + } + + impl TaskSource for StaticTestSource { + fn tasks_for_path( + &mut self, + _path: Option<&Path>, + _cx: &mut ModelContext>, + ) -> Vec> { + self.tasks + .clone() + .into_iter() + .map(|task| Arc::new(task) as Arc) + .collect() + } + + fn as_any(&mut self) -> &mut dyn std::any::Any { + self + } + } + + pub fn list_task_names( + inventory: &Model, + path: Option<&Path>, + worktree: Option, + lru: bool, + cx: &mut TestAppContext, + ) -> Vec { + inventory.update(cx, |inventory, cx| { + inventory + .list_tasks(path, worktree, lru, cx) + .into_iter() + .map(|(_, task)| task.name().to_string()) + .collect() + }) + } + + pub fn register_task_used( + inventory: &Model, + task_name: &str, + cx: &mut TestAppContext, + ) { + inventory.update(cx, |inventory, cx| { + let task = inventory + .list_tasks(None, None, false, cx) + .into_iter() + .find(|(_, task)| task.name() == task_name) + .unwrap_or_else(|| panic!("Failed to find task with name {task_name}")); + inventory.task_scheduled(task.1.id().clone(), TaskContext::default()); + }); + } + + pub fn list_tasks( + inventory: &Model, + path: Option<&Path>, + worktree: Option, + lru: bool, + cx: &mut TestAppContext, + ) -> Vec<(TaskSourceKind, String)> { + inventory.update(cx, |inventory, cx| { + inventory + .list_tasks(path, worktree, lru, cx) + .into_iter() + .map(|(source_kind, task)| (source_kind, task.name().to_string())) + .collect() + }) + } +} + +#[cfg(test)] +mod tests { + use gpui::TestAppContext; + + use super::test_inventory::*; + use super::*; + + #[gpui::test] + fn test_task_list_sorting(cx: &mut TestAppContext) { + let inventory = cx.update(Inventory::new); + let initial_tasks = list_task_names(&inventory, None, None, true, cx); + assert!( + initial_tasks.is_empty(), + "No tasks expected for empty inventory, but got {initial_tasks:?}" + ); + let initial_tasks = list_task_names(&inventory, None, None, false, cx); + assert!( + initial_tasks.is_empty(), + "No tasks expected for empty inventory, but got {initial_tasks:?}" + ); + + inventory.update(cx, |inventory, cx| { + inventory.add_source( + TaskSourceKind::UserInput, + |cx| StaticTestSource::new(vec!["3_task".to_string()], cx), + cx, + ); + }); + inventory.update(cx, |inventory, cx| { + inventory.add_source( + TaskSourceKind::UserInput, + |cx| { + StaticTestSource::new( + vec![ + "1_task".to_string(), + "2_task".to_string(), + "1_a_task".to_string(), + ], + cx, + ) + }, + cx, + ); + }); + + let expected_initial_state = [ + "1_a_task".to_string(), + "1_task".to_string(), + "2_task".to_string(), + "3_task".to_string(), + ]; + assert_eq!( + list_task_names(&inventory, None, None, false, cx), + &expected_initial_state, + "Task list without lru sorting, should be sorted alphanumerically" + ); + assert_eq!( + list_task_names(&inventory, None, None, true, cx), + &expected_initial_state, + "Tasks with equal amount of usages should be sorted alphanumerically" + ); + + register_task_used(&inventory, "2_task", cx); + assert_eq!( + list_task_names(&inventory, None, None, false, cx), + &expected_initial_state, + "Task list without lru sorting, should be sorted alphanumerically" + ); + assert_eq!( + list_task_names(&inventory, None, None, true, cx), + vec![ + "2_task".to_string(), + "1_a_task".to_string(), + "1_task".to_string(), + "3_task".to_string() + ], + ); + + register_task_used(&inventory, "1_task", cx); + register_task_used(&inventory, "1_task", cx); + register_task_used(&inventory, "1_task", cx); + register_task_used(&inventory, "3_task", cx); + assert_eq!( + list_task_names(&inventory, None, None, false, cx), + &expected_initial_state, + "Task list without lru sorting, should be sorted alphanumerically" + ); + assert_eq!( + list_task_names(&inventory, None, None, true, cx), + vec![ + "3_task".to_string(), + "1_task".to_string(), + "2_task".to_string(), + "1_a_task".to_string(), + ], + ); + + inventory.update(cx, |inventory, cx| { + inventory.add_source( + TaskSourceKind::UserInput, + |cx| { + StaticTestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx) + }, + cx, + ); + }); + let expected_updated_state = [ + "1_a_task".to_string(), + "1_task".to_string(), + "2_task".to_string(), + "3_task".to_string(), + "10_hello".to_string(), + "11_hello".to_string(), + ]; + assert_eq!( + list_task_names(&inventory, None, None, false, cx), + &expected_updated_state, + "Task list without lru sorting, should be sorted alphanumerically" + ); + assert_eq!( + list_task_names(&inventory, None, None, true, cx), + vec![ + "3_task".to_string(), + "1_task".to_string(), + "2_task".to_string(), + "1_a_task".to_string(), + "10_hello".to_string(), + "11_hello".to_string(), + ], + ); + + register_task_used(&inventory, "11_hello", cx); + assert_eq!( + list_task_names(&inventory, None, None, false, cx), + &expected_updated_state, + "Task list without lru sorting, should be sorted alphanumerically" + ); + assert_eq!( + list_task_names(&inventory, None, None, true, cx), + vec![ + "11_hello".to_string(), + "3_task".to_string(), + "1_task".to_string(), + "2_task".to_string(), + "1_a_task".to_string(), + "10_hello".to_string(), + ], + ); + } + + #[gpui::test] + fn test_inventory_static_task_filters(cx: &mut TestAppContext) { + let inventory_with_statics = cx.update(Inventory::new); + let common_name = "common_task_name"; + let path_1 = Path::new("path_1"); + let path_2 = Path::new("path_2"); + let worktree_1 = WorktreeId::from_usize(1); + let worktree_path_1 = Path::new("worktree_path_1"); + let worktree_2 = WorktreeId::from_usize(2); + let worktree_path_2 = Path::new("worktree_path_2"); + inventory_with_statics.update(cx, |inventory, cx| { + inventory.add_source( + TaskSourceKind::UserInput, + |cx| { + StaticTestSource::new( + vec!["user_input".to_string(), common_name.to_string()], + cx, + ) + }, + cx, + ); + inventory.add_source( + TaskSourceKind::AbsPath(path_1.to_path_buf()), + |cx| { + StaticTestSource::new( + vec!["static_source_1".to_string(), common_name.to_string()], + cx, + ) + }, + cx, + ); + inventory.add_source( + TaskSourceKind::AbsPath(path_2.to_path_buf()), + |cx| { + StaticTestSource::new( + vec!["static_source_2".to_string(), common_name.to_string()], + cx, + ) + }, + cx, + ); + inventory.add_source( + TaskSourceKind::Worktree { + id: worktree_1, + abs_path: worktree_path_1.to_path_buf(), + }, + |cx| { + StaticTestSource::new( + vec!["worktree_1".to_string(), common_name.to_string()], + cx, + ) + }, + cx, + ); + inventory.add_source( + TaskSourceKind::Worktree { + id: worktree_2, + abs_path: worktree_path_2.to_path_buf(), + }, + |cx| { + StaticTestSource::new( + vec!["worktree_2".to_string(), common_name.to_string()], + cx, + ) + }, + cx, + ); + }); + + let worktree_independent_tasks = vec![ + ( + TaskSourceKind::AbsPath(path_1.to_path_buf()), + common_name.to_string(), + ), + ( + TaskSourceKind::AbsPath(path_1.to_path_buf()), + "static_source_1".to_string(), + ), + ( + TaskSourceKind::AbsPath(path_2.to_path_buf()), + common_name.to_string(), + ), + ( + TaskSourceKind::AbsPath(path_2.to_path_buf()), + "static_source_2".to_string(), + ), + (TaskSourceKind::UserInput, common_name.to_string()), + (TaskSourceKind::UserInput, "user_input".to_string()), + ]; + let worktree_1_tasks = vec![ + ( + TaskSourceKind::Worktree { + id: worktree_1, + abs_path: worktree_path_1.to_path_buf(), + }, + common_name.to_string(), + ), + ( + TaskSourceKind::Worktree { + id: worktree_1, + abs_path: worktree_path_1.to_path_buf(), + }, + "worktree_1".to_string(), + ), + ]; + let worktree_2_tasks = vec![ + ( + TaskSourceKind::Worktree { + id: worktree_2, + abs_path: worktree_path_2.to_path_buf(), + }, + common_name.to_string(), + ), + ( + TaskSourceKind::Worktree { + id: worktree_2, + abs_path: worktree_path_2.to_path_buf(), + }, + "worktree_2".to_string(), + ), + ]; + + let all_tasks = worktree_1_tasks + .iter() + .chain(worktree_2_tasks.iter()) + // worktree-less tasks come later in the list + .chain(worktree_independent_tasks.iter()) + .cloned() + .collect::>(); + + for path in [ + None, + Some(path_1), + Some(path_2), + Some(worktree_path_1), + Some(worktree_path_2), + ] { + assert_eq!( + list_tasks(&inventory_with_statics, path, None, false, cx), + all_tasks, + "Path {path:?} choice should not adjust static runnables" + ); + assert_eq!( + list_tasks(&inventory_with_statics, path, Some(worktree_1), false, cx), + worktree_1_tasks + .iter() + .chain(worktree_independent_tasks.iter()) + .cloned() + .collect::>(), + "Path {path:?} choice should not adjust static runnables for worktree_1" + ); + assert_eq!( + list_tasks(&inventory_with_statics, path, Some(worktree_2), false, cx), + worktree_2_tasks + .iter() + .chain(worktree_independent_tasks.iter()) + .cloned() + .collect::>(), + "Path {path:?} choice should not adjust static runnables for worktree_2" + ); + } + } +} diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b4c8bb6bee..e9d09a0ff1 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -5,7 +5,7 @@ use smol::channel::bounded; use std::path::{Path, PathBuf}; use terminal::{ terminal_settings::{self, Shell, TerminalSettings, VenvSettingsContent}, - RunableState, SpawnRunnable, Terminal, TerminalBuilder, + SpawnTask, TaskState, Terminal, TerminalBuilder, }; // #[cfg(target_os = "macos")] @@ -19,7 +19,7 @@ impl Project { pub fn create_terminal( &mut self, working_directory: Option, - spawn_runnable: Option, + spawn_task: Option, window: AnyWindowHandle, cx: &mut ModelContext, ) -> anyhow::Result> { @@ -32,18 +32,18 @@ impl Project { let python_settings = settings.detect_venv.clone(); let (completion_tx, completion_rx) = bounded(1); let mut env = settings.env.clone(); - let (spawn_runnable, shell) = if let Some(spawn_runnable) = spawn_runnable { - env.extend(spawn_runnable.env); + let (spawn_task, shell) = if let Some(spawn_task) = spawn_task { + env.extend(spawn_task.env); ( - Some(RunableState { - id: spawn_runnable.id, - label: spawn_runnable.label, + Some(TaskState { + id: spawn_task.id, + label: spawn_task.label, completed: false, completion_rx, }), Shell::WithArguments { - program: spawn_runnable.command, - args: spawn_runnable.args, + program: spawn_task.command, + args: spawn_task.args, }, ) } else { @@ -52,11 +52,12 @@ impl Project { let terminal = TerminalBuilder::new( working_directory.clone(), - spawn_runnable, + spawn_task, shell, env, Some(settings.blinking.clone()), settings.alternate_scroll, + settings.max_scroll_history_lines, window, completion_tx, ) diff --git a/crates/project_core/Cargo.toml b/crates/project_core/Cargo.toml new file mode 100644 index 0000000000..c72fe909da --- /dev/null +++ b/crates/project_core/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "project_core" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +test-support = [ + "client/test-support", + "language/test-support", + "settings/test-support", + "text/test-support", + "gpui/test-support", +] + +[dependencies] +anyhow.workspace = true +client.workspace = true +clock.workspace = true +collections.workspace = true +fs.workspace = true +futures.workspace = true +fuzzy.workspace = true +git.workspace = true +gpui.workspace = true +ignore.workspace = true +itertools.workspace = true +language.workspace = true +log.workspace = true +lsp.workspace = true +parking_lot.workspace = true +postage.workspace = true +rpc.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +smol.workspace = true +sum_tree.workspace = true +text.workspace = true +util.workspace = true + +[dev-dependencies] +clock = {workspace = true, features = ["test-support"]} +collections = { workspace = true, features = ["test-support"] } +git2.workspace = true +gpui = {workspace = true, features = ["test-support"]} +rand.workspace = true +settings = {workspace = true, features = ["test-support"]} +pretty_assertions.workspace = true diff --git a/crates/plugin_runtime/LICENSE-GPL b/crates/project_core/LICENSE-GPL similarity index 100% rename from crates/plugin_runtime/LICENSE-GPL rename to crates/project_core/LICENSE-GPL diff --git a/crates/project/src/ignore.rs b/crates/project_core/src/ignore.rs similarity index 100% rename from crates/project/src/ignore.rs rename to crates/project_core/src/ignore.rs diff --git a/crates/project_core/src/lib.rs b/crates/project_core/src/lib.rs new file mode 100644 index 0000000000..ffba767a2e --- /dev/null +++ b/crates/project_core/src/lib.rs @@ -0,0 +1,81 @@ +use std::path::Path; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering::SeqCst; + +use language::DiagnosticEntry; +use lsp::{DiagnosticSeverity, LanguageServerId}; +use rpc::proto; +use serde::Serialize; + +mod ignore; +pub mod project_settings; +pub mod worktree; +#[cfg(test)] +mod worktree_tests; + +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct ProjectEntryId(usize); + +impl ProjectEntryId { + pub const MAX: Self = Self(usize::MAX); + + pub fn new(counter: &AtomicUsize) -> Self { + Self(counter.fetch_add(1, SeqCst)) + } + + pub fn from_proto(id: u64) -> Self { + Self(id as usize) + } + + pub fn to_proto(&self) -> u64 { + self.0 as u64 + } + + pub fn to_usize(&self) -> usize { + self.0 + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)] +pub struct DiagnosticSummary { + pub error_count: usize, + pub warning_count: usize, +} + +impl DiagnosticSummary { + fn new<'a, T: 'a>(diagnostics: impl IntoIterator>) -> Self { + let mut this = Self { + error_count: 0, + warning_count: 0, + }; + + for entry in diagnostics { + if entry.diagnostic.is_primary { + match entry.diagnostic.severity { + DiagnosticSeverity::ERROR => this.error_count += 1, + DiagnosticSeverity::WARNING => this.warning_count += 1, + _ => {} + } + } + } + + this + } + + pub fn is_empty(&self) -> bool { + self.error_count == 0 && self.warning_count == 0 + } + + pub fn to_proto( + &self, + language_server_id: LanguageServerId, + path: &Path, + ) -> proto::DiagnosticSummary { + proto::DiagnosticSummary { + path: path.to_string_lossy().to_string(), + language_server_id: language_server_id.0 as u64, + error_count: self.error_count as u32, + warning_count: self.warning_count as u32, + } + } +} diff --git a/crates/project/src/project_settings.rs b/crates/project_core/src/project_settings.rs similarity index 98% rename from crates/project/src/project_settings.rs rename to crates/project_core/src/project_settings.rs index d26209043e..835ca61e7d 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project_core/src/project_settings.rs @@ -64,6 +64,7 @@ pub enum GitGutterSetting { #[serde(rename_all = "snake_case")] pub struct LspSettings { pub initialization_options: Option, + pub settings: Option, } impl Settings for ProjectSettings { diff --git a/crates/project/src/worktree.rs b/crates/project_core/src/worktree.rs similarity index 98% rename from crates/project/src/worktree.rs rename to crates/project_core/src/worktree.rs index d41aecbd7d..daab1c8cec 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project_core/src/worktree.rs @@ -1,12 +1,12 @@ use crate::{ - copy_recursive, ignore::IgnoreStack, project_settings::ProjectSettings, DiagnosticSummary, - ProjectEntryId, RemoveOptions, + ignore::IgnoreStack, project_settings::ProjectSettings, DiagnosticSummary, ProjectEntryId, }; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context as _, Result}; use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; +use fs::{copy_recursive, RemoveOptions}; use fs::{ repository::{GitFileStatus, GitRepository, RepoPath}, Fs, @@ -64,6 +64,11 @@ use util::{ ResultExt, }; +#[cfg(feature = "test-support")] +pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); +#[cfg(not(feature = "test-support"))] +const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); + #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); @@ -211,7 +216,7 @@ impl Deref for WorkDirectoryEntry { } } -impl<'a> From for WorkDirectoryEntry { +impl From for WorkDirectoryEntry { fn from(value: ProjectEntryId) -> Self { WorkDirectoryEntry(value) } @@ -253,6 +258,12 @@ pub struct LocalRepositoryEntry { pub(crate) git_dir_path: Arc, } +impl LocalRepositoryEntry { + pub fn load_index_text(&self, relative_file_path: &Path) -> Option { + self.repo_ptr.lock().load_index_text(relative_file_path) + } +} + impl Deref for LocalSnapshot { type Target = Snapshot; @@ -642,7 +653,7 @@ fn start_background_scan_tasks( let abs_path = abs_path.to_path_buf(); let background = cx.background_executor().clone(); async move { - let events = fs.watch(&abs_path, Duration::from_millis(100)).await; + let events = fs.watch(&abs_path, FS_WATCH_LATENCY).await; let case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { log::error!( "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}" @@ -714,7 +725,7 @@ impl LocalWorktree { path.starts_with(&self.abs_path) } - pub(crate) fn load_buffer( + pub fn load_buffer( &mut self, id: BufferId, path: &Path, @@ -976,7 +987,7 @@ impl LocalWorktree { pub fn scan_complete(&self) -> impl Future { let mut is_scanning_rx = self.is_scanning.1.clone(); async move { - let mut is_scanning = is_scanning_rx.borrow().clone(); + let mut is_scanning = *is_scanning_rx.borrow(); while is_scanning { if let Some(value) = is_scanning_rx.recv().await { is_scanning = value; @@ -1591,7 +1602,7 @@ impl RemoteWorktree { self.completed_scan_id >= scan_id } - pub(crate) fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future> { + pub fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future> { let (tx, rx) = oneshot::channel(); if self.observed_snapshot(scan_id) { let _ = tx.send(()); @@ -1657,7 +1668,7 @@ impl RemoteWorktree { }) } - pub(crate) fn delete_entry( + pub fn delete_entry( &mut self, id: ProjectEntryId, scan_id: usize, @@ -2091,7 +2102,7 @@ impl Snapshot { } impl LocalSnapshot { - pub(crate) fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> { + pub fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> { self.git_repositories.get(&repo.work_directory.0) } @@ -2250,11 +2261,16 @@ impl LocalSnapshot { fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc { let mut new_ignores = Vec::new(); - for ancestor in abs_path.ancestors().skip(1) { - if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { - new_ignores.push((ancestor, Some(ignore.clone()))); - } else { - new_ignores.push((ancestor, None)); + for (index, ancestor) in abs_path.ancestors().enumerate() { + if index > 0 { + if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { + new_ignores.push((ancestor, Some(ignore.clone()))); + } else { + new_ignores.push((ancestor, None)); + } + } + if ancestor.join(&*DOT_GIT).is_dir() { + break; } } @@ -2737,7 +2753,7 @@ impl WorktreeId { Self(handle_id) } - pub(crate) fn from_proto(id: u64) -> Self { + pub fn from_proto(id: u64) -> Self { Self(id as usize) } @@ -2822,10 +2838,10 @@ pub struct File { pub worktree: Model, pub path: Arc, pub mtime: SystemTime, - pub(crate) entry_id: Option, - pub(crate) is_local: bool, - pub(crate) is_deleted: bool, - pub(crate) is_private: bool, + pub entry_id: Option, + pub is_local: bool, + pub is_deleted: bool, + pub is_private: bool, } impl language::File for File { @@ -3282,6 +3298,7 @@ enum BackgroundScannerPhase { } impl BackgroundScanner { + #[allow(clippy::too_many_arguments)] fn new( snapshot: LocalSnapshot, next_entry_id: Arc, @@ -3318,14 +3335,21 @@ impl BackgroundScanner { // Populate ignores above the root. let root_abs_path = self.state.lock().snapshot.abs_path.clone(); - for ancestor in root_abs_path.ancestors().skip(1) { - if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await - { - self.state - .lock() - .snapshot - .ignores_by_parent_abs_path - .insert(ancestor.into(), (ignore.into(), false)); + for (index, ancestor) in root_abs_path.ancestors().enumerate() { + if index != 0 { + if let Ok(ignore) = + build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await + { + self.state + .lock() + .snapshot + .ignores_by_parent_abs_path + .insert(ancestor.into(), (ignore.into(), false)); + } + } + if ancestor.join(&*DOT_GIT).is_dir() { + // Reached root of git repository. + break; } } @@ -3893,16 +3917,14 @@ impl BackgroundScanner { let repository = dotgit_path.and_then(|path| state.build_git_repository(path, self.fs.as_ref())); - for new_job in new_jobs { - if let Some(mut new_job) = new_job { - if let Some(containing_repository) = &repository { - new_job.containing_repository = Some(containing_repository.clone()); - } - - job.scan_queue - .try_send(new_job) - .expect("channel is unbounded"); + for mut new_job in new_jobs.into_iter().flatten() { + if let Some(containing_repository) = &repository { + new_job.containing_repository = Some(containing_repository.clone()); } + + job.scan_queue + .try_send(new_job) + .expect("channel is unbounded"); } Ok(()) @@ -4321,7 +4343,7 @@ impl BackgroundScanner { return self.executor.simulate_random_delay().await; } - smol::Timer::after(Duration::from_millis(100)).await; + smol::Timer::after(FS_WATCH_LATENCY).await; } } diff --git a/crates/project/src/worktree_tests.rs b/crates/project_core/src/worktree_tests.rs similarity index 98% rename from crates/project/src/worktree_tests.rs rename to crates/project_core/src/worktree_tests.rs index e3725d43d5..c16a0dc649 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project_core/src/worktree_tests.rs @@ -1,10 +1,11 @@ use crate::{ project_settings::ProjectSettings, + worktree::{Entry, EntryKind, PathChange, Worktree}, worktree::{Event, Snapshot, WorktreeModelHandle}, - Entry, EntryKind, PathChange, Project, Worktree, }; use anyhow::Result; use client::Client; +use clock::FakeSystemClock; use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions}; use git::GITIGNORE; use gpui::{ModelContext, Task, TestAppContext}; @@ -13,7 +14,7 @@ use postage::stream::Stream; use pretty_assertions::assert_eq; use rand::prelude::*; use serde_json::json; -use settings::SettingsStore; +use settings::{Settings, SettingsStore}; use std::{ env, fmt::Write, @@ -1263,7 +1264,13 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); - let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client_fake = cx.update(|cx| { + Client::new( + Arc::new(FakeSystemClock::default()), + FakeHttpClient::with_404_response(), + cx, + ) + }); let fs_fake = FakeFs::new(cx.background_executor.clone()); fs_fake @@ -1304,7 +1311,13 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { assert!(tree.entry_for_path("a/b/").unwrap().is_dir()); }); - let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client_real = cx.update(|cx| { + Client::new( + Arc::new(FakeSystemClock::default()), + FakeHttpClient::with_404_response(), + cx, + ) + }); let fs_real = Arc::new(RealFs); let temp_root = temp_tree(json!({ @@ -2089,7 +2102,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { async fn test_git_status(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); - const IGNORE_RULE: &'static str = "**/target"; + const IGNORE_RULE: &str = "**/target"; let root = temp_tree(json!({ "project": { @@ -2109,12 +2122,12 @@ async fn test_git_status(cx: &mut TestAppContext) { })); - const A_TXT: &'static str = "a.txt"; - const B_TXT: &'static str = "b.txt"; - const E_TXT: &'static str = "c/d/e.txt"; - const F_TXT: &'static str = "f.txt"; - const DOTGITIGNORE: &'static str = ".gitignore"; - const BUILD_FILE: &'static str = "target/build_file"; + const A_TXT: &str = "a.txt"; + const B_TXT: &str = "b.txt"; + const E_TXT: &str = "c/d/e.txt"; + const F_TXT: &str = "f.txt"; + const DOTGITIGNORE: &str = ".gitignore"; + const BUILD_FILE: &str = "target/build_file"; let project_path = Path::new("project"); // Set up git repository before creating the worktree. @@ -2230,7 +2243,7 @@ async fn test_git_status(cx: &mut TestAppContext) { cx.executor().run_until_parked(); let mut renamed_dir_name = "first_directory/second_directory"; - const RENAMED_FILE: &'static str = "rf.txt"; + const RENAMED_FILE: &str = "rf.txt"; std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap(); std::fs::write( @@ -2396,8 +2409,9 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { } fn build_client(cx: &mut TestAppContext) -> Arc { + let clock = Arc::new(FakeSystemClock::default()); let http_client = FakeHttpClient::with_404_response(); - cx.update(|cx| Client::new(http_client, cx)) + cx.update(|cx| Client::new(clock, http_client, cx)) } #[track_caller] @@ -2521,6 +2535,6 @@ fn init_test(cx: &mut gpui::TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); - Project::init_settings(cx); + ProjectSettings::register(cx); }); } diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 8946ca7d15..093d47e124 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -14,10 +14,8 @@ anyhow.workspace = true collections.workspace = true db.workspace = true editor.workspace = true -futures.workspace = true gpui.workspace = true menu.workspace = true -postage.workspace = true pretty_assertions.workspace = true project.workspace = true schemars.workspace = true @@ -26,10 +24,9 @@ serde.workspace = true serde_derive.workspace = true serde_json.workspace = true settings.workspace = true -smallvec.workspace = true theme.workspace = true ui.workspace = true -unicase = "2.6" +unicase.workspace = true util.workspace = true client.workspace = true workspace.workspace = true diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index 5f596440ac..5db7606ed2 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -13,17 +13,18 @@ struct TypeConfig { #[derive(Deserialize, Debug)] pub struct FileAssociations { + stems: HashMap, suffixes: HashMap, types: HashMap, } impl Global for FileAssociations {} -const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder"; -const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder"; -const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron"; -const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron"; -pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json"; +const COLLAPSED_DIRECTORY_TYPE: &str = "collapsed_folder"; +const EXPANDED_DIRECTORY_TYPE: &str = "expanded_folder"; +const COLLAPSED_CHEVRON_TYPE: &str = "collapsed_chevron"; +const EXPANDED_CHEVRON_TYPE: &str = "expanded_chevron"; +pub const FILE_TYPES_ASSET: &str = "icons/file_icons/file_types.json"; pub fn init(assets: impl AssetSource, cx: &mut AppContext) { cx.set_global(FileAssociations::new(assets)) @@ -38,6 +39,7 @@ impl FileAssociations { .map_err(Into::into) }) .unwrap_or_else(|_| FileAssociations { + stems: HashMap::default(), suffixes: HashMap::default(), types: HashMap::default(), }) @@ -49,7 +51,14 @@ impl FileAssociations { // FIXME: Associate a type with the languages and have the file's language // override these associations maybe!({ - let suffix = path.icon_suffix()?; + let suffix = path.icon_stem_or_suffix()?; + + if let Some(type_str) = this.stems.get(suffix) { + return this + .types + .get(type_str) + .map(|type_config| type_config.icon.clone()); + } this.suffixes .get(suffix) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 3744a36c00..481086a8bb 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -27,14 +27,14 @@ use std::{cmp::Ordering, ffi::OsStr, ops::Range, path::Path, sync::Arc}; use theme::ThemeSettings; use ui::{prelude::*, v_flex, ContextMenu, Icon, KeyBinding, Label, ListItem}; use unicase::UniCase; -use util::{maybe, ResultExt, TryFutureExt}; +use util::{maybe, NumericPrefixWithSuffix, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::DetachAndPromptErr, Workspace, }; -const PROJECT_PANEL_KEY: &'static str = "ProjectPanel"; +const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; pub struct ProjectPanel { @@ -166,13 +166,7 @@ impl ProjectPanel { fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { let project = workspace.project().clone(); let project_panel = cx.new_view(|cx: &mut ViewContext| { - cx.observe(&project, |this, _, cx| { - this.update_visible_entries(None, cx); - cx.notify(); - }) - .detach(); let focus_handle = cx.focus_handle(); - cx.on_focus(&focus_handle, Self::focus_in).detach(); cx.subscribe(&project, |this, project, event, cx| match event { @@ -193,6 +187,10 @@ impl ProjectPanel { this.update_visible_entries(None, cx); cx.notify(); } + project::Event::WorktreeUpdatedEntries(_, _) | project::Event::WorktreeAdded => { + this.update_visible_entries(None, cx); + cx.notify(); + } _ => {} }) .detach(); @@ -338,7 +336,7 @@ impl ProjectPanel { let panel = ProjectPanel::new(workspace, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width; + panel.width = serialized_panel.width.map(|px| px.round()); cx.notify(); }); } @@ -607,7 +605,7 @@ impl ProjectPanel { worktree_id, entry_id: NEW_ENTRY_ID, }); - let new_path = entry.path.join(&filename.trim_start_matches("/")); + let new_path = entry.path.join(&filename.trim_start_matches('/')); if path_already_exists(new_path.as_path()) { return None; } @@ -908,7 +906,8 @@ impl ProjectPanel { .to_os_string(); let mut new_path = entry.path.to_path_buf(); - if entry.is_file() { + // If we're pasting into a file, or a directory into itself, go up one level. + if entry.is_file() || (entry.is_dir() && entry.id == clipboard_entry.entry_id()) { new_path.pop(); } @@ -1027,7 +1026,7 @@ impl ProjectPanel { cx.foreground_executor().spawn(task).detach_and_log_err(cx); } - Some(project.worktree_id_for_entry(destination, cx)?) + project.worktree_id_for_entry(destination, cx) }); if let Some(destination_worktree) = destination_worktree { @@ -1177,11 +1176,31 @@ impl ProjectPanel { let a_is_file = components_a.peek().is_none() && entry_a.is_file(); let b_is_file = components_b.peek().is_none() && entry_b.is_file(); let ordering = a_is_file.cmp(&b_is_file).then_with(|| { - let name_a = - UniCase::new(component_a.as_os_str().to_string_lossy()); - let name_b = - UniCase::new(component_b.as_os_str().to_string_lossy()); - name_a.cmp(&name_b) + let maybe_numeric_ordering = maybe!({ + let num_and_remainder_a = Path::new(component_a.as_os_str()) + .file_stem() + .and_then(|s| s.to_str()) + .and_then( + NumericPrefixWithSuffix::from_numeric_prefixed_str, + )?; + let num_and_remainder_b = Path::new(component_b.as_os_str()) + .file_stem() + .and_then(|s| s.to_str()) + .and_then( + NumericPrefixWithSuffix::from_numeric_prefixed_str, + )?; + + num_and_remainder_a.partial_cmp(&num_and_remainder_b) + }); + + maybe_numeric_ordering.unwrap_or_else(|| { + let name_a = + UniCase::new(component_a.as_os_str().to_string_lossy()); + let name_b = + UniCase::new(component_b.as_os_str().to_string_lossy()); + + name_a.cmp(&name_b) + }) }); if !ordering.is_eq() { return ordering; diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index 078d09f0a1..93b122eaf0 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -16,12 +16,9 @@ fuzzy.workspace = true gpui.workspace = true ordered-float.workspace = true picker.workspace = true -postage.workspace = true project.workspace = true serde_json.workspace = true settings.workspace = true -smol.workspace = true -text.workspace = true theme.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index bd5b2e64c4..035eca6ffd 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -2,7 +2,7 @@ use editor::{scroll::Autoscroll, styled_runs_for_code_label, Bias, Editor}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, rems, AppContext, DismissEvent, FontWeight, Model, ParentElement, StyledText, Task, - View, ViewContext, WeakView, + View, ViewContext, WeakView, WindowContext, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; @@ -106,7 +106,7 @@ impl ProjectSymbolsDelegate { impl PickerDelegate for ProjectSymbolsDelegate { type ListItem = ListItem; - fn placeholder_text(&self) -> Arc { + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Search project symbols...".into() } @@ -127,13 +127,14 @@ impl PickerDelegate for ProjectSymbolsDelegate { let position = buffer .read(cx) .clip_point_utf16(symbol.range.start, Bias::Left); - - let editor = if secondary { - workspace.split_project_item::(buffer, cx) + let pane = if secondary { + workspace.adjacent_pane(cx) } else { - workspace.open_project_item::(buffer, cx) + workspace.active_pane().clone() }; + let editor = workspace.open_project_item::(pane, buffer, cx); + editor.update(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::center()), cx, |s| { s.select_ranges([position..position]) @@ -270,7 +271,13 @@ mod tests { async fn test_project_symbols(cx: &mut TestAppContext) { init_test(cx); - let mut language = Language::new( + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/dir", json!({ "test.rs": "" })).await; + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(Arc::new(Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { @@ -280,16 +287,9 @@ mod tests { ..Default::default() }, None, - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::::default()) - .await; - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/dir", json!({ "test.rs": "" })).await; - - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages().add(Arc::new(language))); + ))); + let mut fake_servers = + language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default()); let _buffer = project .update(cx, |project, cx| { diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 48e3cff72d..f96dfbdd7d 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -10,21 +10,20 @@ path = "src/recent_projects.rs" doctest = false [dependencies] -editor.workspace = true -futures.workspace = true fuzzy.workspace = true gpui.workspace = true -language.workspace = true +menu.workspace = true ordered-float.workspace = true picker.workspace = true -postage.workspace = true -settings.workspace = true +serde.workspace = true smol.workspace = true -text.workspace = true -theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } +language = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } +serde_json.workspace = true +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/src/highlighted_workspace_location.rs b/crates/recent_projects/src/highlighted_workspace_location.rs deleted file mode 100644 index 436bafb062..0000000000 --- a/crates/recent_projects/src/highlighted_workspace_location.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::path::Path; - -use fuzzy::StringMatch; -use ui::{prelude::*, HighlightedLabel}; -use util::paths::PathExt; -use workspace::WorkspaceLocation; - -#[derive(Clone, IntoElement)] -pub struct HighlightedText { - pub text: String, - pub highlight_positions: Vec, - char_count: usize, -} - -impl HighlightedText { - fn join(components: impl Iterator, separator: &str) -> Self { - let mut char_count = 0; - let separator_char_count = separator.chars().count(); - let mut text = String::new(); - let mut highlight_positions = Vec::new(); - for component in components { - if char_count != 0 { - text.push_str(separator); - char_count += separator_char_count; - } - - highlight_positions.extend( - component - .highlight_positions - .iter() - .map(|position| position + char_count), - ); - text.push_str(&component.text); - char_count += component.text.chars().count(); - } - - Self { - text, - highlight_positions, - char_count, - } - } -} - -impl RenderOnce for HighlightedText { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - HighlightedLabel::new(self.text, self.highlight_positions) - } -} - -#[derive(Clone)] -pub struct HighlightedWorkspaceLocation { - pub names: HighlightedText, - pub paths: Vec, -} - -impl HighlightedWorkspaceLocation { - pub fn new(string_match: &StringMatch, location: &WorkspaceLocation) -> Self { - let mut path_start_offset = 0; - let (names, paths): (Vec<_>, Vec<_>) = location - .paths() - .iter() - .map(|path| { - let path = path.compact(); - let highlighted_text = Self::highlights_for_path( - path.as_ref(), - &string_match.positions, - path_start_offset, - ); - - path_start_offset += highlighted_text.1.char_count; - - highlighted_text - }) - .unzip(); - - Self { - names: HighlightedText::join(names.into_iter().filter_map(|name| name), ", "), - paths, - } - } - - // Compute the highlighted text for the name and path - fn highlights_for_path( - path: &Path, - match_positions: &Vec, - path_start_offset: usize, - ) -> (Option, HighlightedText) { - let path_string = path.to_string_lossy(); - let path_char_count = path_string.chars().count(); - // Get the subset of match highlight positions that line up with the given path. - // Also adjusts them to start at the path start - let path_positions = match_positions - .iter() - .copied() - .skip_while(|position| *position < path_start_offset) - .take_while(|position| *position < path_start_offset + path_char_count) - .map(|position| position - path_start_offset) - .collect::>(); - - // Again subset the highlight positions to just those that line up with the file_name - // again adjusted to the start of the file_name - let file_name_text_and_positions = path.file_name().map(|file_name| { - let text = file_name.to_string_lossy(); - let char_count = text.chars().count(); - let file_name_start = path_char_count - char_count; - let highlight_positions = path_positions - .iter() - .copied() - .skip_while(|position| *position < file_name_start) - .take_while(|position| *position < file_name_start + char_count) - .map(|position| position - file_name_start) - .collect::>(); - HighlightedText { - text: text.to_string(), - highlight_positions, - char_count, - } - }); - - ( - file_name_text_and_positions, - HighlightedText { - text: path_string.to_string(), - highlight_positions: path_positions, - char_count: path_char_count, - }, - ) - } -} diff --git a/crates/recent_projects/src/projects.rs b/crates/recent_projects/src/projects.rs deleted file mode 100644 index d9cfbd08ad..0000000000 --- a/crates/recent_projects/src/projects.rs +++ /dev/null @@ -1 +0,0 @@ -gpui::actions!(projects, [OpenRecent]); diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 0fd2552902..76d42c2780 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1,20 +1,30 @@ -mod highlighted_workspace_location; -mod projects; - use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task, - View, ViewContext, WeakView, + AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, + Subscription, Task, View, ViewContext, WeakView, }; -use highlighted_workspace_location::HighlightedWorkspaceLocation; use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate}; -use std::sync::Arc; -use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing}; +use picker::{ + highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText}, + Picker, PickerDelegate, +}; +use serde::Deserialize; +use std::{path::Path, sync::Arc}; +use ui::{prelude::*, tooltip_container, ListItem, ListItemSpacing, Tooltip}; use util::paths::PathExt; -use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB}; +use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB}; -pub use projects::OpenRecent; +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct OpenRecent { + #[serde(default = "default_create_new_window")] + pub create_new_window: bool, +} + +fn default_create_new_window() -> bool { + true +} + +gpui::impl_actions!(projects, [OpenRecent]); pub fn init(cx: &mut AppContext) { cx.observe_new_views(RecentProjects::register).detach(); @@ -45,13 +55,11 @@ impl RecentProjects { let workspaces = WORKSPACE_DB .recent_workspaces_on_disk() .await - .unwrap_or_default() - .into_iter() - .map(|(_, location)| location) - .collect(); + .unwrap_or_default(); + this.update(&mut cx, move |this, cx| { this.picker.update(cx, move |picker, cx| { - picker.delegate.workspace_locations = workspaces; + picker.delegate.workspaces = workspaces; picker.update_matches(picker.query(cx), cx) }) }) @@ -66,9 +74,9 @@ impl RecentProjects { } fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|workspace, _: &OpenRecent, cx| { + workspace.register_action(|workspace, open_recent: &OpenRecent, cx| { let Some(recent_projects) = workspace.active_modal::(cx) else { - if let Some(handler) = Self::open(workspace, cx) { + if let Some(handler) = Self::open(workspace, open_recent.create_new_window, cx) { handler.detach_and_log_err(cx); } return; @@ -82,12 +90,17 @@ impl RecentProjects { }); } - fn open(_: &mut Workspace, cx: &mut ViewContext) -> Option>> { + fn open( + _: &mut Workspace, + create_new_window: bool, + cx: &mut ViewContext, + ) -> Option>> { Some(cx.spawn(|workspace, mut cx| async move { workspace.update(&mut cx, |workspace, cx| { let weak_workspace = cx.view().downgrade(); workspace.toggle_modal(cx, |cx| { - let delegate = RecentProjectsDelegate::new(weak_workspace, true); + let delegate = + RecentProjectsDelegate::new(weak_workspace, create_new_window, true); let modal = Self::new(delegate, 34., cx); modal @@ -96,8 +109,15 @@ impl RecentProjects { Ok(()) })) } + pub fn open_popover(workspace: WeakView, cx: &mut WindowContext<'_>) -> View { - cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx)) + cx.new_view(|cx| { + Self::new( + RecentProjectsDelegate::new(workspace, false, false), + 20., + cx, + ) + }) } } @@ -124,20 +144,25 @@ impl Render for RecentProjects { pub struct RecentProjectsDelegate { workspace: WeakView, - workspace_locations: Vec, + workspaces: Vec<(WorkspaceId, WorkspaceLocation)>, selected_match_index: usize, matches: Vec, render_paths: bool, + create_new_window: bool, + // Flag to reset index when there is a new query vs not reset index when user delete an item + reset_selected_match_index: bool, } impl RecentProjectsDelegate { - fn new(workspace: WeakView, render_paths: bool) -> Self { + fn new(workspace: WeakView, create_new_window: bool, render_paths: bool) -> Self { Self { workspace, - workspace_locations: vec![], + workspaces: vec![], selected_match_index: 0, matches: Default::default(), + create_new_window, render_paths, + reset_selected_match_index: true, } } } @@ -145,8 +170,21 @@ impl EventEmitter for RecentProjectsDelegate {} impl PickerDelegate for RecentProjectsDelegate { type ListItem = ListItem; - fn placeholder_text(&self) -> Arc { - "Search recent projects...".into() + fn placeholder_text(&self, cx: &mut WindowContext) -> Arc { + let (create_window, reuse_window) = if self.create_new_window { + ( + cx.keystroke_text_for(&menu::Confirm), + cx.keystroke_text_for(&menu::SecondaryConfirm), + ) + } else { + ( + cx.keystroke_text_for(&menu::SecondaryConfirm), + cx.keystroke_text_for(&menu::Confirm), + ) + }; + Arc::from(format!( + "{reuse_window} reuses the window, {create_window} opens a new one", + )) } fn match_count(&self) -> usize { @@ -169,10 +207,10 @@ impl PickerDelegate for RecentProjectsDelegate { let query = query.trim_start(); let smart_case = query.chars().any(|c| c.is_uppercase()); let candidates = self - .workspace_locations + .workspaces .iter() .enumerate() - .map(|(id, location)| { + .map(|(id, (_, location))| { let combined_string = location .paths() .iter() @@ -192,28 +230,64 @@ impl PickerDelegate for RecentProjectsDelegate { )); self.matches.sort_unstable_by_key(|m| m.candidate_id); - self.selected_match_index = self - .matches - .iter() - .enumerate() - .rev() - .max_by_key(|(_, m)| OrderedFloat(m.score)) - .map(|(ix, _)| ix) - .unwrap_or(0); + if self.reset_selected_match_index { + self.selected_match_index = self + .matches + .iter() + .enumerate() + .rev() + .max_by_key(|(_, m)| OrderedFloat(m.score)) + .map(|(ix, _)| ix) + .unwrap_or(0); + } + self.reset_selected_match_index = true; Task::ready(()) } - fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { if let Some((selected_match, workspace)) = self .matches .get(self.selected_index()) .zip(self.workspace.upgrade()) { - let workspace_location = &self.workspace_locations[selected_match.candidate_id]; + let (candidate_workspace_id, candidate_workspace_location) = + &self.workspaces[selected_match.candidate_id]; + let replace_current_window = if self.create_new_window { + secondary + } else { + !secondary + }; workspace .update(cx, |workspace, cx| { - workspace - .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx) + if workspace.database_id() != *candidate_workspace_id { + let candidate_paths = candidate_workspace_location.paths().as_ref().clone(); + if replace_current_window { + cx.spawn(move |workspace, mut cx| async move { + let continue_replacing = workspace + .update(&mut cx, |workspace, cx| { + workspace.prepare_to_close(true, cx) + })? + .await?; + if continue_replacing { + workspace + .update(&mut cx, |workspace, cx| { + workspace.open_workspace_for_paths( + true, + candidate_paths, + cx, + ) + })? + .await + } else { + Ok(()) + } + }) + } else { + workspace.open_workspace_for_paths(false, candidate_paths, cx) + } + } else { + Task::ready(Ok(())) + } }) .detach_and_log_err(cx); cx.emit(DismissEvent); @@ -226,37 +300,62 @@ impl PickerDelegate for RecentProjectsDelegate { &self, ix: usize, selected: bool, - _cx: &mut ViewContext>, + cx: &mut ViewContext>, ) -> Option { - let Some(r#match) = self.matches.get(ix) else { + let Some(hit) = self.matches.get(ix) else { return None; }; - let highlighted_location = HighlightedWorkspaceLocation::new( - &r#match, - &self.workspace_locations[r#match.candidate_id], - ); + let (workspace_id, location) = &self.workspaces[hit.candidate_id]; + let is_current_workspace = self.is_current_workspace(*workspace_id, cx); - let tooltip_highlighted_location = highlighted_location.clone(); + let mut path_start_offset = 0; + let (match_labels, paths): (Vec<_>, Vec<_>) = location + .paths() + .iter() + .map(|path| { + let path = path.compact(); + let highlighted_text = + highlights_for_path(path.as_ref(), &hit.positions, path_start_offset); + path_start_offset += highlighted_text.1.char_count; + highlighted_text + }) + .unzip(); + + let highlighted_match = HighlightedMatchWithPaths { + match_label: HighlightedText::join(match_labels.into_iter().flatten(), ", "), + paths: if self.render_paths { paths } else { Vec::new() }, + }; Some( ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) .selected(selected) - .child( - v_flex() - .child(highlighted_location.names) - .when(self.render_paths, |this| { - this.children(highlighted_location.paths.into_iter().map(|path| { - HighlightedLabel::new(path.text, path.highlight_positions) - .size(LabelSize::Small) - .color(Color::Muted) - })) - }), - ) + .child(highlighted_match.clone().render(cx)) + .when(!is_current_workspace, |el| { + let delete_button = div() + .child( + IconButton::new("delete", IconName::Close) + .icon_size(IconSize::Small) + .on_click(cx.listener(move |this, _event, cx| { + cx.stop_propagation(); + cx.prevent_default(); + + this.delegate.delete_recent_project(ix, cx) + })) + .tooltip(|cx| Tooltip::text("Delete From Recent Projects...", cx)), + ) + .into_any_element(); + + if self.selected_index() == ix { + el.end_slot::(delete_button) + } else { + el.end_hover_slot::(delete_button) + } + }) .tooltip(move |cx| { - let tooltip_highlighted_location = tooltip_highlighted_location.clone(); + let tooltip_highlighted_location = highlighted_match.clone(); cx.new_view(move |_| MatchTooltip { highlighted_location: tooltip_highlighted_location, }) @@ -266,24 +365,236 @@ impl PickerDelegate for RecentProjectsDelegate { } } +// Compute the highlighted text for the name and path +fn highlights_for_path( + path: &Path, + match_positions: &Vec, + path_start_offset: usize, +) -> (Option, HighlightedText) { + let path_string = path.to_string_lossy(); + let path_char_count = path_string.chars().count(); + // Get the subset of match highlight positions that line up with the given path. + // Also adjusts them to start at the path start + let path_positions = match_positions + .iter() + .copied() + .skip_while(|position| *position < path_start_offset) + .take_while(|position| *position < path_start_offset + path_char_count) + .map(|position| position - path_start_offset) + .collect::>(); + + // Again subset the highlight positions to just those that line up with the file_name + // again adjusted to the start of the file_name + let file_name_text_and_positions = path.file_name().map(|file_name| { + let text = file_name.to_string_lossy(); + let char_count = text.chars().count(); + let file_name_start = path_char_count - char_count; + let highlight_positions = path_positions + .iter() + .copied() + .skip_while(|position| *position < file_name_start) + .take_while(|position| *position < file_name_start + char_count) + .map(|position| position - file_name_start) + .collect::>(); + HighlightedText { + text: text.to_string(), + highlight_positions, + char_count, + } + }); + + ( + file_name_text_and_positions, + HighlightedText { + text: path_string.to_string(), + highlight_positions: path_positions, + char_count: path_char_count, + }, + ) +} + +impl RecentProjectsDelegate { + fn delete_recent_project(&self, ix: usize, cx: &mut ViewContext>) { + if let Some(selected_match) = self.matches.get(ix) { + let (workspace_id, _) = self.workspaces[selected_match.candidate_id]; + cx.spawn(move |this, mut cx| async move { + let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await; + let workspaces = WORKSPACE_DB + .recent_workspaces_on_disk() + .await + .unwrap_or_default(); + this.update(&mut cx, move |picker, cx| { + picker.delegate.workspaces = workspaces; + picker.delegate.set_selected_index(ix - 1, cx); + picker.delegate.reset_selected_match_index = false; + picker.update_matches(picker.query(cx), cx) + }) + }) + .detach(); + } + } + + fn is_current_workspace( + &self, + workspace_id: WorkspaceId, + cx: &mut ViewContext>, + ) -> bool { + if let Some(workspace) = self.workspace.upgrade() { + let workspace = workspace.read(cx); + if workspace_id == workspace.database_id() { + return true; + } + } + + false + } +} struct MatchTooltip { - highlighted_location: HighlightedWorkspaceLocation, + highlighted_location: HighlightedMatchWithPaths, } impl Render for MatchTooltip { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { tooltip_container(cx, |div, _| { - div.children( - self.highlighted_location - .paths - .clone() - .into_iter() - .map(|path| { - HighlightedLabel::new(path.text, path.highlight_positions) - .size(LabelSize::Small) - .color(Color::Muted) - }), - ) + self.highlighted_location.render_paths_children(div) + }) + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use editor::Editor; + use gpui::{TestAppContext, WindowHandle}; + use project::Project; + use serde_json::json; + use workspace::{open_paths, AppState}; + + use super::*; + + #[gpui::test] + async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/dir", + json!({ + "main.ts": "a" + }), + ) + .await; + cx.update(|cx| open_paths(&[PathBuf::from("/dir/main.ts")], &app_state, None, cx)) + .await + .unwrap(); + assert_eq!(cx.update(|cx| cx.windows().len()), 1); + + let workspace = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + workspace + .update(cx, |workspace, _| assert!(!workspace.is_edited())) + .unwrap(); + + let editor = workspace + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + workspace + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); + }) + .unwrap(); + workspace + .update(cx, |workspace, _| assert!(workspace.is_edited(), "After inserting more text into the editor without saving, we should have a dirty project")) + .unwrap(); + + let recent_projects_picker = open_recent_projects(&workspace, cx); + workspace + .update(cx, |_, cx| { + recent_projects_picker.update(cx, |picker, cx| { + assert_eq!(picker.query(cx), ""); + let delegate = &mut picker.delegate; + delegate.matches = vec![StringMatch { + candidate_id: 0, + score: 1.0, + positions: Vec::new(), + string: "fake candidate".to_string(), + }]; + delegate.workspaces = vec![(0, WorkspaceLocation::new(vec!["/test/path/"]))]; + }); + }) + .unwrap(); + + assert!( + !cx.has_pending_prompt(), + "Should have no pending prompt on dirty project before opening the new recent project" + ); + cx.dispatch_action(*workspace, menu::Confirm); + workspace + .update(cx, |workspace, cx| { + assert!( + workspace.active_modal::(cx).is_none(), + "Should remove the modal after selecting new recent project" + ) + }) + .unwrap(); + assert!( + cx.has_pending_prompt(), + "Dirty workspace should prompt before opening the new recent project" + ); + // Cancel + cx.simulate_prompt_answer(0); + assert!( + !cx.has_pending_prompt(), + "Should have no pending prompt after cancelling" + ); + workspace + .update(cx, |workspace, _| { + assert!( + workspace.is_edited(), + "Should be in the same dirty project after cancelling" + ) + }) + .unwrap(); + } + + fn open_recent_projects( + workspace: &WindowHandle, + cx: &mut TestAppContext, + ) -> View> { + cx.dispatch_action( + (*workspace).into(), + OpenRecent { + create_new_window: false, + }, + ); + workspace + .update(cx, |workspace, cx| { + workspace + .active_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }) + .unwrap() + } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let state = AppState::test(cx); + language::init(cx); + crate::init(cx); + editor::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + state }) } } diff --git a/crates/refineable/Cargo.toml b/crates/refineable/Cargo.toml index cc1b5d0344..97f03f1f2f 100644 --- a/crates/refineable/Cargo.toml +++ b/crates/refineable/Cargo.toml @@ -11,6 +11,3 @@ doctest = false [dependencies] derive_refineable = { path = "./derive_refineable" } -proc-macro2 = "1.0.66" -quote = "1.0.9" -syn = "1.0.72" diff --git a/crates/release_channel/src/lib.rs b/crates/release_channel/src/lib.rs index e83ef0e069..864df387c0 100644 --- a/crates/release_channel/src/lib.rs +++ b/crates/release_channel/src/lib.rs @@ -1,8 +1,12 @@ -use gpui::{AppContext, Global, SemanticVersion}; -use once_cell::sync::Lazy; +//! Provides constructs for the Zed app version and release channel. + +#![deny(missing_docs)] + use std::env; -#[doc(hidden)] +use gpui::{AppContext, Global, SemanticVersion}; +use once_cell::sync::Lazy; + static RELEASE_CHANNEL_NAME: Lazy = if cfg!(debug_assertions) { Lazy::new(|| { env::var("ZED_RELEASE_CHANNEL") @@ -22,6 +26,7 @@ pub static RELEASE_CHANNEL: Lazy = _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), }); +/// The Git commit SHA that Zed was built at. #[derive(Clone)] pub struct AppCommitSha(pub String); @@ -30,11 +35,13 @@ struct GlobalAppCommitSha(AppCommitSha); impl Global for GlobalAppCommitSha {} impl AppCommitSha { + /// Returns the global [`AppCommitSha`], if one is set. pub fn try_global(cx: &AppContext) -> Option { cx.try_global::() .map(|sha| sha.0.clone()) } + /// Sets the global [`AppCommitSha`]. pub fn set_global(sha: AppCommitSha, cx: &mut AppContext) { cx.set_global(GlobalAppCommitSha(sha)) } @@ -44,11 +51,18 @@ struct GlobalAppVersion(SemanticVersion); impl Global for GlobalAppVersion {} +/// The version of Zed. pub struct AppVersion; impl AppVersion { + /// Initializes the global [`AppVersion`]. + /// + /// Attempts to read the version number from the following locations, in order: + /// 1. the `ZED_APP_VERSION` environment variable, + /// 2. the [`AppContext::app_metadata`], + /// 3. the passed in `pkg_version`. pub fn init(pkg_version: &str, cx: &mut AppContext) { - let version = if let Some(from_env) = env::var("ZED_APP_VERSION").ok() { + let version = if let Ok(from_env) = env::var("ZED_APP_VERSION") { from_env.parse().expect("invalid ZED_APP_VERSION") } else { cx.app_metadata() @@ -58,17 +72,28 @@ impl AppVersion { cx.set_global(GlobalAppVersion(version)) } + /// Returns the global version number. pub fn global(cx: &AppContext) -> SemanticVersion { cx.global::().0 } } +/// A Zed release channel. #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub enum ReleaseChannel { + /// The development release channel. + /// + /// Used for local debug builds of Zed. #[default] Dev, + + /// The Nightly release channel. Nightly, + + /// The Preview release channel. Preview, + + /// The Stable release channel. Stable, } @@ -76,21 +101,25 @@ struct GlobalReleaseChannel(ReleaseChannel); impl Global for GlobalReleaseChannel {} +/// Initializes the release channel. pub fn init(pkg_version: &str, cx: &mut AppContext) { AppVersion::init(pkg_version, cx); cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL)) } impl ReleaseChannel { + /// Returns the global [`ReleaseChannel`]. pub fn global(cx: &AppContext) -> Self { cx.global::().0 } + /// Returns the global [`ReleaseChannel`], if one is set. pub fn try_global(cx: &AppContext) -> Option { cx.try_global::() .map(|channel| channel.0) } + /// Returns the display name for this [`ReleaseChannel`]. pub fn display_name(&self) -> &'static str { match self { ReleaseChannel::Dev => "Zed Dev", @@ -100,6 +129,7 @@ impl ReleaseChannel { } } + /// Returns the programmatic name for this [`ReleaseChannel`]. pub fn dev_name(&self) -> &'static str { match self { ReleaseChannel::Dev => "dev", @@ -109,24 +139,7 @@ impl ReleaseChannel { } } - pub fn url_scheme(&self) -> &'static str { - match self { - ReleaseChannel::Dev => "zed-dev://", - ReleaseChannel::Nightly => "zed-nightly://", - ReleaseChannel::Preview => "zed-preview://", - ReleaseChannel::Stable => "zed://", - } - } - - pub fn link_prefix(&self) -> &'static str { - match self { - ReleaseChannel::Dev => "https://zed.dev/dev/", - ReleaseChannel::Nightly => "https://zed.dev/nightly/", - ReleaseChannel::Preview => "https://zed.dev/preview/", - ReleaseChannel::Stable => "https://zed.dev/", - } - } - + /// Returns the query parameter for this [`ReleaseChannel`]. pub fn release_query_param(&self) -> Option<&'static str> { match self { Self::Dev => None, @@ -136,20 +149,3 @@ impl ReleaseChannel { } } } - -pub fn parse_zed_link(link: &str) -> Option<&str> { - for release in [ - ReleaseChannel::Dev, - ReleaseChannel::Nightly, - ReleaseChannel::Preview, - ReleaseChannel::Stable, - ] { - if let Some(stripped) = link.strip_prefix(release.link_prefix()) { - return Some(stripped); - } - if let Some(stripped) = link.strip_prefix(release.url_scheme()) { - return Some(stripped); - } - } - None -} diff --git a/crates/rich_text/Cargo.toml b/crates/rich_text/Cargo.toml index b9f765d123..5a12d0e56f 100644 --- a/crates/rich_text/Cargo.toml +++ b/crates/rich_text/Cargo.toml @@ -16,16 +16,11 @@ test-support = [ ] [dependencies] -anyhow.workspace = true -collections.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true -lazy_static.workspace = true +linkify.workspace = true pulldown-cmark.workspace = true -smallvec.workspace = true -smol.workspace = true -sum_tree.workspace = true theme.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index 1063d89db3..2e5a28351d 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -1,7 +1,7 @@ use futures::FutureExt; use gpui::{ AnyElement, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText, IntoElement, - SharedString, StyledText, UnderlineStyle, WindowContext, + SharedString, StrikethroughStyle, StyledText, UnderlineStyle, WindowContext, }; use language::{HighlightId, Language, LanguageRegistry}; use std::{ops::Range, sync::Arc}; @@ -124,6 +124,7 @@ impl RichText { } } +#[allow(clippy::too_many_arguments)] pub fn render_markdown_mut( block: &str, mut mentions: &[Mention], @@ -134,10 +135,11 @@ pub fn render_markdown_mut( link_ranges: &mut Vec>, link_urls: &mut Vec, ) { - use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd}; let mut bold_depth = 0; let mut italic_depth = 0; + let mut strikethrough_depth = 0; let mut link_url = None; let mut current_language = None; let mut list_stack = Vec::new(); @@ -175,19 +177,60 @@ pub fn render_markdown_mut( if italic_depth > 0 { style.font_style = Some(FontStyle::Italic); } - if let Some(link_url) = link_url.clone() { + if strikethrough_depth > 0 { + style.strikethrough = Some(StrikethroughStyle { + thickness: 1.0.into(), + ..Default::default() + }); + } + let last_run_len = if let Some(link_url) = link_url.clone() { link_ranges.push(prev_len..text.len()); link_urls.push(link_url); style.underline = Some(UnderlineStyle { thickness: 1.0.into(), ..Default::default() }); - } + prev_len + } else { + // Manually scan for links + let mut finder = linkify::LinkFinder::new(); + finder.kinds(&[linkify::LinkKind::Url]); + let mut last_link_len = prev_len; + for link in finder.links(&t) { + let start = link.start(); + let end = link.end(); + let range = (prev_len + start)..(prev_len + end); + link_ranges.push(range.clone()); + link_urls.push(link.as_str().to_string()); - if style != HighlightStyle::default() { + // If there is a style before we match a link, we have to add this to the highlighted ranges + if style != HighlightStyle::default() && last_link_len < link.start() { + highlights.push(( + last_link_len..link.start(), + Highlight::Highlight(style), + )); + } + + highlights.push(( + range, + Highlight::Highlight(HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..style + }), + )); + + last_link_len = end; + } + last_link_len + }; + + if style != HighlightStyle::default() && last_run_len < text.len() { let mut new_highlight = true; if let Some((last_range, last_style)) = highlights.last_mut() { - if last_range.end == prev_len + if last_range.end == last_run_len && last_style == &Highlight::Highlight(style) { last_range.end = text.len(); @@ -195,7 +238,8 @@ pub fn render_markdown_mut( } } if new_highlight { - highlights.push((prev_len..text.len(), Highlight::Highlight(style))); + highlights + .push((last_run_len..text.len(), Highlight::Highlight(style))); } } } @@ -213,7 +257,12 @@ pub fn render_markdown_mut( } Event::Start(tag) => match tag { Tag::Paragraph => new_paragraph(text, &mut list_stack), - Tag::Heading(_, _, _) => { + Tag::Heading { + level: _, + id: _, + classes: _, + attrs: _, + } => { new_paragraph(text, &mut list_stack); bold_depth += 1; } @@ -230,7 +279,13 @@ pub fn render_markdown_mut( } Tag::Emphasis => italic_depth += 1, Tag::Strong => bold_depth += 1, - Tag::Link(_, url, _) => link_url = Some(url.to_string()), + Tag::Strikethrough => strikethrough_depth += 1, + Tag::Link { + link_type: _, + dest_url, + title: _, + id: _, + } => link_url = Some(dest_url.to_string()), Tag::List(number) => { list_stack.push((number, false)); } @@ -256,12 +311,13 @@ pub fn render_markdown_mut( _ => {} }, Event::End(tag) => match tag { - Tag::Heading(_, _, _) => bold_depth -= 1, - Tag::CodeBlock(_) => current_language = None, - Tag::Emphasis => italic_depth -= 1, - Tag::Strong => bold_depth -= 1, - Tag::Link(_, _, _) => link_url = None, - Tag::List(_) => drop(list_stack.pop()), + TagEnd::Heading(_) => bold_depth -= 1, + TagEnd::CodeBlock => current_language = None, + TagEnd::Emphasis => italic_depth -= 1, + TagEnd::Strong => bold_depth -= 1, + TagEnd::Strikethrough => strikethrough_depth -= 1, + TagEnd::Link => link_url = None, + TagEnd::List(_) => drop(list_stack.pop()), _ => {} }, Event::HardBreak => text.push('\n'), diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 0f2772b3d7..7f6750bce5 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -15,10 +15,8 @@ test-support = ["collections/test-support", "gpui/test-support"] [dependencies] anyhow.workspace = true -async-lock = "2.4" async-tungstenite = "0.16" base64 = "0.13" -clock.workspace = true collections.workspace = true futures.workspace = true gpui = { workspace = true, optional = true } @@ -27,9 +25,7 @@ prost.workspace = true rand.workspace = true rsa = "0.4" serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true -smol-timeout = "0.6" strum.workspace = true tracing = { version = "0.1.34", features = ["log"] } util.workspace = true @@ -39,9 +35,6 @@ zstd = "0.11" prost-build = "0.9" [dev-dependencies] -collections = { workspace = true, features = ["test-support"] } -ctor.workspace = true +collections.workspace = true env_logger.workspace = true -gpui = { workspace = true, features = ["test-support"] } -smol.workspace = true -tempfile.workspace = true +gpui.workspace = true diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 8a409d6219..8fd2c6eb69 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -12,6 +12,14 @@ message Envelope { uint32 id = 1; optional uint32 responding_to = 2; optional PeerId original_sender_id = 3; + + /* + When you are adding a new message type, instead of adding it in semantic order + and bumping the message ID's of everything that follows, add it at the end of the + file and bump the max number. See this + https://github.com/zed-industries/zed/pull/7890#discussion_r1496621823 + + */ oneof payload { Hello hello = 4; Ack ack = 5; @@ -48,6 +56,7 @@ message Envelope { GetDefinitionResponse get_definition_response = 33; GetTypeDefinition get_type_definition = 34; GetTypeDefinitionResponse get_type_definition_response = 35; + GetReferences get_references = 36; GetReferencesResponse get_references_response = 37; GetDocumentHighlights get_document_highlights = 38; @@ -184,6 +193,11 @@ message Envelope { SetRoomParticipantRole set_room_participant_role = 156; UpdateUserChannels update_user_channels = 157; + + GetImplementation get_implementation = 162; + GetImplementationResponse get_implementation_response = 163; + + JoinHostedProject join_hosted_project = 164; } reserved 158 to 161; @@ -218,6 +232,7 @@ enum ErrorCode { CircularNesting = 10; WrongMoveTarget = 11; UnsharedItem = 12; + NoSuchProject = 13; reserved 6; } @@ -392,11 +407,17 @@ message JoinProject { uint64 project_id = 1; } +message JoinHostedProject { + uint64 id = 1; +} + message JoinProjectResponse { + uint64 project_id = 5; uint32 replica_id = 1; repeated WorktreeMetadata worktrees = 2; repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; + ChannelRole role = 6; } message LeaveProject { @@ -503,6 +524,16 @@ message GetTypeDefinition { message GetTypeDefinitionResponse { repeated LocationLink links = 1; } +message GetImplementation { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + repeated VectorClockEntry version = 4; + } + +message GetImplementationResponse { + repeated LocationLink links = 1; +} message GetReferences { uint64 project_id = 1; @@ -1006,6 +1037,9 @@ message UpdateChannels { repeated ChannelParticipants channel_participants = 7; repeated ChannelMessageId latest_channel_message_ids = 8; repeated ChannelBufferVersion latest_channel_buffer_versions = 9; + + repeated HostedProject hosted_projects = 10; + repeated uint64 deleted_hosted_projects = 11; } message UpdateUserChannels { @@ -1034,6 +1068,13 @@ message ChannelParticipants { repeated uint64 participant_user_ids = 2; } +message HostedProject { + uint64 id = 1; + uint64 channel_id = 2; + string name = 3; + ChannelVisibility visibility = 4; +} + message JoinChannel { uint64 channel_id = 1; } @@ -1087,6 +1128,7 @@ enum ChannelRole { Member = 1; Guest = 2; Banned = 3; + Talker = 4; } message SetChannelMemberRole { @@ -1292,6 +1334,8 @@ message Follow { } message FollowResponse { + View active_view = 3; + // TODO: after 0.124.0 is retired, remove these. optional ViewId active_view_id = 1; repeated View views = 2; } @@ -1299,10 +1343,11 @@ message FollowResponse { message UpdateFollowers { uint64 room_id = 1; optional uint64 project_id = 2; - repeated PeerId follower_ids = 3; + reserved 3; oneof variant { - UpdateActiveView update_active_view = 4; View create_view = 5; + // TODO: after 0.124.0 is retired, remove these. + UpdateActiveView update_active_view = 4; UpdateView update_view = 6; } } @@ -1331,6 +1376,7 @@ message ViewId { message UpdateActiveView { optional ViewId id = 1; optional PeerId leader_id = 2; + View view = 3; } message UpdateView { diff --git a/crates/rpc/src/conn.rs b/crates/rpc/src/conn.rs index ae5c9fd226..18bce4abba 100644 --- a/crates/rpc/src/conn.rs +++ b/crates/rpc/src/conn.rs @@ -84,7 +84,6 @@ impl Connection { }); let rx = rx.then({ - let killed = killed; let executor = executor.clone(); move |msg| { let killed = killed.clone(); diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index 57131d3421..bf76436468 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use serde_json::{map, Value}; use strum::{EnumVariantNames, VariantNames as _}; -const KIND: &'static str = "kind"; -const ENTITY_ID: &'static str = "entity_id"; +const KIND: &str = "kind"; +const ENTITY_ID: &str = "entity_id"; /// A notification that can be stored, associated with a given recipient. /// diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index f3f74899b9..c73f52a99e 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -315,10 +315,13 @@ impl Peer { "incoming response: requester resumed" ); } else { + let message_type = proto::build_typed_envelope(connection_id, incoming) + .map(|p| p.payload_type_name()); tracing::warn!( %connection_id, message_id, responding_to, + message_type, "incoming response: unknown request" ); } @@ -358,8 +361,8 @@ impl Peer { self.connections.write().remove(&connection_id); } + #[cfg(any(test, feature = "test-support"))] pub fn reset(&self, epoch: u32) { - self.teardown(); self.next_connection_id.store(0, SeqCst); self.epoch.store(epoch, SeqCst); } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 9b885d1840..38e80103ca 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -192,6 +192,8 @@ messages!( (GetReferencesResponse, Background), (GetTypeDefinition, Background), (GetTypeDefinitionResponse, Background), + (GetImplementation, Background), + (GetImplementationResponse, Background), (GetUsers, Foreground), (Hello, Foreground), (IncomingCall, Foreground), @@ -204,6 +206,7 @@ messages!( (JoinChannelChat, Foreground), (JoinChannelChatResponse, Foreground), (JoinProject, Foreground), + (JoinHostedProject, Foreground), (JoinProjectResponse, Foreground), (JoinRoom, Foreground), (JoinRoomResponse, Foreground), @@ -312,6 +315,7 @@ request_messages!( (GetCodeActions, GetCodeActionsResponse), (GetCompletions, GetCompletionsResponse), (GetDefinition, GetDefinitionResponse), + (GetImplementation, GetImplementationResponse), (GetDocumentHighlights, GetDocumentHighlightsResponse), (GetHover, GetHoverResponse), (GetNotifications, GetNotificationsResponse), @@ -326,6 +330,7 @@ request_messages!( (JoinChannel, JoinRoomResponse), (JoinChannelBuffer, JoinChannelBufferResponse), (JoinChannelChat, JoinChannelChatResponse), + (JoinHostedProject, JoinProjectResponse), (JoinProject, JoinProjectResponse), (JoinRoom, JoinRoomResponse), (LeaveChannelBuffer, Ack), @@ -388,6 +393,7 @@ entity_messages!( GetCodeActions, GetCompletions, GetDefinition, + GetImplementation, GetDocumentHighlights, GetHover, GetProjectSymbols, diff --git a/crates/runnable/src/lib.rs b/crates/runnable/src/lib.rs deleted file mode 100644 index 99c485c1a4..0000000000 --- a/crates/runnable/src/lib.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Baseline interface of Runnables in Zed: all runnables in Zed are intended to use those for implementing their own logic. -#![deny(missing_docs)] - -mod static_runnable; -pub mod static_source; - -pub use static_runnable::StaticRunnable; - -use collections::HashMap; -use gpui::ModelContext; -use std::any::Any; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -/// Runnable identifier, unique within the application. -/// Based on it, runnable reruns and terminal tabs are managed. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct RunnableId(String); - -/// Contains all information needed by Zed to spawn a new terminal tab for the given runnable. -#[derive(Debug, Clone)] -pub struct SpawnInTerminal { - /// Id of the runnable to use when determining task tab affinity. - pub id: RunnableId, - /// Human readable name of the terminal tab. - pub label: String, - /// Executable command to spawn. - pub command: String, - /// Arguments to the command. - pub args: Vec, - /// Current working directory to spawn the command into. - pub cwd: Option, - /// Env overrides for the command, will be appended to the terminal's environment from the settings. - pub env: HashMap, - /// Whether to use a new terminal tab or reuse the existing one to spawn the process. - pub use_new_terminal: bool, - /// Whether to allow multiple instances of the same runnable to be run, or rather wait for the existing ones to finish. - pub allow_concurrent_runs: bool, -} - -/// Represents a short lived recipe of a runnable, whose main purpose -/// is to get spawned. -pub trait Runnable { - /// Unique identifier of the runnable to spawn. - fn id(&self) -> &RunnableId; - /// Human readable name of the runnable to display in the UI. - fn name(&self) -> &str; - /// Task's current working directory. If `None`, current project's root will be used. - fn cwd(&self) -> Option<&Path>; - /// Sets up everything needed to spawn the runnable in the given directory (`cwd`). - /// If a runnable is intended to be spawned in the terminal, it should return the corresponding struct filled with the data necessary. - fn exec(&self, cwd: Option) -> Option; -} - -/// [`Source`] produces runnables that can be scheduled. -/// -/// Implementations of this trait could be e.g. [`StaticSource`] that parses runnables from a .json files and provides process templates to be spawned; -/// another one could be a language server providing lenses with tests or build server listing all targets for a given project. -pub trait Source: Any { - /// A way to erase the type of the source, processing and storing them generically. - fn as_any(&mut self) -> &mut dyn Any; - /// Collects all runnables available for scheduling, for the path given. - fn runnables_for_path( - &mut self, - path: Option<&Path>, - cx: &mut ModelContext>, - ) -> Vec>; -} diff --git a/crates/runnable/src/static_runnable.rs b/crates/runnable/src/static_runnable.rs deleted file mode 100644 index 7138dc91c2..0000000000 --- a/crates/runnable/src/static_runnable.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Definitions of runnables with a static file config definition, not dependent on the application state. - -use std::path::{Path, PathBuf}; - -use crate::{static_source::Definition, Runnable, RunnableId, SpawnInTerminal}; - -/// A single config file entry with the deserialized runnable definition. -#[derive(Clone, Debug, PartialEq)] -pub struct StaticRunnable { - id: RunnableId, - definition: Definition, -} - -impl StaticRunnable { - pub(super) fn new(id: usize, runnable: Definition) -> Self { - Self { - id: RunnableId(format!("static_{}_{}", runnable.label, id)), - definition: runnable, - } - } -} - -impl Runnable for StaticRunnable { - fn exec(&self, cwd: Option) -> Option { - Some(SpawnInTerminal { - id: self.id.clone(), - cwd, - use_new_terminal: self.definition.use_new_terminal, - allow_concurrent_runs: self.definition.allow_concurrent_runs, - label: self.definition.label.clone(), - command: self.definition.command.clone(), - args: self.definition.args.clone(), - env: self.definition.env.clone(), - }) - } - - fn name(&self) -> &str { - &self.definition.label - } - - fn id(&self) -> &RunnableId { - &self.id - } - - fn cwd(&self) -> Option<&Path> { - self.definition.cwd.as_deref() - } -} diff --git a/crates/runnable/src/static_source.rs b/crates/runnable/src/static_source.rs deleted file mode 100644 index 3a274b0a12..0000000000 --- a/crates/runnable/src/static_source.rs +++ /dev/null @@ -1,157 +0,0 @@ -//! A source of runnables, based on a static configuration, deserialized from the runnables config file, and related infrastructure for tracking changes to the file. - -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use collections::HashMap; -use futures::StreamExt; -use gpui::{AppContext, Context, Model, ModelContext, Subscription}; -use schemars::{gen::SchemaSettings, JsonSchema}; -use serde::{Deserialize, Serialize}; -use util::ResultExt; - -use crate::{Runnable, Source, StaticRunnable}; -use futures::channel::mpsc::UnboundedReceiver; - -/// The source of runnables defined in a runnables config file. -pub struct StaticSource { - runnables: Vec, - _definitions: Model>, - _subscription: Subscription, -} - -/// Static runnable definition from the runnables config file. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub(crate) struct Definition { - /// Human readable name of the runnable to display in the UI. - pub label: String, - /// Executable command to spawn. - pub command: String, - /// Arguments to the command. - #[serde(default)] - pub args: Vec, - /// Env overrides for the command, will be appended to the terminal's environment from the settings. - #[serde(default)] - pub env: HashMap, - /// Current working directory to spawn the command into, defaults to current project root. - #[serde(default)] - pub cwd: Option, - /// Whether to use a new terminal tab or reuse the existing one to spawn the process. - #[serde(default)] - pub use_new_terminal: bool, - /// Whether to allow multiple instances of the same runnable to be run, or rather wait for the existing ones to finish. - #[serde(default)] - pub allow_concurrent_runs: bool, -} - -/// A group of Runnables defined in a JSON file. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct DefinitionProvider { - version: String, - runnables: Vec, -} - -impl DefinitionProvider { - /// Generates JSON schema of Runnables JSON definition format. - pub fn generate_json_schema() -> serde_json_lenient::Value { - let schema = SchemaSettings::draft07() - .with(|settings| settings.option_add_null_type = false) - .into_generator() - .into_root_schema_for::(); - - serde_json_lenient::to_value(schema).unwrap() - } -} -/// A Wrapper around deserializable T that keeps track of it's contents -/// via a provided channel. Once T value changes, the observers of [`TrackedFile`] are -/// notified. -struct TrackedFile { - parsed_contents: T, -} - -impl Deserialize<'a> + PartialEq + 'static> TrackedFile { - fn new( - parsed_contents: T, - mut tracker: UnboundedReceiver, - cx: &mut AppContext, - ) -> Model { - cx.new_model(move |cx| { - cx.spawn(|tracked_file, mut cx| async move { - while let Some(new_contents) = tracker.next().await { - let Some(new_contents) = serde_json_lenient::from_str(&new_contents).log_err() - else { - continue; - }; - tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile, cx| { - if tracked_file.parsed_contents != new_contents { - tracked_file.parsed_contents = new_contents; - cx.notify(); - }; - })?; - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - Self { parsed_contents } - }) - } - - fn get(&self) -> &T { - &self.parsed_contents - } -} - -impl StaticSource { - /// Initializes the static source, reacting on runnables config changes. - pub fn new( - runnables_file_tracker: UnboundedReceiver, - cx: &mut AppContext, - ) -> Model> { - let definitions = - TrackedFile::new(DefinitionProvider::default(), runnables_file_tracker, cx); - cx.new_model(|cx| { - let _subscription = cx.observe( - &definitions, - |source: &mut Box<(dyn Source + 'static)>, new_definitions, cx| { - if let Some(static_source) = source.as_any().downcast_mut::() { - static_source.runnables = new_definitions - .read(cx) - .get() - .runnables - .clone() - .into_iter() - .enumerate() - .map(|(id, definition)| StaticRunnable::new(id, definition)) - .collect(); - cx.notify(); - } - }, - ); - Box::new(Self { - runnables: Vec::new(), - _definitions: definitions, - _subscription, - }) - }) - } -} - -impl Source for StaticSource { - fn runnables_for_path( - &mut self, - _: Option<&Path>, - _: &mut ModelContext>, - ) -> Vec> { - self.runnables - .clone() - .into_iter() - .map(|runnable| Arc::new(runnable) as Arc) - .collect() - } - - fn as_any(&mut self) -> &mut dyn std::any::Any { - self - } -} diff --git a/crates/runnables_ui/Cargo.toml b/crates/runnables_ui/Cargo.toml deleted file mode 100644 index f78edd1d70..0000000000 --- a/crates/runnables_ui/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "runnables_ui" -version = "0.1.0" -edition = "2021" -publish = false -license = "GPL-3.0-or-later" - -[dependencies] -anyhow.workspace = true -db.workspace = true -editor.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -log.workspace = true -picker.workspace = true -project.workspace = true -runnable.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true diff --git a/crates/runnables_ui/src/lib.rs b/crates/runnables_ui/src/lib.rs deleted file mode 100644 index 90a642bb58..0000000000 --- a/crates/runnables_ui/src/lib.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::path::PathBuf; - -use gpui::{AppContext, ViewContext, WindowContext}; -use modal::RunnablesModal; -use runnable::Runnable; -use util::ResultExt; -use workspace::Workspace; - -mod modal; - -pub fn init(cx: &mut AppContext) { - cx.observe_new_views( - |workspace: &mut Workspace, _: &mut ViewContext| { - workspace - .register_action(|workspace, _: &modal::Spawn, cx| { - let inventory = workspace.project().read(cx).runnable_inventory().clone(); - let workspace_handle = workspace.weak_handle(); - workspace.toggle_modal(cx, |cx| { - RunnablesModal::new(inventory, workspace_handle, cx) - }) - }) - .register_action(move |workspace, _: &modal::Rerun, cx| { - if let Some(runnable) = workspace.project().update(cx, |project, cx| { - project - .runnable_inventory() - .update(cx, |inventory, cx| inventory.last_scheduled_runnable(cx)) - }) { - schedule_runnable(workspace, runnable.as_ref(), cx) - }; - }); - }, - ) - .detach(); -} - -fn schedule_runnable( - workspace: &Workspace, - runnable: &dyn Runnable, - cx: &mut ViewContext<'_, Workspace>, -) { - let cwd = match runnable.cwd() { - Some(cwd) => Some(cwd.to_path_buf()), - None => runnable_cwd(workspace, cx).log_err().flatten(), - }; - let spawn_in_terminal = runnable.exec(cwd); - if let Some(spawn_in_terminal) = spawn_in_terminal { - workspace.project().update(cx, |project, cx| { - project.runnable_inventory().update(cx, |inventory, _| { - inventory.last_scheduled_runnable = Some(runnable.id().clone()); - }) - }); - cx.emit(workspace::Event::SpawnRunnable(spawn_in_terminal)); - } -} - -fn runnable_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result> { - let project = workspace.project().read(cx); - let available_worktrees = project - .worktrees() - .filter(|worktree| { - let worktree = worktree.read(cx); - worktree.is_visible() - && worktree.is_local() - && worktree.root_entry().map_or(false, |e| e.is_dir()) - }) - .collect::>(); - let cwd = match available_worktrees.len() { - 0 => None, - 1 => Some(available_worktrees[0].read(cx).abs_path()), - _ => { - let cwd_for_active_entry = project.active_entry().and_then(|entry_id| { - available_worktrees.into_iter().find_map(|worktree| { - let worktree = worktree.read(cx); - if worktree.contains_entry(entry_id) { - Some(worktree.abs_path()) - } else { - None - } - }) - }); - anyhow::ensure!( - cwd_for_active_entry.is_some(), - "Cannot determine runnable cwd for multiple worktrees" - ); - cwd_for_active_entry - } - }; - Ok(cwd.map(|path| path.to_path_buf())) -} diff --git a/crates/runnables_ui/src/modal.rs b/crates/runnables_ui/src/modal.rs deleted file mode 100644 index f15bf86a2f..0000000000 --- a/crates/runnables_ui/src/modal.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::sync::Arc; - -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - actions, rems, DismissEvent, EventEmitter, FocusableView, InteractiveElement, Model, - ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext, - VisualContext, WeakView, -}; -use picker::{Picker, PickerDelegate}; -use project::Inventory; -use runnable::Runnable; -use ui::{v_flex, HighlightedLabel, ListItem, ListItemSpacing, Selectable}; -use util::ResultExt; -use workspace::{ModalView, Workspace}; - -use crate::schedule_runnable; - -actions!(runnables, [Spawn, Rerun]); - -/// A modal used to spawn new runnables. -pub(crate) struct RunnablesModalDelegate { - inventory: Model, - candidates: Vec>, - matches: Vec, - selected_index: usize, - placeholder_text: Arc, - workspace: WeakView, -} - -impl RunnablesModalDelegate { - fn new(inventory: Model, workspace: WeakView) -> Self { - Self { - inventory, - workspace, - candidates: Vec::new(), - matches: Vec::new(), - selected_index: 0, - placeholder_text: Arc::from("Select runnable..."), - } - } -} - -pub(crate) struct RunnablesModal { - picker: View>, - _subscription: Subscription, -} - -impl RunnablesModal { - pub(crate) fn new( - inventory: Model, - workspace: WeakView, - cx: &mut ViewContext, - ) -> Self { - let picker = cx.new_view(|cx| { - Picker::uniform_list(RunnablesModalDelegate::new(inventory, workspace), cx) - }); - let _subscription = cx.subscribe(&picker, |_, _, _, cx| { - cx.emit(DismissEvent); - }); - Self { - picker, - _subscription, - } - } -} -impl Render for RunnablesModal { - fn render(&mut self, cx: &mut ViewContext) -> impl gpui::prelude::IntoElement { - v_flex() - .w(rems(34.)) - .child(self.picker.clone()) - .on_mouse_down_out(cx.listener(|modal, _, cx| { - modal.picker.update(cx, |picker, cx| { - picker.cancel(&Default::default(), cx); - }) - })) - } -} - -impl EventEmitter for RunnablesModal {} -impl FocusableView for RunnablesModal { - fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { - self.picker.read(cx).focus_handle(cx) - } -} -impl ModalView for RunnablesModal {} - -impl PickerDelegate for RunnablesModalDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { - self.selected_index = ix; - } - - fn placeholder_text(&self) -> Arc { - self.placeholder_text.clone() - } - - fn update_matches( - &mut self, - query: String, - cx: &mut ViewContext>, - ) -> Task<()> { - cx.spawn(move |picker, mut cx| async move { - let Some(candidates) = picker - .update(&mut cx, |picker, cx| { - picker.delegate.candidates = picker - .delegate - .inventory - .update(cx, |inventory, cx| inventory.list_runnables(None, cx)); - picker - .delegate - .candidates - .sort_by(|a, b| a.name().cmp(&b.name())); - - picker - .delegate - .candidates - .iter() - .enumerate() - .map(|(index, candidate)| StringMatchCandidate { - id: index, - char_bag: candidate.name().chars().collect(), - string: candidate.name().into(), - }) - .collect::>() - }) - .ok() - else { - return; - }; - let matches = fuzzy::match_strings( - &candidates, - &query, - true, - 1000, - &Default::default(), - cx.background_executor().clone(), - ) - .await; - picker - .update(&mut cx, |picker, _| { - let delegate = &mut picker.delegate; - delegate.matches = matches; - - if delegate.matches.is_empty() { - delegate.selected_index = 0; - } else { - delegate.selected_index = - delegate.selected_index.min(delegate.matches.len() - 1); - } - }) - .log_err(); - }) - } - - fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { - let current_match_index = self.selected_index(); - let Some(current_match) = self.matches.get(current_match_index) else { - return; - }; - - let ix = current_match.candidate_id; - let runnable = &self.candidates[ix]; - self.workspace - .update(cx, |workspace, cx| { - schedule_runnable(workspace, runnable.as_ref(), cx); - }) - .ok(); - cx.emit(DismissEvent); - } - - fn dismissed(&mut self, cx: &mut ViewContext>) { - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _cx: &mut ViewContext>, - ) -> Option { - let hit = &self.matches[ix]; - //let runnable = self.candidates[target_index].metadata(); - let highlights: Vec<_> = hit.positions.iter().copied().collect(); - Some( - ListItem::new(SharedString::from(format!("runnables-modal-{ix}"))) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .selected(selected) - .start_slot(HighlightedLabel::new(hit.string.clone(), highlights)), - ) - } -} diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index d4e2c70a23..8daa5e743e 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -11,19 +11,16 @@ doctest = false [dependencies] anyhow.workspace = true -bitflags = "1" +bitflags.workspace = true collections.workspace = true editor.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true -log.workspace = true menu.workspace = true -postage.workspace = true project.workspace = true semantic_index.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true settings.workspace = true smallvec.workspace = true diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index e86de37322..7d7626dd2f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -38,7 +38,7 @@ pub use registrar::DivRegistrar; use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults}; const MIN_INPUT_WIDTH_REMS: f32 = 15.; -const MAX_INPUT_WIDTH_REMS: f32 = 25.; +const MAX_INPUT_WIDTH_REMS: f32 = 30.; #[derive(PartialEq, Clone, Deserialize)] pub struct Deploy { @@ -98,7 +98,7 @@ impl BufferSearchBar { font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, - line_height: relative(1.3).into(), + line_height: relative(1.3), background_color: None, underline: None, strikethrough: None, @@ -127,7 +127,9 @@ impl Render for BufferSearchBar { let supported_options = self.supported_options(); - if self.query_editor.read(cx).placeholder_text().is_none() { + if self.query_editor.update(cx, |query_editor, cx| { + query_editor.placeholder_text(cx).is_none() + }) { let query_focus_handle = self.query_editor.focus_handle(cx); let up_keystrokes = cx .bindings_for_action_in(&PreviousHistoryQuery {}, &query_focus_handle) @@ -217,7 +219,6 @@ impl Render for BufferSearchBar { .flex_1() .px_2() .py_1() - .gap_2() .border_1() .border_color(editor_border) .min_w(rems(MIN_INPUT_WIDTH_REMS)) @@ -1476,7 +1477,7 @@ mod tests { buffer_text, ) }); - let window = cx.add_window(|_| ()); + let window = cx.add_window(|_| gpui::Empty); let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); diff --git a/crates/search/src/history.rs b/crates/search/src/history.rs index 5571313acb..9d76d48e85 100644 --- a/crates/search/src/history.rs +++ b/crates/search/src/history.rs @@ -16,7 +16,7 @@ impl SearchHistory { } if let Some(previously_searched) = self.history.last_mut() { - if search_string.find(previously_searched.as_str()).is_some() { + if search_string.contains(previously_searched.as_str()) { *previously_searched = search_string; self.selected = Some(self.history.len() - 1); return; diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index ad2e894527..c5d5f667dc 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -7,16 +7,18 @@ use crate::{ use anyhow::{Context as _, Result}; use collections::HashMap; use editor::{ - actions::SelectAll, items::active_match_index, scroll::Autoscroll, Anchor, Editor, EditorEvent, - MultiBuffer, MAX_TAB_TITLE_LEN, + actions::SelectAll, + items::active_match_index, + scroll::{Autoscroll, Axis}, + Anchor, Editor, EditorEvent, MultiBuffer, MAX_TAB_TITLE_LEN, }; use editor::{EditorElement, EditorStyle}; use gpui::{ actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Element, EntityId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global, Hsla, - InteractiveElement, IntoElement, KeyContext, Model, ModelContext, ParentElement, PromptLevel, - Render, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, - WeakModel, WeakView, WhiteSpace, WindowContext, + InteractiveElement, IntoElement, KeyContext, Model, ModelContext, ParentElement, Point, + PromptLevel, Render, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, + VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext, }; use menu::Confirm; use project::{ @@ -50,6 +52,9 @@ use workspace::{ WorkspaceId, }; +const MIN_INPUT_WIDTH_REMS: f32 = 15.; +const MAX_INPUT_WIDTH_REMS: f32 = 30.; + actions!( project_search, [SearchInNew, ToggleFocus, NextField, ToggleFilters] @@ -214,7 +219,7 @@ impl ProjectSearch { active_query: self.active_query.clone(), search_id: self.search_id, search_history: self.search_history.clone(), - no_results: self.no_results.clone(), + no_results: self.no_results, }) } @@ -534,11 +539,12 @@ impl Item for ProjectSearchView { fn save( &mut self, + format: bool, project: Model, cx: &mut ViewContext, ) -> Task> { self.results_editor - .update(cx, |editor, cx| editor.save(project, cx)) + .update(cx, |editor, cx| editor.save(format, project, cx)) } fn save_as( @@ -996,7 +1002,7 @@ impl ProjectSearchView { let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); let search = cx.new_view(|cx| ProjectSearchView::new(model, cx, None)); - workspace.add_item(Box::new(search.clone()), cx); + workspace.add_item_to_active_pane(Box::new(search.clone()), cx); search.update(cx, |search, cx| { search .included_files_editor @@ -1045,7 +1051,7 @@ impl ProjectSearchView { model.search(new_query, cx); model }); - workspace.add_item( + workspace.add_item_to_active_pane( Box::new(cx.new_view(|cx| ProjectSearchView::new(model, cx, None))), cx, ); @@ -1095,7 +1101,7 @@ impl ProjectSearchView { let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); let view = cx.new_view(|cx| ProjectSearchView::new(model, cx, settings)); - workspace.add_item(Box::new(view.clone()), cx); + workspace.add_item_to_active_pane(Box::new(view.clone()), cx); view }; @@ -1274,7 +1280,7 @@ impl ProjectSearchView { fn focus_results_editor(&mut self, cx: &mut ViewContext) { self.query_editor.update(cx, |query_editor, cx| { let cursor = query_editor.selections.newest_anchor().head(); - query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor])); + query_editor.change_selections(None, cx, |s| s.select_ranges([cursor..cursor])); }); self.query_editor_was_focused = false; let results_handle = self.results_editor.focus_handle(cx); @@ -1294,11 +1300,11 @@ impl ProjectSearchView { if is_new_search { let range_to_select = match_ranges .first() - .clone() .map(|range| editor.range_for_match(range)); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges(range_to_select) }); + editor.scroll(Point::default(), Some(Axis::Vertical), cx); } editor.highlight_background::( match_ranges, @@ -1409,25 +1415,22 @@ impl ProjectSearchBar { active_project_search.update(cx, |project_view, cx| { let mut views = vec![&project_view.query_editor]; + if project_view.replace_enabled { + views.push(&project_view.replacement_editor); + } if project_view.filters_enabled { views.extend([ &project_view.included_files_editor, &project_view.excluded_files_editor, ]); } - if project_view.replace_enabled { - views.push(&project_view.replacement_editor); - } let current_index = match views .iter() .enumerate() .find(|(_, view)| view.focus_handle(cx).is_focused(cx)) { Some((index, _)) => index, - - None => { - return; - } + None => return, }; let new_index = match direction { @@ -1629,7 +1632,7 @@ impl ProjectSearchBar { font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, - line_height: relative(1.3).into(), + line_height: relative(1.3), background_color: None, underline: None, strikethrough: None, @@ -1665,78 +1668,64 @@ impl Render for ProjectSearchBar { let search = search.read(cx); let semantic_is_available = SemanticIndex::enabled(cx); - let query_column = v_flex().child( - h_flex() - .min_w(rems(512. / 16.)) - .px_2() - .py_1() - .gap_2() - .bg(cx.theme().colors().editor_background) - .border_1() - .border_color(search.border_color_for(InputPanel::Query, cx)) - .rounded_lg() - .on_action(cx.listener(|this, action, cx| this.confirm(action, cx))) - .on_action(cx.listener(|this, action, cx| this.previous_history_query(action, cx))) - .on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx))) - .child(Icon::new(IconName::MagnifyingGlass)) - .child(self.render_text_input(&search.query_editor, cx)) - .child( - h_flex() - .child( - IconButton::new("project-search-filter-button", IconName::Filter) - .tooltip(|cx| { - Tooltip::for_action("Toggle filters", &ToggleFilters, cx) - }) - .on_click(cx.listener(|this, _, cx| { - this.toggle_filters(cx); - })) - .selected( - self.active_project_search - .as_ref() - .map(|search| search.read(cx).filters_enabled) - .unwrap_or_default(), - ), - ) - .when(search.current_mode != SearchMode::Semantic, |this| { - this.child( - IconButton::new( - "project-search-case-sensitive", - IconName::CaseSensitive, + let query_column = h_flex() + .flex_1() + .px_2() + .py_1() + .border_1() + .border_color(search.border_color_for(InputPanel::Query, cx)) + .rounded_lg() + .min_w(rems(MIN_INPUT_WIDTH_REMS)) + .max_w(rems(MAX_INPUT_WIDTH_REMS)) + .on_action(cx.listener(|this, action, cx| this.confirm(action, cx))) + .on_action(cx.listener(|this, action, cx| this.previous_history_query(action, cx))) + .on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx))) + .child(self.render_text_input(&search.query_editor, cx)) + .child( + h_flex() + .child( + IconButton::new("project-search-filter-button", IconName::Filter) + .tooltip(|cx| Tooltip::for_action("Toggle filters", &ToggleFilters, cx)) + .on_click(cx.listener(|this, _, cx| { + this.toggle_filters(cx); + })) + .selected( + self.active_project_search + .as_ref() + .map(|search| search.read(cx).filters_enabled) + .unwrap_or_default(), + ), + ) + .when(search.current_mode != SearchMode::Semantic, |this| { + this.child( + IconButton::new( + "project-search-case-sensitive", + IconName::CaseSensitive, + ) + .tooltip(|cx| { + Tooltip::for_action( + "Toggle case sensitive", + &ToggleCaseSensitive, + cx, ) + }) + .selected(self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx)) + .on_click(cx.listener(|this, _, cx| { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + })), + ) + .child( + IconButton::new("project-search-whole-word", IconName::WholeWord) .tooltip(|cx| { - Tooltip::for_action( - "Toggle case sensitive", - &ToggleCaseSensitive, - cx, - ) + Tooltip::for_action("Toggle whole word", &ToggleWholeWord, cx) }) - .selected(self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx)) - .on_click(cx.listener( - |this, _, cx| { - this.toggle_search_option( - SearchOptions::CASE_SENSITIVE, - cx, - ); - }, - )), - ) - .child( - IconButton::new("project-search-whole-word", IconName::WholeWord) - .tooltip(|cx| { - Tooltip::for_action( - "Toggle whole word", - &ToggleWholeWord, - cx, - ) - }) - .selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx)) - .on_click(cx.listener(|this, _, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); - })), - ) - }), - ), - ); + .selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx)) + .on_click(cx.listener(|this, _, cx| { + this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + })), + ) + }), + ); let mode_column = v_flex().items_start().justify_start().child( h_flex() @@ -1807,56 +1796,23 @@ impl Render for ProjectSearchBar { .tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)), ), ); - let replace_column = if search.replace_enabled { - h_flex() - .flex_1() - .h_full() - .gap_2() - .px_2() - .py_1() - .border_1() - .border_color(cx.theme().colors().border) - .rounded_lg() - .child(Icon::new(IconName::Replace).size(ui::IconSize::Small)) - .child(self.render_text_input(&search.replacement_editor, cx)) - } else { - // Fill out the space if we don't have a replacement editor. - h_flex().flex_1() - }; - let actions_column = h_flex() - .when(search.replace_enabled, |this| { - this.child( - IconButton::new("project-search-replace-next", IconName::ReplaceNext) - .on_click(cx.listener(|this, _, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_next(&ReplaceNext, cx); - }) - } - })) - .tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)), - ) - .child( - IconButton::new("project-search-replace-all", IconName::ReplaceAll) - .on_click(cx.listener(|this, _, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_all(&ReplaceAll, cx); - }) - } - })) - .tooltip(|cx| Tooltip::for_action("Replace all matches", &ReplaceAll, cx)), - ) - }) - .when_some(search.active_match_index, |mut this, index| { + + let match_text = search + .active_match_index + .and_then(|index| { let index = index + 1; let match_quantity = search.model.read(cx).match_ranges.len(); if match_quantity > 0 { debug_assert!(match_quantity >= index); - this = this.child(Label::new(format!("{index}/{match_quantity}"))) + Some(format!("{index}/{match_quantity}").to_string()) + } else { + None } - this }) + .unwrap_or_else(|| "No matches".to_string()); + + let matches_column = h_flex() + .child(div().min_w(rems(6.)).child(Label::new(match_text))) .child( IconButton::new("project-search-prev-match", IconName::ChevronLeft) .disabled(search.active_match_index.is_none()) @@ -1884,10 +1840,104 @@ impl Render for ProjectSearchBar { .tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)), ); + let search_line = h_flex() + .gap_2() + .flex_1() + .child(query_column) + .child(mode_column) + .child(matches_column); + + let replace_line = search.replace_enabled.then(|| { + let replace_column = h_flex() + .flex_1() + .min_w(rems(MIN_INPUT_WIDTH_REMS)) + .max_w(rems(MAX_INPUT_WIDTH_REMS)) + .h_8() + .px_2() + .py_1() + .border_1() + .border_color(cx.theme().colors().border) + .rounded_lg() + .child(self.render_text_input(&search.replacement_editor, cx)); + let replace_actions = h_flex().when(search.replace_enabled, |this| { + this.child( + IconButton::new("project-search-replace-next", IconName::ReplaceNext) + .on_click(cx.listener(|this, _, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_next(&ReplaceNext, cx); + }) + } + })) + .tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)), + ) + .child( + IconButton::new("project-search-replace-all", IconName::ReplaceAll) + .on_click(cx.listener(|this, _, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_all(&ReplaceAll, cx); + }) + } + })) + .tooltip(|cx| Tooltip::for_action("Replace all matches", &ReplaceAll, cx)), + ) + }); + h_flex() + .gap_2() + .child(replace_column) + .child(replace_actions) + }); + + let filter_line = search.filters_enabled.then(|| { + h_flex() + .w_full() + .gap_2() + .child( + h_flex() + .flex_1() + .min_w(rems(MIN_INPUT_WIDTH_REMS)) + .max_w(rems(MAX_INPUT_WIDTH_REMS)) + .h_8() + .px_2() + .py_1() + .border_1() + .border_color(search.border_color_for(InputPanel::Include, cx)) + .rounded_lg() + .child(self.render_text_input(&search.included_files_editor, cx)) + .when(search.current_mode != SearchMode::Semantic, |this| { + this.child( + SearchOptions::INCLUDE_IGNORED.as_button( + search + .search_options + .contains(SearchOptions::INCLUDE_IGNORED), + cx.listener(|this, _, cx| { + this.toggle_search_option( + SearchOptions::INCLUDE_IGNORED, + cx, + ); + }), + ), + ) + }), + ) + .child( + h_flex() + .flex_1() + .min_w(rems(MIN_INPUT_WIDTH_REMS)) + .max_w(rems(MAX_INPUT_WIDTH_REMS)) + .h_8() + .px_2() + .py_1() + .border_1() + .border_color(search.border_color_for(InputPanel::Exclude, cx)) + .rounded_lg() + .child(self.render_text_input(&search.excluded_files_editor, cx)), + ) + }); + v_flex() .key_context(key_context) - .flex_grow() - .gap_2() .on_action(cx.listener(|this, _: &ToggleFocus, cx| this.move_focus_to_results(cx))) .on_action(cx.listener(|this, _: &ToggleFilters, cx| { this.toggle_filters(cx); @@ -1945,60 +1995,11 @@ impl Render for ProjectSearchBar { }) .on_action(cx.listener(Self::select_next_match)) .on_action(cx.listener(Self::select_prev_match)) - .child( - h_flex() - .justify_between() - .gap_2() - .child(query_column) - .child(mode_column) - .child(replace_column) - .child(actions_column), - ) - .when(search.filters_enabled, |this| { - this.child( - h_flex() - .flex_1() - .gap_2() - .justify_between() - .child( - h_flex() - .flex_1() - .h_full() - .px_2() - .py_1() - .border_1() - .border_color(search.border_color_for(InputPanel::Include, cx)) - .rounded_lg() - .child(self.render_text_input(&search.included_files_editor, cx)) - .when(search.current_mode != SearchMode::Semantic, |this| { - this.child( - SearchOptions::INCLUDE_IGNORED.as_button( - search - .search_options - .contains(SearchOptions::INCLUDE_IGNORED), - cx.listener(|this, _, cx| { - this.toggle_search_option( - SearchOptions::INCLUDE_IGNORED, - cx, - ); - }), - ), - ) - }), - ) - .child( - h_flex() - .flex_1() - .h_full() - .px_2() - .py_1() - .border_1() - .border_color(search.border_color_for(InputPanel::Exclude, cx)) - .rounded_lg() - .child(self.render_text_input(&search.excluded_files_editor, cx)), - ), - ) - }) + .gap_2() + .w_full() + .child(search_line) + .children(replace_line) + .children(filter_line) } } @@ -2096,11 +2097,12 @@ fn register_workspace_action_for_present_search( pub mod tests { use super::*; use editor::DisplayPoint; - use gpui::{Action, TestAppContext}; + use gpui::{Action, TestAppContext, WindowHandle}; use project::FakeFs; use semantic_index::semantic_index_settings::SemanticIndexSettings; use serde_json::json; use settings::{Settings, SettingsStore}; + use std::sync::Arc; use workspace::DeploySearch; #[gpui::test] @@ -2122,15 +2124,7 @@ pub mod tests { let search = cx.new_model(|cx| ProjectSearch::new(project, cx)); let search_view = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx, None)); - search_view - .update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); - search_view.search(cx); - }) - .unwrap(); - cx.background_executor.run_until_parked(); + perform_search(search_view, "TWO", cx); search_view.update(cx, |search_view, cx| { assert_eq!( search_view @@ -2251,7 +2245,7 @@ pub mod tests { .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.clone(); + let workspace = window; let search_bar = window.build_view(cx, |_| ProjectSearchBar::new()); let active_item = cx.read(|cx| { @@ -2481,7 +2475,7 @@ pub mod tests { .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.clone(); + let workspace = window; let search_bar = window.build_view(cx, |_| ProjectSearchBar::new()); let active_item = cx.read(|cx| { @@ -3379,7 +3373,78 @@ pub mod tests { .unwrap(); } - pub fn init_test(cx: &mut TestAppContext) { + #[gpui::test] + async fn test_scroll_search_results_to_top(cx: &mut TestAppContext) { + init_test(cx); + + // We need many lines in the search results to be able to scroll the window + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + "1.txt": "\n\n\n\n\n A \n\n\n\n\n", + "2.txt": "\n\n\n\n\n A \n\n\n\n\n", + "3.rs": "\n\n\n\n\n A \n\n\n\n\n", + "4.rs": "\n\n\n\n\n A \n\n\n\n\n", + "5.rs": "\n\n\n\n\n A \n\n\n\n\n", + "6.rs": "\n\n\n\n\n A \n\n\n\n\n", + "7.rs": "\n\n\n\n\n A \n\n\n\n\n", + "8.rs": "\n\n\n\n\n A \n\n\n\n\n", + "9.rs": "\n\n\n\n\n A \n\n\n\n\n", + "a.rs": "\n\n\n\n\n A \n\n\n\n\n", + "b.rs": "\n\n\n\n\n B \n\n\n\n\n", + "c.rs": "\n\n\n\n\n B \n\n\n\n\n", + "d.rs": "\n\n\n\n\n B \n\n\n\n\n", + "e.rs": "\n\n\n\n\n B \n\n\n\n\n", + "f.rs": "\n\n\n\n\n B \n\n\n\n\n", + "g.rs": "\n\n\n\n\n B \n\n\n\n\n", + "h.rs": "\n\n\n\n\n B \n\n\n\n\n", + "i.rs": "\n\n\n\n\n B \n\n\n\n\n", + "j.rs": "\n\n\n\n\n B \n\n\n\n\n", + "k.rs": "\n\n\n\n\n B \n\n\n\n\n", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let search = cx.new_model(|cx| ProjectSearch::new(project, cx)); + let search_view = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx, None)); + + // First search + perform_search(search_view, "A", cx); + search_view + .update(cx, |search_view, cx| { + search_view.results_editor.update(cx, |results_editor, cx| { + // Results are correct and scrolled to the top + assert_eq!( + results_editor.display_text(cx).match_indices(" A ").count(), + 10 + ); + assert_eq!(results_editor.scroll_position(cx), Point::default()); + + // Scroll results all the way down + results_editor.scroll(Point::new(0., f32::MAX), Some(Axis::Vertical), cx); + }); + }) + .expect("unable to update search view"); + + // Second search + perform_search(search_view, "B", cx); + search_view + .update(cx, |search_view, cx| { + search_view.results_editor.update(cx, |results_editor, cx| { + // Results are correct... + assert_eq!( + results_editor.display_text(cx).match_indices(" B ").count(), + 10 + ); + // ...and scrolled back to the top + assert_eq!(results_editor.scroll_position(cx), Point::default()); + }); + }) + .expect("unable to update search view"); + } + + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings = SettingsStore::test(cx); cx.set_global(settings); @@ -3396,4 +3461,20 @@ pub mod tests { super::init(cx); }); } + + fn perform_search( + search_view: WindowHandle, + text: impl Into>, + cx: &mut TestAppContext, + ) { + search_view + .update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text(text, cx)); + search_view.search(cx); + }) + .unwrap(); + cx.background_executor.run_until_parked(); + } } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 748996c389..18e287bfee 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -41,7 +41,7 @@ actions!( ); bitflags! { - #[derive(Default)] + #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] pub struct SearchOptions: u8 { const NONE = 0b000; const WHOLE_WORD = 0b001; diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 4ee5baa662..5c922a503a 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -12,10 +12,8 @@ doctest = false [dependencies] ai.workspace = true anyhow.workspace = true -async-trait.workspace = true collections.workspace = true futures.workspace = true -globset.workspace = true gpui.workspace = true language.workspace = true lazy_static.workspace = true @@ -35,25 +33,21 @@ serde_json.workspace = true settings.workspace = true sha1 = "0.10.5" smol.workspace = true -tiktoken-rs.workspace = true tree-sitter.workspace = true util.workspace = true workspace.workspace = true [dev-dependencies] ai = { workspace = true, features = ["test-support"] } -client.workspace = true collections = { workspace = true, features = ["test-support"] } ctor.workspace = true env_logger.workspace = true gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } -node_runtime.workspace = true pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } rand.workspace = true rpc = { workspace = true, features = ["test-support"] } -rust-embed.workspace = true settings = { workspace = true, features = ["test-support"]} tempfile.workspace = true tree-sitter-cpp.workspace = true diff --git a/crates/semantic_index/src/db.rs b/crates/semantic_index/src/db.rs index f34baeaaae..242e80026a 100644 --- a/crates/semantic_index/src/db.rs +++ b/crates/semantic_index/src/db.rs @@ -125,7 +125,7 @@ impl VectorDatabase { // Delete existing tables, if SEMANTIC_INDEX_VERSION is bumped let version_query = db.prepare("SELECT version from semantic_index_config"); let version = version_query - .and_then(|mut query| query.query_row([], |row| Ok(row.get::<_, i64>(0)?))); + .and_then(|mut query| query.query_row([], |row| row.get::<_, i64>(0))); if version.map_or(false, |version| version == SEMANTIC_INDEX_VERSION as i64) { log::trace!("vector database schema up to date"); return Ok(()); @@ -275,14 +275,10 @@ impl VectorDatabase { self.transact(move |db| { let mut worktree_query = db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?; - let worktree_id = worktree_query - .query_row(params![worktree_root_path], |row| Ok(row.get::<_, i64>(0)?)); + let worktree_id = + worktree_query.query_row(params![worktree_root_path], |row| row.get::<_, i64>(0)); - if worktree_id.is_ok() { - return Ok(true); - } else { - return Ok(false); - } + Ok(worktree_id.is_ok()) }) } @@ -302,17 +298,15 @@ impl VectorDatabase { let digests = Rc::new( digests .into_iter() - .map(|p| Value::Blob(p.0.to_vec())) + .map(|digest| Value::Blob(digest.0.to_vec())) .collect::>(), ); let rows = query.query_map(params![digests], |row| { Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?)) })?; - for row in rows { - if let Ok(row) = row { - embeddings_by_digest.insert(row.0, row.1); - } + for (digest, embedding) in rows.flatten() { + embeddings_by_digest.insert(digest, embedding); } Ok(embeddings_by_digest) @@ -344,10 +338,8 @@ impl VectorDatabase { Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?)) })?; - for row in rows { - if let Ok(row) = row { - embeddings_by_digest.insert(row.0, row.1); - } + for (digest, embedding) in rows.flatten() { + embeddings_by_digest.insert(digest, embedding); } } @@ -364,7 +356,7 @@ impl VectorDatabase { db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?; let worktree_id = worktree_query .query_row(params![worktree_root_path.to_string_lossy()], |row| { - Ok(row.get::<_, i64>(0)?) + row.get::<_, i64>(0) }); if worktree_id.is_ok() { @@ -456,8 +448,7 @@ impl VectorDatabase { if batch_ids.len() == batch_n { let embeddings = std::mem::take(&mut batch_embeddings); let ids = std::mem::take(&mut batch_ids); - let array = - Array2::from_shape_vec((ids.len(), embedding_len.clone()), embeddings); + let array = Array2::from_shape_vec((ids.len(), embedding_len), embeddings); match array { Ok(array) => { batches.push((ids, array)); diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index df277fbc9b..e57da7bc9b 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -329,7 +329,7 @@ impl SemanticIndex { SemanticIndexStatus::Indexed } else { SemanticIndexStatus::Indexing { - remaining_files: project_state.pending_file_count_rx.borrow().clone(), + remaining_files: *project_state.pending_file_count_rx.borrow(), rate_limit_expiry: self.embedding_provider.rate_limit_expiration(), } } @@ -497,7 +497,7 @@ impl SemanticIndex { changes: Arc<[(Arc, ProjectEntryId, PathChange)]>, cx: &mut ModelContext, ) { - let Some(worktree) = project.read(cx).worktree_for_id(worktree_id.clone(), cx) else { + let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else { return; }; let project = project.downgrade(); @@ -657,9 +657,9 @@ impl SemanticIndex { if register.await.log_err().is_none() { // Stop tracking this worktree if the registration failed. this.update(&mut cx, |this, _| { - this.projects.get_mut(&project).map(|project_state| { + if let Some(project_state) = this.projects.get_mut(&project) { project_state.worktrees.remove(&worktree_id); - }); + } }) .ok(); } @@ -840,7 +840,6 @@ impl SemanticIndex { let mut batch_results = Vec::new(); for batch in file_ids.chunks(batch_size) { let batch = batch.into_iter().map(|v| *v).collect::>(); - let limit = limit.clone(); let fs = fs.clone(); let db_path = db_path.clone(); let query = query.clone(); diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 23ed45ff1d..f660aa8fb3 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -414,7 +414,7 @@ async fn test_code_context_retrieval_json() { } }"# .unindent(), - text.find("{").unwrap(), + text.find('{').unwrap(), )], ); @@ -443,7 +443,7 @@ async fn test_code_context_retrieval_json() { "age": 42 }]"# .unindent(), - text.find("[").unwrap(), + text.find('[').unwrap(), )], ); } diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 459bcf6703..d9ad9d3f9a 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -15,12 +15,10 @@ test-support = ["gpui/test-support", "fs/test-support"] [dependencies] anyhow.workspace = true collections.workspace = true -feature_flags.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true lazy_static.workspace = true -postage.workspace = true release_channel.workspace = true rust-embed.workspace = true schemars.workspace = true @@ -29,7 +27,6 @@ serde_derive.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true smallvec.workspace = true -toml.workspace = true tree-sitter-json = "*" tree-sitter.workspace = true util.workspace = true diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index b7145aac5e..3004100e50 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -21,8 +21,14 @@ pub fn default_settings() -> Cow<'static, str> { asset_str::("settings/default.json") } +#[cfg(target_os = "macos")] +pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json"; + +#[cfg(not(target_os = "macos"))] +pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json"; + pub fn default_keymap() -> Cow<'static, str> { - asset_str::("keymaps/default.json") + asset_str::(DEFAULT_KEYMAP_PATH) } pub fn vim_keymap() -> Cow<'static, str> { @@ -37,6 +43,6 @@ pub fn initial_local_settings_content() -> Cow<'static, str> { asset_str::("settings/initial_local_settings.json") } -pub fn initial_runnables_content() -> Cow<'static, str> { - asset_str::("settings/initial_runnables.json") +pub fn initial_tasks_content() -> Cow<'static, str> { + asset_str::("settings/initial_tasks.json") } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 7304fc5f1e..9106bbfb0a 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -6,7 +6,7 @@ use gpui::{AppContext, BackgroundExecutor}; use std::{io::ErrorKind, path::PathBuf, sync::Arc, time::Duration}; use util::{paths, ResultExt}; -pub const EMPTY_THEME_NAME: &'static str = "empty-theme"; +pub const EMPTY_THEME_NAME: &str = "empty-theme"; #[cfg(any(test, feature = "test-support"))] pub fn test_settings() -> String { @@ -52,7 +52,7 @@ pub fn watch_config_file( } if let Ok(contents) = fs.load(&path).await { - if !tx.unbounded_send(contents).is_ok() { + if tx.unbounded_send(contents).is_err() { break; } } @@ -100,7 +100,7 @@ async fn load_settings(fs: &Arc) -> Result { return Ok(crate::initial_user_settings_content().to_string()); } } - return Err(err); + Err(err) } } } @@ -116,13 +116,20 @@ pub fn update_settings_file( store.new_text_for_update::(old_text, update) })?; let initial_path = paths::SETTINGS.as_path(); - let resolved_path = fs - .canonicalize(initial_path) - .await - .with_context(|| format!("Failed to canonicalize settings path {:?}", initial_path))?; - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| format!("Failed to write settings to file {:?}", resolved_path))?; + if !fs.is_file(initial_path).await { + fs.atomic_write(initial_path.to_path_buf(), new_text) + .await + .with_context(|| format!("Failed to write settings to file {:?}", initial_path))?; + } else { + let resolved_path = fs.canonicalize(initial_path).await.with_context(|| { + format!("Failed to canonicalize settings path {:?}", initial_path) + })?; + + fs.atomic_write(resolved_path.clone(), new_text) + .await + .with_context(|| format!("Failed to write settings to file {:?}", resolved_path))?; + } + anyhow::Ok(()) }) .detach_and_log_err(cx); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 116dc1520f..2cde1187ab 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -96,7 +96,7 @@ pub trait Settings: 'static + Send + Sync { } #[track_caller] - fn get_global<'a>(cx: &'a AppContext) -> &'a Self + fn get_global(cx: &AppContext) -> &Self where Self: Sized, { @@ -104,7 +104,7 @@ pub trait Settings: 'static + Send + Sync { } #[track_caller] - fn try_read_global<'a, R>(cx: &'a AsyncAppContext, f: impl FnOnce(&Self) -> R) -> Option + fn try_read_global(cx: &AsyncAppContext, f: impl FnOnce(&Self) -> R) -> Option where Self: Sized, { @@ -112,7 +112,7 @@ pub trait Settings: 'static + Send + Sync { } #[track_caller] - fn override_global<'a>(settings: Self, cx: &'a mut AppContext) + fn override_global(settings: Self, cx: &mut AppContext) where Self: Sized, { @@ -210,10 +210,10 @@ impl SettingsStore { if let Some(release_settings) = &self .raw_user_settings - .get(&*release_channel::RELEASE_CHANNEL.dev_name()) + .get(release_channel::RELEASE_CHANNEL.dev_name()) { if let Some(release_settings) = setting_value - .deserialize_setting(&release_settings) + .deserialize_setting(release_settings) .log_err() { user_values_stack.push(release_settings); @@ -316,7 +316,7 @@ impl SettingsStore { let raw_settings = parse_json_with_comments::(text).unwrap_or_default(); let old_content = match setting.deserialize_setting(&raw_settings) { Ok(content) => content.0.downcast::().unwrap(), - Err(_) => Box::new(T::FileContent::default()), + Err(_) => Box::<::FileContent>::default(), }; let mut new_content = old_content.clone(); update(&mut new_content); @@ -340,7 +340,7 @@ impl SettingsStore { &new_value, &mut edits, ); - return edits; + edits } /// Configure the tab sized when updating JSON files. @@ -543,10 +543,10 @@ impl SettingsStore { if let Some(release_settings) = &self .raw_user_settings - .get(&*release_channel::RELEASE_CHANNEL.dev_name()) + .get(release_channel::RELEASE_CHANNEL.dev_name()) { if let Some(release_settings) = setting_value - .deserialize_setting(&release_settings) + .deserialize_setting(release_settings) .log_err() { user_settings_stack.push(release_settings); @@ -662,7 +662,7 @@ impl AnySettingValue for SettingValue { fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any { if let Some((root_id, path)) = path { for (settings_root_id, settings_path, value) in self.local_values.iter().rev() { - if root_id == *settings_root_id && path.starts_with(&settings_path) { + if root_id == *settings_root_id && path.starts_with(settings_path) { return value; } } @@ -755,8 +755,8 @@ fn replace_value_in_json_text( tab_size: usize, new_value: &serde_json::Value, ) -> (Range, String) { - const LANGUAGE_OVERRIDES: &'static str = "language_overrides"; - const LANGUAGES: &'static str = "languages"; + const LANGUAGE_OVERRIDES: &str = "language_overrides"; + const LANGUAGES: &str = "languages"; lazy_static! { static ref PAIR_QUERY: tree_sitter::Query = tree_sitter::Query::new( @@ -799,15 +799,15 @@ fn replace_value_in_json_text( break; } - first_key_start.get_or_insert_with(|| key_range.start); + first_key_start.get_or_insert(key_range.start); let found_key = text .get(key_range.clone()) .map(|key_text| { if key_path[depth] == LANGUAGES && has_language_overrides { - return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES); + key_text == format!("\"{}\"", LANGUAGE_OVERRIDES) } else { - return key_text == format!("\"{}\"", key_path[depth]); + key_text == format!("\"{}\"", key_path[depth]) } }) .unwrap_or(false); @@ -820,9 +820,9 @@ fn replace_value_in_json_text( if depth == key_path.len() { break; - } else { - first_key_start = None; } + + first_key_start = None; } } diff --git a/crates/sqlez_macros/Cargo.toml b/crates/sqlez_macros/Cargo.toml index 8b9b29dd57..aab2596ddd 100644 --- a/crates/sqlez_macros/Cargo.toml +++ b/crates/sqlez_macros/Cargo.toml @@ -12,8 +12,6 @@ doctest = false [dependencies] lazy_static.workspace = true -proc-macro2 = "1.0" -quote = "1.0" sqlez.workspace = true sqlformat = "0.2" syn = "1.0" diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index c95ca3c2e9..caf66c4c4f 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -11,10 +11,7 @@ path = "src/storybook.rs" [dependencies] anyhow.workspace = true -# TODO: Remove after diagnosing stack overflow. -backtrace-on-stack-overflow = "0.3.0" -chrono = "0.4" -clap = { version = "4.4", features = ["derive", "string"] } +clap = { workspace = true, features = ["derive", "string"] } collab_ui = { workspace = true, features = ["stories"] } ctrlc = "3.4" dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } @@ -22,21 +19,17 @@ editor.workspace = true fuzzy.workspace = true gpui.workspace = true indoc.workspace = true -itertools = "0.11.0" language.workspace = true log.workspace = true menu.workspace = true picker.workspace = true rust-embed.workspace = true -serde.workspace = true settings.workspace = true simplelog = "0.9" -smallvec.workspace = true story.workspace = true strum = { version = "0.25.0", features = ["derive"] } theme.workspace = true ui = { workspace = true, features = ["stories"] } -util.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/storybook/src/actions.rs b/crates/storybook/src/actions.rs new file mode 100644 index 0000000000..03ee5b580c --- /dev/null +++ b/crates/storybook/src/actions.rs @@ -0,0 +1,2 @@ +use gpui::actions; +actions!(storybook, [Quit]); diff --git a/crates/storybook/src/app_menus.rs b/crates/storybook/src/app_menus.rs new file mode 100644 index 0000000000..14c5e073ad --- /dev/null +++ b/crates/storybook/src/app_menus.rs @@ -0,0 +1,10 @@ +use gpui::{Menu, MenuItem}; + +pub fn app_menus() -> Vec> { + use crate::actions::Quit; + + vec![Menu { + name: "Storybook", + items: vec![MenuItem::action("Quit", Quit)], + }] +} diff --git a/crates/storybook/src/stories/picker.rs b/crates/storybook/src/stories/picker.rs index cdc0a0907e..5d4c8dfc89 100644 --- a/crates/storybook/src/stories/picker.rs +++ b/crates/storybook/src/stories/picker.rs @@ -41,7 +41,7 @@ impl PickerDelegate for Delegate { self.candidates.len() } - fn placeholder_text(&self) -> Arc { + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Test".into() } diff --git a/crates/storybook/src/stories/scroll.rs b/crates/storybook/src/stories/scroll.rs index a0318dc30e..096afaccf6 100644 --- a/crates/storybook/src/stories/scroll.rs +++ b/crates/storybook/src/stories/scroll.rs @@ -38,7 +38,7 @@ impl Render for ScrollStory { .id(id) .tooltip(move |cx| Tooltip::text(format!("{}, {}", row, column), cx)) .bg(bg) - .size(px(100. as f32)) + .size(px(100_f32)) .when(row >= 5 && column >= 5, |d| { d.overflow_scroll() .child(div().size(px(50.)).bg(color_1)) diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index efbd46665c..2bd60cc680 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -1,3 +1,5 @@ +mod actions; +mod app_menus; mod assets; mod stories; mod story_selector; @@ -9,14 +11,16 @@ use gpui::{ WindowOptions, }; use log::LevelFilter; -use settings::{default_settings, Settings, SettingsStore}; +use settings::{default_settings, KeymapFile, Settings, SettingsStore}; use simplelog::SimpleLogger; use strum::IntoEnumIterator; use theme::{ThemeRegistry, ThemeSettings}; use ui::prelude::*; +use crate::app_menus::app_menus; use crate::assets::Assets; use crate::story_selector::{ComponentStory, StorySelector}; +use actions::Quit; pub use indoc::indoc; #[derive(Parser)] @@ -33,13 +37,12 @@ struct Args { } fn main() { - // unsafe { backtrace_on_stack_overflow::enable() }; - SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + menu::init(); let args = Args::parse(); - let story_selector = args.story.clone().unwrap_or_else(|| { + let story_selector = args.story.unwrap_or_else(|| { let stories = ComponentStory::iter().collect::>(); ctrlc::set_handler(move || {}).unwrap(); @@ -78,6 +81,9 @@ fn main() { language::init(cx); editor::init(cx); + init(cx); + load_storybook_keymap(cx); + cx.set_menus(app_menus()); let _window = cx.open_window( WindowOptions { @@ -133,3 +139,19 @@ fn load_embedded_fonts(cx: &AppContext) -> gpui::Result<()> { cx.text_system().add_fonts(embedded_fonts) } + +fn load_storybook_keymap(cx: &mut AppContext) { + KeymapFile::load_asset("keymaps/storybook.json", cx).unwrap(); +} + +pub fn init(cx: &mut AppContext) { + cx.on_action(quit); +} + +fn quit(_: &Quit, cx: &mut AppContext) { + cx.spawn(|cx| async move { + cx.update(|cx| cx.quit())?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); +} diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index f39e3ed19a..b46150e3c3 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -357,8 +357,8 @@ mod tests { .collect::>(); assert_eq!(result.len(), 2); - assert!(result.iter().find(|(k, _)| k == &&"baa").is_some()); - assert!(result.iter().find(|(k, _)| k == &&"baaab").is_some()); + assert!(result.iter().any(|(k, _)| k == &&"baa")); + assert!(result.iter().any(|(k, _)| k == &&"baaab")); let result = map .iter_from(&"c") @@ -366,7 +366,7 @@ mod tests { .collect::>(); assert_eq!(result.len(), 1); - assert!(result.iter().find(|(k, _)| k == &&"c").is_some()); + assert!(result.iter().any(|(k, _)| k == &&"c")); } #[test] diff --git a/crates/runnable/Cargo.toml b/crates/task/Cargo.toml similarity index 76% rename from crates/runnable/Cargo.toml rename to crates/task/Cargo.toml index 3906d0448a..47f3170c74 100644 --- a/crates/runnable/Cargo.toml +++ b/crates/task/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "runnable" +name = "task" version = "0.1.0" edition = "2021" publish = false @@ -10,13 +10,10 @@ anyhow.workspace = true collections.workspace = true futures.workspace = true gpui.workspace = true -parking_lot.workspace = true schemars.workspace = true serde.workspace = true -serde_json.workspace = true serde_json_lenient.workspace = true -settings.workspace = true -smol.workspace = true +subst = "0.3.0" util.workspace = true [dev-dependencies] diff --git a/crates/task/LICENSE-GPL b/crates/task/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/task/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs new file mode 100644 index 0000000000..37107e2569 --- /dev/null +++ b/crates/task/src/lib.rs @@ -0,0 +1,75 @@ +//! Baseline interface of Tasks in Zed: all tasks in Zed are intended to use those for implementing their own logic. +#![deny(missing_docs)] + +pub mod oneshot_source; +pub mod static_source; + +use collections::HashMap; +use gpui::ModelContext; +use std::any::Any; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +/// Task identifier, unique within the application. +/// Based on it, task reruns and terminal tabs are managed. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TaskId(pub String); + +/// Contains all information needed by Zed to spawn a new terminal tab for the given task. +#[derive(Debug, Clone)] +pub struct SpawnInTerminal { + /// Id of the task to use when determining task tab affinity. + pub id: TaskId, + /// Human readable name of the terminal tab. + pub label: String, + /// Executable command to spawn. + pub command: String, + /// Arguments to the command. + pub args: Vec, + /// Current working directory to spawn the command into. + pub cwd: Option, + /// Env overrides for the command, will be appended to the terminal's environment from the settings. + pub env: HashMap, + /// Whether to use a new terminal tab or reuse the existing one to spawn the process. + pub use_new_terminal: bool, + /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish. + pub allow_concurrent_runs: bool, +} + +/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function) +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TaskContext { + /// A path to a directory in which the task should be executed. + pub cwd: Option, + /// Additional environment variables associated with a given task. + pub env: HashMap, +} + +/// Represents a short lived recipe of a task, whose main purpose +/// is to get spawned. +pub trait Task { + /// Unique identifier of the task to spawn. + fn id(&self) -> &TaskId; + /// Human readable name of the task to display in the UI. + fn name(&self) -> &str; + /// Task's current working directory. If `None`, current project's root will be used. + fn cwd(&self) -> Option<&str>; + /// Sets up everything needed to spawn the task in the given directory (`cwd`). + /// If a task is intended to be spawned in the terminal, it should return the corresponding struct filled with the data necessary. + fn exec(&self, cx: TaskContext) -> Option; +} + +/// [`Source`] produces tasks that can be scheduled. +/// +/// Implementations of this trait could be e.g. [`StaticSource`] that parses tasks from a .json files and provides process templates to be spawned; +/// another one could be a language server providing lenses with tests or build server listing all targets for a given project. +pub trait TaskSource: Any { + /// A way to erase the type of the source, processing and storing them generically. + fn as_any(&mut self) -> &mut dyn Any; + /// Collects all tasks available for scheduling, for the path given. + fn tasks_for_path( + &mut self, + path: Option<&Path>, + cx: &mut ModelContext>, + ) -> Vec>; +} diff --git a/crates/task/src/oneshot_source.rs b/crates/task/src/oneshot_source.rs new file mode 100644 index 0000000000..85257bee54 --- /dev/null +++ b/crates/task/src/oneshot_source.rs @@ -0,0 +1,81 @@ +//! A source of tasks, based on ad-hoc user command prompt input. + +use std::sync::Arc; + +use crate::{SpawnInTerminal, Task, TaskContext, TaskId, TaskSource}; +use gpui::{AppContext, Context, Model}; + +/// A storage and source of tasks generated out of user command prompt inputs. +pub struct OneshotSource { + tasks: Vec>, +} + +#[derive(Clone)] +struct OneshotTask { + id: TaskId, +} + +impl OneshotTask { + fn new(prompt: String) -> Self { + Self { id: TaskId(prompt) } + } +} + +impl Task for OneshotTask { + fn id(&self) -> &TaskId { + &self.id + } + + fn name(&self) -> &str { + &self.id.0 + } + + fn cwd(&self) -> Option<&str> { + None + } + + fn exec(&self, cx: TaskContext) -> Option { + if self.id().0.is_empty() { + return None; + } + let TaskContext { cwd, env } = cx; + Some(SpawnInTerminal { + id: self.id().clone(), + label: self.name().to_owned(), + command: self.id().0.clone(), + args: vec![], + cwd, + env, + use_new_terminal: Default::default(), + allow_concurrent_runs: Default::default(), + }) + } +} + +impl OneshotSource { + /// Initializes the oneshot source, preparing to store user prompts. + pub fn new(cx: &mut AppContext) -> Model> { + cx.new_model(|_| Box::new(Self { tasks: Vec::new() }) as Box) + } + + /// Spawns a certain task based on the user prompt. + pub fn spawn(&mut self, prompt: String) -> Arc { + let ret = Arc::new(OneshotTask::new(prompt)); + self.tasks.push(ret.clone()); + ret + } +} + +impl TaskSource for OneshotSource { + fn as_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn tasks_for_path( + &mut self, + _path: Option<&std::path::Path>, + _cx: &mut gpui::ModelContext>, + ) -> Vec> { + self.tasks.clone() + } +} diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs new file mode 100644 index 0000000000..dedf5a6384 --- /dev/null +++ b/crates/task/src/static_source.rs @@ -0,0 +1,201 @@ +//! A source of tasks, based on a static configuration, deserialized from the tasks config file, and related infrastructure for tracking changes to the file. + +use std::{borrow::Cow, path::Path, sync::Arc}; + +use collections::HashMap; +use futures::StreamExt; +use gpui::{AppContext, Context, Model, ModelContext, Subscription}; +use schemars::{gen::SchemaSettings, JsonSchema}; +use serde::{Deserialize, Serialize}; +use util::ResultExt; + +use crate::{SpawnInTerminal, Task, TaskContext, TaskId, TaskSource}; +use futures::channel::mpsc::UnboundedReceiver; + +/// A single config file entry with the deserialized task definition. +#[derive(Clone, Debug, PartialEq)] +struct StaticTask { + id: TaskId, + definition: Definition, +} + +impl Task for StaticTask { + fn exec(&self, cx: TaskContext) -> Option { + let TaskContext { cwd, env } = cx; + let cwd = self + .definition + .cwd + .clone() + .and_then(|path| subst::substitute(&path, &env).map(Into::into).ok()) + .or(cwd); + let mut definition_env = self.definition.env.clone(); + definition_env.extend(env); + Some(SpawnInTerminal { + id: self.id.clone(), + cwd, + use_new_terminal: self.definition.use_new_terminal, + allow_concurrent_runs: self.definition.allow_concurrent_runs, + label: self.definition.label.clone(), + command: self.definition.command.clone(), + args: self.definition.args.clone(), + env: definition_env, + }) + } + + fn name(&self) -> &str { + &self.definition.label + } + + fn id(&self) -> &TaskId { + &self.id + } + + fn cwd(&self) -> Option<&str> { + self.definition.cwd.as_deref() + } +} + +/// The source of tasks defined in a tasks config file. +pub struct StaticSource { + tasks: Vec, + _definitions: Model>, + _subscription: Subscription, +} + +/// Static task definition from the tasks config file. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub(crate) struct Definition { + /// Human readable name of the task to display in the UI. + pub label: String, + /// Executable command to spawn. + pub command: String, + /// Arguments to the command. + #[serde(default)] + pub args: Vec, + /// Env overrides for the command, will be appended to the terminal's environment from the settings. + #[serde(default)] + pub env: HashMap, + /// Current working directory to spawn the command into, defaults to current project root. + #[serde(default)] + pub cwd: Option, + /// Whether to use a new terminal tab or reuse the existing one to spawn the process. + #[serde(default)] + pub use_new_terminal: bool, + /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish. + #[serde(default)] + pub allow_concurrent_runs: bool, +} + +/// A group of Tasks defined in a JSON file. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct DefinitionProvider(Vec); + +impl DefinitionProvider { + /// Generates JSON schema of Tasks JSON definition format. + pub fn generate_json_schema() -> serde_json_lenient::Value { + let schema = SchemaSettings::draft07() + .with(|settings| settings.option_add_null_type = false) + .into_generator() + .into_root_schema_for::(); + + serde_json_lenient::to_value(schema).unwrap() + } +} +/// A Wrapper around deserializable T that keeps track of its contents +/// via a provided channel. Once T value changes, the observers of [`TrackedFile`] are +/// notified. +struct TrackedFile { + parsed_contents: T, +} + +impl Deserialize<'a> + PartialEq + 'static> TrackedFile { + fn new( + parsed_contents: T, + mut tracker: UnboundedReceiver, + cx: &mut AppContext, + ) -> Model { + cx.new_model(move |cx| { + cx.spawn(|tracked_file, mut cx| async move { + while let Some(new_contents) = tracker.next().await { + if !new_contents.trim().is_empty() { + let Some(new_contents) = + serde_json_lenient::from_str(&new_contents).log_err() + else { + continue; + }; + tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile, cx| { + if tracked_file.parsed_contents != new_contents { + tracked_file.parsed_contents = new_contents; + cx.notify(); + }; + })?; + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + Self { parsed_contents } + }) + } + + fn get(&self) -> &T { + &self.parsed_contents + } +} + +impl StaticSource { + /// Initializes the static source, reacting on tasks config changes. + pub fn new( + id_base: impl Into>, + tasks_file_tracker: UnboundedReceiver, + cx: &mut AppContext, + ) -> Model> { + let definitions = TrackedFile::new(DefinitionProvider::default(), tasks_file_tracker, cx); + cx.new_model(|cx| { + let id_base = id_base.into(); + let _subscription = cx.observe( + &definitions, + move |source: &mut Box<(dyn TaskSource + 'static)>, new_definitions, cx| { + if let Some(static_source) = source.as_any().downcast_mut::() { + static_source.tasks = new_definitions + .read(cx) + .get() + .0 + .clone() + .into_iter() + .enumerate() + .map(|(i, definition)| StaticTask { + id: TaskId(format!("static_{id_base}_{i}_{}", definition.label)), + definition, + }) + .collect(); + cx.notify(); + } + }, + ); + Box::new(Self { + tasks: Vec::new(), + _definitions: definitions, + _subscription, + }) + }) + } +} + +impl TaskSource for StaticSource { + fn tasks_for_path( + &mut self, + _: Option<&Path>, + _: &mut ModelContext>, + ) -> Vec> { + self.tasks + .clone() + .into_iter() + .map(|task| Arc::new(task) as Arc) + .collect() + } + + fn as_any(&mut self) -> &mut dyn std::any::Any { + self + } +} diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml new file mode 100644 index 0000000000..cbf5280ef6 --- /dev/null +++ b/crates/tasks_ui/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "tasks_ui" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[dependencies] +anyhow.workspace = true +editor.workspace = true +fuzzy.workspace = true +gpui.workspace = true +menu.workspace = true +picker.workspace = true +project.workspace = true +task.workspace = true +serde.workspace = true +ui.workspace = true +util.workspace = true +workspace.workspace = true +language.workspace = true + +[dev-dependencies] +editor = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } +language = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } +serde_json.workspace = true +tree-sitter-rust.workspace = true +tree-sitter-typescript.workspace = true +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/tasks_ui/LICENSE-GPL b/crates/tasks_ui/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/tasks_ui/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs new file mode 100644 index 0000000000..278d01ef39 --- /dev/null +++ b/crates/tasks_ui/src/lib.rs @@ -0,0 +1,373 @@ +use std::{collections::HashMap, path::PathBuf}; + +use editor::Editor; +use gpui::{AppContext, ViewContext, WindowContext}; +use language::Point; +use modal::TasksModal; +use project::{Location, WorktreeId}; +use task::{Task, TaskContext}; +use util::ResultExt; +use workspace::Workspace; + +mod modal; + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views( + |workspace: &mut Workspace, _: &mut ViewContext| { + workspace + .register_action(|workspace, _: &modal::Spawn, cx| { + let inventory = workspace.project().read(cx).task_inventory().clone(); + let workspace_handle = workspace.weak_handle(); + let cwd = task_cwd(workspace, cx).log_err().flatten(); + let task_context = task_context(workspace, cwd, cx); + workspace.toggle_modal(cx, |cx| { + TasksModal::new(inventory, task_context, workspace_handle, cx) + }) + }) + .register_action(move |workspace, action: &modal::Rerun, cx| { + if let Some((task, old_context)) = + workspace.project().update(cx, |project, cx| { + project + .task_inventory() + .update(cx, |inventory, cx| inventory.last_scheduled_task(cx)) + }) + { + let task_context = if action.reevaluate_context { + let cwd = task_cwd(workspace, cx).log_err().flatten(); + task_context(workspace, cwd, cx) + } else { + old_context + }; + + schedule_task(workspace, task.as_ref(), task_context, cx) + }; + }); + }, + ) + .detach(); +} + +fn task_context( + workspace: &Workspace, + cwd: Option, + cx: &mut WindowContext<'_>, +) -> TaskContext { + let current_editor = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + .clone(); + if let Some(current_editor) = current_editor { + (|| { + let editor = current_editor.read(cx); + let selection = editor.selections.newest::(cx); + let (buffer, _, _) = editor + .buffer() + .read(cx) + .point_to_buffer_offset(selection.start, cx)?; + + current_editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let selection_range = selection.range(); + let start = snapshot + .display_snapshot + .buffer_snapshot + .anchor_after(selection_range.start) + .text_anchor; + let end = snapshot + .display_snapshot + .buffer_snapshot + .anchor_after(selection_range.end) + .text_anchor; + let Point { row, column } = snapshot + .display_snapshot + .buffer_snapshot + .offset_to_point(selection_range.start); + let row = row + 1; + let column = column + 1; + let location = Location { + buffer: buffer.clone(), + range: start..end, + }; + + let current_file = location + .buffer + .read(cx) + .file() + .map(|file| file.path().to_string_lossy().to_string()); + let worktree_id = location + .buffer + .read(cx) + .file() + .map(|file| WorktreeId::from_usize(file.worktree_id())); + let context = buffer + .read(cx) + .language() + .and_then(|language| language.context_provider()) + .and_then(|provider| provider.build_context(location, cx).ok()); + + let worktree_path = worktree_id.and_then(|worktree_id| { + workspace + .project() + .read(cx) + .worktree_for_id(worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string()) + }); + + let mut env = HashMap::from_iter([ + ("ZED_ROW".into(), row.to_string()), + ("ZED_COLUMN".into(), column.to_string()), + ]); + if let Some(path) = current_file { + env.insert("ZED_FILE".into(), path); + } + if let Some(worktree_path) = worktree_path { + env.insert("ZED_WORKTREE_ROOT".into(), worktree_path); + } + if let Some(language_context) = context { + if let Some(symbol) = language_context.symbol { + env.insert("ZED_SYMBOL".into(), symbol); + } + } + + Some(TaskContext { + cwd: cwd.clone(), + env, + }) + }) + })() + .unwrap_or_else(|| TaskContext { + cwd, + env: Default::default(), + }) + } else { + TaskContext { + cwd, + env: Default::default(), + } + } +} + +fn schedule_task( + workspace: &Workspace, + task: &dyn Task, + task_cx: TaskContext, + cx: &mut ViewContext<'_, Workspace>, +) { + let spawn_in_terminal = task.exec(task_cx.clone()); + if let Some(spawn_in_terminal) = spawn_in_terminal { + workspace.project().update(cx, |project, cx| { + project.task_inventory().update(cx, |inventory, _| { + inventory.task_scheduled(task.id().clone(), task_cx); + }) + }); + cx.emit(workspace::Event::SpawnTask(spawn_in_terminal)); + } +} + +fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result> { + let project = workspace.project().read(cx); + let available_worktrees = project + .worktrees() + .filter(|worktree| { + let worktree = worktree.read(cx); + worktree.is_visible() + && worktree.is_local() + && worktree.root_entry().map_or(false, |e| e.is_dir()) + }) + .collect::>(); + let cwd = match available_worktrees.len() { + 0 => None, + 1 => Some(available_worktrees[0].read(cx).abs_path()), + _ => { + let cwd_for_active_entry = project.active_entry().and_then(|entry_id| { + available_worktrees.into_iter().find_map(|worktree| { + let worktree = worktree.read(cx); + if worktree.contains_entry(entry_id) { + Some(worktree.abs_path()) + } else { + None + } + }) + }); + anyhow::ensure!( + cwd_for_active_entry.is_some(), + "Cannot determine task cwd for multiple worktrees" + ); + cwd_for_active_entry + } + }; + Ok(cwd.map(|path| path.to_path_buf())) +} + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, sync::Arc}; + + use editor::Editor; + use gpui::{Entity, TestAppContext}; + use language::{DefaultContextProvider, Language, LanguageConfig}; + use project::{FakeFs, Project, TaskSourceKind}; + use serde_json::json; + use task::{oneshot_source::OneshotSource, TaskContext}; + use ui::VisualContext; + use workspace::{AppState, Workspace}; + + use crate::{task_context, task_cwd}; + + #[gpui::test] + async fn test_default_language_context(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/dir", + json!({ + ".zed": { + "tasks.json": r#"[ + { + "label": "example task", + "command": "echo", + "args": ["4"] + }, + { + "label": "another one", + "command": "echo", + "args": ["55"] + }, + ]"#, + }, + "a.ts": "function this_is_a_test() { }", + "rust": { + "b.rs": "use std; fn this_is_a_rust_file() { }", + } + + }), + ) + .await; + + let rust_language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_outline_query( + r#"(function_item + "fn" @context + name: (_) @name) @item"#, + ) + .unwrap() + .with_context_provider(Some(Arc::new(DefaultContextProvider))), + ); + + let typescript_language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_typescript::language_typescript()), + ) + .with_outline_query( + r#"(function_declaration + "async"? @context + "function" @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item"#, + ) + .unwrap() + .with_context_provider(Some(Arc::new(DefaultContextProvider))), + ); + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, cx| { + project.task_inventory().update(cx, |inventory, cx| { + inventory.add_source(TaskSourceKind::UserInput, |cx| OneshotSource::new(cx), cx) + }) + }); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + + let buffer1 = workspace + .update(cx, |this, cx| { + this.project() + .update(cx, |this, cx| this.open_buffer((worktree_id, "a.ts"), cx)) + }) + .await + .unwrap(); + buffer1.update(cx, |this, cx| { + this.set_language(Some(typescript_language), cx) + }); + let editor1 = cx.new_view(|cx| Editor::for_buffer(buffer1, Some(project.clone()), cx)); + let buffer2 = workspace + .update(cx, |this, cx| { + this.project().update(cx, |this, cx| { + this.open_buffer((worktree_id, "rust/b.rs"), cx) + }) + }) + .await + .unwrap(); + buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx)); + let editor2 = cx.new_view(|cx| Editor::for_buffer(buffer2, Some(project), cx)); + workspace.update(cx, |this, cx| { + this.add_item_to_center(Box::new(editor1.clone()), cx); + this.add_item_to_center(Box::new(editor2.clone()), cx); + assert_eq!(this.active_item(cx).unwrap().item_id(), editor2.entity_id()); + assert_eq!( + task_context(this, task_cwd(this, cx).unwrap(), cx), + TaskContext { + cwd: Some("/dir".into()), + env: HashMap::from_iter([ + ("ZED_FILE".into(), "rust/b.rs".into()), + ("ZED_WORKTREE_ROOT".into(), "/dir".into()), + ("ZED_ROW".into(), "1".into()), + ("ZED_COLUMN".into(), "1".into()), + ]) + } + ); + // And now, let's select an identifier. + editor2.update(cx, |this, cx| { + this.change_selections(None, cx, |selections| selections.select_ranges([14..18])) + }); + assert_eq!( + task_context(this, task_cwd(this, cx).unwrap(), cx), + TaskContext { + cwd: Some("/dir".into()), + env: HashMap::from_iter([ + ("ZED_FILE".into(), "rust/b.rs".into()), + ("ZED_WORKTREE_ROOT".into(), "/dir".into()), + ("ZED_SYMBOL".into(), "this_is_a_rust_file".into()), + ("ZED_ROW".into(), "1".into()), + ("ZED_COLUMN".into(), "15".into()), + ]) + } + ); + + // Now, let's switch the active item to .ts file. + this.activate_item(&editor1, cx); + assert_eq!( + task_context(this, task_cwd(this, cx).unwrap(), cx), + TaskContext { + cwd: Some("/dir".into()), + env: HashMap::from_iter([ + ("ZED_FILE".into(), "a.ts".into()), + ("ZED_WORKTREE_ROOT".into(), "/dir".into()), + ("ZED_SYMBOL".into(), "this_is_a_test".into()), + ("ZED_ROW".into(), "1".into()), + ("ZED_COLUMN".into(), "1".into()), + ]) + } + ); + }); + } + + pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let state = AppState::test(cx); + language::init(cx); + crate::init(cx); + editor::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + state + }) + } +} diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs new file mode 100644 index 0000000000..491ed05971 --- /dev/null +++ b/crates/tasks_ui/src/modal.rs @@ -0,0 +1,457 @@ +use std::{path::PathBuf, sync::Arc}; + +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, impl_actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, + InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, View, + ViewContext, VisualContext, WeakView, +}; +use picker::{ + highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText}, + Picker, PickerDelegate, +}; +use project::{Inventory, ProjectPath, TaskSourceKind}; +use task::{oneshot_source::OneshotSource, Task, TaskContext}; +use ui::{v_flex, ListItem, ListItemSpacing, RenderOnce, Selectable, WindowContext}; +use util::{paths::PathExt, ResultExt}; +use workspace::{ModalView, Workspace}; + +use crate::schedule_task; +use serde::Deserialize; +actions!(task, [Spawn]); + +/// Rerun last task +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct Rerun { + #[serde(default)] + /// Controls whether the task context is reevaluated prior to execution of a task. + /// If it is not, environment variables such as ZED_COLUMN, ZED_FILE are gonna be the same as in the last execution of a task + /// If it is, these variables will be updated to reflect current state of editor at the time task::Rerun is executed. + /// default: false + pub reevaluate_context: bool, +} + +impl_actions!(task, [Rerun]); + +/// A modal used to spawn new tasks. +pub(crate) struct TasksModalDelegate { + inventory: Model, + candidates: Vec<(TaskSourceKind, Arc)>, + matches: Vec, + selected_index: usize, + workspace: WeakView, + prompt: String, + task_context: TaskContext, +} + +impl TasksModalDelegate { + fn new( + inventory: Model, + task_context: TaskContext, + workspace: WeakView, + ) -> Self { + Self { + inventory, + workspace, + candidates: Vec::new(), + matches: Vec::new(), + selected_index: 0, + prompt: String::default(), + task_context, + } + } + + fn spawn_oneshot(&mut self, cx: &mut AppContext) -> Option> { + self.inventory + .update(cx, |inventory, _| inventory.source::())? + .update(cx, |oneshot_source, _| { + Some( + oneshot_source + .as_any() + .downcast_mut::()? + .spawn(self.prompt.clone()), + ) + }) + } + + fn active_item_path( + &mut self, + cx: &mut ViewContext<'_, Picker>, + ) -> Option<(PathBuf, ProjectPath)> { + let workspace = self.workspace.upgrade()?.read(cx); + let project = workspace.project().read(cx); + let active_item = workspace.active_item(cx)?; + active_item.project_path(cx).and_then(|project_path| { + project + .worktree_for_id(project_path.worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path)) + .zip(Some(project_path)) + }) + } +} + +pub(crate) struct TasksModal { + picker: View>, + _subscription: Subscription, +} + +impl TasksModal { + pub(crate) fn new( + inventory: Model, + task_context: TaskContext, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Self { + let picker = cx.new_view(|cx| { + Picker::uniform_list( + TasksModalDelegate::new(inventory, task_context, workspace), + cx, + ) + }); + let _subscription = cx.subscribe(&picker, |_, _, _, cx| { + cx.emit(DismissEvent); + }); + Self { + picker, + _subscription, + } + } +} + +impl Render for TasksModal { + fn render(&mut self, cx: &mut ViewContext) -> impl gpui::prelude::IntoElement { + v_flex() + .key_context("TasksModal") + .w(rems(34.)) + .child(self.picker.clone()) + .on_mouse_down_out(cx.listener(|modal, _, cx| { + modal.picker.update(cx, |picker, cx| { + picker.cancel(&Default::default(), cx); + }) + })) + } +} + +impl EventEmitter for TasksModal {} + +impl FocusableView for TasksModal { + fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { + self.picker.read(cx).focus_handle(cx) + } +} + +impl ModalView for TasksModal {} + +impl PickerDelegate for TasksModalDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { + self.selected_index = ix; + } + + fn placeholder_text(&self, cx: &mut WindowContext) -> Arc { + Arc::from(format!( + "{} use task name as prompt, {} spawns a bash-like task from the prompt, {} runs the selected task", + cx.keystroke_text_for(&menu::UseSelectedQuery), + cx.keystroke_text_for(&menu::SecondaryConfirm), + cx.keystroke_text_for(&menu::Confirm), + )) + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + cx.spawn(move |picker, mut cx| async move { + let Some(candidates) = picker + .update(&mut cx, |picker, cx| { + let (path, worktree) = match picker.delegate.active_item_path(cx) { + Some((abs_path, project_path)) => { + (Some(abs_path), Some(project_path.worktree_id)) + } + None => (None, None), + }; + picker.delegate.candidates = + picker.delegate.inventory.update(cx, |inventory, cx| { + inventory.list_tasks(path.as_deref(), worktree, true, cx) + }); + picker + .delegate + .candidates + .iter() + .enumerate() + .map(|(index, (_, candidate))| StringMatchCandidate { + id: index, + char_bag: candidate.name().chars().collect(), + string: candidate.name().into(), + }) + .collect::>() + }) + .ok() + else { + return; + }; + let matches = fuzzy::match_strings( + &candidates, + &query, + true, + 1000, + &Default::default(), + cx.background_executor().clone(), + ) + .await; + picker + .update(&mut cx, |picker, _| { + let delegate = &mut picker.delegate; + delegate.matches = matches; + delegate.prompt = query; + + if delegate.matches.is_empty() { + delegate.selected_index = 0; + } else { + delegate.selected_index = + delegate.selected_index.min(delegate.matches.len() - 1); + } + }) + .log_err(); + }) + } + + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { + let current_match_index = self.selected_index(); + let task = if secondary { + if !self.prompt.trim().is_empty() { + self.spawn_oneshot(cx) + } else { + None + } + } else { + self.matches.get(current_match_index).map(|current_match| { + let ix = current_match.candidate_id; + self.candidates[ix].1.clone() + }) + }; + + let Some(task) = task else { + return; + }; + + self.workspace + .update(cx, |workspace, cx| { + schedule_task(workspace, task.as_ref(), self.task_context.clone(), cx); + }) + .ok(); + cx.emit(DismissEvent); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + cx.emit(DismissEvent); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Option { + let hit = &self.matches[ix]; + let (source_kind, _) = &self.candidates[hit.candidate_id]; + let details = match source_kind { + TaskSourceKind::UserInput => "user input".to_string(), + TaskSourceKind::Worktree { abs_path, .. } | TaskSourceKind::AbsPath(abs_path) => { + abs_path.compact().to_string_lossy().to_string() + } + }; + + let highlighted_location = HighlightedMatchWithPaths { + match_label: HighlightedText { + text: hit.string.clone(), + highlight_positions: hit.positions.clone(), + char_count: hit.string.chars().count(), + }, + paths: vec![HighlightedText { + char_count: details.chars().count(), + highlight_positions: Vec::new(), + text: details, + }], + }; + Some( + ListItem::new(SharedString::from(format!("tasks-modal-{ix}"))) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child(highlighted_location.render(cx)), + ) + } + + fn selected_as_query(&self) -> Option { + Some(self.matches.get(self.selected_index())?.string.clone()) + } +} + +#[cfg(test)] +mod tests { + use gpui::{TestAppContext, VisualTestContext}; + use project::{FakeFs, Project}; + use serde_json::json; + + use super::*; + + #[gpui::test] + async fn test_spawn_tasks_modal_query_reuse(cx: &mut TestAppContext) { + crate::tests::init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/dir", + json!({ + ".zed": { + "tasks.json": r#"[ + { + "label": "example task", + "command": "echo", + "args": ["4"] + }, + { + "label": "another one", + "command": "echo", + "args": ["55"] + }, + ]"#, + }, + "a.ts": "a" + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.update(cx, |project, cx| { + project.task_inventory().update(cx, |inventory, cx| { + inventory.add_source(TaskSourceKind::UserInput, |cx| OneshotSource::new(cx), cx) + }) + }); + + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + + let tasks_picker = open_spawn_tasks(&workspace, cx); + assert_eq!( + query(&tasks_picker, cx), + "", + "Initial query should be empty" + ); + assert_eq!( + task_names(&tasks_picker, cx), + vec!["another one", "example task"], + "Initial tasks should be listed in alphabetical order" + ); + + let query_str = "tas"; + cx.simulate_input(query_str); + assert_eq!(query(&tasks_picker, cx), query_str); + assert_eq!( + task_names(&tasks_picker, cx), + vec!["example task"], + "Only one task should match the query {query_str}" + ); + + cx.dispatch_action(menu::UseSelectedQuery); + assert_eq!( + query(&tasks_picker, cx), + "example task", + "Query should be set to the selected task's name" + ); + assert_eq!( + task_names(&tasks_picker, cx), + vec!["example task"], + "No other tasks should be listed" + ); + cx.dispatch_action(menu::Confirm); + + let tasks_picker = open_spawn_tasks(&workspace, cx); + assert_eq!( + query(&tasks_picker, cx), + "", + "Query should be reset after confirming" + ); + assert_eq!( + task_names(&tasks_picker, cx), + vec!["example task", "another one"], + "Last recently used task should be listed first" + ); + + let query_str = "echo 4"; + cx.simulate_input(query_str); + assert_eq!(query(&tasks_picker, cx), query_str); + assert_eq!( + task_names(&tasks_picker, cx), + Vec::::new(), + "No tasks should match custom command query" + ); + + cx.dispatch_action(menu::SecondaryConfirm); + let tasks_picker = open_spawn_tasks(&workspace, cx); + assert_eq!( + query(&tasks_picker, cx), + "", + "Query should be reset after confirming" + ); + assert_eq!( + task_names(&tasks_picker, cx), + vec![query_str, "example task", "another one"], + "Last recently used one show task should be listed first" + ); + + cx.dispatch_action(menu::UseSelectedQuery); + assert_eq!( + query(&tasks_picker, cx), + query_str, + "Query should be set to the custom task's name" + ); + assert_eq!( + task_names(&tasks_picker, cx), + vec![query_str], + "Only custom task should be listed" + ); + } + + fn open_spawn_tasks( + workspace: &View, + cx: &mut VisualTestContext, + ) -> View> { + cx.dispatch_action(crate::modal::Spawn); + workspace.update(cx, |workspace, cx| { + workspace + .active_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }) + } + + fn query(spawn_tasks: &View>, cx: &mut VisualTestContext) -> String { + spawn_tasks.update(cx, |spawn_tasks, cx| spawn_tasks.query(cx)) + } + + fn task_names( + spawn_tasks: &View>, + cx: &mut VisualTestContext, + ) -> Vec { + spawn_tasks.update(cx, |spawn_tasks, _| { + spawn_tasks + .delegate + .matches + .iter() + .map(|hit| hit.string.clone()) + .collect::>() + }) + } +} diff --git a/crates/plugin/Cargo.toml b/crates/telemetry_events/Cargo.toml similarity index 58% rename from crates/plugin/Cargo.toml rename to crates/telemetry_events/Cargo.toml index 859ec0467b..6893e7c183 100644 --- a/crates/plugin/Cargo.toml +++ b/crates/telemetry_events/Cargo.toml @@ -1,12 +1,13 @@ [package] -name = "plugin" +name = "telemetry_events" version = "0.1.0" edition = "2021" publish = false license = "GPL-3.0-or-later" +[lib] +path = "src/telemetry_events.rs" + [dependencies] -bincode = "1.3" -plugin_macros.workspace = true serde.workspace = true -serde_derive.workspace = true +util.workspace = true diff --git a/crates/telemetry_events/LICENSE-GPL b/crates/telemetry_events/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/telemetry_events/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs new file mode 100644 index 0000000000..d2ea0610db --- /dev/null +++ b/crates/telemetry_events/src/telemetry_events.rs @@ -0,0 +1,131 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; +use util::SemanticVersion; + +#[derive(Serialize, Deserialize, Debug)] +pub struct EventRequestBody { + pub installation_id: Option, + pub session_id: Option, + pub is_staff: Option, + pub app_version: String, + pub os_name: String, + pub os_version: Option, + pub architecture: String, + pub release_channel: Option, + pub events: Vec, +} + +impl EventRequestBody { + pub fn semver(&self) -> Option { + self.app_version.parse().ok() + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct EventWrapper { + pub signed_in: bool, + pub milliseconds_since_first_event: i64, + #[serde(flatten)] + pub event: Event, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AssistantKind { + Panel, + Inline, +} + +impl Display for AssistantKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Panel => "panel", + Self::Inline => "inline", + } + ) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Event { + Editor(EditorEvent), + Copilot(CopilotEvent), + Call(CallEvent), + Assistant(AssistantEvent), + Cpu(CpuEvent), + Memory(MemoryEvent), + App(AppEvent), + Setting(SettingEvent), + Edit(EditEvent), + Action(ActionEvent), +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct EditorEvent { + pub operation: String, + pub file_extension: Option, + pub vim_mode: bool, + pub copilot_enabled: bool, + pub copilot_enabled_for_language: bool, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CopilotEvent { + pub suggestion_id: Option, + pub suggestion_accepted: bool, + pub file_extension: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CallEvent { + pub operation: String, + pub room_id: Option, + pub channel_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct AssistantEvent { + pub conversation_id: Option, + pub kind: AssistantKind, + pub model: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CpuEvent { + pub usage_as_percentage: f32, + pub core_count: u32, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MemoryEvent { + pub memory_in_bytes: u64, + pub virtual_memory_in_bytes: u64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ActionEvent { + pub source: String, + pub action: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct EditEvent { + pub duration: i64, + pub environment: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct SettingEvent { + pub setting: String, + pub value: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct AppEvent { + pub operation: String, +} diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 4dc535d846..884b754aac 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -14,28 +14,24 @@ doctest = false alacritty_terminal = "0.22.0" anyhow.workspace = true collections.workspace = true -db.workspace = true dirs = "4.0.0" futures.workspace = true gpui.workspace = true -itertools = "0.10" -lazy_static.workspace = true libc = "0.2" -mio-extras = "2.0.6" -ordered-float.workspace = true -procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } -runnable.workspace = true +procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "0c13436f4fa8b126f46dd4a20106419b41666897", default-features = false } +task.workspace = true schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true settings.workspace = true -shellexpand = "2.1.0" -smallvec.workspace = true smol.workspace = true theme.workspace = true thiserror.workspace = true util.workspace = true +[target.'cfg(windows)'.dependencies] +windows.workspace = true + [dev-dependencies] rand.workspace = true diff --git a/crates/terminal/src/mappings/mouse.rs b/crates/terminal/src/mappings/mouse.rs index af3e6b640f..2d00431cad 100644 --- a/crates/terminal/src/mappings/mouse.rs +++ b/crates/terminal/src/mappings/mouse.rs @@ -183,7 +183,7 @@ pub fn grid_point_and_side( let col = min(col, cur_size.last_column()); let mut line = (pos.y / cur_size.line_height) as i32; if line > cur_size.bottommost_line() { - line = cur_size.bottommost_line().0 as i32; + line = cur_size.bottommost_line().0; side = Side::Right; } else if line < 0 { side = Side::Left; diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 0ac714dd7b..342d4c6cc2 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,5 +1,7 @@ pub mod mappings; + pub use alacritty_terminal; + pub mod terminal_settings; use alacritty_terminal::{ @@ -33,10 +35,10 @@ use mappings::mouse::{ use collections::{HashMap, VecDeque}; use futures::StreamExt; use procinfo::LocalProcessInfo; -use runnable::RunnableId; use serde::{Deserialize, Serialize}; use settings::Settings; use smol::channel::{Receiver, Sender}; +use task::TaskId; use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings}; use theme::{ActiveTheme, Theme}; use util::truncate_and_trailoff; @@ -45,13 +47,15 @@ use std::{ cmp::{self, min}, fmt::Display, ops::{Deref, Index, RangeInclusive}, - os::unix::prelude::AsRawFd, path::PathBuf, sync::Arc, time::Duration, }; use thiserror::Error; +#[cfg(unix)] +use std::os::unix::prelude::AsRawFd; + use gpui::{ actions, black, px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla, Keystroke, ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, @@ -154,11 +158,11 @@ impl TerminalSize { } pub fn num_lines(&self) -> usize { - f32::from((self.size.height / self.line_height).floor()) as usize + (self.size.height / self.line_height).floor() as usize } pub fn num_columns(&self) -> usize { - f32::from((self.size.width / self.cell_width).floor()) as usize + (self.size.width / self.cell_width).floor() as usize } pub fn height(&self) -> Pixels { @@ -177,6 +181,7 @@ impl TerminalSize { self.line_height } } + impl Default for TerminalSize { fn default() -> Self { TerminalSize::new( @@ -280,27 +285,33 @@ impl Display for TerminalError { } } -pub struct SpawnRunnable { - pub id: RunnableId, +pub struct SpawnTask { + pub id: TaskId, pub label: String, pub command: String, pub args: Vec, pub env: HashMap, } +// https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213 +const DEFAULT_SCROLL_HISTORY_LINES: usize = 10_000; +const MAX_SCROLL_HISTORY_LINES: usize = 100_000; + pub struct TerminalBuilder { terminal: Terminal, events_rx: UnboundedReceiver, } impl TerminalBuilder { + #[allow(clippy::too_many_arguments)] pub fn new( working_directory: Option, - runnable: Option, + task: Option, shell: Shell, env: HashMap, blink_settings: Option, alternate_scroll: AlternateScroll, + max_scroll_history_lines: Option, window: AnyWindowHandle, completion_tx: Sender<()>, ) -> Result { @@ -333,8 +344,18 @@ impl TerminalBuilder { std::env::set_var("LC_ALL", "en_US.UTF-8"); std::env::set_var("ZED_TERM", "true"); + let scrolling_history = if task.is_some() { + // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling. + // After the task finishes, we do not allow appending to that terminal, so small tasks output should not + // cause excessive memory usage over time. + MAX_SCROLL_HISTORY_LINES + } else { + max_scroll_history_lines + .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) + .min(MAX_SCROLL_HISTORY_LINES) + }; let config = Config { - scrolling_history: 10000, + scrolling_history, ..Config::default() }; @@ -376,8 +397,12 @@ impl TerminalBuilder { } }; - let fd = pty.file().as_raw_fd(); - let shell_pid = pty.child().id(); + #[cfg(unix)] + let (fd, shell_pid) = (pty.file().as_raw_fd(), pty.child().id()); + + // todo("windows") + #[cfg(windows)] + let (fd, shell_pid) = (-1, 0); //And connect them together let event_loop = EventLoop::new( @@ -393,10 +418,10 @@ impl TerminalBuilder { let _io_thread = event_loop.spawn(); // DANGER let url_regex = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap(); - let word_regex = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap(); + let word_regex = RegexSearch::new(r#"[\$\+\w.\[\]:/@\-~]+"#).unwrap(); let terminal = Terminal { - runnable, + task, pty_tx: Notifier(pty_tx), completion_tx, term, @@ -462,21 +487,21 @@ impl TerminalBuilder { } } - if events.is_empty() && wakeup == false { + if events.is_empty() && !wakeup { smol::future::yield_now().await; break 'outer; - } else { - terminal.update(&mut cx, |this, cx| { - if wakeup { - this.process_event(&AlacTermEvent::Wakeup, cx); - } - - for event in events { - this.process_event(&event, cx); - } - })?; - smol::future::yield_now().await; } + + terminal.update(&mut cx, |this, cx| { + if wakeup { + this.process_event(&AlacTermEvent::Wakeup, cx); + } + + for event in events { + this.process_event(&event, cx); + } + })?; + smol::future::yield_now().await; } } @@ -572,11 +597,11 @@ pub struct Terminal { hovered_word: bool, url_regex: RegexSearch, word_regex: RegexSearch, - runnable: Option, + task: Option, } -pub struct RunableState { - pub id: RunnableId, +pub struct TaskState { + pub id: TaskId, pub label: String, pub completed: bool, pub completion_rx: Receiver<()>, @@ -611,9 +636,9 @@ impl Terminal { AlacTermEvent::Bell => { cx.emit(Event::Bell); } - AlacTermEvent::Exit => match &mut self.runnable { - Some(runnable) => { - runnable.completed = true; + AlacTermEvent::Exit => match &mut self.task { + Some(task) => { + task.completed = true; self.completion_tx.try_send(()).ok(); } None => cx.emit(Event::CloseTerminal), @@ -641,7 +666,11 @@ impl Terminal { /// Updates the cached process info, returns whether the Zed-relevant info has changed fn update_process_info(&mut self) -> bool { + #[cfg(unix)] let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) }; + // todo("windows") + #[cfg(windows)] + let mut pid = unsafe { windows::Win32::System::Threading::GetCurrentProcessId() } as i32; if pid < 0 { pid = self.shell_pid as i32; } @@ -687,7 +716,7 @@ impl Terminal { new_size.size.height = cmp::max(new_size.line_height, new_size.height()); new_size.size.width = cmp::max(new_size.cell_width, new_size.width()); - self.last_content.size = new_size.clone(); + self.last_content.size = new_size; self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); @@ -1266,8 +1295,7 @@ impl Terminal { self.last_content.display_offset, ); - if let Some(scrolls) = - scroll_report(point, scroll_lines as i32, e, self.last_content.mode) + if let Some(scrolls) = scroll_report(point, scroll_lines, e, self.last_content.mode) { for scroll in scrolls { self.pty_tx.notify(scroll); @@ -1336,8 +1364,8 @@ impl Terminal { pub fn title(&self, truncate: bool) -> String { const MAX_CHARS: usize = 25; - match &self.runnable { - Some(runnable_state) => truncate_and_trailoff(&runnable_state.label, MAX_CHARS), + match &self.task { + Some(task_state) => truncate_and_trailoff(&task_state.label, MAX_CHARS), None => self .foreground_process_info .as_ref() @@ -1374,17 +1402,17 @@ impl Terminal { self.cmd_pressed && self.hovered_word } - pub fn runnable(&self) -> Option<&RunableState> { - self.runnable.as_ref() + pub fn task(&self) -> Option<&TaskState> { + self.task.as_ref() } - pub fn wait_for_completed_runnable(&self, cx: &mut AppContext) -> Task<()> { - match self.runnable() { - Some(runnable) => { - if runnable.completed { + pub fn wait_for_completed_task(&self, cx: &mut AppContext) -> Task<()> { + match self.task() { + Some(task) => { + if task.completed { Task::ready(()) } else { - let mut completion_receiver = runnable.completion_rx.clone(); + let mut completion_receiver = task.completion_rx.clone(); cx.spawn(|_| async move { completion_receiver.next().await; }) @@ -1450,7 +1478,7 @@ fn content_index_for_mouse(pos: Point, size: &TerminalSize) -> usize { clamped_row * size.columns() + clamped_col } -/// Converts an 8 bit ANSI color to it's GPUI equivalent. +/// Converts an 8 bit ANSI color to its GPUI equivalent. /// Accepts `usize` for compatibility with the `alacritty::Colors` interface, /// Other than that use case, should only be called with values in the [0,255] range pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla { @@ -1476,7 +1504,7 @@ pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla { 15 => colors.terminal_ansi_bright_white, // 16-231 are mapped to their RGB colors on a 0-5 range per channel 16..=231 => { - let (r, g, b) = rgb_for_index(&(index as u8)); // Split the index into it's ANSI-RGB components + let (r, g, b) = rgb_for_index(index as u8); // Split the index into its ANSI-RGB components let step = (u8::MAX as f32 / 5.).floor() as u8; // Split the RGB range into 5 chunks, with floor so no overflow rgba_color(r * step, g * step, b * step) // Map the ANSI-RGB components to an RGB color } @@ -1515,8 +1543,8 @@ pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla { /// ``` /// /// This function does the reverse, calculating the `r`, `g`, and `b` components from a given index. -fn rgb_for_index(i: &u8) -> (u8, u8, u8) { - debug_assert!((&16..=&231).contains(&i)); +fn rgb_for_index(i: u8) -> (u8, u8, u8) { + debug_assert!((16..=231).contains(&i)); let i = i - 16; let r = (i - (i % 36)) / 36; let g = ((i % 36) - (i % 6)) / 6; @@ -1526,9 +1554,9 @@ fn rgb_for_index(i: &u8) -> (u8, u8, u8) { pub fn rgba_color(r: u8, g: u8, b: u8) -> Hsla { Rgba { - r: (r as f32 / 255.) as f32, - g: (g as f32 / 255.) as f32, - b: (b as f32 / 255.) as f32, + r: (r as f32 / 255.), + g: (g as f32 / 255.), + b: (b as f32 / 255.), a: 1., } .into() @@ -1549,9 +1577,9 @@ mod tests { #[test] fn test_rgb_for_index() { - //Test every possible value in the color cube + // Test every possible value in the color cube. for i in 16..=231 { - let (r, g, b) = rgb_for_index(&(i as u8)); + let (r, g, b) = rgb_for_index(i); assert_eq!(i, 16 + 36 * r + 6 * g + b); } } @@ -1617,7 +1645,7 @@ mod tests { assert_eq!( content.cells[content_index_for_mouse( point(Pixels::from(-10.), Pixels::from(-10.)), - &content.size + &content.size, )] .c, cells[0][0] @@ -1625,7 +1653,7 @@ mod tests { assert_eq!( content.cells[content_index_for_mouse( point(Pixels::from(1000.), Pixels::from(1000.)), - &content.size + &content.size, )] .c, cells[9][9] @@ -1635,9 +1663,9 @@ mod tests { fn get_cells(size: TerminalSize, rng: &mut ThreadRng) -> Vec> { let mut cells = Vec::new(); - for _ in 0..(f32::from(size.height() / size.line_height()) as usize) { + for _ in 0..((size.height() / size.line_height()) as usize) { let mut row_vec = Vec::new(); - for _ in 0..(f32::from(size.width() / size.cell_width()) as usize) { + for _ in 0..((size.width() / size.cell_width()) as usize) { let cell_char = rng.sample(Alphanumeric) as char; row_vec.push(cell_char) } diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 1a072ca8bc..59b39751d1 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -18,6 +18,11 @@ pub enum TerminalDockPosition { Right, } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct Toolbar { + pub title: bool, +} + #[derive(Deserialize)] pub struct TerminalSettings { pub shell: Shell, @@ -35,6 +40,8 @@ pub struct TerminalSettings { pub default_width: Pixels, pub default_height: Pixels, pub detect_venv: VenvSettings, + pub max_scroll_history_lines: Option, + pub toolbar: Toolbar, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -146,6 +153,16 @@ pub struct TerminalSettingsContent { /// /// Default: on pub detect_venv: Option, + /// The maximum number of lines to keep in the scrollback history. + /// Maximum allowed value is 100_000, all values above that will be treated as 100_000. + /// 0 disables the scrolling. + /// Existing terminals will not pick up this change until they are recreated. + /// See Alacritty documentation for more information. + /// + /// Default: 10_000 + pub max_scroll_history_lines: Option, + /// Toolbar related settings + pub toolbar: Option, } impl settings::Settings for TerminalSettings { @@ -265,3 +282,12 @@ pub enum WorkingDirectory { /// this platform's home directory (if it can be found). Always { directory: String }, } + +// Toolbar related settings +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct ToolbarContent { + /// Whether to display the terminal title in its toolbar. + /// + /// Default: true + pub title: Option, +} diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 64972a78e8..8daf4b3f02 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -17,26 +17,18 @@ dirs = "4.0.0" editor.workspace = true futures.workspace = true gpui.workspace = true -itertools = "0.10" +itertools.workspace = true language.workspace = true -lazy_static.workspace = true -libc = "0.2" -mio-extras = "2.0.6" -ordered-float.workspace = true -procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } project.workspace = true -runnable.workspace = true +task.workspace = true search.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true settings.workspace = true -shellexpand = "2.1.0" -smallvec.workspace = true +shellexpand.workspace = true smol.workspace = true terminal.workspace = true theme.workspace = true -thiserror.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index b4ee253355..c70cd87df1 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -365,7 +365,7 @@ impl TerminalElement { }; let mut result = TextRun { - len: indexed.c.len_utf8() as usize, + len: indexed.c.len_utf8(), color: fg, background_color: None, font: Font { @@ -406,11 +406,10 @@ impl TerminalElement { let font_features = terminal_settings .font_features - .clone() - .unwrap_or(settings.buffer_font.features.clone()); + .unwrap_or(settings.buffer_font.features); let line_height = terminal_settings.line_height.value(); - let font_size = terminal_settings.font_size.clone(); + let font_size = terminal_settings.font_size; let font_size = font_size.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)); @@ -462,7 +461,7 @@ impl TerminalElement { .width; gutter = cell_width; - let mut size = bounds.size.clone(); + let mut size = bounds.size; size.width -= gutter; // https://github.com/zed-industries/zed/issues/2750 @@ -489,15 +488,12 @@ impl TerminalElement { } }); - let interactive_text_bounds = InteractiveBounds { - bounds, - stacking_order: cx.stacking_order().clone(), - }; - if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) { + if bounds.contains(&cx.mouse_position()) { + let stacking_order = cx.stacking_order().clone(); if self.can_navigate_to_selected_word && last_hovered_word.is_some() { - cx.set_cursor_style(gpui::CursorStyle::PointingHand) + cx.set_cursor_style(gpui::CursorStyle::PointingHand, stacking_order); } else { - cx.set_cursor_style(gpui::CursorStyle::IBeam) + cx.set_cursor_style(gpui::CursorStyle::IBeam, stacking_order); } } @@ -649,7 +645,6 @@ impl TerminalElement { }); cx.on_mouse_event({ - let bounds = bounds.clone(); let focus = self.focus.clone(); let terminal = self.terminal.clone(); move |e: &MouseMoveEvent, phase, cx| { @@ -831,7 +826,7 @@ impl Element for TerminalElement { start_y, //Need to change this line_height: layout.dimensions.line_height, lines: highlighted_range_lines, - color: color.clone(), + color: *color, //Copied from editor. TODO: move to theme or something corner_radius: 0.15 * layout.dimensions.line_height, }; @@ -1018,10 +1013,10 @@ fn to_highlighted_range_lines( let mut line_end = layout.dimensions.columns(); if line == clamped_start_line { - line_start = unclamped_start.column.0 as usize; + line_start = unclamped_start.column.0; } if line == clamped_end_line { - line_end = unclamped_end.column.0 as usize + 1; //+1 for inclusive + line_end = unclamped_end.column.0 + 1; // +1 for inclusive } highlighted_range_lines.push(HighlightedRangeLine { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index aee6e907b6..d4174d61c8 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -11,13 +11,13 @@ use gpui::{ }; use itertools::Itertools; use project::{Fs, ProjectEntryId}; -use runnable::RunnableId; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use serde::{Deserialize, Serialize}; use settings::Settings; +use task::{SpawnInTerminal, TaskId}; use terminal::{ - terminal_settings::{TerminalDockPosition, TerminalSettings}, - SpawnRunnable, + terminal_settings::{Shell, TerminalDockPosition, TerminalSettings}, + SpawnTask, }; use ui::{h_flex, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip}; use util::{ResultExt, TryFutureExt}; @@ -31,7 +31,7 @@ use workspace::{ use anyhow::Result; -const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel"; +const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; actions!(terminal_panel, [ToggleFocus]); @@ -57,7 +57,7 @@ pub struct TerminalPanel { pending_serialization: Task>, pending_terminals_to_add: usize, _subscriptions: Vec, - deferred_runnables: HashMap>, + deferred_tasks: HashMap>, } impl TerminalPanel { @@ -166,7 +166,7 @@ impl TerminalPanel { width: None, height: None, pending_terminals_to_add: 0, - deferred_runnables: HashMap::default(), + deferred_tasks: HashMap::default(), _subscriptions: subscriptions, }; this @@ -192,8 +192,8 @@ impl TerminalPanel { let items = if let Some(serialized_panel) = serialized_panel.as_ref() { panel.update(cx, |panel, cx| { cx.notify(); - panel.height = serialized_panel.height; - panel.width = serialized_panel.width; + panel.height = serialized_panel.height.map(|h| h.round()); + panel.width = serialized_panel.width.map(|w| w.round()); panel.pane.update(cx, |_, cx| { serialized_panel .items @@ -223,8 +223,8 @@ impl TerminalPanel { panel._subscriptions.push(cx.subscribe( &workspace, |terminal_panel, _, e, cx| { - if let workspace::Event::SpawnRunnable(spawn_in_terminal) = e { - terminal_panel.spawn_runnable(spawn_in_terminal, cx); + if let workspace::Event::SpawnTask(spawn_in_terminal) = e { + terminal_panel.spawn_task(spawn_in_terminal, cx); }; }, )) @@ -295,33 +295,48 @@ impl TerminalPanel { }) } - pub fn spawn_runnable( - &mut self, - spawn_in_terminal: &runnable::SpawnInTerminal, - cx: &mut ViewContext, - ) { - let spawn_runnable = SpawnRunnable { + pub fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext) { + let mut spawn_task = SpawnTask { id: spawn_in_terminal.id.clone(), label: spawn_in_terminal.label.clone(), command: spawn_in_terminal.command.clone(), args: spawn_in_terminal.args.clone(), env: spawn_in_terminal.env.clone(), }; + // Set up shell args unconditionally, as tasks are always spawned inside of a shell. + let Some((shell, mut user_args)) = (match TerminalSettings::get_global(cx).shell.clone() { + Shell::System => std::env::var("SHELL").ok().map(|shell| (shell, vec![])), + Shell::Program(shell) => Some((shell, vec![])), + Shell::WithArguments { program, args } => Some((program, args)), + }) else { + return; + }; + + let mut command = std::mem::take(&mut spawn_task.command); + let args = std::mem::take(&mut spawn_task.args); + for arg in args { + command.push(' '); + command.push_str(&arg); + } + spawn_task.command = shell; + user_args.extend(["-i".to_owned(), "-c".to_owned(), command]); + spawn_task.args = user_args; + let working_directory = spawn_in_terminal.cwd.clone(); let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs; let use_new_terminal = spawn_in_terminal.use_new_terminal; if allow_concurrent_runs && use_new_terminal { - self.spawn_in_new_terminal(spawn_runnable, working_directory, cx); + self.spawn_in_new_terminal(spawn_task, working_directory, cx); return; } - let terminals_for_runnable = self.terminals_for_runnable(&spawn_in_terminal.id, cx); - if terminals_for_runnable.is_empty() { - self.spawn_in_new_terminal(spawn_runnable, working_directory, cx); + let terminals_for_task = self.terminals_for_task(&spawn_in_terminal.id, cx); + if terminals_for_task.is_empty() { + self.spawn_in_new_terminal(spawn_task, working_directory, cx); return; } - let (existing_item_index, existing_terminal) = terminals_for_runnable + let (existing_item_index, existing_terminal) = terminals_for_task .last() .expect("covered no terminals case above") .clone(); @@ -332,28 +347,28 @@ impl TerminalPanel { ); self.replace_terminal( working_directory, - spawn_runnable, + spawn_task, existing_item_index, existing_terminal, cx, ); } else { - self.deferred_runnables.insert( + self.deferred_tasks.insert( spawn_in_terminal.id.clone(), cx.spawn(|terminal_panel, mut cx| async move { - wait_for_terminals_tasks(terminals_for_runnable, &mut cx).await; + wait_for_terminals_tasks(terminals_for_task, &mut cx).await; terminal_panel .update(&mut cx, |terminal_panel, cx| { if use_new_terminal { terminal_panel.spawn_in_new_terminal( - spawn_runnable, + spawn_task, working_directory, cx, ); } else { terminal_panel.replace_terminal( working_directory, - spawn_runnable, + spawn_task, existing_item_index, existing_terminal, cx, @@ -368,11 +383,11 @@ impl TerminalPanel { fn spawn_in_new_terminal( &mut self, - spawn_runnable: SpawnRunnable, + spawn_task: SpawnTask, working_directory: Option, cx: &mut ViewContext, ) { - self.add_terminal(working_directory, Some(spawn_runnable), cx); + self.add_terminal(working_directory, Some(spawn_task), cx); let task_workspace = self.workspace.clone(); cx.spawn(|_, mut cx| async move { task_workspace @@ -395,9 +410,9 @@ impl TerminalPanel { this.update(cx, |this, cx| this.add_terminal(None, None, cx)) } - fn terminals_for_runnable( + fn terminals_for_task( &self, - id: &RunnableId, + id: &TaskId, cx: &mut AppContext, ) -> Vec<(usize, View)> { self.pane @@ -406,8 +421,8 @@ impl TerminalPanel { .enumerate() .filter_map(|(index, item)| Some((index, item.act_as::(cx)?))) .filter_map(|(index, terminal_view)| { - let runnable_state = terminal_view.read(cx).terminal().read(cx).runnable()?; - if &runnable_state.id == id { + let task_state = terminal_view.read(cx).terminal().read(cx).task()?; + if &task_state.id == id { Some((index, terminal_view)) } else { None @@ -425,7 +440,7 @@ impl TerminalPanel { fn add_terminal( &mut self, working_directory: Option, - spawn_runnable: Option, + spawn_task: Option, cx: &mut ViewContext, ) { let workspace = self.workspace.clone(); @@ -444,7 +459,7 @@ impl TerminalPanel { let window = cx.window_handle(); if let Some(terminal) = workspace.project().update(cx, |project, cx| { project - .create_terminal(working_directory, spawn_runnable, window, cx) + .create_terminal(working_directory, spawn_task, window, cx) .log_err() }) { let terminal = Box::new(cx.new_view(|cx| { @@ -478,13 +493,7 @@ impl TerminalPanel { .items() .filter_map(|item| { let terminal_view = item.act_as::(cx)?; - if terminal_view - .read(cx) - .terminal() - .read(cx) - .runnable() - .is_some() - { + if terminal_view.read(cx).terminal().read(cx).task().is_some() { None } else { let id = item.item_id().as_u64(); @@ -523,7 +532,7 @@ impl TerminalPanel { fn replace_terminal( &self, working_directory: Option, - spawn_runnable: SpawnRunnable, + spawn_task: SpawnTask, terminal_item_index: usize, terminal_to_replace: View, cx: &mut ViewContext<'_, Self>, @@ -535,27 +544,34 @@ impl TerminalPanel { let window = cx.window_handle(); let new_terminal = project.update(cx, |project, cx| { project - .create_terminal(working_directory, Some(spawn_runnable), window, cx) + .create_terminal(working_directory, Some(spawn_task), window, cx) .log_err() })?; terminal_to_replace.update(cx, |terminal_to_replace, cx| { terminal_to_replace.set_terminal(new_terminal, cx); }); self.activate_terminal_view(terminal_item_index, cx); + let task_workspace = self.workspace.clone(); + cx.spawn(|_, mut cx| async move { + task_workspace + .update(&mut cx, |workspace, cx| workspace.focus_panel::(cx)) + .ok() + }) + .detach(); Some(()) } } async fn wait_for_terminals_tasks( - terminals_for_runnable: Vec<(usize, View)>, + terminals_for_task: Vec<(usize, View)>, cx: &mut AsyncWindowContext, ) { - let pending_tasks = terminals_for_runnable.iter().filter_map(|(_, terminal)| { + let pending_tasks = terminals_for_task.iter().filter_map(|(_, terminal)| { terminal .update(cx, |terminal_view, cx| { terminal_view .terminal() - .update(cx, |terminal, cx| terminal.wait_for_completed_runnable(cx)) + .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx)) }) .ok() }); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index fbf2448702..00cda8fc32 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -13,6 +13,7 @@ use gpui::{ use language::Bias; use persistence::TERMINAL_DB; use project::{search::SearchQuery, Fs, LocalWorktree, Metadata, Project}; +use settings::SettingsStore; use terminal::{ alacritty_terminal::{ index::Point, @@ -90,6 +91,7 @@ pub struct TerminalView { blink_epoch: usize, can_navigate_to_selected_word: bool, workspace_id: WorkspaceId, + show_title: bool, _subscriptions: Vec, _terminal_subscriptions: Vec, } @@ -132,7 +134,7 @@ impl TerminalView { cx, ) }); - workspace.add_item(Box::new(view), cx) + workspace.add_item_to_active_pane(Box::new(view), cx) } } @@ -165,7 +167,12 @@ impl TerminalView { blink_epoch: 0, can_navigate_to_selected_word: false, workspace_id, - _subscriptions: vec![focus_in, focus_out], + show_title: TerminalSettings::get_global(cx).toolbar.title, + _subscriptions: vec![ + focus_in, + focus_out, + cx.observe_global::(Self::settings_changed), + ], _terminal_subscriptions: terminal_subscriptions, } } @@ -208,6 +215,12 @@ impl TerminalView { self.context_menu = Some((context_menu, position, subscription)); } + fn settings_changed(&mut self, cx: &mut ViewContext) { + let settings = TerminalSettings::get_global(cx); + self.show_title = settings.toolbar.title; + cx.notify(); + } + fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext) { if !self .terminal @@ -441,7 +454,7 @@ fn subscribe_for_terminal_events( Event::TitleChanged => { cx.emit(ItemEvent::UpdateTab); let terminal = this.terminal().read(cx); - if !terminal.runnable().is_some() { + if terminal.task().is_none() { if let Some(foreground_info) = &terminal.foreground_process_info { let cwd = foreground_info.cwd.clone(); @@ -777,7 +790,7 @@ impl Item for TerminalView { ) -> AnyElement { let terminal = self.terminal().read(cx); let title = terminal.title(true); - let icon = if terminal.runnable().is_some() { + let icon = if terminal.task().is_some() { IconName::Play } else { IconName::Terminal @@ -817,8 +830,8 @@ impl Item for TerminalView { } fn is_dirty(&self, cx: &gpui::AppContext) -> bool { - match self.terminal.read(cx).runnable() { - Some(runnable) => !runnable.completed, + match self.terminal.read(cx).task() { + Some(task) => !task.completed, None => self.has_bell(), } } @@ -832,7 +845,11 @@ impl Item for TerminalView { } fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft + if self.show_title { + ToolbarItemLocation::PrimaryLeft + } else { + ToolbarItemLocation::Hidden + } } fn breadcrumbs(&self, _: &theme::Theme, cx: &AppContext) -> Option> { @@ -862,12 +879,9 @@ impl Item for TerminalView { .or_else(|| { cx.update(|cx| { let strategy = TerminalSettings::get_global(cx).working_directory.clone(); - workspace - .upgrade() - .map(|workspace| { - get_working_directory(workspace.read(cx), cx, strategy) - }) - .flatten() + workspace.upgrade().and_then(|workspace| { + get_working_directory(workspace.read(cx), cx, strategy) + }) }) .ok() .flatten() @@ -883,7 +897,7 @@ impl Item for TerminalView { } fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - if !self.terminal().read(cx).runnable().is_some() { + if self.terminal().read(cx).task().is_none() { cx.background_executor() .spawn(TERMINAL_DB.update_workspace_id( workspace.database_id(), diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index 1a7f8177a9..798b506b5e 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -16,7 +16,6 @@ test-support = ["rand"] anyhow.workspace = true clock.workspace = true collections.workspace = true -digest = { version = "0.9", features = ["std"] } lazy_static.workspace = true log.workspace = true parking_lot.workspace = true diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index c060627ad6..fff134af84 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -7,7 +7,7 @@ license = "GPL-3.0-or-later" [features] default = [] -stories = ["dep:itertools", "dep:story"] +stories = ["dep:story"] test-support = ["gpui/test-support", "fs/test-support", "settings/test-support"] [lib] @@ -23,8 +23,7 @@ fs.workspace = true futures.workspace = true gpui.workspace = true indexmap = { version = "1.6.2", features = ["serde"] } -itertools = { version = "0.11.0", optional = true } -palette = { version = "0.7.3", default-features = false, features = ["std"] } +palette = { workspace = true, default-features = false, features = ["std"] } parking_lot.workspace = true refineable.workspace = true schemars = { workspace = true, features = ["indexmap"] } @@ -35,7 +34,6 @@ serde_json_lenient.workspace = true serde_repr.workspace = true settings.workspace = true story = { workspace = true, optional = true } -toml.workspace = true util.workspace = true uuid.workspace = true diff --git a/crates/theme/src/one_themes.rs b/crates/theme/src/one_themes.rs index c461779bd1..c556b87de2 100644 --- a/crates/theme/src/one_themes.rs +++ b/crates/theme/src/one_themes.rs @@ -72,7 +72,7 @@ pub(crate) fn one_dark() -> Theme { icon_muted: hsla(220.0 / 360., 12.1 / 100., 66.1 / 100., 1.0), icon_disabled: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), icon_placeholder: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), - icon_accent: blue.into(), + icon_accent: blue, status_bar_background: bg, title_bar_background: bg, toolbar_background: editor, @@ -100,7 +100,7 @@ pub(crate) fn one_dark() -> Theme { editor_document_highlight_write_background: gpui::red(), terminal_background: bg, - // todo!("Use one colors for terminal") + // todo("Use one colors for terminal") terminal_foreground: crate::white().dark().step_12(), terminal_bright_foreground: crate::white().dark().step_11(), terminal_dim_foreground: crate::white().dark().step_10(), @@ -218,7 +218,7 @@ pub(crate) fn one_dark() -> Theme { ( "link_uri".into(), HighlightStyle { - color: Some(teal.into()), + color: Some(teal), font_style: Some(FontStyle::Italic), ..HighlightStyle::default() }, diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs index 24733506fa..830a461726 100644 --- a/crates/theme/src/registry.rs +++ b/crates/theme/src/registry.rs @@ -12,9 +12,8 @@ use refineable::Refineable; use util::ResultExt; use crate::{ - try_parse_color, Appearance, AppearanceContent, PlayerColor, PlayerColors, StatusColors, - SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent, - ThemeStyles, + try_parse_color, Appearance, AppearanceContent, PlayerColors, StatusColors, SyntaxTheme, + SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent, ThemeStyles, }; #[derive(Debug, Clone)] @@ -117,36 +116,7 @@ impl ThemeRegistry { AppearanceContent::Light => PlayerColors::light(), AppearanceContent::Dark => PlayerColors::dark(), }; - if !user_theme.style.players.is_empty() { - for (idx, player) in user_theme.style.players.clone().into_iter().enumerate() { - let cursor = player - .cursor - .as_ref() - .and_then(|color| try_parse_color(&color).ok()); - let background = player - .background - .as_ref() - .and_then(|color| try_parse_color(&color).ok()); - let selection = player - .selection - .as_ref() - .and_then(|color| try_parse_color(&color).ok()); - - if let Some(player_color) = player_colors.0.get_mut(idx) { - *player_color = PlayerColor { - cursor: cursor.unwrap_or(player_color.cursor), - background: background.unwrap_or(player_color.background), - selection: selection.unwrap_or(player_color.selection), - }; - } else { - player_colors.0.push(PlayerColor { - cursor: cursor.unwrap_or_default(), - background: background.unwrap_or_default(), - selection: selection.unwrap_or_default(), - }); - } - } - } + player_colors.merge(&user_theme.style.players); let mut syntax_colors = match user_theme.appearance { AppearanceContent::Light => SyntaxTheme::light(), @@ -164,7 +134,7 @@ impl ThemeRegistry { color: highlight .color .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), font_style: highlight.font_style.map(Into::into), font_weight: highlight.font_weight.map(Into::into), ..Default::default() @@ -278,7 +248,7 @@ impl ThemeRegistry { } pub async fn read_user_theme(theme_path: &Path, fs: Arc) -> Result { - let reader = fs.open_sync(&theme_path).await?; + let reader = fs.open_sync(theme_path).await?; let theme = serde_json_lenient::from_reader(reader)?; Ok(theme) diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index 37fa235be7..603cf035d7 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -91,7 +91,7 @@ impl ThemeStyleContent { color: style .color .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), ..Default::default() }, ) @@ -290,7 +290,10 @@ pub struct ThemeColorsContent { pub pane_focused_border: Option, /// The color of the scrollbar thumb. - #[serde(rename = "scrollbar_thumb.background")] + #[serde( + rename = "scrollbar.thumb.background", + alias = "scrollbar_thumb.background" + )] pub scrollbar_thumb_background: Option, /// The color of the scrollbar thumb when hovered over. @@ -486,351 +489,351 @@ impl ThemeColorsContent { border: self .border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), border_variant: self .border_variant .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), border_focused: self .border_focused .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), border_selected: self .border_selected .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), border_transparent: self .border_transparent .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), border_disabled: self .border_disabled .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), elevated_surface_background: self .elevated_surface_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), surface_background: self .surface_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), background: self .background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), element_background: self .element_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), element_hover: self .element_hover .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), element_active: self .element_active .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), element_selected: self .element_selected .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), element_disabled: self .element_disabled .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), drop_target_background: self .drop_target_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), ghost_element_background: self .ghost_element_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), ghost_element_hover: self .ghost_element_hover .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), ghost_element_active: self .ghost_element_active .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), ghost_element_selected: self .ghost_element_selected .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), ghost_element_disabled: self .ghost_element_disabled .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), text: self .text .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), text_muted: self .text_muted .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), text_placeholder: self .text_placeholder .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), text_disabled: self .text_disabled .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), text_accent: self .text_accent .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), icon: self .icon .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), icon_muted: self .icon_muted .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), icon_disabled: self .icon_disabled .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), icon_placeholder: self .icon_placeholder .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), icon_accent: self .icon_accent .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), status_bar_background: self .status_bar_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), title_bar_background: self .title_bar_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), toolbar_background: self .toolbar_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), tab_bar_background: self .tab_bar_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), tab_inactive_background: self .tab_inactive_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), tab_active_background: self .tab_active_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), search_match_background: self .search_match_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), panel_background: self .panel_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), panel_focused_border: self .panel_focused_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), pane_focused_border: self .pane_focused_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), scrollbar_thumb_background: self .scrollbar_thumb_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), scrollbar_thumb_hover_background: self .scrollbar_thumb_hover_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), scrollbar_thumb_border: self .scrollbar_thumb_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), scrollbar_track_background: self .scrollbar_track_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), scrollbar_track_border: self .scrollbar_track_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_foreground: self .editor_foreground .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_background: self .editor_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_gutter_background: self .editor_gutter_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_subheader_background: self .editor_subheader_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_active_line_background: self .editor_active_line_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_highlighted_line_background: self .editor_highlighted_line_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_line_number: self .editor_line_number .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_active_line_number: self .editor_active_line_number .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_invisible: self .editor_invisible .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_wrap_guide: self .editor_wrap_guide .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_active_wrap_guide: self .editor_active_wrap_guide .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_document_highlight_read_background: self .editor_document_highlight_read_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), editor_document_highlight_write_background: self .editor_document_highlight_write_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_background: self .terminal_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_foreground: self .terminal_foreground .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_bright_foreground: self .terminal_bright_foreground .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_dim_foreground: self .terminal_dim_foreground .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_black: self .terminal_ansi_black .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_bright_black: self .terminal_ansi_bright_black .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_dim_black: self .terminal_ansi_dim_black .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_red: self .terminal_ansi_red .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_bright_red: self .terminal_ansi_bright_red .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_dim_red: self .terminal_ansi_dim_red .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_green: self .terminal_ansi_green .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_bright_green: self .terminal_ansi_bright_green .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_dim_green: self .terminal_ansi_dim_green .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_yellow: self .terminal_ansi_yellow .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_bright_yellow: self .terminal_ansi_bright_yellow .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_dim_yellow: self .terminal_ansi_dim_yellow .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_blue: self .terminal_ansi_blue .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_bright_blue: self .terminal_ansi_bright_blue .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_dim_blue: self .terminal_ansi_dim_blue .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_magenta: self .terminal_ansi_magenta .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_bright_magenta: self .terminal_ansi_bright_magenta .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_dim_magenta: self .terminal_ansi_dim_magenta .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_cyan: self .terminal_ansi_cyan .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_bright_cyan: self .terminal_ansi_bright_cyan .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_dim_cyan: self .terminal_ansi_dim_cyan .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_white: self .terminal_ansi_white .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_bright_white: self .terminal_ansi_bright_white .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), terminal_ansi_dim_white: self .terminal_ansi_dim_white .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), link_text_hover: self .link_text_hover .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), } } } @@ -987,171 +990,171 @@ impl StatusColorsContent { conflict: self .conflict .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), conflict_background: self .conflict_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), conflict_border: self .conflict_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), created: self .created .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), created_background: self .created_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), created_border: self .created_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), deleted: self .deleted .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), deleted_background: self .deleted_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), deleted_border: self .deleted_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), error: self .error .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), error_background: self .error_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), error_border: self .error_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), hidden: self .hidden .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), hidden_background: self .hidden_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), hidden_border: self .hidden_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), hint: self .hint .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), hint_background: self .hint_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), hint_border: self .hint_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), ignored: self .ignored .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), ignored_background: self .ignored_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), ignored_border: self .ignored_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), info: self .info .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), info_background: self .info_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), info_border: self .info_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), modified: self .modified .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), modified_background: self .modified_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), modified_border: self .modified_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), predictive: self .predictive .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), predictive_background: self .predictive_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), predictive_border: self .predictive_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), renamed: self .renamed .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), renamed_background: self .renamed_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), renamed_border: self .renamed_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), success: self .success .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), success_background: self .success_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), success_border: self .success_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), unreachable: self .unreachable .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), unreachable_background: self .unreachable_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), unreachable_border: self .unreachable_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), warning: self .warning .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), warning_background: self .warning_background .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), warning_border: self .warning_border .as_ref() - .and_then(|color| try_parse_color(&color).ok()), + .and_then(|color| try_parse_color(color).ok()), } } } @@ -1205,19 +1208,21 @@ impl JsonSchema for FontWeightContent { } fn json_schema(_: &mut SchemaGenerator) -> Schema { - let mut schema_object = SchemaObject::default(); - schema_object.enum_values = Some(vec![ - 100.into(), - 200.into(), - 300.into(), - 400.into(), - 500.into(), - 600.into(), - 700.into(), - 800.into(), - 900.into(), - ]); - schema_object.into() + SchemaObject { + enum_values: Some(vec![ + 100.into(), + 200.into(), + 300.into(), + 400.into(), + 500.into(), + 600.into(), + 700.into(), + 800.into(), + 900.into(), + ]), + ..Default::default() + } + .into() } } diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 48f745513e..1b860dec92 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -48,14 +48,14 @@ impl ThemeSettings { // If the selected theme doesn't exist, fall back to a default theme // based on the system appearance. let theme_registry = ThemeRegistry::global(cx); - if theme_registry.get(&theme_name).ok().is_none() { + if theme_registry.get(theme_name).ok().is_none() { theme_name = match *system_appearance { Appearance::Light => "One Light", Appearance::Dark => "One Dark", }; }; - if let Some(_theme) = theme_settings.switch_theme(&theme_name, cx) { + if let Some(_theme) = theme_settings.switch_theme(theme_name, cx) { ThemeSettings::override_global(theme_settings, cx); } } @@ -220,7 +220,7 @@ impl ThemeSettings { let mut new_theme = None; - if let Some(theme) = themes.get(&theme).log_err() { + if let Some(theme) = themes.get(theme).log_err() { self.active_theme = theme.clone(); new_theme = Some(theme); } @@ -243,6 +243,7 @@ impl ThemeSettings { .styles .status .refine(&theme_overrides.status_colors_refinement()); + base_theme.styles.player.merge(&theme_overrides.players); base_theme.styles.syntax = Arc::new(SyntaxTheme { highlights: { let mut highlights = base_theme.styles.syntax.highlights.clone(); @@ -312,13 +313,13 @@ impl settings::Settings for ThemeSettings { ui_font_size: defaults.ui_font_size.unwrap().into(), ui_font: Font { family: defaults.ui_font_family.clone().unwrap().into(), - features: defaults.ui_font_features.clone().unwrap(), + features: defaults.ui_font_features.unwrap(), weight: Default::default(), style: Default::default(), }, buffer_font: Font { family: defaults.buffer_font_family.clone().unwrap().into(), - features: defaults.buffer_font_features.clone().unwrap(), + features: defaults.buffer_font_features.unwrap(), weight: FontWeight::default(), style: FontStyle::default(), }, @@ -332,7 +333,7 @@ impl settings::Settings for ThemeSettings { theme_overrides: None, }; - for value in user_values.into_iter().copied().cloned() { + for value in user_values.iter().copied().cloned() { if let Some(value) = value.buffer_font_family { this.buffer_font.family = value.into(); } diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 17910ef973..b4bdc8f342 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -130,7 +130,7 @@ pub struct ThemeColors { /// The border color of the scrollbar track. pub scrollbar_track_border: Hsla, // /// The opacity of the scrollbar status marks, like diagnostic states and git status. - // todo!() + // todo() // pub scrollbar_status_opacity: Hsla, // === diff --git a/crates/theme/src/styles/players.rs b/crates/theme/src/styles/players.rs index 508b091b8b..e80c7161b1 100644 --- a/crates/theme/src/styles/players.rs +++ b/crates/theme/src/styles/players.rs @@ -1,7 +1,9 @@ use gpui::Hsla; use serde_derive::Deserialize; -use crate::{amber, blue, jade, lime, orange, pink, purple, red}; +use crate::{ + amber, blue, jade, lime, orange, pink, purple, red, try_parse_color, PlayerColorContent, +}; #[derive(Debug, Clone, Copy, Deserialize, Default)] pub struct PlayerColor { @@ -142,4 +144,40 @@ impl PlayerColors { let len = self.0.len() - 1; self.0[(participant_index as usize % len) + 1] } + + /// Merges the given player colors into this [`PlayerColors`] instance. + pub fn merge(&mut self, user_player_colors: &[PlayerColorContent]) { + if user_player_colors.is_empty() { + return; + } + + for (idx, player) in user_player_colors.iter().enumerate() { + let cursor = player + .cursor + .as_ref() + .and_then(|color| try_parse_color(color).ok()); + let background = player + .background + .as_ref() + .and_then(|color| try_parse_color(color).ok()); + let selection = player + .selection + .as_ref() + .and_then(|color| try_parse_color(color).ok()); + + if let Some(player_color) = self.0.get_mut(idx) { + *player_color = PlayerColor { + cursor: cursor.unwrap_or(player_color.cursor), + background: background.unwrap_or(player_color.background), + selection: selection.unwrap_or(player_color.selection), + }; + } else { + self.0.push(PlayerColor { + cursor: cursor.unwrap_or_default(), + background: background.unwrap_or_default(), + selection: selection.unwrap_or_default(), + }); + } + } + } } diff --git a/crates/theme_importer/Cargo.toml b/crates/theme_importer/Cargo.toml index 9e2052e2bb..ddff83f385 100644 --- a/crates/theme_importer/Cargo.toml +++ b/crates/theme_importer/Cargo.toml @@ -6,16 +6,12 @@ publish = false license = "GPL-3.0-or-later" [dependencies] -any_ascii = "0.3.2" anyhow.workspace = true -clap = { version = "4.4", features = ["derive"] } -convert_case = "0.6.0" +clap = { workspace = true, features = ["derive"] } gpui.workspace = true indexmap = { version = "1.6.2", features = ["serde"] } -indoc.workspace = true log.workspace = true -palette = { version = "0.7.3", default-features = false, features = ["std"] } -pathfinder_color = "0.5" +palette.workspace = true rust-embed.workspace = true schemars = { workspace = true, features = ["indexmap"] } serde.workspace = true @@ -24,5 +20,4 @@ serde_json_lenient.workspace = true simplelog = "0.9" strum = { version = "0.25.0", features = ["derive"] } theme.workspace = true -uuid.workspace = true vscode_theme = "0.2.0" diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index 869204be07..7e7e6fe9b6 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -11,21 +11,16 @@ doctest = false [dependencies] client.workspace = true -editor.workspace = true feature_flags.workspace = true fs.workspace = true fuzzy.workspace = true gpui.workspace = true log.workspace = true -parking_lot.workspace = true picker.workspace = true -postage.workspace = true settings.workspace = true -smol.workspace = true theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true [dev-dependencies] -editor = { workspace = true, features = ["test-support"] } diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 2ad1085f66..c0008e90d6 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -153,7 +153,7 @@ impl ThemeSelectorDelegate { impl PickerDelegate for ThemeSelectorDelegate { type ListItem = ui::ListItem; - fn placeholder_text(&self) -> Arc { + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Select Theme...".into() } diff --git a/crates/time_format/Cargo.toml b/crates/time_format/Cargo.toml new file mode 100644 index 0000000000..ea1fb336d5 --- /dev/null +++ b/crates/time_format/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "time_format" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lib] +path = "src/time_format.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +sys-locale.workspace = true +time.workspace = true + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation.workspace = true +core-foundation-sys.workspace = true diff --git a/crates/time_format/LICENSE-GPL b/crates/time_format/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/time_format/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/time_format/src/time_format.rs b/crates/time_format/src/time_format.rs new file mode 100644 index 0000000000..b80d52ab65 --- /dev/null +++ b/crates/time_format/src/time_format.rs @@ -0,0 +1,303 @@ +use std::sync::OnceLock; + +use time::{OffsetDateTime, UtcOffset}; + +/// Formats a timestamp, which respects the user's date and time preferences/custom format. +pub fn format_localized_timestamp( + reference: OffsetDateTime, + timestamp: OffsetDateTime, + timezone: UtcOffset, +) -> String { + #[cfg(target_os = "macos")] + { + let timestamp_local = timestamp.to_offset(timezone); + let reference_local = reference.to_offset(timezone); + let reference_local_date = reference_local.date(); + let timestamp_local_date = timestamp_local.date(); + + let native_fmt = if timestamp_local_date == reference_local_date { + macos::format_time(×tamp) + } else if reference_local_date.previous_day() == Some(timestamp_local_date) { + macos::format_time(×tamp).map(|t| format!("yesterday at {}", t).to_string()) + } else { + macos::format_date(×tamp) + }; + native_fmt.unwrap_or_else(|_| format_timestamp_fallback(reference, timestamp, timezone)) + } + #[cfg(not(target_os = "macos"))] + { + // todo(linux) respect user's date/time preferences + // todo(windows) respect user's date/time preferences + format_timestamp_fallback(reference, timestamp, timezone) + } +} + +fn format_timestamp_fallback( + reference: OffsetDateTime, + timestamp: OffsetDateTime, + timezone: UtcOffset, +) -> String { + static CURRENT_LOCALE: OnceLock = OnceLock::new(); + let current_locale = CURRENT_LOCALE + .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"))); + + let is_12_hour_time = is_12_hour_time_by_locale(current_locale.as_str()); + format_timestamp_naive(reference, timestamp, timezone, is_12_hour_time) +} + +/// Formats a timestamp, which is either in 12-hour or 24-hour time format. +/// Note: +/// This function does not respect the user's date and time preferences. +/// This should only be used as a fallback mechanism when the OS time formatting fails. +pub fn format_timestamp_naive( + reference: OffsetDateTime, + timestamp: OffsetDateTime, + timezone: UtcOffset, + is_12_hour_time: bool, +) -> String { + let timestamp_local = timestamp.to_offset(timezone); + let timestamp_local_hour = timestamp_local.hour(); + let timestamp_local_minute = timestamp_local.minute(); + + let (hour, meridiem) = if is_12_hour_time { + let meridiem = if timestamp_local_hour >= 12 { + "pm" + } else { + "am" + }; + + let hour_12 = match timestamp_local_hour { + 0 => 12, // Midnight + 13..=23 => timestamp_local_hour - 12, // PM hours + _ => timestamp_local_hour, // AM hours + }; + + (hour_12, Some(meridiem)) + } else { + (timestamp_local_hour, None) + }; + + let formatted_time = match meridiem { + Some(meridiem) => format!("{:02}:{:02} {}", hour, timestamp_local_minute, meridiem), + None => format!("{:02}:{:02}", hour, timestamp_local_minute), + }; + + let reference_local = reference.to_offset(timezone); + let reference_local_date = reference_local.date(); + let timestamp_local_date = timestamp_local.date(); + + if timestamp_local_date == reference_local_date { + return formatted_time; + } + + if reference_local_date.previous_day() == Some(timestamp_local_date) { + return format!("yesterday at {}", formatted_time); + } + + match meridiem { + Some(_) => format!( + "{:02}/{:02}/{}", + timestamp_local_date.month() as u32, + timestamp_local_date.day(), + timestamp_local_date.year() + ), + None => format!( + "{:02}/{:02}/{}", + timestamp_local_date.day(), + timestamp_local_date.month() as u32, + timestamp_local_date.year() + ), + } +} + +/// Returns `true` if the locale is recognized as a 12-hour time locale. +fn is_12_hour_time_by_locale(locale: &str) -> bool { + [ + "es-MX", "es-CO", "es-SV", "es-NI", + "es-HN", // Mexico, Colombia, El Salvador, Nicaragua, Honduras + "en-US", "en-CA", "en-AU", "en-NZ", // U.S, Canada, Australia, New Zealand + "ar-SA", "ar-EG", "ar-JO", // Saudi Arabia, Egypt, Jordan + "en-IN", "hi-IN", // India, Hindu + "en-PK", "ur-PK", // Pakistan, Urdu + "en-PH", "fil-PH", // Philippines, Filipino + "bn-BD", "ccp-BD", // Bangladesh, Chakma + "en-IE", "ga-IE", // Ireland, Irish + "en-MY", "ms-MY", // Malaysia, Malay + ] + .contains(&locale) +} + +#[cfg(target_os = "macos")] +mod macos { + use anyhow::Result; + use core_foundation::base::TCFType; + use core_foundation::date::CFAbsoluteTime; + use core_foundation::string::CFString; + use core_foundation_sys::date_formatter::CFDateFormatterCreateStringWithAbsoluteTime; + use core_foundation_sys::date_formatter::CFDateFormatterRef; + use core_foundation_sys::locale::CFLocaleRef; + use core_foundation_sys::{ + base::kCFAllocatorDefault, + date_formatter::{ + kCFDateFormatterNoStyle, kCFDateFormatterShortStyle, CFDateFormatterCreate, + }, + locale::CFLocaleCopyCurrent, + }; + + pub fn format_time(timestamp: &time::OffsetDateTime) -> Result { + format_with_date_formatter(timestamp, TIME_FORMATTER.with(|f| *f)) + } + + pub fn format_date(timestamp: &time::OffsetDateTime) -> Result { + format_with_date_formatter(timestamp, DATE_FORMATTER.with(|f| *f)) + } + + fn format_with_date_formatter( + timestamp: &time::OffsetDateTime, + fmt: CFDateFormatterRef, + ) -> Result { + const UNIX_TO_CF_ABSOLUTE_TIME_OFFSET: i64 = 978307200; + // Convert timestamp to macOS absolute time + let timestamp_macos = timestamp.unix_timestamp() - UNIX_TO_CF_ABSOLUTE_TIME_OFFSET; + let cf_absolute_time = timestamp_macos as CFAbsoluteTime; + unsafe { + let s = CFDateFormatterCreateStringWithAbsoluteTime( + kCFAllocatorDefault, + fmt, + cf_absolute_time, + ); + Ok(CFString::wrap_under_create_rule(s).to_string()) + } + } + + thread_local! { + static CURRENT_LOCALE: CFLocaleRef = unsafe { CFLocaleCopyCurrent() }; + static TIME_FORMATTER: CFDateFormatterRef = unsafe { + CFDateFormatterCreate( + kCFAllocatorDefault, + CURRENT_LOCALE.with(|locale| *locale), + kCFDateFormatterNoStyle, + kCFDateFormatterShortStyle, + ) + }; + static DATE_FORMATTER: CFDateFormatterRef = unsafe { + CFDateFormatterCreate( + kCFAllocatorDefault, + CURRENT_LOCALE.with(|locale| *locale), + kCFDateFormatterShortStyle, + kCFDateFormatterNoStyle, + ) + }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_24_hour_time() { + let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0); + let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0); + + assert_eq!( + format_timestamp_naive(reference, timestamp, test_timezone(), false), + "15:30" + ); + } + + #[test] + fn test_format_today() { + let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0); + let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0); + + assert_eq!( + format_timestamp_naive(reference, timestamp, test_timezone(), true), + "03:30 pm" + ); + } + + #[test] + fn test_format_yesterday() { + let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); + let timestamp = create_offset_datetime(1990, 4, 11, 9, 0, 0); + + assert_eq!( + format_timestamp_naive(reference, timestamp, test_timezone(), true), + "yesterday at 09:00 am" + ); + } + + #[test] + fn test_format_yesterday_less_than_24_hours_ago() { + let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0); + let timestamp = create_offset_datetime(1990, 4, 11, 20, 0, 0); + + assert_eq!( + format_timestamp_naive(reference, timestamp, test_timezone(), true), + "yesterday at 08:00 pm" + ); + } + + #[test] + fn test_format_yesterday_more_than_24_hours_ago() { + let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0); + let timestamp = create_offset_datetime(1990, 4, 11, 18, 0, 0); + + assert_eq!( + format_timestamp_naive(reference, timestamp, test_timezone(), true), + "yesterday at 06:00 pm" + ); + } + + #[test] + fn test_format_yesterday_over_midnight() { + let reference = create_offset_datetime(1990, 4, 12, 0, 5, 0); + let timestamp = create_offset_datetime(1990, 4, 11, 23, 55, 0); + + assert_eq!( + format_timestamp_naive(reference, timestamp, test_timezone(), true), + "yesterday at 11:55 pm" + ); + } + + #[test] + fn test_format_yesterday_over_month() { + let reference = create_offset_datetime(1990, 4, 2, 9, 0, 0); + let timestamp = create_offset_datetime(1990, 4, 1, 20, 0, 0); + + assert_eq!( + format_timestamp_naive(reference, timestamp, test_timezone(), true), + "yesterday at 08:00 pm" + ); + } + + #[test] + fn test_format_before_yesterday() { + let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); + let timestamp = create_offset_datetime(1990, 4, 10, 20, 20, 0); + + assert_eq!( + format_timestamp_naive(reference, timestamp, test_timezone(), true), + "04/10/1990" + ); + } + + fn test_timezone() -> UtcOffset { + UtcOffset::from_hms(0, 0, 0).expect("Valid timezone offset") + } + + fn create_offset_datetime( + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + ) -> OffsetDateTime { + let date = time::Date::from_calendar_date(year, time::Month::try_from(month).unwrap(), day) + .unwrap(); + let time = time::Time::from_hms(hour, minute, second).unwrap(); + date.with_time(time).assume_utc() // Assume UTC for simplicity + } +} diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 26fece87a2..1776f151d3 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -10,13 +10,10 @@ name = "ui" path = "src/ui.rs" [dependencies] -anyhow.workspace = true -chrono = "0.4" +chrono.workspace = true gpui.workspace = true -itertools = { version = "0.11.0", optional = true } +itertools = { workspace = true, optional = true } menu.workspace = true -rand = "0.8" -serde.workspace = true settings.workspace = true smallvec.workspace = true story = { workspace = true, optional = true } diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index cddd849b89..04fb193660 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -7,6 +7,7 @@ use crate::prelude::*; pub enum LabelSize { #[default] Default, + Large, Small, XSmall, } @@ -97,6 +98,7 @@ impl RenderOnce for LabelLike { ) }) .map(|this| match self.size { + LabelSize::Large => this.text_ui_lg(), LabelSize::Default => this.text_ui(), LabelSize::Small => this.text_ui_sm(), LabelSize::XSmall => this.text_ui_xs(), diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 0bbbee2900..de4de73f42 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -51,8 +51,10 @@ impl PopoverMenu { cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| { if modal.focus_handle(cx).contains_focused(cx) { - if previous_focus_handle.is_some() { - cx.focus(previous_focus_handle.as_ref().unwrap()) + if let Some(previous_focus_handle) = + previous_focus_handle.as_ref() + { + cx.focus(previous_focus_handle); } } *menu2.borrow_mut() = None; diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index b08f3911cb..4f7f062ec4 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -132,8 +132,8 @@ impl Element for RightClickMenu { }; let menu = element_state.menu.clone(); let position = element_state.position.clone(); - let attach = self.attach.clone(); - let child_layout_id = element_state.child_layout_id.clone(); + let attach = self.attach; + let child_layout_id = element_state.child_layout_id; let child_bounds = cx.layout_bounds(child_layout_id.unwrap()); let interactive_bounds = InteractiveBounds { @@ -154,8 +154,8 @@ impl Element for RightClickMenu { cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| { if modal.focus_handle(cx).contains_focused(cx) { - if previous_focus_handle.is_some() { - cx.focus(previous_focus_handle.as_ref().unwrap()) + if let Some(previous_focus_handle) = previous_focus_handle.as_ref() { + cx.focus(previous_focus_handle); } } *menu2.borrow_mut() = None; @@ -165,11 +165,12 @@ impl Element for RightClickMenu { cx.focus_view(&new_menu); *menu.borrow_mut() = Some(new_menu); - *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() { - attach.unwrap().corner(child_bounds) - } else { - cx.mouse_position() - }; + *position.borrow_mut() = + if let Some(attach) = attach.filter(|_| child_layout_id.is_some()) { + attach.corner(child_bounds) + } else { + cx.mouse_position() + }; cx.refresh(); } }); diff --git a/crates/ui/src/components/stories/keybinding.rs b/crates/ui/src/components/stories/keybinding.rs index 980a57d7da..f7a9a86cd0 100644 --- a/crates/ui/src/components/stories/keybinding.rs +++ b/crates/ui/src/components/stories/keybinding.rs @@ -42,7 +42,7 @@ impl Render for KeybindingStory { .gap_4() .py_3() .children(chunk.map(|permutation| { - KeyBinding::new(binding(&*(permutation.join("-") + "-x"))) + KeyBinding::new(binding(&(permutation.join("-") + "-x"))) })) }), ), diff --git a/crates/ui/src/components/stories/list_item.rs b/crates/ui/src/components/stories/list_item.rs index 67ec36816f..b23622a613 100644 --- a/crates/ui/src/components/stories/list_item.rs +++ b/crates/ui/src/components/stories/list_item.rs @@ -4,7 +4,7 @@ use story::Story; use crate::{prelude::*, Avatar}; use crate::{IconName, ListItem}; -const OVERFLOWING_TEXT: &'static str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean mauris ligula, luctus vel dignissim eu, vestibulum sed libero. Sed at convallis velit."; +const OVERFLOWING_TEXT: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean mauris ligula, luctus vel dignissim eu, vestibulum sed libero. Sed at convallis velit."; pub struct ListItemStory; diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index b2eaf75ed9..2b4cc2b395 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -35,6 +35,17 @@ pub trait StyledExt: Styled + Sized { self.text_size(size.rems()) } + /// The large size for UI text. + /// + /// `1rem` or `16px` at the default scale of `1rem` = `16px`. + /// + /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. + /// + /// Use `text_ui` for regular-sized text. + fn text_ui_lg(self) -> Self { + self.text_size(UiTextSize::Large.rems()) + } + /// The default size for UI text. /// /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`. diff --git a/crates/ui/src/styles/typography.rs b/crates/ui/src/styles/typography.rs index 70cd797d51..1931672af7 100644 --- a/crates/ui/src/styles/typography.rs +++ b/crates/ui/src/styles/typography.rs @@ -13,6 +13,13 @@ pub enum UiTextSize { /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. #[default] Default, + /// The large size for UI text. + /// + /// `1rem` or `16px` at the default scale of `1rem` = `16px`. + /// + /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. + Large, + /// The small size for UI text. /// /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`. @@ -31,6 +38,7 @@ pub enum UiTextSize { impl UiTextSize { pub fn rems(self) -> Rems { match self { + Self::Large => rems(16. / 16.), Self::Default => rems(14. / 16.), Self::Small => rems(12. / 16.), Self::XSmall => rems(10. / 16.), diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index fc06ebeb4a..9725320507 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -31,6 +31,7 @@ serde_json.workspace = true smol.workspace = true take-until = "0.2.0" tempfile = { workspace = true, optional = true } +unicase.workspace = true url.workspace = true [target.'cfg(windows)'.dependencies] diff --git a/crates/util/src/http.rs b/crates/util/src/http.rs index aff5093b73..3df8886fa4 100644 --- a/crates/util/src/http.rs +++ b/crates/util/src/http.rs @@ -14,43 +14,62 @@ use std::fmt; use std::{sync::Arc, time::Duration}; pub use url::Url; -pub struct ZedHttpClient { - pub zed_host: Mutex, - client: Box, +/// An [`HttpClient`] that has a base URL. +pub struct HttpClientWithUrl { + base_url: Mutex, + client: Arc, } -impl ZedHttpClient { - pub fn zed_url(&self, path: &str) -> String { - format!("{}{}", self.zed_host.lock(), path) +impl HttpClientWithUrl { + /// Returns a new [`HttpClientWithUrl`] with the given base URL. + pub fn new(base_url: impl Into) -> Self { + Self { + base_url: Mutex::new(base_url.into()), + client: client(), + } + } + + /// Returns the base URL. + pub fn base_url(&self) -> String { + self.base_url.lock().clone() + } + + /// Sets the base URL. + pub fn set_base_url(&self, base_url: impl Into) { + *self.base_url.lock() = base_url.into(); + } + + /// Builds a URL using the given path. + pub fn build_url(&self, path: &str) -> String { + format!("{}{}", self.base_url.lock(), path) + } + + /// Builds a Zed API URL using the given path. + pub fn build_zed_api_url(&self, path: &str) -> String { + let base_url = self.base_url.lock().clone(); + let base_api_url = match base_url.as_ref() { + "https://zed.dev" => "https://api.zed.dev", + "https://staging.zed.dev" => "https://api-staging.zed.dev", + "http://localhost:3000" => "http://localhost:8080", + other => other, + }; + + format!("{}{}", base_api_url, path) } } -impl HttpClient for Arc { +impl HttpClient for Arc { fn send(&self, req: Request) -> BoxFuture, Error>> { self.client.send(req) } } -impl HttpClient for ZedHttpClient { +impl HttpClient for HttpClientWithUrl { fn send(&self, req: Request) -> BoxFuture, Error>> { self.client.send(req) } } -pub fn zed_client(zed_host: &str) -> Arc { - Arc::new(ZedHttpClient { - zed_host: Mutex::new(zed_host.to_string()), - client: Box::new( - isahc::HttpClient::builder() - .connect_timeout(Duration::from_secs(5)) - .low_speed_timeout(100, Duration::from_secs(5)) - .proxy(http_proxy_from_env()) - .build() - .unwrap(), - ), - }) -} - pub trait HttpClient: Send + Sync { fn send(&self, req: Request) -> BoxFuture, Error>>; @@ -109,32 +128,35 @@ impl HttpClient for isahc::HttpClient { } } +#[cfg(feature = "test-support")] +type FakeHttpHandler = Box< + dyn Fn(Request) -> BoxFuture<'static, Result, Error>> + + Send + + Sync + + 'static, +>; + #[cfg(feature = "test-support")] pub struct FakeHttpClient { - handler: Box< - dyn 'static - + Send - + Sync - + Fn(Request) -> BoxFuture<'static, Result, Error>>, - >, + handler: FakeHttpHandler, } #[cfg(feature = "test-support")] impl FakeHttpClient { - pub fn create(handler: F) -> Arc + pub fn create(handler: F) -> Arc where - Fut: 'static + Send + futures::Future, Error>>, - F: 'static + Send + Sync + Fn(Request) -> Fut, + Fut: futures::Future, Error>> + Send + 'static, + F: Fn(Request) -> Fut + Send + Sync + 'static, { - Arc::new(ZedHttpClient { - zed_host: Mutex::new("http://test.example".into()), - client: Box::new(Self { + Arc::new(HttpClientWithUrl { + base_url: Mutex::new("http://test.example".into()), + client: Arc::new(Self { handler: Box::new(move |req| Box::pin(handler(req))), }), }) } - pub fn with_404_response() -> Arc { + pub fn with_404_response() -> Arc { Self::create(|_| async move { Ok(Response::builder() .status(404) @@ -143,7 +165,7 @@ impl FakeHttpClient { }) } - pub fn with_200_response() -> Arc { + pub fn with_200_response() -> Arc { Self::create(|_| async move { Ok(Response::builder() .status(200) diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 81be37237e..27306bcfb7 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -39,16 +39,18 @@ lazy_static::lazy_static! { }; pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); - pub static ref RUNNABLES: PathBuf = CONFIG_DIR.join("runnables.json"); + pub static ref TASKS: PathBuf = CONFIG_DIR.join("tasks.json"); pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt"); pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old"); pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json"); + pub static ref LOCAL_TASKS_RELATIVE_PATH: &'static Path = Path::new(".zed/tasks.json"); + pub static ref TEMP_DIR: PathBuf = HOME.join(".cache").join("zed"); } pub trait PathExt { fn compact(&self) -> PathBuf; - fn icon_suffix(&self) -> Option<&str>; + fn icon_stem_or_suffix(&self) -> Option<&str>; fn extension_or_hidden_file_name(&self) -> Option<&str>; fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result where @@ -100,17 +102,17 @@ impl> PathExt for T { } } - /// Returns a suffix of the path that is used to determine which file icon to use - fn icon_suffix(&self) -> Option<&str> { - let file_name = self.as_ref().file_name()?.to_str()?; - + /// Returns either the suffix if available, or the file stem otherwise to determine which file icon to use + fn icon_stem_or_suffix(&self) -> Option<&str> { + let path = self.as_ref(); + let file_name = path.file_name()?.to_str()?; if file_name.starts_with('.') { return file_name.strip_prefix('.'); } - self.as_ref() - .extension() - .and_then(|extension| extension.to_str()) + path.extension() + .and_then(|e| e.to_str()) + .or_else(|| path.file_stem()?.to_str()) } /// Returns a file's extension or, if the file is hidden, its name without the leading dot @@ -403,26 +405,30 @@ mod tests { } #[test] - fn test_icon_suffix() { + fn test_icon_stem_or_suffix() { // No dots in name let path = Path::new("/a/b/c/file_name.rs"); - assert_eq!(path.icon_suffix(), Some("rs")); + assert_eq!(path.icon_stem_or_suffix(), Some("rs")); // Single dot in name let path = Path::new("/a/b/c/file.name.rs"); - assert_eq!(path.icon_suffix(), Some("rs")); + assert_eq!(path.icon_stem_or_suffix(), Some("rs")); + + // No suffix + let path = Path::new("/a/b/c/file"); + assert_eq!(path.icon_stem_or_suffix(), Some("file")); // Multiple dots in name let path = Path::new("/a/b/c/long.file.name.rs"); - assert_eq!(path.icon_suffix(), Some("rs")); + assert_eq!(path.icon_stem_or_suffix(), Some("rs")); // Hidden file, no extension let path = Path::new("/a/b/c/.gitignore"); - assert_eq!(path.icon_suffix(), Some("gitignore")); + assert_eq!(path.icon_stem_or_suffix(), Some("gitignore")); // Hidden file, with extension let path = Path::new("/a/b/c/.eslintrc.js"); - assert_eq!(path.icon_suffix(), Some("eslintrc.js")); + assert_eq!(path.icon_stem_or_suffix(), Some("eslintrc.js")); } #[test] @@ -453,7 +459,7 @@ mod tests { let path = Path::new("/work/node_modules"); let path_matcher = PathMatcher::new("**/node_modules/**").unwrap(); assert!( - path_matcher.is_match(&path), + path_matcher.is_match(path), "Path matcher {path_matcher} should match {path:?}" ); } @@ -463,7 +469,7 @@ mod tests { let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules"); let path_matcher = PathMatcher::new("**/node_modules/**").unwrap(); assert!( - path_matcher.is_match(&path), + path_matcher.is_match(path), "Path matcher {path_matcher} should match {path:?}" ); } diff --git a/crates/util/src/semantic_version.rs b/crates/util/src/semantic_version.rs index f5e4562adf..851bedc7aa 100644 --- a/crates/util/src/semantic_version.rs +++ b/crates/util/src/semantic_version.rs @@ -14,9 +14,18 @@ pub struct SemanticVersion { pub patch: usize, } +impl SemanticVersion { + pub fn new(major: usize, minor: usize, patch: usize) -> Self { + Self { + major, + minor, + patch, + } + } +} + impl FromStr for SemanticVersion { type Err = anyhow::Error; - fn from_str(s: &str) -> Result { let mut components = s.trim().split('.'); let major = components diff --git a/crates/util/src/test.rs b/crates/util/src/test.rs index e728281ea6..9915a6c159 100644 --- a/crates/util/src/test.rs +++ b/crates/util/src/test.rs @@ -29,8 +29,8 @@ fn write_tree(path: &Path, tree: serde_json::Value) { Value::Object(_) => { fs::create_dir(&path).unwrap(); - if path.file_name() == Some(&OsStr::new(".git")) { - git2::Repository::init(&path.parent().unwrap()).unwrap(); + if path.file_name() == Some(OsStr::new(".git")) { + git2::Repository::init(path.parent().unwrap()).unwrap(); } write_tree(&path, contents); diff --git a/crates/util/src/test/marked_text.rs b/crates/util/src/test/marked_text.rs index cf85a806e4..7ab45e9b20 100644 --- a/crates/util/src/test/marked_text.rs +++ b/crates/util/src/test/marked_text.rs @@ -12,7 +12,7 @@ pub fn marked_text_offsets_by( for char in marked_text.chars() { if markers.contains(&char) { - let char_offsets = extracted_markers.entry(char).or_insert(Vec::new()); + let char_offsets = extracted_markers.entry(char).or_default(); char_offsets.push(unmarked_text.len()); } else { unmarked_text.push(char); @@ -119,7 +119,7 @@ pub fn marked_text_ranges( let mut current_range_start = None; let mut current_range_cursor = None; - let marked_text = marked_text.replace("β€’", " "); + let marked_text = marked_text.replace('β€’', " "); for (marked_ix, marker) in marked_text.match_indices(&['Β«', 'Β»', 'Λ‡']) { unmarked_text.push_str(&marked_text[prev_marked_ix..marked_ix]); let unmarked_len = unmarked_text.len(); @@ -131,9 +131,9 @@ pub fn marked_text_ranges( if current_range_start.is_some() { if current_range_cursor.is_some() { panic!("duplicate point marker 'Λ‡' at index {marked_ix}"); - } else { - current_range_cursor = Some(unmarked_len); } + + current_range_cursor = Some(unmarked_len); } else { ranges.push(unmarked_len..unmarked_len); } @@ -252,6 +252,7 @@ impl From<(char, char)> for TextRangeMarker { mod tests { use super::{generate_marked_text, marked_text_ranges}; + #[allow(clippy::reversed_empty_ranges)] #[test] fn test_marked_text() { let (text, ranges) = marked_text_ranges("one Β«Λ‡twoΒ» Β«threeΛ‡Β» Β«Λ‡fourΒ» fiveΛ‡ six", true); diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 8f7a6fffbd..4bc939dece 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -22,6 +22,7 @@ use std::{ task::{Context, Poll}, time::Instant, }; +use unicase::UniCase; pub use take_until::*; @@ -487,6 +488,43 @@ impl RangeExt for RangeInclusive { } } +/// A way to sort strings with starting numbers numerically first, falling back to alphanumeric one, +/// case-insensitive. +/// +/// This is useful for turning regular alphanumerically sorted sequences as `1-abc, 10, 11-def, .., 2, 21-abc` +/// into `1-abc, 2, 10, 11-def, .., 21-abc` +#[derive(Debug, PartialEq, Eq)] +pub struct NumericPrefixWithSuffix<'a>(i32, &'a str); + +impl<'a> NumericPrefixWithSuffix<'a> { + pub fn from_numeric_prefixed_str(str: &'a str) -> Option { + let mut chars = str.chars(); + let prefix: String = chars.by_ref().take_while(|c| c.is_ascii_digit()).collect(); + let remainder = chars.as_str(); + + match prefix.parse::() { + Ok(prefix) => Some(NumericPrefixWithSuffix(prefix, remainder)), + Err(_) => None, + } + } +} + +impl Ord for NumericPrefixWithSuffix<'_> { + fn cmp(&self, other: &Self) -> Ordering { + let NumericPrefixWithSuffix(num_a, remainder_a) = self; + let NumericPrefixWithSuffix(num_b, remainder_b) = other; + num_a + .cmp(num_b) + .then_with(|| UniCase::new(remainder_a).cmp(&UniCase::new(remainder_b))) + } +} + +impl<'a> PartialOrd for NumericPrefixWithSuffix<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -526,4 +564,23 @@ mod tests { assert_eq!(truncate_and_trailoff("èèèèèè", 6), "èèèèèè"); assert_eq!(truncate_and_trailoff("èèèèèè", 5), "èèèèè…"); } + + #[test] + fn test_numeric_prefix_with_suffix() { + let mut sorted = vec!["1-abc", "10", "11def", "2", "21-abc"]; + sorted.sort_by_key(|s| { + NumericPrefixWithSuffix::from_numeric_prefixed_str(s).unwrap_or_else(|| { + panic!("Cannot convert string `{s}` into NumericPrefixWithSuffix") + }) + }); + assert_eq!(sorted, ["1-abc", "2", "10", "11def", "21-abc"]); + + for numeric_prefix_less in ["numeric_prefix_less", "aaa", "~β„’Β£"] { + assert_eq!( + NumericPrefixWithSuffix::from_numeric_prefixed_str(numeric_prefix_less), + None, + "String without numeric prefix `{numeric_prefix_less}` should not be converted into NumericPrefixWithSuffix" + ) + } + } } diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 645c3e7128..2f88328084 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -9,7 +9,7 @@ use gpui::{ use picker::{Picker, PickerDelegate}; use std::{ops::Not, sync::Arc}; use ui::{ - h_flex, v_flex, Button, ButtonCommon, Clickable, HighlightedLabel, Label, LabelCommon, + h_flex, v_flex, Button, ButtonCommon, Clickable, Color, HighlightedLabel, Label, LabelCommon, LabelSize, ListItem, ListItemSpacing, Selectable, }; use util::ResultExt; @@ -135,7 +135,7 @@ impl BranchListDelegate { impl PickerDelegate for BranchListDelegate { type ListItem = ListItem; - fn placeholder_text(&self) -> Arc { + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Select branch...".into() } @@ -292,11 +292,13 @@ impl PickerDelegate for BranchListDelegate { let label = if self.last_query.is_empty() { h_flex() .ml_3() - .child(Label::new("Recent branches").size(LabelSize::Small)) + .child(Label::new("Recent Branches").size(LabelSize::Small)) } else { let match_label = self.matches.is_empty().not().then(|| { let suffix = if self.matches.len() == 1 { "" } else { "es" }; - Label::new(format!("{} match{}", self.matches.len(), suffix)).size(LabelSize::Small) + Label::new(format!("{} match{}", self.matches.len(), suffix)) + .color(Color::Muted) + .size(LabelSize::Small) }); h_flex() .px_3() @@ -305,7 +307,7 @@ impl PickerDelegate for BranchListDelegate { .child(Label::new("Branches").size(LabelSize::Small)) .children(match_label) }; - Some(label.into_any()) + Some(label.mt_1().into_any()) } fn render_footer(&self, cx: &mut ViewContext>) -> Option { if self.last_query.is_empty() { diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 05f8dafe3d..071421e74e 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -17,12 +17,9 @@ anyhow.workspace = true async-compat = { version = "0.2.1", "optional" = true } async-trait = { workspace = true, "optional" = true } collections.workspace = true -command_palette.workspace = true -# HACK: We're only depending on `copilot` here for `CommandPaletteFilter`. See the attached comment on that type. -copilot.workspace = true +command_palette_hooks.workspace = true editor.workspace = true gpui.workspace = true -itertools = "0.10" language.workspace = true log.workspace = true nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = [ @@ -34,13 +31,14 @@ serde.workspace = true serde_derive.workspace = true serde_json.workspace = true settings.workspace = true -theme.workspace = true tokio = { version = "1.15", "optional" = true } ui.workspace = true workspace.workspace = true zed_actions.workspace = true +schemars.workspace = true [dev-dependencies] +command_palette.workspace = true editor = { workspace = true, features = ["test-support"] } futures.workspace = true gpui = { workspace = true, features = ["test-support"] } @@ -49,8 +47,6 @@ indoc.workspace = true language = { workspace = true, features = ["test-support"] } lsp = { workspace = true, features = ["test-support"] } parking_lot.workspace = true -project = { workspace = true, features = ["test-support"] } settings.workspace = true -theme = { workspace = true, features = ["test-support"] } util = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index d60964041f..d8623d9c75 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1,4 +1,4 @@ -use command_palette::CommandInterceptResult; +use command_palette_hooks::CommandInterceptResult; use editor::actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive}; use gpui::{impl_actions, Action, AppContext, ViewContext}; use serde_derive::Deserialize; @@ -41,7 +41,7 @@ pub fn command_interceptor(mut query: &str, cx: &AppContext) -> Option Option ( + "tabonly", + workspace::CloseInactiveItems { + save_intent: Some(SaveIntent::Close), + } + .boxed_clone(), + ), + "tabo!" | "tabon!" | "tabonl!" | "tabonly!" => ( + "tabonly!", + workspace::CloseInactiveItems { + save_intent: Some(SaveIntent::Skip), + } + .boxed_clone(), + ), + "on" | "onl" | "only" => ( + "only", + workspace::CloseInactiveTabsAndPanes { + save_intent: Some(SaveIntent::Close), + } + .boxed_clone(), + ), + "on!" | "onl!" | "only!" => ( + "only!", + workspace::CloseInactiveTabsAndPanes { + save_intent: Some(SaveIntent::Skip), + } + .boxed_clone(), + ), // quickfix / loclist (merged together for now) "cl" | "cli" | "clis" | "clist" => ( @@ -293,16 +321,16 @@ pub fn command_interceptor(mut query: &str, cx: &AppContext) -> Option ("0", StartOfDocument.boxed_clone()), _ => { - if query.starts_with("/") || query.starts_with("?") { + if query.starts_with('/') || query.starts_with('?') { ( query, FindCommand { query: query[1..].to_string(), - backwards: query.starts_with("?"), + backwards: query.starts_with('?'), } .boxed_clone(), ) - } else if query.starts_with("%") { + } else if query.starts_with('%') { ( query, ReplaceCommand { @@ -330,7 +358,7 @@ pub fn command_interceptor(mut query: &str, cx: &AppContext) -> Option Vec { let mut positions = Vec::new(); - let mut chars = query.chars().into_iter(); + let mut chars = query.chars(); let Some(mut current) = chars.next() else { return positions; @@ -462,9 +490,7 @@ mod test { assert_eq!(fs.load(&path).await.unwrap(), "@\n"); - fs.as_fake() - .write_file_internal(path, "oops\n".to_string()) - .unwrap(); + fs.as_fake().insert_file(path, b"oops\n".to_vec()).await; // conflict! cx.simulate_keystrokes(["i", "@", "escape"]); diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index a063d37475..064c4b67a0 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -15,8 +15,8 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext< let count = vim.take_count(cx).unwrap_or(1); vim.stop_recording_immediately(action.boxed_clone()); if count <= 1 || vim.workspace_state.replaying { - vim.update_active_editor(cx, |editor, cx| { - editor.cancel(&Default::default(), cx); + vim.update_active_editor(cx, |_, editor, cx| { + editor.dismiss_menus_and_popups(cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, mut cursor, _| { *cursor.column_mut() = cursor.column().saturating_sub(1); diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 7afa22b876..a093eab9a2 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -41,14 +41,9 @@ impl Render for ModeIndicator { return div().into_any(); }; - let text = match mode { - Mode::Normal => "-- NORMAL --", - Mode::Insert => "-- INSERT --", - Mode::Visual => "-- VISUAL --", - Mode::VisualLine => "-- VISUAL LINE --", - Mode::VisualBlock => "-- VISUAL BLOCK --", - }; - Label::new(text).size(LabelSize::Small).into_any_element() + Label::new(format!("-- {} --", mode)) + .size(LabelSize::Small) + .into_any_element() } } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index de628a4c55..b46035ce2e 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -22,27 +22,57 @@ use crate::{ pub enum Motion { Left, Backspace, - Down { display_lines: bool }, - Up { display_lines: bool }, + Down { + display_lines: bool, + }, + Up { + display_lines: bool, + }, Right, Space, - NextWordStart { ignore_punctuation: bool }, - NextWordEnd { ignore_punctuation: bool }, - PreviousWordStart { ignore_punctuation: bool }, - PreviousWordEnd { ignore_punctuation: bool }, - FirstNonWhitespace { display_lines: bool }, + NextWordStart { + ignore_punctuation: bool, + }, + NextWordEnd { + ignore_punctuation: bool, + }, + PreviousWordStart { + ignore_punctuation: bool, + }, + PreviousWordEnd { + ignore_punctuation: bool, + }, + FirstNonWhitespace { + display_lines: bool, + }, CurrentLine, - StartOfLine { display_lines: bool }, - EndOfLine { display_lines: bool }, + StartOfLine { + display_lines: bool, + }, + EndOfLine { + display_lines: bool, + }, StartOfParagraph, EndOfParagraph, StartOfDocument, EndOfDocument, Matching, - FindForward { before: bool, char: char }, - FindBackward { after: bool, char: char }, - RepeatFind { last_find: Box }, - RepeatFindReversed { last_find: Box }, + FindForward { + before: bool, + char: char, + mode: FindRange, + }, + FindBackward { + after: bool, + char: char, + mode: FindRange, + }, + RepeatFind { + last_find: Box, + }, + RepeatFindReversed { + last_find: Box, + }, NextLineStart, StartOfLineDownward, EndOfLineDownward, @@ -462,9 +492,10 @@ impl Motion { start_of_line(map, *display_lines, point), SelectionGoal::None, ), - EndOfLine { display_lines } => { - (end_of_line(map, *display_lines, point), SelectionGoal::None) - } + EndOfLine { display_lines } => ( + end_of_line(map, *display_lines, point, times), + SelectionGoal::None, + ), StartOfParagraph => ( movement::start_of_paragraph(map, point, times), SelectionGoal::None, @@ -481,30 +512,30 @@ impl Motion { ), Matching => (matching(map, point), SelectionGoal::None), // t f - FindForward { before, char } => { - return find_forward(map, point, *before, *char, times) + FindForward { before, char, mode } => { + return find_forward(map, point, *before, *char, times, *mode) .map(|new_point| (new_point, SelectionGoal::None)) } // T F - FindBackward { after, char } => ( - find_backward(map, point, *after, *char, times), + FindBackward { after, char, mode } => ( + find_backward(map, point, *after, *char, times, *mode), SelectionGoal::None, ), // ; -- repeat the last find done with t, f, T, F RepeatFind { last_find } => match **last_find { - Motion::FindForward { before, char } => { - let mut new_point = find_forward(map, point, before, char, times); + Motion::FindForward { before, char, mode } => { + let mut new_point = find_forward(map, point, before, char, times, mode); if new_point == Some(point) { - new_point = find_forward(map, point, before, char, times + 1); + new_point = find_forward(map, point, before, char, times + 1, mode); } return new_point.map(|new_point| (new_point, SelectionGoal::None)); } - Motion::FindBackward { after, char } => { - let mut new_point = find_backward(map, point, after, char, times); + Motion::FindBackward { after, char, mode } => { + let mut new_point = find_backward(map, point, after, char, times, mode); if new_point == point { - new_point = find_backward(map, point, after, char, times + 1); + new_point = find_backward(map, point, after, char, times + 1, mode); } (new_point, SelectionGoal::None) @@ -513,19 +544,19 @@ impl Motion { }, // , -- repeat the last find done with t, f, T, F, in opposite direction RepeatFindReversed { last_find } => match **last_find { - Motion::FindForward { before, char } => { - let mut new_point = find_backward(map, point, before, char, times); + Motion::FindForward { before, char, mode } => { + let mut new_point = find_backward(map, point, before, char, times, mode); if new_point == point { - new_point = find_backward(map, point, before, char, times + 1); + new_point = find_backward(map, point, before, char, times + 1, mode); } (new_point, SelectionGoal::None) } - Motion::FindBackward { after, char } => { - let mut new_point = find_forward(map, point, after, char, times); + Motion::FindBackward { after, char, mode } => { + let mut new_point = find_forward(map, point, after, char, times, mode); if new_point == Some(point) { - new_point = find_forward(map, point, after, char, times + 1); + new_point = find_forward(map, point, after, char, times + 1, mode); } return new_point.map(|new_point| (new_point, SelectionGoal::None)); @@ -828,7 +859,7 @@ fn next_word_end( let mut new_point = point; if new_point.column() < map.line_len(new_point.row()) { *new_point.column_mut() += 1; - } else if new_point.row() < map.max_buffer_row() { + } else if new_point < map.max_point() { *new_point.row_mut() += 1; *new_point.column_mut() = 0; } @@ -919,8 +950,12 @@ pub(crate) fn start_of_line( pub(crate) fn end_of_line( map: &DisplaySnapshot, display_lines: bool, - point: DisplayPoint, + mut point: DisplayPoint, + times: usize, ) -> DisplayPoint { + if times > 1 { + point = start_of_relative_buffer_row(map, point, times as isize - 1); + } if display_lines { map.clip_point( DisplayPoint::new(point.row(), map.line_len(point.row())), @@ -1011,13 +1046,14 @@ fn find_forward( before: bool, target: char, times: usize, + mode: FindRange, ) -> Option { let mut to = from; let mut found = false; for _ in 0..times { found = false; - let new_to = find_boundary(map, to, FindRange::SingleLine, |_, right| { + let new_to = find_boundary(map, to, mode, |_, right| { found = right == target; found }); @@ -1045,14 +1081,13 @@ fn find_backward( after: bool, target: char, times: usize, + mode: FindRange, ) -> DisplayPoint { let mut to = from; for _ in 0..times { let new_to = - find_preceding_boundary_display_point(map, to, FindRange::SingleLine, |_, right| { - right == target - }); + find_preceding_boundary_display_point(map, to, mode, |_, right| right == target); if to == new_to { break; } @@ -1089,7 +1124,7 @@ pub(crate) fn next_line_end( if times > 1 { point = start_of_relative_buffer_row(map, point, times as isize - 1); } - end_of_line(map, false, point) + end_of_line(map, false, point, 1) } fn window_top( @@ -1110,13 +1145,15 @@ fn window_top( if let Some(visible_rows) = text_layout_details.visible_rows { let bottom_row = first_visible_line.row() + visible_rows as u32; - let new_row = (first_visible_line.row() + (times as u32)).min(bottom_row); + let new_row = (first_visible_line.row() + (times as u32)) + .min(bottom_row) + .min(map.max_point().row()); let new_col = point.column().min(map.line_len(first_visible_line.row())); let new_point = DisplayPoint::new(new_row, new_col); (map.clip_point(new_point, Bias::Left), SelectionGoal::None) } else { - let new_row = first_visible_line.row() + (times as u32); + let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row()); let new_col = point.column().min(map.line_len(first_visible_line.row())); let new_point = DisplayPoint::new(new_row, new_col); @@ -1134,8 +1171,12 @@ fn window_middle( .scroll_anchor .anchor .to_display_point(map); - let max_rows = (visible_rows as u32).min(map.max_buffer_row()); - let new_row = first_visible_line.row() + (max_rows.div_euclid(2)); + + let max_visible_rows = + (visible_rows as u32).min(map.max_point().row() - first_visible_line.row()); + + let new_row = + (first_visible_line.row() + (max_visible_rows / 2)).min(map.max_point().row()); let new_col = point.column().min(map.line_len(new_row)); let new_point = DisplayPoint::new(new_row, new_col); (map.clip_point(new_point, Bias::Left), SelectionGoal::None) @@ -1157,12 +1198,12 @@ fn window_bottom( .to_display_point(map); let bottom_row = first_visible_line.row() + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32; - if bottom_row < map.max_buffer_row() + if bottom_row < map.max_point().row() && text_layout_details.vertical_scroll_margin as usize > times { times = text_layout_details.vertical_scroll_margin.ceil() as usize; } - let bottom_row_capped = bottom_row.min(map.max_buffer_row()); + let bottom_row_capped = bottom_row.min(map.max_point().row()); let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() { first_visible_line.row() } else { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 399180fea4..380cbcb393 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -51,6 +51,8 @@ actions!( ConvertToUpperCase, ConvertToLowerCase, JoinLines, + Indent, + Outdent, ] ); @@ -119,13 +121,40 @@ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext, cx: &mut WindowContext, ) { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, cursor, goal| { @@ -198,7 +227,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext = old_selections @@ -272,7 +301,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, cursor, _| { let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1); - let insert_point = motion::end_of_line(map, false, previous_line); + let insert_point = motion::end_of_line(map, false, previous_line, 1); (insert_point, SelectionGoal::None) }); }); @@ -285,7 +314,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex Vim::update(cx, |vim, cx| { vim.start_recording(cx); vim.switch_mode(Mode::Insert, false, cx); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { let (map, old_selections) = editor.selections.all_display(cx); @@ -297,7 +326,8 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex let edits = selection_end_rows.into_iter().map(|row| { let (indent, _) = map.line_indent(row); let end_of_line = - motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map); + motion::end_of_line(&map, false, DisplayPoint::new(row, 0), 1) + .to_point(&map); let mut new_text = "\n".to_string(); new_text.push_str(&" ".repeat(indent as usize)); @@ -330,7 +360,7 @@ fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext) { pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.stop_recording(); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let (map, display_selections) = editor.selections.all_display(cx); @@ -379,10 +409,12 @@ pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { mod test { use gpui::TestAppContext; use indoc::indoc; + use settings::SettingsStore; use crate::{ state::Mode::{self}, - test::NeovimBackedTestContext, + test::{NeovimBackedTestContext, VimTestContext}, + VimSettings, }; #[gpui::test] @@ -903,6 +935,90 @@ mod test { } } + #[gpui::test] + async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| { + s.use_multiline_find = Some(true); + }); + }); + + cx.assert_binding( + ["f", "l"], + indoc! {" + Λ‡function print() { + console.log('ok') + } + "}, + Mode::Normal, + indoc! {" + function print() { + consoΛ‡le.log('ok') + } + "}, + Mode::Normal, + ); + + cx.assert_binding( + ["t", "l"], + indoc! {" + Λ‡function print() { + console.log('ok') + } + "}, + Mode::Normal, + indoc! {" + function print() { + consΛ‡ole.log('ok') + } + "}, + Mode::Normal, + ); + } + + #[gpui::test] + async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| { + s.use_multiline_find = Some(true); + }); + }); + + cx.assert_binding( + ["shift-f", "p"], + indoc! {" + function print() { + console.Λ‡log('ok') + } + "}, + Mode::Normal, + indoc! {" + function Λ‡print() { + console.log('ok') + } + "}, + Mode::Normal, + ); + + cx.assert_binding( + ["shift-t", "p"], + indoc! {" + function print() { + console.Λ‡log('ok') + } + "}, + Mode::Normal, + indoc! {" + function pΛ‡rint() { + console.log('ok') + } + "}, + Mode::Normal, + ); + } + #[gpui::test] async fn test_percent(cx: &mut TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]); @@ -911,4 +1027,22 @@ mod test { .await; cx.assert_all("let result = curried_funΛ‡(Λ‡)Λ‡(Λ‡)Λ‡;").await; } + + #[gpui::test] + async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + // goes to current line end + cx.set_shared_state(indoc! {"Λ‡aa\nbb\ncc"}).await; + cx.simulate_shared_keystrokes(["$"]).await; + cx.assert_shared_state(indoc! {"aΛ‡a\nbb\ncc"}).await; + + // goes to next line end + cx.simulate_shared_keystrokes(["2", "$"]).await; + cx.assert_shared_state("aa\nbΛ‡b\ncc").await; + + // try to exceed the final line. + cx.simulate_shared_keystrokes(["4", "$"]).await; + cx.assert_shared_state("aa\nbb\ncΛ‡c").await; + } } diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index db33e48c34..d8111312f9 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -40,7 +40,7 @@ where Vim::update(cx, |vim, cx| { vim.record_current_action(cx); let count = vim.take_count(cx).unwrap_or(1) as u32; - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let mut ranges = Vec::new(); let mut cursor_positions = Vec::new(); let snapshot = editor.buffer().read(cx).snapshot(cx); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 52de1f7e0a..08f3202e79 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -24,7 +24,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m | Motion::Backspace | Motion::StartOfLine { .. } ); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now @@ -45,7 +45,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m }; }); }); - copy_selections_content(editor, motion.linewise(), cx); + copy_selections_content(vim, editor, motion.linewise(), cx); editor.insert("", cx); }); }); @@ -59,7 +59,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { let mut objects_found = false; - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { @@ -69,7 +69,7 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo }); }); if objects_found { - copy_selections_content(editor, false, cx); + copy_selections_content(vim, editor, false, cx); editor.insert("", cx); } }); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index cbcdcadca9..b81c0b16a5 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,12 +1,16 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::{HashMap, HashSet}; -use editor::{display_map::ToDisplayPoint, scroll::Autoscroll, Bias}; +use editor::{ + display_map::{DisplaySnapshot, ToDisplayPoint}, + scroll::Autoscroll, + Bias, DisplayPoint, +}; use gpui::WindowContext; -use language::Point; +use language::{Point, Selection}; pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.stop_recording(); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); @@ -39,7 +43,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m } }); }); - copy_selections_content(editor, motion.linewise(), cx); + copy_selections_content(vim, editor, motion.linewise(), cx); editor.insert("", cx); // Fixup cursor position after the deletion @@ -62,7 +66,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { vim.stop_recording(); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); // Emulates behavior in vim where if we expanded backwards to include a newline @@ -72,6 +76,14 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo s.move_with(|map, selection| { object.expand_selection(map, selection, around); let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range(); + let mut move_selection_start_to_previous_line = + |map: &DisplaySnapshot, selection: &mut Selection| { + let start = selection.start.to_offset(map, Bias::Left); + if selection.start.row() > 0 { + should_move_to_start.insert(selection.id); + selection.start = (start - '\n'.len_utf8()).to_display_point(map); + } + }; let contains_only_newlines = map .chars_at(selection.start) .take_while(|(_, p)| p < &selection.end) @@ -88,17 +100,28 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo // at the end or start if (around || object == Object::Sentence) && contains_only_newlines { if end_at_newline { - selection.end = - (offset_range.end + '\n'.len_utf8()).to_display_point(map); - } else if selection.start.row() > 0 { - should_move_to_start.insert(selection.id); - selection.start = - (offset_range.start - '\n'.len_utf8()).to_display_point(map); + move_selection_end_to_next_line(map, selection); + } else { + move_selection_start_to_previous_line(map, selection); + } + } + + // Does post-processing for the trailing newline and EOF + // when not cancelled. + let cancelled = around && selection.start == selection.end; + if object == Object::Paragraph && !cancelled { + // EOF check should be done before including a trailing newline. + if ends_at_eof(map, selection) { + move_selection_start_to_previous_line(map, selection); + } + + if end_at_newline { + move_selection_end_to_next_line(map, selection); } } }); }); - copy_selections_content(editor, false, cx); + copy_selections_content(vim, editor, false, cx); editor.insert("", cx); // Fixup cursor position after the deletion @@ -117,6 +140,15 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo }); } +fn move_selection_end_to_next_line(map: &DisplaySnapshot, selection: &mut Selection) { + let end = selection.end.to_offset(map, Bias::Left); + selection.end = (end + '\n'.len_utf8()).to_display_point(map); +} + +fn ends_at_eof(map: &DisplaySnapshot, selection: &mut Selection) -> bool { + selection.end.to_point(map) == map.buffer_snapshot.max_point() +} + #[cfg(test)] mod test { use indoc::indoc; diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 6353a881ed..e70fce99e1 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -44,7 +44,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { } fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let mut edits = Vec::new(); let mut new_anchors = Vec::new(); diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index a65a816654..b56bb33b5c 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -1,14 +1,15 @@ -use std::{borrow::Cow, cmp}; +use std::cmp; use editor::{ display_map::ToDisplayPoint, movement, scroll::Autoscroll, ClipboardSelection, DisplayPoint, }; -use gpui::{impl_actions, ViewContext}; +use gpui::{impl_actions, AppContext, ViewContext}; use language::{Bias, SelectionGoal}; use serde::Deserialize; +use settings::Settings; use workspace::Workspace; -use crate::{state::Mode, utils::copy_selections_content, Vim}; +use crate::{state::Mode, utils::copy_selections_content, UseSystemClipboard, Vim, VimSettings}; #[derive(Clone, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] @@ -25,34 +26,60 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext workspace.register_action(paste); } +fn system_clipboard_is_newer(vim: &Vim, cx: &mut AppContext) -> bool { + cx.read_from_clipboard().is_some_and(|item| { + if let Some(last_state) = vim.workspace_state.registers.get(".system.") { + last_state != item.text() + } else { + true + } + }) +} + fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); - let Some(item) = cx.read_from_clipboard() else { - return; - }; - let clipboard_text = Cow::Borrowed(item.text()); + let (clipboard_text, clipboard_selections): (String, Option<_>) = + if VimSettings::get_global(cx).use_system_clipboard == UseSystemClipboard::Never + || VimSettings::get_global(cx).use_system_clipboard + == UseSystemClipboard::OnYank + && !system_clipboard_is_newer(vim, cx) + { + ( + vim.workspace_state + .registers + .get("\"") + .cloned() + .unwrap_or_else(|| "".to_string()), + None, + ) + } else { + if let Some(item) = cx.read_from_clipboard() { + let clipboard_selections = item + .metadata::>() + .filter(|clipboard_selections| { + clipboard_selections.len() > 1 + && vim.state().mode != Mode::VisualLine + }); + (item.text().clone(), clipboard_selections) + } else { + ("".into(), None) + } + }; + if clipboard_text.is_empty() { return; } if !action.preserve_clipboard && vim.state().mode.is_visual() { - copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx); + copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx); } - // if we are copying from multi-cursor (of visual block mode), we want - // to - let clipboard_selections = - item.metadata::>() - .filter(|clipboard_selections| { - clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine - }); - let (display_map, current_selections) = editor.selections.all_adjusted_display(cx); // unlike zed, if you have a multi-cursor selection from vim block mode, @@ -108,8 +135,8 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { } else { (clipboard_text.to_string(), first_selection_indent_column) }; - let line_mode = to_insert.ends_with("\n"); - let is_multiline = to_insert.contains("\n"); + let line_mode = to_insert.ends_with('\n'); + let is_multiline = to_insert.contains('\n'); if line_mode && !before { if selection.is_empty() { @@ -201,8 +228,11 @@ mod test { use crate::{ state::Mode, test::{NeovimBackedTestContext, VimTestContext}, + UseSystemClipboard, VimSettings, }; + use gpui::ClipboardItem; use indoc::indoc; + use settings::SettingsStore; #[gpui::test] async fn test_paste(cx: &mut gpui::TestAppContext) { @@ -291,6 +321,103 @@ mod test { .await; } + #[gpui::test] + async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| { + s.use_system_clipboard = Some(UseSystemClipboard::Never) + }); + }); + + cx.set_state( + indoc! {" + The quick brown + fox jΛ‡umps over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystrokes(["v", "i", "w", "y"]); + cx.assert_state( + indoc! {" + The quick brown + fox Λ‡jumps over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystroke("p"); + cx.assert_state( + indoc! {" + The quick brown + fox jjumpΛ‡sumps over + the lazy dog"}, + Mode::Normal, + ); + assert_eq!(cx.read_from_clipboard(), None); + } + + #[gpui::test] + async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| { + s.use_system_clipboard = Some(UseSystemClipboard::OnYank) + }); + }); + + // copy in visual mode + cx.set_state( + indoc! {" + The quick brown + fox jΛ‡umps over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystrokes(["v", "i", "w", "y"]); + cx.assert_state( + indoc! {" + The quick brown + fox Λ‡jumps over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystroke("p"); + cx.assert_state( + indoc! {" + The quick brown + fox jjumpΛ‡sumps over + the lazy dog"}, + Mode::Normal, + ); + assert_eq!( + cx.read_from_clipboard().map(|item| item.text().clone()), + Some("jumps".into()) + ); + cx.simulate_keystrokes(["d", "d", "p"]); + cx.assert_state( + indoc! {" + The quick brown + the lazy dog + Λ‡fox jjumpsumps over"}, + Mode::Normal, + ); + assert_eq!( + cx.read_from_clipboard().map(|item| item.text().clone()), + Some("jumps".into()) + ); + cx.write_to_clipboard(ClipboardItem::new("test-copy".to_string())); + cx.simulate_keystroke("shift-p"); + cx.assert_state( + indoc! {" + The quick brown + the lazy dog + test-copΛ‡yfox jjumpsumps over"}, + Mode::Normal, + ); + } + #[gpui::test] async fn test_paste_visual(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; @@ -353,7 +480,7 @@ mod test { the_ Λ‡fox jumps over _dog"} - .replace("_", " "), // Hack for trailing whitespace + .replace('_', " "), // Hack for trailing whitespace ) .await; cx.assert_shared_clipboard("lazy").await; diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 4850151d94..a91327e497 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -294,7 +294,7 @@ mod test { lsp::CompletionItem { label: "first".to_string(), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new(position.clone(), position.clone()), + range: lsp::Range::new(position, position), new_text: "first".to_string(), })), ..Default::default() @@ -302,7 +302,7 @@ mod test { lsp::CompletionItem { label: "second".to_string(), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new(position.clone(), position.clone()), + range: lsp::Range::new(position, position), new_text: "second".to_string(), })), ..Default::default() diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 0bccf24977..2b8b192225 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -52,7 +52,7 @@ fn scroll( ) { Vim::update(cx, |vim, cx| { let amount = by(vim.take_count(cx).map(|c| c as f32)); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { scroll_editor(editor, move_cursor, &amount, cx) }); }) diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 63fa58dbed..7c138b2faf 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -29,7 +29,7 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext } pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut WindowContext) { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { let text_layout_details = editor.text_layout_details(cx); @@ -72,7 +72,7 @@ pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut } }) }); - copy_selections_content(editor, line_mode, cx); + copy_selections_content(vim, editor, line_mode, cx); let selections = editor.selections.all::(cx).into_iter(); let edits = selections.map(|selection| (selection.start..selection.end, "")); editor.edit(edits, cx); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 7792fc4dd7..1220460d29 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -1,9 +1,9 @@ -use crate::{motion::Motion, object::Object, utils::copy_and_flash_selections_content, Vim}; +use crate::{motion::Motion, object::Object, utils::yank_selections_content, Vim}; use collections::HashMap; use gpui::WindowContext; pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); @@ -15,7 +15,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut motion.expand_selection(map, selection, times, true, &text_layout_details); }); }); - copy_and_flash_selections_content(editor, motion.linewise(), cx); + yank_selections_content(vim, editor, motion.linewise(), cx); editor.change_selections(None, cx, |s| { s.move_with(|_, selection| { let (head, goal) = original_positions.remove(&selection.id).unwrap(); @@ -27,7 +27,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut } pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let mut original_positions: HashMap<_, _> = Default::default(); @@ -38,7 +38,7 @@ pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowC original_positions.insert(selection.id, original_position); }); }); - copy_and_flash_selections_content(editor, false, cx); + yank_selections_content(vim, editor, false, cx); editor.change_selections(None, cx, |s| { s.move_with(|_, selection| { let (head, goal) = original_positions.remove(&selection.id).unwrap(); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index a3106e923e..56b9499594 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1,24 +1,24 @@ use std::ops::Range; +use crate::{ + motion::right, normal::normal_object, state::Mode, utils::coerce_punctuation, + visual::visual_object, Vim, +}; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, movement::{self, FindRange}, Bias, DisplayPoint, }; use gpui::{actions, impl_actions, ViewContext, WindowContext}; -use language::{char_kind, CharKind, Selection}; +use language::{char_kind, BufferSnapshot, CharKind, Point, Selection}; use serde::Deserialize; use workspace::Workspace; -use crate::{ - motion::right, normal::normal_object, state::Mode, utils::coerce_punctuation, - visual::visual_object, Vim, -}; - #[derive(Copy, Clone, Debug, PartialEq)] pub enum Object { Word { ignore_punctuation: bool }, Sentence, + Paragraph, Quotes, BackQuotes, DoubleQuotes, @@ -27,6 +27,8 @@ pub enum Object { SquareBrackets, CurlyBrackets, AngleBrackets, + Argument, + Tag, } #[derive(Clone, Deserialize, PartialEq)] @@ -42,6 +44,7 @@ actions!( vim, [ Sentence, + Paragraph, Quotes, BackQuotes, DoubleQuotes, @@ -49,7 +52,9 @@ actions!( Parentheses, SquareBrackets, CurlyBrackets, - AngleBrackets + AngleBrackets, + Argument, + Tag ] ); @@ -59,8 +64,11 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { object(Object::Word { ignore_punctuation }, cx) }, ); + workspace.register_action(|_: &mut Workspace, _: &Tag, cx: _| object(Object::Tag, cx)); workspace .register_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx)); + workspace + .register_action(|_: &mut Workspace, _: &Paragraph, cx: _| object(Object::Paragraph, cx)); workspace.register_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx)); workspace .register_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx)); @@ -82,6 +90,8 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|_: &mut Workspace, _: &VerticalBars, cx: _| { object(Object::VerticalBars, cx) }); + workspace + .register_action(|_: &mut Workspace, _: &Argument, cx: _| object(Object::Argument, cx)); } fn object(object: Object, cx: &mut WindowContext) { @@ -103,22 +113,26 @@ impl Object { | Object::VerticalBars | Object::DoubleQuotes => false, Object::Sentence + | Object::Paragraph | Object::Parentheses + | Object::Tag | Object::AngleBrackets | Object::CurlyBrackets - | Object::SquareBrackets => true, + | Object::SquareBrackets + | Object::Argument => true, } } pub fn always_expands_both_ways(self) -> bool { match self { - Object::Word { .. } | Object::Sentence => false, + Object::Word { .. } | Object::Sentence | Object::Paragraph | Object::Argument => false, Object::Quotes | Object::BackQuotes | Object::DoubleQuotes | Object::VerticalBars | Object::Parentheses | Object::SquareBrackets + | Object::Tag | Object::CurlyBrackets | Object::AngleBrackets => true, } @@ -126,17 +140,25 @@ impl Object { pub fn target_visual_mode(self, current_mode: Mode) -> Mode { match self { - Object::Word { .. } if current_mode == Mode::VisualLine => Mode::Visual, - Object::Word { .. } => current_mode, - Object::Sentence + Object::Word { .. } + | Object::Sentence | Object::Quotes | Object::BackQuotes - | Object::DoubleQuotes - | Object::VerticalBars - | Object::Parentheses + | Object::DoubleQuotes => { + if current_mode == Mode::VisualBlock { + Mode::VisualBlock + } else { + Mode::Visual + } + } + Object::Parentheses | Object::SquareBrackets | Object::CurlyBrackets - | Object::AngleBrackets => Mode::Visual, + | Object::AngleBrackets + | Object::VerticalBars + | Object::Tag + | Object::Argument => Mode::Visual, + Object::Paragraph => Mode::VisualLine, } } @@ -155,6 +177,7 @@ impl Object { } } Object::Sentence => sentence(map, relative_to, around), + Object::Paragraph => paragraph(map, relative_to, around), Object::Quotes => { surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'') } @@ -170,6 +193,7 @@ impl Object { Object::Parentheses => { surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')') } + Object::Tag => surrounding_html_tag(map, relative_to, around), Object::SquareBrackets => { surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']') } @@ -179,6 +203,7 @@ impl Object { Object::AngleBrackets => { surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>') } + Object::Argument => argument(map, relative_to, around), } } @@ -229,6 +254,72 @@ fn in_word( Some(start..end) } +fn surrounding_html_tag( + map: &DisplaySnapshot, + relative_to: DisplayPoint, + surround: bool, +) -> Option> { + fn read_tag(chars: impl Iterator) -> String { + chars + .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.') + .collect() + } + fn open_tag(mut chars: impl Iterator) -> Option { + if Some('<') != chars.next() { + return None; + } + Some(read_tag(chars)) + } + fn close_tag(mut chars: impl Iterator) -> Option { + if (Some('<'), Some('/')) != (chars.next(), chars.next()) { + return None; + } + Some(read_tag(chars)) + } + + let snapshot = &map.buffer_snapshot; + let offset = relative_to.to_offset(map, Bias::Left); + let excerpt = snapshot.excerpt_containing(offset..offset)?; + let buffer = excerpt.buffer(); + let offset = excerpt.map_offset_to_buffer(offset); + + // Find the most closest to current offset + let mut cursor = buffer.syntax_layer_at(offset)?.node().walk(); + let mut last_child_node = cursor.node(); + while cursor.goto_first_child_for_byte(offset).is_some() { + last_child_node = cursor.node(); + } + + let mut last_child_node = Some(last_child_node); + while let Some(cur_node) = last_child_node { + if cur_node.child_count() >= 2 { + let first_child = cur_node.child(0); + let last_child = cur_node.child(cur_node.child_count() - 1); + if let (Some(first_child), Some(last_child)) = (first_child, last_child) { + let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range())); + let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range())); + if open_tag.is_some() + && open_tag == close_tag + && (first_child.end_byte() + 1..last_child.start_byte()).contains(&offset) + { + let range = if surround { + first_child.byte_range().start..last_child.byte_range().end + } else { + first_child.byte_range().end..last_child.byte_range().start + }; + if excerpt.contains_buffer_range(range.clone()) { + let result = excerpt.map_range_from_buffer(range); + return Some( + result.start.to_display_point(map)..result.end.to_display_point(map), + ); + } + } + } + } + last_child_node = cur_node.parent(); + } + None +} /// Returns a range that surrounds the word and following whitespace /// relative_to is in. /// @@ -308,6 +399,157 @@ fn around_next_word( Some(start..end) } +fn argument( + map: &DisplaySnapshot, + relative_to: DisplayPoint, + around: bool, +) -> Option> { + let snapshot = &map.buffer_snapshot; + let offset = relative_to.to_offset(map, Bias::Left); + + // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level + let excerpt = snapshot.excerpt_containing(offset..offset)?; + let buffer = excerpt.buffer(); + + fn comma_delimited_range_at( + buffer: &BufferSnapshot, + mut offset: usize, + include_comma: bool, + ) -> Option> { + // Seek to the first non-whitespace character + offset += buffer + .chars_at(offset) + .take_while(|c| c.is_whitespace()) + .map(char::len_utf8) + .sum::(); + + let bracket_filter = |open: Range, close: Range| { + // Filter out empty ranges + if open.end == close.start { + return false; + } + + // If the cursor is outside the brackets, ignore them + if open.start == offset || close.end == offset { + return false; + } + + // TODO: Is there any better way to filter out string brackets? + // Used to filter out string brackets + return matches!( + buffer.chars_at(open.start).next(), + Some('(' | '[' | '{' | '<' | '|') + ); + }; + + // Find the brackets containing the cursor + let (open_bracket, close_bracket) = + buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?; + + let inner_bracket_range = open_bracket.end..close_bracket.start; + + let layer = buffer.syntax_layer_at(offset)?; + let node = layer.node(); + let mut cursor = node.walk(); + + // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list + let mut parent_covers_bracket_range = false; + loop { + let node = cursor.node(); + let range = node.byte_range(); + let covers_bracket_range = + range.start == open_bracket.start && range.end == close_bracket.end; + if parent_covers_bracket_range && !covers_bracket_range { + break; + } + parent_covers_bracket_range = covers_bracket_range; + + // Unable to find a child node with a parent that covers the bracket range, so no argument to select + if cursor.goto_first_child_for_byte(offset).is_none() { + return None; + } + } + + let mut argument_node = cursor.node(); + + // If the child node is the open bracket, move to the next sibling. + if argument_node.byte_range() == open_bracket { + if !cursor.goto_next_sibling() { + return Some(inner_bracket_range); + } + argument_node = cursor.node(); + } + // While the child node is the close bracket or a comma, move to the previous sibling + while argument_node.byte_range() == close_bracket || argument_node.kind() == "," { + if !cursor.goto_previous_sibling() { + return Some(inner_bracket_range); + } + argument_node = cursor.node(); + if argument_node.byte_range() == open_bracket { + return Some(inner_bracket_range); + } + } + + // The start and end of the argument range, defaulting to the start and end of the argument node + let mut start = argument_node.start_byte(); + let mut end = argument_node.end_byte(); + + let mut needs_surrounding_comma = include_comma; + + // Seek backwards to find the start of the argument - either the previous comma or the opening bracket. + // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]` + while cursor.goto_previous_sibling() { + let prev = cursor.node(); + + if prev.start_byte() < open_bracket.end { + start = open_bracket.end; + break; + } else if prev.kind() == "," { + if needs_surrounding_comma { + start = prev.start_byte(); + needs_surrounding_comma = false; + } + break; + } else if prev.start_byte() < start { + start = prev.start_byte(); + } + } + + // Do the same for the end of the argument, extending to next comma or the end of the argument list + while cursor.goto_next_sibling() { + let next = cursor.node(); + + if next.end_byte() > close_bracket.start { + end = close_bracket.start; + break; + } else if next.kind() == "," { + if needs_surrounding_comma { + // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma + if let Some(next_arg) = next.next_sibling() { + end = next_arg.start_byte(); + } else { + end = next.end_byte(); + } + } + break; + } else if next.end_byte() > end { + end = next.end_byte(); + } + } + + Some(start..end) + } + + let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?; + + if excerpt.contains_buffer_range(result.clone()) { + let result = excerpt.map_range_from_buffer(result); + Some(result.start.to_display_point(map)..result.end.to_display_point(map)) + } else { + None + } +} + fn sentence( map: &DisplaySnapshot, relative_to: DisplayPoint, @@ -449,6 +691,99 @@ fn expand_to_include_whitespace( range } +/// If not `around` (i.e. inner), returns a range that surrounds the paragraph +/// where `relative_to` is in. If `around`, principally returns the range ending +/// at the end of the next paragraph. +/// +/// Here, the "paragraph" is defined as a block of non-blank lines or a block of +/// blank lines. If the paragraph ends with a trailing newline (i.e. not with +/// EOF), the returned range ends at the trailing newline of the paragraph (i.e. +/// the trailing newline is not subject to subsequent operations). +/// +/// Edge cases: +/// - If `around` and if the current paragraph is the last paragraph of the +/// file and is blank, then the selection results in an error. +/// - If `around` and if the current paragraph is the last paragraph of the +/// file and is not blank, then the returned range starts at the start of the +/// previous paragraph, if it exists. +fn paragraph( + map: &DisplaySnapshot, + relative_to: DisplayPoint, + around: bool, +) -> Option> { + let mut paragraph_start = start_of_paragraph(map, relative_to); + let mut paragraph_end = end_of_paragraph(map, relative_to); + + let paragraph_end_row = paragraph_end.row(); + let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row(); + let point = relative_to.to_point(map); + let current_line_is_empty = map.buffer_snapshot.is_line_blank(point.row); + + if around { + if paragraph_ends_with_eof { + if current_line_is_empty { + return None; + } + + let paragraph_start_row = paragraph_start.row(); + if paragraph_start_row != 0 { + let previous_paragraph_last_line_start = + Point::new(paragraph_start_row - 1, 0).to_display_point(map); + paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start); + } + } else { + let next_paragraph_start = Point::new(paragraph_end_row + 1, 0).to_display_point(map); + paragraph_end = end_of_paragraph(map, next_paragraph_start); + } + } + + let range = paragraph_start..paragraph_end; + Some(range) +} + +/// Returns a position of the start of the current paragraph, where a paragraph +/// is defined as a run of non-blank lines or a run of blank lines. +pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { + let point = display_point.to_point(map); + if point.row == 0 { + return DisplayPoint::zero(); + } + + let is_current_line_blank = map.buffer_snapshot.is_line_blank(point.row); + + for row in (0..point.row).rev() { + let blank = map.buffer_snapshot.is_line_blank(row); + if blank != is_current_line_blank { + return Point::new(row + 1, 0).to_display_point(map); + } + } + + DisplayPoint::zero() +} + +/// Returns a position of the end of the current paragraph, where a paragraph +/// is defined as a run of non-blank lines or a run of blank lines. +/// The trailing newline is excluded from the paragraph. +pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { + let point = display_point.to_point(map); + if point.row == map.max_buffer_row() { + return map.max_point(); + } + + let is_current_line_blank = map.buffer_snapshot.is_line_blank(point.row); + + for row in point.row + 1..map.max_buffer_row() + 1 { + let blank = map.buffer_snapshot.is_line_blank(row); + if blank != is_current_line_blank { + let previous_row = row - 1; + return Point::new(previous_row, map.buffer_snapshot.line_len(previous_row)) + .to_display_point(map); + } + } + + map.max_point() +} + fn surrounding_markers( map: &DisplaySnapshot, relative_to: DisplayPoint, @@ -600,7 +935,7 @@ mod test { test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext}, }; - const WORD_LOCATIONS: &'static str = indoc! {" + const WORD_LOCATIONS: &str = indoc! {" The quick Λ‡browΛ‡nΛ‡β€’β€’β€’ fox Λ‡juΛ‡mpsΛ‡ over the lazy dogΛ‡β€’β€’ @@ -812,6 +1147,168 @@ mod test { } } + const PARAGRAPH_EXAMPLES: &[&'static str] = &[ + // Single line + "Λ‡The quick brown fox jumpΛ‡s over the lazy dogΛ‡.Λ‡", + // Multiple lines without empty lines + indoc! {" + Λ‡The quick brownΛ‡ + Λ‡fox jumps overΛ‡ + the lazy dog.Λ‡ + "}, + // Heading blank paragraph and trailing normal paragraph + indoc! {" + Λ‡ + Λ‡ + Λ‡The quick brown fox jumps + Λ‡over the lazy dog. + Λ‡ + Λ‡ + Λ‡The quick brown fox jumpsΛ‡ + Λ‡over the lazy dog.Λ‡ + "}, + // Inserted blank paragraph and trailing blank paragraph + indoc! {" + Λ‡The quick brown fox jumps + Λ‡over the lazy dog. + Λ‡ + Λ‡ + Λ‡ + Λ‡The quick brown fox jumpsΛ‡ + Λ‡over the lazy dog.Λ‡ + Λ‡ + Λ‡ + Λ‡ + "}, + // "Blank" paragraph with whitespace characters + indoc! {" + Λ‡The quick brown fox jumps + over the lazy dog. + + Λ‡ \t + + Λ‡The quick brown fox jumps + over the lazy dog.Λ‡ + Λ‡ + Λ‡ \t + \t \t + "}, + // Single line "paragraphs", where selection size might be zero. + indoc! {" + Λ‡The quick brown fox jumps over the lazy dog. + Λ‡ + Λ‡The quick brown fox jumpΛ‡s over the lazy dog.Λ‡ + Λ‡ + "}, + ]; + + #[gpui::test] + async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + for paragraph_example in PARAGRAPH_EXAMPLES { + cx.assert_binding_matches_all(["c", "i", "p"], paragraph_example) + .await; + cx.assert_binding_matches_all(["c", "a", "p"], paragraph_example) + .await; + } + } + + #[gpui::test] + async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + for paragraph_example in PARAGRAPH_EXAMPLES { + cx.assert_binding_matches_all(["d", "i", "p"], paragraph_example) + .await; + cx.assert_binding_matches_all(["d", "a", "p"], paragraph_example) + .await; + } + } + + #[gpui::test] + async fn test_paragraph_object_with_landing_positions_not_at_beginning_of_line( + cx: &mut gpui::TestAppContext, + ) { + // Landing position not at the beginning of the line + const PARAGRAPH_LANDING_POSITION_EXAMPLE: &'static str = indoc! {" + The quick brown fox jumpsΛ‡ + over the lazy dog.Λ‡ + Λ‡ Λ‡\tΛ‡ + Λ‡ Λ‡ + Λ‡\tΛ‡ Λ‡\tΛ‡ + Λ‡The quick brown fox jumpsΛ‡ + Λ‡over the lazy dog.Λ‡ + Λ‡ Λ‡\tΛ‡ + Λ‡ + Λ‡ Λ‡\tΛ‡ + Λ‡\tΛ‡ Λ‡\tΛ‡ + "}; + + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.assert_binding_matches_all_exempted( + ["c", "i", "p"], + PARAGRAPH_LANDING_POSITION_EXAMPLE, + ExemptionFeatures::IncorrectLandingPosition, + ) + .await; + cx.assert_binding_matches_all_exempted( + ["c", "a", "p"], + PARAGRAPH_LANDING_POSITION_EXAMPLE, + ExemptionFeatures::IncorrectLandingPosition, + ) + .await; + cx.assert_binding_matches_all_exempted( + ["d", "i", "p"], + PARAGRAPH_LANDING_POSITION_EXAMPLE, + ExemptionFeatures::IncorrectLandingPosition, + ) + .await; + cx.assert_binding_matches_all_exempted( + ["d", "a", "p"], + PARAGRAPH_LANDING_POSITION_EXAMPLE, + ExemptionFeatures::IncorrectLandingPosition, + ) + .await; + } + + #[gpui::test] + async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + const EXAMPLES: &[&'static str] = &[ + indoc! {" + Λ‡The quick brown + fox jumps over + the lazy dog. + "}, + indoc! {" + Λ‡ + + Λ‡The quick brown fox jumps + over the lazy dog. + Λ‡ + + Λ‡The quick brown fox jumps + over the lazy dog. + "}, + indoc! {" + Λ‡The quick brown fox jumps over the lazy dog. + Λ‡ + Λ‡The quick brown fox jumps over the lazy dog. + + "}, + ]; + + for paragraph_example in EXAMPLES { + cx.assert_binding_matches_all(["v", "i", "p"], paragraph_example) + .await; + cx.assert_binding_matches_all(["v", "a", "p"], paragraph_example) + .await; + } + } + // Test string with "`" for opening surrounders and "'" for closing surrounders const SURROUNDING_MARKER_STRING: &str = indoc! {" Λ‡Th'Λ‡e Λ‡`Λ‡'Λ‡quΛ‡i`Λ‡ck broΛ‡'wn` @@ -1007,6 +1504,63 @@ mod test { ); } + #[gpui::test] + async fn test_argument_object(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Generic arguments + cx.set_state("fn boop() {}", Mode::Normal); + cx.simulate_keystrokes(["v", "i", "a"]); + cx.assert_state("fn boop<Β«A: DebugΛ‡Β», B>() {}", Mode::Visual); + + // Function arguments + cx.set_state( + "fn boop(Λ‡arg_a: (Tuple, Of, Types), arg_b: String) {}", + Mode::Normal, + ); + cx.simulate_keystrokes(["d", "a", "a"]); + cx.assert_state("fn boop(Λ‡arg_b: String) {}", Mode::Normal); + + cx.set_state("std::namespace::test(\"strinΛ‡g\", a.b.c())", Mode::Normal); + cx.simulate_keystrokes(["v", "a", "a"]); + cx.assert_state("std::namespace::test(Β«\"string\", Λ‡Β»a.b.c())", Mode::Visual); + + // Tuple, vec, and array arguments + cx.set_state( + "fn boop(arg_a: (Tuple, OfΛ‡, Types), arg_b: String) {}", + Mode::Normal, + ); + cx.simulate_keystrokes(["c", "i", "a"]); + cx.assert_state( + "fn boop(arg_a: (Tuple, Λ‡, Types), arg_b: String) {}", + Mode::Insert, + ); + + cx.set_state("let a = (test::call(), 'p', my_macro!{Λ‡});", Mode::Normal); + cx.simulate_keystrokes(["c", "a", "a"]); + cx.assert_state("let a = (test::call(), 'p'Λ‡);", Mode::Insert); + + cx.set_state("let a = [test::call(Λ‡), 300];", Mode::Normal); + cx.simulate_keystrokes(["c", "i", "a"]); + cx.assert_state("let a = [Λ‡, 300];", Mode::Insert); + + cx.set_state( + "let a = vec![Vec::new(), vecΛ‡![test::call(), 300]];", + Mode::Normal, + ); + cx.simulate_keystrokes(["c", "a", "a"]); + cx.assert_state("let a = vec![Vec::new()Λ‡];", Mode::Insert); + + // Cursor immediately before / after brackets + cx.set_state("let a = [test::call(first_arg)Λ‡]", Mode::Normal); + cx.simulate_keystrokes(["v", "i", "a"]); + cx.assert_state("let a = [Β«test::call(first_arg)Λ‡Β»]", Mode::Visual); + + cx.set_state("let a = [test::callΛ‡(first_arg)]", Mode::Normal); + cx.simulate_keystrokes(["v", "i", "a"]); + cx.assert_state("let a = [Β«test::call(first_arg)Λ‡Β»]", Mode::Visual); + } + #[gpui::test] async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; @@ -1026,4 +1580,26 @@ mod test { .await; } } + + #[gpui::test] + async fn test_tags(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new_html(cx).await; + + cx.set_state("hΛ‡i!", Mode::Normal); + cx.simulate_keystrokes(["v", "i", "t"]); + cx.assert_state( + "Β«hi!Λ‡Β»", + Mode::Visual, + ); + cx.simulate_keystrokes(["a", "t"]); + cx.assert_state( + "Β«hi!Λ‡Β»", + Mode::Visual, + ); + cx.simulate_keystrokes(["a", "t"]); + cx.assert_state( + "Β«hi!Λ‡Β»", + Mode::Visual, + ); + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 6dcb9c3ac3..994a8f2b76 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -1,5 +1,6 @@ -use std::{ops::Range, sync::Arc}; +use std::{fmt::Display, ops::Range, sync::Arc}; +use collections::HashMap; use gpui::{Action, KeyContext}; use language::CursorShape; use serde::{Deserialize, Serialize}; @@ -16,6 +17,18 @@ pub enum Mode { VisualBlock, } +impl Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Mode::Normal => write!(f, "NORMAL"), + Mode::Insert => write!(f, "INSERT"), + Mode::Visual => write!(f, "VISUAL"), + Mode::VisualLine => write!(f, "VISUAL LINE"), + Mode::VisualBlock => write!(f, "VISUAL BLOCK"), + } + } +} + impl Mode { pub fn is_visual(&self) -> bool { match self { @@ -86,6 +99,8 @@ pub struct WorkspaceState { pub recorded_count: Option, pub recorded_actions: Vec, pub recorded_selection: RecordedSelection, + + pub registers: HashMap, } #[derive(Debug)] diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 8051a4761a..8628be7298 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -152,6 +152,24 @@ async fn test_end_of_document_710(cx: &mut gpui::TestAppContext) { cx.assert_editor_state("aΛ‡a\nbb\ncc"); } +#[gpui::test] +async fn test_end_of_line_with_times(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // goes to current line end + cx.set_state(indoc! {"Λ‡aa\nbb\ncc"}, Mode::Normal); + cx.simulate_keystrokes(["$"]); + cx.assert_editor_state("aΛ‡a\nbb\ncc"); + + // goes to next line end + cx.simulate_keystrokes(["2", "$"]); + cx.assert_editor_state("aa\nbΛ‡b\ncc"); + + // try to exceed the final line. + cx.simulate_keystrokes(["4", "$"]); + cx.assert_editor_state("aa\nbb\ncΛ‡c"); +} + #[gpui::test] async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; @@ -163,9 +181,9 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { cx.simulate_keystrokes(["<", "<"]); cx.assert_editor_state("aa\nbΛ‡b\ncc"); - // works in visuial mode + // works in visual mode cx.simulate_keystrokes(["shift-v", "down", ">"]); - cx.assert_editor_state("aa\n bΒ«b\n ccΛ‡Β»"); + cx.assert_editor_state("aa\n bb\n cΛ‡c"); } #[gpui::test] @@ -884,3 +902,82 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { rename_request.next().await.unwrap(); cx.assert_state("const afterΛ‡ = 2; console.log(after)", Mode::Normal) } + +#[gpui::test] +async fn test_remap(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // test moving the cursor + cx.update(|cx| { + cx.bind_keys([KeyBinding::new( + "g z", + workspace::SendKeystrokes("l l l l".to_string()), + None, + )]) + }); + cx.set_state("Λ‡123456789", Mode::Normal); + cx.simulate_keystrokes(["g", "z"]); + cx.assert_state("1234Λ‡56789", Mode::Normal); + + // test switching modes + cx.update(|cx| { + cx.bind_keys([KeyBinding::new( + "g y", + workspace::SendKeystrokes("i f o o escape l".to_string()), + None, + )]) + }); + cx.set_state("Λ‡123456789", Mode::Normal); + cx.simulate_keystrokes(["g", "y"]); + cx.assert_state("fooΛ‡123456789", Mode::Normal); + + // test recursion + cx.update(|cx| { + cx.bind_keys([KeyBinding::new( + "g x", + workspace::SendKeystrokes("g z g y".to_string()), + None, + )]) + }); + cx.set_state("Λ‡123456789", Mode::Normal); + cx.simulate_keystrokes(["g", "x"]); + cx.assert_state("1234fooΛ‡56789", Mode::Normal); + + cx.executor().allow_parking(); + + // test command + cx.update(|cx| { + cx.bind_keys([KeyBinding::new( + "g w", + workspace::SendKeystrokes(": j enter".to_string()), + None, + )]) + }); + cx.set_state("Λ‡1234\n56789", Mode::Normal); + cx.simulate_keystrokes(["g", "w"]); + cx.assert_state("1234Λ‡ 56789", Mode::Normal); + + // test leaving command + cx.update(|cx| { + cx.bind_keys([KeyBinding::new( + "g u", + workspace::SendKeystrokes("g w g z".to_string()), + None, + )]) + }); + cx.set_state("Λ‡1234\n56789", Mode::Normal); + cx.simulate_keystrokes(["g", "u"]); + cx.assert_state("1234 567Λ‡89", Mode::Normal); + + // test leaving command + cx.update(|cx| { + cx.bind_keys([KeyBinding::new( + "g t", + workspace::SendKeystrokes("i space escape".to_string()), + None, + )]) + }); + cx.set_state("12Λ‡34", Mode::Normal); + cx.simulate_keystrokes(["g", "t"]); + cx.assert_state("12Λ‡ 34", Mode::Normal); +} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 0c12d64f58..f94770307b 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -72,7 +72,7 @@ impl NeovimBackedTestContext { let test_name = thread .name() .expect("thread is not named") - .split(":") + .split(':') .last() .unwrap() .to_string(); @@ -122,7 +122,7 @@ impl NeovimBackedTestContext { } pub async fn set_shared_state(&mut self, marked_text: &str) { - let mode = if marked_text.contains("Β»") { + let mode = if marked_text.contains('Β»') { Mode::Visual } else { Mode::Normal @@ -188,10 +188,12 @@ impl NeovimBackedTestContext { pub async fn assert_shared_state(&mut self, marked_text: &str) { self.is_dirty = false; - let marked_text = marked_text.replace("β€’", " "); + let marked_text = marked_text.replace('β€’', " "); let neovim = self.neovim_state().await; + let neovim_mode = self.neovim_mode().await; let editor = self.editor_state(); - if neovim == marked_text && neovim == editor { + let editor_mode = self.mode(); + if neovim == marked_text && neovim == editor && neovim_mode == editor_mode { return; } let initial_state = self @@ -213,16 +215,18 @@ impl NeovimBackedTestContext { {} # currently expected: {} - # neovim state: + # neovim ({}): {} - # zed state: + # zed ({}): {}"}, message, initial_state, self.recent_keystrokes.join(" "), marked_text.replace(" \n", "β€’\n"), + neovim_mode, neovim.replace(" \n", "β€’\n"), - editor.replace(" \n", "β€’\n") + editor_mode, + editor.replace(" \n", "β€’\n"), ) } @@ -296,27 +300,31 @@ impl NeovimBackedTestContext { pub async fn assert_state_matches(&mut self) { self.is_dirty = false; let neovim = self.neovim_state().await; + let neovim_mode = self.neovim_mode().await; let editor = self.editor_state(); + let editor_mode = self.mode(); let initial_state = self .last_set_state .as_ref() .unwrap_or(&"N/A".to_string()) .clone(); - if neovim != editor { + if neovim != editor || neovim_mode != editor_mode { panic!( indoc! {"Test failed (zed does not match nvim behaviour) # initial state: {} # keystrokes: {} - # neovim state: + # neovim ({}): {} - # zed state: + # zed ({}): {}"}, initial_state, self.recent_keystrokes.join(" "), + neovim_mode, neovim, + editor_mode, editor, ) } diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 4de0943321..3d47789fac 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -272,10 +272,7 @@ impl NeovimConnection { #[cfg(feature = "neovim")] pub async fn exec(&mut self, value: &str) { - self.nvim - .command_output(format!("{}", value).as_str()) - .await - .unwrap(); + self.nvim.command_output(value).await.unwrap(); self.data.push_back(NeovimData::Exec { command: value.to_string(), @@ -392,7 +389,7 @@ impl NeovimConnection { // the content of the selection via the "a register to get the shape correctly. self.nvim.input("\"aygv").await.unwrap(); let content = self.nvim.command_output("echo getreg('a')").await.unwrap(); - let lines = content.split("\n").collect::>(); + let lines = content.split('\n').collect::>(); let top = cmp::min(selection_row, cursor_row); let left = cmp::min(selection_col, cursor_col); for row in top..=cmp::max(selection_row, cursor_row) { @@ -419,7 +416,7 @@ impl NeovimConnection { } } Some(Mode::Visual) | Some(Mode::VisualLine) | Some(Mode::VisualBlock) => { - if selection_col > cursor_col { + if (selection_row, selection_col) > (cursor_row, cursor_col) { let selection_line_length = self.read_position("echo strlen(getline(line('v')))").await; if selection_line_length > selection_col { diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 7fdbe292b5..f8cc658394 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -31,6 +31,11 @@ impl VimTestContext { Self::new_with_lsp(lsp, enabled) } + pub async fn new_html(cx: &mut gpui::TestAppContext) -> VimTestContext { + Self::init(cx); + Self::new_with_lsp(EditorLspTestContext::new_html(cx).await, true) + } + pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> VimTestContext { Self::init(cx); Self::new_with_lsp( @@ -54,7 +59,7 @@ impl VimTestContext { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, |s| *s = Some(enabled)); }); - settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap(); + settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap(); if enabled { settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap(); } @@ -62,7 +67,6 @@ impl VimTestContext { // Setup search toolbars and keypress hook cx.update_workspace(|workspace, cx| { - observe_keystrokes(cx); workspace.active_pane().update(cx, |pane, cx| { pane.toolbar().update(cx, |toolbar, cx| { let buffer_search_bar = cx.new_view(BufferSearchBar::new); @@ -86,7 +90,7 @@ impl VimTestContext { T: 'static, F: FnOnce(&mut T, &mut ViewContext) -> R + 'static, { - let window = self.window.clone(); + let window = self.window; self.update_window(window, move |_, cx| view.update(cx, update)) .unwrap() } diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index 8b913fabbd..d0c099f64d 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -3,25 +3,35 @@ use std::time::Duration; use editor::{ClipboardSelection, Editor}; use gpui::{ClipboardItem, ViewContext}; use language::{CharKind, Point}; +use settings::Settings; + +use crate::{state::Mode, UseSystemClipboard, Vim, VimSettings}; pub struct HighlightOnYank; -pub fn copy_and_flash_selections_content( +pub fn yank_selections_content( + vim: &mut Vim, editor: &mut Editor, linewise: bool, cx: &mut ViewContext, ) { - copy_selections_content_internal(editor, linewise, true, cx); + copy_selections_content_internal(vim, editor, linewise, true, cx); } -pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut ViewContext) { - copy_selections_content_internal(editor, linewise, false, cx); +pub fn copy_selections_content( + vim: &mut Vim, + editor: &mut Editor, + linewise: bool, + cx: &mut ViewContext, +) { + copy_selections_content_internal(vim, editor, linewise, false, cx); } fn copy_selections_content_internal( + vim: &mut Vim, editor: &mut Editor, linewise: bool, - highlight: bool, + is_yank: bool, cx: &mut ViewContext, ) { let selections = editor.selections.all_adjusted(cx); @@ -73,8 +83,22 @@ fn copy_selections_content_internal( } } - cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); - if !highlight { + let setting = VimSettings::get_global(cx).use_system_clipboard; + if setting == UseSystemClipboard::Always || setting == UseSystemClipboard::OnYank && is_yank { + cx.write_to_clipboard(ClipboardItem::new(text.clone()).with_metadata(clipboard_selections)); + vim.workspace_state + .registers + .insert(".system.".to_string(), text.clone()); + } else { + vim.workspace_state.registers.insert( + ".system.".to_string(), + cx.read_from_clipboard() + .map(|item| item.text().clone()) + .unwrap_or_default(), + ); + } + vim.workspace_state.registers.insert("\"".to_string(), text); + if !is_yank || vim.state().mode == Mode::Visual { return; } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e8cca7b03c..39fec5d0c8 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -16,18 +16,22 @@ mod visual; use anyhow::Result; use collections::HashMap; -use command_palette::CommandPaletteInterceptor; -use copilot::CommandPaletteFilter; -use editor::{movement, Editor, EditorEvent, EditorMode}; +use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; +use editor::{ + movement::{self, FindRange}, + Editor, EditorEvent, EditorMode, +}; use gpui::{ - actions, impl_actions, Action, AppContext, EntityId, Global, Subscription, View, ViewContext, - WeakView, WindowContext, + actions, impl_actions, Action, AppContext, EntityId, Global, KeystrokeEvent, Subscription, + View, ViewContext, WeakView, WindowContext, }; use language::{CursorShape, Point, Selection, SelectionGoal}; pub use mode_indicator::ModeIndicator; use motion::Motion; use normal::normal_replace; +use schemars::JsonSchema; use serde::Deserialize; +use serde_derive::Serialize; use settings::{update_settings_file, Settings, SettingsStore}; use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState}; use std::{ops::Range, sync::Arc}; @@ -70,7 +74,9 @@ impl_actions!(vim, [SwitchMode, PushOperator, Number]); pub fn init(cx: &mut AppContext) { cx.set_global(Vim::default()); VimModeSetting::register(cx); + VimSettings::register(cx); + cx.observe_keystrokes(observe_keystrokes).detach(); editor_events::init(cx); cx.observe_new_views(|workspace: &mut Workspace, cx| register(workspace, cx)) @@ -130,46 +136,42 @@ fn register(workspace: &mut Workspace, cx: &mut ViewContext) { visual::register(workspace, cx); } -/// Registers a keystroke observer to observe keystrokes for the Vim integration. -pub fn observe_keystrokes(cx: &mut WindowContext) { - cx.observe_keystrokes(|keystroke_event, cx| { - if let Some(action) = keystroke_event - .action - .as_ref() - .map(|action| action.boxed_clone()) - { - Vim::update(cx, |vim, _| { - if vim.workspace_state.recording { - vim.workspace_state - .recorded_actions - .push(ReplayableAction::Action(action.boxed_clone())); +/// Called whenever an keystroke is typed so vim can observe all actions +/// and keystrokes accordingly. +fn observe_keystrokes(keystroke_event: &KeystrokeEvent, cx: &mut WindowContext) { + if let Some(action) = keystroke_event + .action + .as_ref() + .map(|action| action.boxed_clone()) + { + Vim::update(cx, |vim, _| { + if vim.workspace_state.recording { + vim.workspace_state + .recorded_actions + .push(ReplayableAction::Action(action.boxed_clone())); - if vim.workspace_state.stop_recording_after_next_action { - vim.workspace_state.recording = false; - vim.workspace_state.stop_recording_after_next_action = false; - } + if vim.workspace_state.stop_recording_after_next_action { + vim.workspace_state.recording = false; + vim.workspace_state.stop_recording_after_next_action = false; } - }); - - // Keystroke is handled by the vim system, so continue forward - if action.name().starts_with("vim::") { - return; } - } else if cx.has_pending_keystrokes() { + }); + + // Keystroke is handled by the vim system, so continue forward + if action.name().starts_with("vim::") { return; } + } else if cx.has_pending_keystrokes() { + return; + } - Vim::update(cx, |vim, cx| match vim.active_operator() { - Some( - Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace, - ) => {} - Some(_) => { - vim.clear_operator(cx); - } - _ => {} - }); - }) - .detach() + Vim::update(cx, |vim, cx| match vim.active_operator() { + Some(Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace) => {} + Some(_) => { + vim.clear_operator(cx); + } + _ => {} + }); } /// The state pertaining to Vim mode. @@ -261,12 +263,12 @@ impl Vim { } fn update_active_editor( - &self, + &mut self, cx: &mut WindowContext, - update: impl FnOnce(&mut Editor, &mut ViewContext) -> S, + update: impl FnOnce(&mut Vim, &mut Editor, &mut ViewContext) -> S, ) -> Option { let editor = self.active_editor.clone()?.upgrade()?; - Some(editor.update(cx, update)) + Some(editor.update(cx, |editor, cx| update(self, editor, cx))) } /// When doing an action that modifies the buffer, we start recording so that `.` @@ -365,7 +367,7 @@ impl Vim { } // Adjust selections - self.update_active_editor(cx, |editor, cx| { + self.update_active_editor(cx, |_, editor, cx| { if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock { visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal))) @@ -480,6 +482,11 @@ impl Vim { let find = Motion::FindForward { before, char: text.chars().next().unwrap(), + mode: if VimSettings::get_global(cx).use_multiline_find { + FindRange::MultiLine + } else { + FindRange::SingleLine + }, }; Vim::update(cx, |vim, _| { vim.workspace_state.last_find = Some(find.clone()) @@ -490,6 +497,11 @@ impl Vim { let find = Motion::FindBackward { after, char: text.chars().next().unwrap(), + mode: if VimSettings::get_global(cx).use_multiline_find { + FindRange::MultiLine + } else { + FindRange::SingleLine + }, }; Vim::update(cx, |vim, _| { vim.workspace_state.last_find = Some(find.clone()) @@ -565,10 +577,9 @@ impl Vim { ret } - fn sync_vim_settings(&self, cx: &mut WindowContext) { - let state = self.state(); - - self.update_active_editor(cx, |editor, cx| { + fn sync_vim_settings(&mut self, cx: &mut WindowContext) { + self.update_active_editor(cx, |vim, editor, cx| { + let state = vim.state(); editor.set_cursor_shape(state.cursor_shape(), cx); editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx); editor.set_collapse_matches(true); @@ -612,6 +623,47 @@ impl Settings for VimModeSetting { } } +/// Controls when to use system clipboard. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum UseSystemClipboard { + /// Don't use system clipboard. + Never, + /// Use system clipboard. + Always, + /// Use system clipboard for yank operations. + OnYank, +} + +#[derive(Deserialize)] +struct VimSettings { + // all vim uses vim clipboard + // vim always uses system cliupbaord + // some magic where yy is system and dd is not. + pub use_system_clipboard: UseSystemClipboard, + pub use_multiline_find: bool, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +struct VimSettingsContent { + pub use_system_clipboard: Option, + pub use_multiline_find: Option, +} + +impl Settings for VimSettings { + const KEY: Option<&'static str> = Some("vim"); + + type FileContent = VimSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut AppContext, + ) -> Result { + Self::load_via_json_merge(default_value, user_values) + } +} + fn local_selections_changed( newest: Selection, is_multicursor: bool, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 09b8e2c1b8..c316340058 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -9,14 +9,14 @@ use editor::{ Bias, DisplayPoint, Editor, }; use gpui::{actions, ViewContext, WindowContext}; -use language::{Selection, SelectionGoal}; +use language::{Point, Selection, SelectionGoal}; use workspace::Workspace; use crate::{ motion::{start_of_line, Motion}, object::Object, state::{Mode, Operator}, - utils::copy_selections_content, + utils::{copy_selections_content, yank_selections_content}, Vim, }; @@ -60,7 +60,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); if vim.state().mode == Mode::VisualBlock && !matches!( @@ -87,6 +87,7 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex // If the file ends with a newline (which is common) we don't do this. // so that if you go to the end of such a file you can use "up" to go // to the previous line and have it work somewhat as expected. + #[allow(clippy::nonminimal_bool)] if !selection.reversed && !selection.is_empty() && !(selection.end.column() == 0 && selection.end == map.max_point()) @@ -222,7 +223,7 @@ pub fn visual_block_motion( start: start.to_point(map), end: end.to_point(map), reversed: is_reversed, - goal: goal.clone(), + goal, }; selections.push(selection); @@ -251,7 +252,7 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { vim.switch_mode(target_mode, true, cx); } - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { let mut head = selection.head(); @@ -278,6 +279,25 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { selection.end = range.end; } } + + // In the visual selection result of a paragraph object, the cursor is + // placed at the start of the last line. And in the visual mode, the + // selection end is located after the end character. So, adjustment of + // selection end is needed. + // + // We don't do this adjustment for a one-line blank paragraph since the + // trailing newline is included in its selection from the beginning. + if object == Object::Paragraph && range.start != range.end { + let row_of_selection_end_line = selection.end.to_point(map).row; + let new_selection_end = + if map.buffer_snapshot.line_len(row_of_selection_end_line) == 0 + { + Point::new(row_of_selection_end_line + 1, 0) + } else { + Point::new(row_of_selection_end_line, 1) + }; + selection.end = new_selection_end.to_display_point(map); + } } }); }); @@ -298,7 +318,7 @@ fn toggle_mode(mode: Mode, cx: &mut ViewContext) { pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { editor.change_selections(None, cx, |s| { s.move_with(|_, selection| { selection.reversed = !selection.reversed; @@ -311,7 +331,7 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let mut original_columns: HashMap<_, _> = Default::default(); let line_mode = editor.selections.line_mode; @@ -328,7 +348,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |vim, editor, cx| { let line_mode = editor.selections.line_mode; - copy_selections_content(editor, line_mode, cx); + yank_selections_content(vim, editor, line_mode, cx); editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { if line_mode { @@ -377,7 +397,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext) pub(crate) fn visual_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.stop_recording(); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { let (display_map, selections) = editor.selections.all_adjusted_display(cx); @@ -426,7 +446,7 @@ pub fn select_next( let count = vim.take_count(cx) .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { for _ in 0..count { match editor.select_next(&Default::default(), cx) { Err(a) => return Err(a), @@ -448,7 +468,7 @@ pub fn select_previous( let count = vim.take_count(cx) .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); - vim.update_active_editor(cx, |editor, cx| { + vim.update_active_editor(cx, |_, editor, cx| { for _ in 0..count { match editor.select_previous(&Default::default(), cx) { Err(a) => return Err(a), @@ -1005,7 +1025,6 @@ mod test { cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await; cx.simulate_shared_keystrokes(["a", "]"]).await; cx.assert_shared_state("hello (in Β«[parens]Λ‡Β» o)").await; - assert_eq!(cx.mode(), Mode::Visual); cx.simulate_shared_keystrokes(["i", "("]).await; cx.assert_shared_state("hello (Β«in [parens] oΛ‡Β»)").await; @@ -1016,7 +1035,6 @@ mod test { assert_eq!(cx.mode(), Mode::VisualBlock); cx.simulate_shared_keystrokes(["o", "a", "s"]).await; cx.assert_shared_state("Β«Λ‡hello in a wordΒ» again.").await; - assert_eq!(cx.mode(), Mode::Visual); } #[gpui::test] diff --git a/crates/vim/test_data/test_change_paragraph_object.json b/crates/vim/test_data/test_change_paragraph_object.json new file mode 100644 index 0000000000..7de16dac5b --- /dev/null +++ b/crates/vim/test_data/test_change_paragraph_object.json @@ -0,0 +1,430 @@ +{"Put":{"state":"Λ‡The quick brown fox jumps over the lazy dog."}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumpΛ‡s over the lazy dog."}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dogΛ‡."}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.Λ‡"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"Λ‡The quick brown fox jumps over the lazy dog."}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumpΛ‡s over the lazy dog."}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dogΛ‡."}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.Λ‡"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"Λ‡The quick brown\nfox jumps over\nthe lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡\n","mode":"Insert"}} +{"Put":{"state":"The quick brownΛ‡\nfox jumps over\nthe lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡\n","mode":"Insert"}} +{"Put":{"state":"The quick brown\nΛ‡fox jumps over\nthe lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡\n","mode":"Insert"}} +{"Put":{"state":"The quick brown\nfox jumps overΛ‡\nthe lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡\n","mode":"Insert"}} +{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.Λ‡\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡\n","mode":"Insert"}} +{"Put":{"state":"Λ‡The quick brown\nfox jumps over\nthe lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"The quick brownΛ‡\nfox jumps over\nthe lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown\nΛ‡fox jumps over\nthe lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown\nfox jumps overΛ‡\nthe lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.Λ‡\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Insert"}} +{"Put":{"state":"Λ‡\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} +{"Put":{"state":"\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} +{"Put":{"state":"\n\nΛ‡The quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nΛ‡\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nΛ‡over the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nΛ‡\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡The quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡\n","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsΛ‡\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡\n","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nΛ‡over the lazy dog.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡\n","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.Λ‡\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡\n","mode":"Insert"}} +{"Put":{"state":"Λ‡\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} +{"Put":{"state":"\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} +{"Put":{"state":"\n\nΛ‡The quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nΛ‡over the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\n","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\n","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡The quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsΛ‡\nover the lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nΛ‡over the lazy dog.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.Λ‡\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"Λ‡The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nΛ‡over the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nΛ‡\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nΛ‡\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nΛ‡The quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nΛ‡\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsΛ‡\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nΛ‡\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nΛ‡over the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nΛ‡\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.Λ‡\n\n\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nΛ‡\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\n\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nΛ‡\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡","mode":"Insert"}} +{"Put":{"state":"Λ‡The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nΛ‡over the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nΛ‡\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nΛ‡\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nΛ‡\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nΛ‡\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nΛ‡\n\n\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nΛ‡The quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsΛ‡\nover the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nΛ‡over the lazy dog.\n\n\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.Λ‡\n\n\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\n\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nΛ‡\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nΛ‡\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nΛ‡\n","mode":"Normal"}} +{"Put":{"state":"Λ‡The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nΛ‡ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nΛ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nΛ‡The quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nΛ‡\n\n \t\n\t \t\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.Λ‡\n\n \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nΛ‡\n\n \t\n\t \t\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\n \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nΛ‡ \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡","mode":"Insert"}} +{"Put":{"state":"Λ‡The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nΛ‡ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nΛ‡\n\n \t\n\t \t\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nΛ‡The quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.Λ‡\n\n \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\n \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nΛ‡\n \t\n\t \t\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nΛ‡ \t\n\t \t\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nΛ‡ \t\n\t \t\n","mode":"Normal"}} +{"Put":{"state":"Λ‡The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡\n\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nΛ‡\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nΛ‡\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nΛ‡The quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nΛ‡\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpΛ‡s over the lazy dog.\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nΛ‡\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.Λ‡\n\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nΛ‡\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nΛ‡\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nΛ‡","mode":"Insert"}} +{"Put":{"state":"Λ‡The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nΛ‡\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nΛ‡\n\n","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nΛ‡The quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpΛ‡s over the lazy dog.\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.Λ‡\n\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nΛ‡","mode":"Insert"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nΛ‡\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nΛ‡\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_paragraph_object.json b/crates/vim/test_data/test_delete_paragraph_object.json new file mode 100644 index 0000000000..2cf1402ae3 --- /dev/null +++ b/crates/vim/test_data/test_delete_paragraph_object.json @@ -0,0 +1,430 @@ +{"Put":{"state":"Λ‡The quick brown fox jumps over the lazy dog."}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumpΛ‡s over the lazy dog."}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dogΛ‡."}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.Λ‡"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"Λ‡The quick brown fox jumps over the lazy dog."}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumpΛ‡s over the lazy dog."}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dogΛ‡."}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.Λ‡"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"Λ‡The quick brown\nfox jumps over\nthe lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"The quick brownΛ‡\nfox jumps over\nthe lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"The quick brown\nΛ‡fox jumps over\nthe lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"The quick brown\nfox jumps overΛ‡\nthe lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.Λ‡\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"Λ‡The quick brown\nfox jumps over\nthe lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Λ‡","mode":"Normal"}} +{"Put":{"state":"The quick brownΛ‡\nfox jumps over\nthe lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown\nfox jumps overˇ\nthe lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇ","mode":"Normal"}} +{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} +{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} +{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} +{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} +{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} +{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} +{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} +{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Normal"}} +{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n \t\n\t \t\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n \t\n\t \t\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} +{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\nˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\nˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n","mode":"Normal"}} +{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇThe quick brown fox jumps over the lazy dog.\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.","mode":"Normal"}} +{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\n","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ","mode":"Normal"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_end_of_line_with_neovim.json b/crates/vim/test_data/test_end_of_line_with_neovim.json new file mode 100644 index 0000000000..58ac05134a --- /dev/null +++ b/crates/vim/test_data/test_end_of_line_with_neovim.json @@ -0,0 +1,9 @@ +{"Put":{"state":"ˇaa\nbb\ncc"}} +{"Key":"$"} +{"Get":{"state":"aˇa\nbb\ncc","mode":"Normal"}} +{"Key":"2"} +{"Key":"$"} +{"Get":{"state":"aa\nbˇb\ncc","mode":"Normal"}} +{"Key":"4"} +{"Key":"$"} +{"Get":{"state":"aa\nbb\ncˇc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_paragraph_object_with_landing_positions_not_at_beginning_of_line.json b/crates/vim/test_data/test_paragraph_object_with_landing_positions_not_at_beginning_of_line.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/vim/test_data/test_visual_paragraph_object.json b/crates/vim/test_data/test_visual_paragraph_object.json new file mode 100644 index 0000000000..604d6dc93f --- /dev/null +++ b/crates/vim/test_data/test_visual_paragraph_object.json @@ -0,0 +1,80 @@ +{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"«The quick brown\nfox jumps over\ntˇ»he lazy dog.\n","mode":"VisualLine"}} +{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"«The quick brown\nfox jumps over\nthe lazy dog.\nˇ»","mode":"VisualLine"}} +{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"«\n\nˇ»The quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} +{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\n«The quick brown fox jumps\noˇ»ver the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n«\n\nˇ»The quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n«The quick brown fox jumps\noˇ»ver the lazy dog.\n","mode":"VisualLine"}} +{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"«\n\nThe quick brown fox jumps\noˇ»ver the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} +{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\n«The quick brown fox jumps\nover the lazy dog.\n\n\nˇ»The quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n«\n\nThe quick brown fox jumps\noˇ»ver the lazy dog.\n","mode":"VisualLine"}} +{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n«The quick brown fox jumps\nover the lazy dog.\nˇ»","mode":"VisualLine"}} +{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"«Tˇ»he quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n«\nˇ»The quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\n«Tˇ»he quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} +{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"«The quick brown fox jumps over the lazy dog.\n\nˇ»The quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n«\nTˇ»he quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} +{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\n«The quick brown fox jumps over the lazy dog.\n\nˇ»","mode":"VisualLine"}} diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 6b8012e0fe..2200db8bbd 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -14,19 +14,16 @@ test-support = [] [dependencies] anyhow.workspace = true client.workspace = true +copilot_ui.workspace = true db.workspace = true -editor.workspace = true -fs.workspace = true fuzzy.workspace = true gpui.workspace = true install_cli.workspace = true -log.workspace = true picker.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true -theme.workspace = true theme_selector.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index c6acd95a96..aa65051c0b 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -99,7 +99,7 @@ impl BaseKeymapSelectorDelegate { impl PickerDelegate for BaseKeymapSelectorDelegate { type ListItem = ui::ListItem; - fn placeholder_text(&self) -> Arc { + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { "Select a base keymap...".into() } diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index aafbd4dbe7..01ffe2a166 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -2,6 +2,7 @@ mod base_keymap_picker; mod base_keymap_setting; use client::{telemetry::Telemetry, TelemetrySettings}; +use copilot_ui; use db::kvp::KEY_VALUE_STORE; use gpui::{ svg, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, @@ -28,7 +29,7 @@ pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _cx| { workspace.register_action(|workspace, _: &Welcome, cx| { let welcome_page = WelcomePage::new(workspace, cx); - workspace.add_item(Box::new(welcome_page), cx) + workspace.add_item_to_active_pane(Box::new(welcome_page), cx) }); }) .detach(); @@ -134,6 +135,16 @@ impl Render for WelcomePage { }) .detach_and_log_err(cx); })), + ) + .child( + Button::new("sign-in-to-copilot", "Sign in to GitHub Copilot") + .full_width() + .on_click(cx.listener(|this, _, cx| { + this.telemetry.report_app_event( + "welcome page: sign in to copilot".to_string(), + ); + copilot_ui::initiate_sign_in(cx); + })), ), ) .child( diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 4e70d93e2a..44c6a98859 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -13,6 +13,7 @@ doctest = false test-support = [ "call/test-support", "client/test-support", + "db/test-support", "project/test-support", "settings/test-support", "gpui/test-support", @@ -25,14 +26,14 @@ async-recursion = "1.0.0" bincode = "1.2.1" call.workspace = true client.workspace = true +clock.workspace = true collections.workspace = true db.workspace = true derive_more.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true -install_cli.workspace = true -itertools = "0.10" +itertools.workspace = true language.workspace = true lazy_static.workspace = true log.workspace = true @@ -40,15 +41,13 @@ node_runtime.workspace = true parking_lot.workspace = true postage.workspace = true project.workspace = true -runnable.workspace = true +task.workspace = true schemars.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true settings.workspace = true smallvec.workspace = true sqlez.workspace = true -terminal.workspace = true theme.workspace = true ui.workspace = true util.workspace = true @@ -61,6 +60,5 @@ db = { workspace = true, features = ["test-support"] } env_logger.workspace = true fs = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true project = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index afedb7645b..278ccded11 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -221,7 +221,7 @@ impl Dock { return; }; if panel.is_zoomed(cx) { - workspace.zoomed = Some(panel.to_any().downgrade().into()); + workspace.zoomed = Some(panel.to_any().downgrade()); workspace.zoomed_position = Some(position); } else { workspace.zoomed = None; @@ -522,7 +522,7 @@ impl Dock { pub fn resize_active_panel(&mut self, size: Option, cx: &mut ViewContext) { if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) { - let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE)); + let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round()); entry.panel.set_size(size, cx); cx.notify(); } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index d5d8aed39d..25d9f5ed89 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -146,7 +146,12 @@ pub trait Item: FocusableView + EventEmitter { fn can_save(&self, _cx: &AppContext) -> bool { false } - fn save(&mut self, _project: Model, _cx: &mut ViewContext) -> Task> { + fn save( + &mut self, + _format: bool, + _project: Model, + _cx: &mut ViewContext, + ) -> Task> { unimplemented!("save() must be implemented if can_save() returns true") } fn save_as( @@ -258,7 +263,12 @@ pub trait ItemHandle: 'static + Send { fn is_dirty(&self, cx: &AppContext) -> bool; fn has_conflict(&self, cx: &AppContext) -> bool; fn can_save(&self, cx: &AppContext) -> bool; - fn save(&self, project: Model, cx: &mut WindowContext) -> Task>; + fn save( + &self, + format: bool, + project: Model, + cx: &mut WindowContext, + ) -> Task>; fn save_as( &self, project: Model, @@ -451,7 +461,7 @@ impl ItemHandle for View { if item.focus_handle(cx).contains_focused(cx) && item.add_event_to_update_proto( event, - &mut *pending_update.borrow_mut(), + &mut pending_update.borrow_mut(), cx, ) && !pending_update_scheduled.load(Ordering::SeqCst) @@ -566,8 +576,13 @@ impl ItemHandle for View { self.read(cx).can_save(cx) } - fn save(&self, project: Model, cx: &mut WindowContext) -> Task> { - self.update(cx, |item, cx| item.save(project, cx)) + fn save( + &self, + format: bool, + project: Model, + cx: &mut WindowContext, + ) -> Task> { + self.update(cx, |item, cx| item.save(format, project, cx)) } fn save_as( @@ -1018,6 +1033,7 @@ pub mod test { fn save( &mut self, + _: bool, _: Model, _: &mut ViewContext, ) -> Task> { diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 33d5834ae7..521ea6a2bb 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1,10 +1,14 @@ use crate::{Toast, Workspace}; use collections::HashMap; use gpui::{ - AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Global, - PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext, + svg, AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, + Global, PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext, }; +use language::DiagnosticSeverity; + use std::{any::TypeId, ops::DerefMut}; +use ui::prelude::*; +use util::ResultExt; pub fn init(cx: &mut AppContext) { cx.set_global(NotificationTracker::new()); @@ -168,6 +172,106 @@ impl Workspace { } } +pub struct LanguageServerPrompt { + request: Option, +} + +impl LanguageServerPrompt { + pub fn new(request: project::LanguageServerPromptRequest) -> Self { + Self { + request: Some(request), + } + } + + async fn select_option(this: View, ix: usize, mut cx: AsyncWindowContext) { + util::async_maybe!({ + let potential_future = this.update(&mut cx, |this, _| { + this.request.take().map(|request| request.respond(ix)) + }); + + potential_future? // App Closed + .ok_or_else(|| anyhow::anyhow!("Response already sent"))? + .await + .ok_or_else(|| anyhow::anyhow!("Stream already closed"))?; + + this.update(&mut cx, |_, cx| cx.emit(DismissEvent))?; + + anyhow::Ok(()) + }) + .await + .log_err(); + } +} + +impl Render for LanguageServerPrompt { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let Some(request) = &self.request else { + return div().id("language_server_prompt_notification"); + }; + + h_flex() + .id("language_server_prompt_notification") + .elevation_3(cx) + .items_start() + .justify_between() + .p_2() + .gap_2() + .w_full() + .child( + v_flex() + .overflow_hidden() + .child( + h_flex() + .children( + match request.level { + PromptLevel::Info => None, + PromptLevel::Warning => Some(DiagnosticSeverity::WARNING), + PromptLevel::Critical => Some(DiagnosticSeverity::ERROR), + } + .map(|severity| { + svg() + .size(cx.text_style().font_size) + .flex_none() + .mr_1() + .map(|icon| { + if severity == DiagnosticSeverity::ERROR { + icon.path(IconName::ExclamationTriangle.path()) + .text_color(Color::Error.color(cx)) + } else { + icon.path(IconName::ExclamationTriangle.path()) + .text_color(Color::Warning.color(cx)) + } + }) + }), + ) + .child( + Label::new(format!("{}:", request.lsp_name)) + .size(LabelSize::Default), + ), + ) + .child(Label::new(request.message.to_string())) + .children(request.actions.iter().enumerate().map(|(ix, action)| { + let this_handle = cx.view().clone(); + ui::Button::new(ix, action.title.clone()) + .size(ButtonSize::Large) + .on_click(move |_, cx| { + let this_handle = this_handle.clone(); + cx.spawn(|cx| async move { + LanguageServerPrompt::select_option(this_handle, ix, cx).await + }) + .detach() + }) + })), + ) + .child( + ui::IconButton::new("close", ui::IconName::Close) + .on_click(cx.listener(|_, _, cx| cx.emit(gpui::DismissEvent))), + ) + } +} + +impl EventEmitter for LanguageServerPrompt {} + pub mod simple_message_notification { use gpui::{ div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 55389cf8d0..e99bb3f193 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -9,9 +9,9 @@ use collections::{HashMap, HashSet, VecDeque}; use futures::{stream::FuturesUnordered, StreamExt}; use gpui::{ actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext, - AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, ExternalPaths, - FocusHandle, FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, - PromptLevel, Render, ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, + AsyncWindowContext, ClickEvent, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, + ExternalPaths, FocusHandle, FocusableView, Model, MouseButton, NavigationDirection, Pixels, + Point, PromptLevel, Render, ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; @@ -44,6 +44,8 @@ pub enum SaveIntent { /// write all files (even if unchanged) /// prompt before overwriting on-disk changes Save, + /// same as Save, but without auto formatting + SaveWithoutFormat, /// write any files that have local changes /// prompt before overwriting on-disk changes SaveAll, @@ -66,6 +68,12 @@ pub struct CloseActiveItem { pub save_intent: Option, } +#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CloseInactiveItems { + pub save_intent: Option, +} + #[derive(Clone, PartialEq, Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct CloseAllItems { @@ -83,6 +91,7 @@ impl_actions!( [ CloseAllItems, CloseActiveItem, + CloseInactiveItems, ActivateItem, RevealInProjectPanel ] @@ -94,7 +103,6 @@ actions!( ActivatePrevItem, ActivateNextItem, ActivateLastItem, - CloseInactiveItems, CloseCleanItems, CloseItemsToTheLeft, CloseItemsToTheRight, @@ -767,7 +775,7 @@ impl Pane { pub fn close_inactive_items( &mut self, - _: &CloseInactiveItems, + action: &CloseInactiveItems, cx: &mut ViewContext, ) -> Option>> { if self.items.is_empty() { @@ -775,9 +783,11 @@ impl Pane { } let active_item_id = self.items[self.active_item_index].item_id(); - Some(self.close_items(cx, SaveIntent::Close, move |item_id| { - item_id != active_item_id - })) + Some(self.close_items( + cx, + action.save_intent.unwrap_or(SaveIntent::Close), + move |item_id| item_id != active_item_id, + )) } pub fn close_clean_items( @@ -891,7 +901,7 @@ impl Pane { if not_shown_files == 1 { file_names.push(".. 1 file not shown".into()); } else { - file_names.push(format!(".. {} files not shown", not_shown_files).into()); + file_names.push(format!(".. {} files not shown", not_shown_files)); } } ( @@ -1114,7 +1124,7 @@ impl Pane { })?; // when saving a single buffer, we ignore whether or not it's dirty. - if save_intent == SaveIntent::Save { + if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat { is_dirty = true; } @@ -1128,6 +1138,8 @@ impl Pane { has_conflict = false; } + let should_format = save_intent != SaveIntent::SaveWithoutFormat; + if has_conflict && can_save { let answer = pane.update(cx, |pane, cx| { pane.activate_item(item_ix, true, true, cx); @@ -1139,7 +1151,10 @@ impl Pane { ) })?; match answer.await { - Ok(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?, + Ok(0) => { + pane.update(cx, |_, cx| item.save(should_format, project, cx))? + .await? + } Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?, _ => return Ok(false), } @@ -1171,7 +1186,8 @@ impl Pane { } if can_save { - pane.update(cx, |_, cx| item.save(project, cx))?.await?; + pane.update(cx, |_, cx| item.save(should_format, project, cx))? + .await?; } else if can_save_as { let start_abs_path = project .update(cx, |project, cx| { @@ -1203,7 +1219,7 @@ impl Pane { cx: &mut WindowContext, ) -> Task> { if Self::can_autosave_item(item, cx) { - item.save(project, cx) + item.save(true, project, cx) } else { Task::ready(Ok(())) } @@ -1397,7 +1413,7 @@ impl Pane { ) .entry( "Close Others", - Some(Box::new(CloseInactiveItems)), + Some(Box::new(CloseInactiveItems { save_intent: None })), cx.handler_for(&pane, move |pane, cx| { pane.close_items(cx, SaveIntent::Close, |id| id != item_id) .detach_and_log_err(cx); @@ -1425,16 +1441,20 @@ impl Pane { "Close Clean", Some(Box::new(CloseCleanItems)), cx.handler_for(&pane, move |pane, cx| { - pane.close_clean_items(&CloseCleanItems, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = pane.close_clean_items(&CloseCleanItems, cx) { + task.detach_and_log_err(cx) + } }), ) .entry( "Close All", Some(Box::new(CloseAllItems { save_intent: None })), cx.handler_for(&pane, |pane, cx| { - pane.close_all_items(&CloseAllItems { save_intent: None }, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = + pane.close_all_items(&CloseAllItems { save_intent: None }, cx) + { + task.detach_and_log_err(cx) + } }), ); @@ -1505,6 +1525,7 @@ impl Pane { ) .child( div() + .id("tab_bar_drop_target") .min_w_6() // HACK: This empty child is currently necessary to force the drop target to appear // despite us setting a min width above. @@ -1528,6 +1549,11 @@ impl Pane { .on_drop(cx.listener(move |this, paths, cx| { this.drag_split_direction = None; this.handle_external_paths_drop(paths, cx) + })) + .on_click(cx.listener(move |_, event: &ClickEvent, cx| { + if event.up.click_count == 2 { + cx.dispatch_action(NewFile.boxed_clone()); + } })), ) } @@ -1769,42 +1795,49 @@ impl Render for Pane { })) .on_action( cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| { - pane.close_active_item(action, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = pane.close_active_item(action, cx) { + task.detach_and_log_err(cx) + } }), ) .on_action( cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| { - pane.close_inactive_items(action, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = pane.close_inactive_items(action, cx) { + task.detach_and_log_err(cx) + } }), ) .on_action( cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| { - pane.close_clean_items(action, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = pane.close_clean_items(action, cx) { + task.detach_and_log_err(cx) + } }), ) .on_action( cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| { - pane.close_items_to_the_left(action, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = pane.close_items_to_the_left(action, cx) { + task.detach_and_log_err(cx) + } }), ) .on_action( cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| { - pane.close_items_to_the_right(action, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = pane.close_items_to_the_right(action, cx) { + task.detach_and_log_err(cx) + } }), ) .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| { - pane.close_all_items(action, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = pane.close_all_items(action, cx) { + task.detach_and_log_err(cx) + } })) .on_action( cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| { - pane.close_active_item(action, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = pane.close_active_item(action, cx) { + task.detach_and_log_err(cx) + } }), ) .on_action( @@ -2426,7 +2459,7 @@ mod tests { set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); pane.update(cx, |pane, cx| { - pane.close_inactive_items(&CloseInactiveItems, cx) + pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx) }) .unwrap() .await @@ -2572,8 +2605,8 @@ mod tests { let mut index = 0; let items = labels.map(|mut label| { - if label.ends_with("*") { - label = label.trim_end_matches("*"); + if label.ends_with('*') { + label = label.trim_end_matches('*'); active_item_index = index; } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 8e129d5bca..0c3f9b9dc0 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -88,6 +88,7 @@ impl PaneGroup { }; } + #[allow(clippy::too_many_arguments)] pub(crate) fn render( &self, project: &Model, @@ -159,6 +160,7 @@ impl Member { } } + #[allow(clippy::too_many_arguments)] pub fn render( &self, project: &Model, @@ -266,7 +268,7 @@ impl Member { this.cursor_pointer().on_mouse_down( MouseButton::Left, cx.listener(move |this, _, cx| { - crate::join_remote_project( + crate::join_in_room_project( leader_project_id, leader_user_id, this.app_state().clone(), @@ -471,6 +473,7 @@ impl PaneAxis { None } + #[allow(clippy::too_many_arguments)] fn render( &self, project: &Model, @@ -588,9 +591,9 @@ mod element { use std::{cell::RefCell, iter, rc::Rc, sync::Arc}; use gpui::{ - px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, InteractiveBounds, - IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, - Size, Style, WeakView, WindowContext, + px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, IntoElement, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Size, Style, + WeakView, WindowContext, }; use parking_lot::Mutex; use settings::Settings; @@ -640,6 +643,7 @@ mod element { self } + #[allow(clippy::too_many_arguments)] fn compute_resize( flexes: &Arc>>, e: &MouseMoveEvent, @@ -728,6 +732,7 @@ mod element { cx.refresh(); } + #[allow(clippy::too_many_arguments)] fn push_handle( flexes: Arc>>, dragged_handle: Rc>>, @@ -754,15 +759,13 @@ mod element { }; cx.with_z_index(3, |cx| { - let interactive_handle_bounds = InteractiveBounds { - bounds: handle_bounds, - stacking_order: cx.stacking_order().clone(), - }; - if interactive_handle_bounds.visibly_contains(&cx.mouse_position(), cx) { - cx.set_cursor_style(match axis { + if handle_bounds.contains(&cx.mouse_position()) { + let stacking_order = cx.stacking_order().clone(); + let cursor_style = match axis { Axis::Vertical => CursorStyle::ResizeUpDown, Axis::Horizontal => CursorStyle::ResizeLeftRight, - }) + }; + cx.set_cursor_style(cursor_style, stacking_order); } cx.add_opaque_layer(handle_bounds); @@ -885,7 +888,8 @@ mod element { let child_size = bounds .size - .apply_along(self.axis, |_| space_per_flex * child_flex); + .apply_along(self.axis, |_| space_per_flex * child_flex) + .map(|d| d.round()); let child_bounds = Bounds { origin, diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 2c6bf95c60..9f99233c27 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -367,7 +367,7 @@ impl WorkspaceDb { conn.exec_bound(sql!( DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ? - ))?((&workspace.location, workspace.id.clone())) + ))?((&workspace.location, workspace.id)) .context("clearing out old locations")?; // Upsert @@ -430,7 +430,7 @@ impl WorkspaceDb { } query! { - async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> { + pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> { DELETE FROM workspaces WHERE workspace_id IS ? } @@ -447,7 +447,7 @@ impl WorkspaceDb { { result.push((id, location)); } else { - delete_tasks.push(self.delete_stale_workspace(id)); + delete_tasks.push(self.delete_workspace_by_id(id)); } } @@ -622,11 +622,11 @@ impl WorkspaceDb { } fn get_items(&self, pane_id: PaneId) -> Result> { - Ok(self.select_bound(sql!( + self.select_bound(sql!( SELECT kind, item_id, active FROM items WHERE pane_id = ? ORDER BY position - ))?(pane_id)?) + ))?(pane_id) } fn save_items( diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 73ae0f9b7e..6c917b495d 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -22,6 +22,16 @@ impl WorkspaceLocation { pub fn paths(&self) -> Arc> { self.0.clone() } + + #[cfg(any(test, feature = "test-support"))] + pub fn new>(paths: Vec

) -> Self { + Self(Arc::new( + paths + .into_iter() + .map(|p| p.as_ref().to_path_buf()) + .collect(), + )) + } } impl, T: IntoIterator> From for WorkspaceLocation { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ffaf37e6f9..b4cb71c45a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,7 +15,7 @@ use anyhow::{anyhow, Context as _, Result}; use call::{call_settings::CallSettings, ActiveCall}; use client::{ proto::{self, ErrorCode, PeerId}, - Client, ErrorExt, Status, TypedEnvelope, UserStore, + ChannelId, Client, ErrorExt, HostedProjectId, Status, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use derive_more::{Deref, DerefMut}; @@ -29,10 +29,10 @@ use gpui::{ actions, canvas, div, impl_actions, point, px, size, Action, AnyElement, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle, - FocusableView, Global, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, - ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, - Render, SharedString, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, - WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, + FocusableView, Global, GlobalPixels, InteractiveElement, IntoElement, KeyContext, Keystroke, + LayoutId, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, + PromptLevel, Render, SharedString, Size, Styled, Subscription, Task, View, ViewContext, + VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -50,7 +50,6 @@ pub use persistence::{ }; use postage::stream::Stream; use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; -use runnable::SpawnInTerminal; use serde::Deserialize; use settings::Settings; use shared_screen::SharedScreen; @@ -59,12 +58,17 @@ pub use status_bar::StatusItemView; use std::{ any::TypeId, borrow::Cow, - cmp, env, + cell::RefCell, + cmp, + collections::hash_map::DefaultHasher, + env, + hash::{Hash, Hasher}, path::{Path, PathBuf}, - sync::Weak, - sync::{atomic::AtomicUsize, Arc}, + rc::Rc, + sync::{atomic::AtomicUsize, Arc, Weak}, time::Duration, }; +use task::SpawnInTerminal; use theme::{ActiveTheme, SystemAppearance, ThemeSettings}; pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; @@ -99,10 +103,10 @@ actions!( NewFile, NewWindow, CloseWindow, - CloseInactiveTabsAndPanes, AddFolderToProject, Unfollow, SaveAs, + SaveWithoutFormat, ReloadActiveItem, ActivatePreviousPane, ActivateNextPane, @@ -118,7 +122,6 @@ actions!( ToggleRightDock, ToggleBottomDock, CloseAllDocks, - ToggleGraphicsProfiler, ] ); @@ -157,17 +160,28 @@ pub struct CloseAllItemsAndPanes { pub save_intent: Option, } +#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CloseInactiveTabsAndPanes { + pub save_intent: Option, +} + +#[derive(Clone, Deserialize, PartialEq)] +pub struct SendKeystrokes(pub String); + impl_actions!( workspace, [ ActivatePane, ActivatePaneInDirection, CloseAllItemsAndPanes, + CloseInactiveTabsAndPanes, NewFileInDirection, OpenTerminal, Save, SaveAll, SwapPaneInDirection, + SendKeystrokes, ] ); @@ -358,7 +372,6 @@ impl Global for GlobalAppState {} pub struct WorkspaceStore { workspaces: HashSet>, - followers: Vec, client: Arc, _subscriptions: Vec, } @@ -393,8 +406,9 @@ impl AppState { let fs = fs::FakeFs::new(cx.background_executor().clone()); let languages = Arc::new(LanguageRegistry::test()); + let clock = Arc::new(clock::FakeSystemClock::default()); let http_client = util::http::FakeHttpClient::with_404_response(); - let client = Client::new(http_client.clone(), cx); + let client = Client::new(clock, http_client.clone(), cx); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); @@ -464,7 +478,7 @@ pub enum Event { PaneAdded(View), ContactRequestedJoin(u64), WorkspaceCreated(WeakView), - SpawnRunnable(SpawnInTerminal), + SpawnTask(SpawnInTerminal), } pub enum OpenVisible { @@ -500,6 +514,7 @@ pub struct Workspace { leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, app_state: Arc, + dispatching_keystrokes: Rc>>, _subscriptions: Vec, _apply_leader_updates: Task>, _observe_current_user: Task>, @@ -573,24 +588,13 @@ impl Workspace { }), project::Event::LanguageServerPrompt(request) => { - let request = request.clone(); + let mut hasher = DefaultHasher::new(); + request.message.as_str().hash(&mut hasher); + let id = hasher.finish(); - cx.spawn(|_, mut cx| async move { - let messages = request - .actions - .iter() - .map(|action| action.title.as_str()) - .collect::>(); - let index = cx - .update(|cx| { - cx.prompt(request.level, "", Some(&request.message), &messages) - })? - .await?; - request.respond(index).await; - - Result::<(), anyhow::Error>::Ok(()) - }) - .detach() + this.show_notification(id as usize, cx, |cx| { + cx.new_view(|_| notifications::LanguageServerPrompt::new(request.clone())) + }); } _ => {} @@ -676,8 +680,7 @@ impl Workspace { let mut active_call = None; if let Some(call) = ActiveCall::try_global(cx) { let call = call.clone(); - let mut subscriptions = Vec::new(); - subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); + let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)]; active_call = Some((call, subscriptions)); } @@ -755,6 +758,7 @@ impl Workspace { project: project.clone(), follower_states: Default::default(), last_leaders_by_pane: Default::default(), + dispatching_keystrokes: Default::default(), window_edited: false, active_call, database_id: workspace_id, @@ -867,7 +871,6 @@ impl Workspace { cx.open_window(options, { let app_state = app_state.clone(); - let workspace_id = workspace_id.clone(); let project_handle = project_handle.clone(); move |cx| { cx.new_view(|cx| { @@ -1240,11 +1243,10 @@ impl Workspace { } } - Ok(this - .update(&mut cx, |this, cx| { - this.save_all_internal(SaveIntent::Close, cx) - })? - .await?) + this.update(&mut cx, |this, cx| { + this.save_all_internal(SaveIntent::Close, cx) + })? + .await }) } @@ -1253,6 +1255,46 @@ impl Workspace { .detach_and_log_err(cx); } + fn send_keystrokes(&mut self, action: &SendKeystrokes, cx: &mut ViewContext) { + let mut keystrokes: Vec = action + .0 + .split(' ') + .flat_map(|k| Keystroke::parse(k).log_err()) + .collect(); + keystrokes.reverse(); + + self.dispatching_keystrokes + .borrow_mut() + .append(&mut keystrokes); + + let keystrokes = self.dispatching_keystrokes.clone(); + cx.window_context() + .spawn(|mut cx| async move { + // limit to 100 keystrokes to avoid infinite recursion. + for _ in 0..100 { + let Some(keystroke) = keystrokes.borrow_mut().pop() else { + return Ok(()); + }; + cx.update(|cx| { + let focused = cx.focused(); + cx.dispatch_keystroke(keystroke.clone()); + if cx.focused() != focused { + // dispatch_keystroke may cause the focus to change. + // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle + // And we need that to happen before the next keystroke to keep vim mode happy... + // (Note that the tests always do this implicitly, so you must manually test with something like: + // "bindings": { "g z": ["workspace::SendKeystrokes", ": j u"]} + // ) + cx.draw(); + } + })?; + } + keystrokes.borrow_mut().clear(); + Err(anyhow!("over 100 keystrokes passed to send_keystrokes")) + }) + .detach_and_log_err(cx); + } + fn save_all_internal( &mut self, mut save_intent: SaveIntent, @@ -1340,7 +1382,9 @@ impl Workspace { }; if let Some(task) = this - .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx)) + .update(&mut cx, |this, cx| { + this.open_workspace_for_paths(false, paths, cx) + }) .log_err() { task.await.log_err(); @@ -1351,6 +1395,7 @@ impl Workspace { pub fn open_workspace_for_paths( &mut self, + replace_current_window: bool, paths: Vec, cx: &mut ViewContext, ) -> Task> { @@ -1358,7 +1403,10 @@ impl Workspace { let is_remote = self.project.read(cx).is_remote(); let has_worktree = self.project.read(cx).worktrees().next().is_some(); let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx)); - let window_to_replace = if is_remote || has_worktree || has_dirty_items { + + let window_to_replace = if replace_current_window { + window + } else if is_remote || has_worktree || has_dirty_items { None } else { window @@ -1575,11 +1623,14 @@ impl Workspace { pub fn close_inactive_items_and_panes( &mut self, - _: &CloseInactiveTabsAndPanes, + action: &CloseInactiveTabsAndPanes, cx: &mut ViewContext, ) { - self.close_all_internal(true, SaveIntent::Close, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = + self.close_all_internal(true, action.save_intent.unwrap_or(SaveIntent::Close), cx) + { + task.detach_and_log_err(cx) + } } pub fn close_all_items_and_panes( @@ -1587,8 +1638,11 @@ impl Workspace { action: &CloseAllItemsAndPanes, cx: &mut ViewContext, ) { - self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = + self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx) + { + task.detach_and_log_err(cx) + } } fn close_all_internal( @@ -1603,7 +1657,7 @@ impl Workspace { if retain_active_pane { if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| { - pane.close_inactive_items(&CloseInactiveItems, cx) + pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx) }) { tasks.push(current_pane_close); }; @@ -1835,15 +1889,23 @@ impl Workspace { } } - pub fn add_item(&mut self, item: Box, cx: &mut WindowContext) { + pub fn add_item_to_active_pane(&mut self, item: Box, cx: &mut WindowContext) { + self.add_item(self.active_pane.clone(), item, cx) + } + + pub fn add_item( + &mut self, + pane: View, + item: Box, + cx: &mut WindowContext, + ) { if let Some(text) = item.telemetry_event_text(cx) { self.client() .telemetry() .report_app_event(format!("{}: open", text)); } - self.active_pane - .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); + pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); } pub fn split_item( @@ -1853,9 +1915,7 @@ impl Workspace { cx: &mut ViewContext, ) { let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx); - new_pane.update(cx, move |new_pane, cx| { - new_pane.add_item(item, true, true, None, cx) - }) + self.add_item(new_pane, item, cx); } pub fn open_abs_path( @@ -1997,6 +2057,7 @@ impl Workspace { pub fn open_project_item( &mut self, + pane: View, project_item: Model, cx: &mut ViewContext, ) -> View @@ -2007,7 +2068,7 @@ impl Workspace { let entry_id = project_item.read(cx).entry_id(cx); if let Some(item) = entry_id - .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) + .and_then(|entry_id| pane.read(cx).item_for_entry(entry_id, cx)) .and_then(|item| item.downcast()) { self.activate_item(&item, cx); @@ -2015,31 +2076,7 @@ impl Workspace { } let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); - self.add_item(Box::new(item.clone()), cx); - item - } - - pub fn split_project_item( - &mut self, - project_item: Model, - cx: &mut ViewContext, - ) -> View - where - T: ProjectItem, - { - use project::Item as _; - - let entry_id = project_item.read(cx).entry_id(cx); - if let Some(item) = entry_id - .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) - .and_then(|item| item.downcast()) - { - self.activate_item(&item, cx); - return item; - } - - let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); - self.split_item(SplitDirection::Right, Box::new(item.clone()), cx); + self.add_item(pane, Box::new(item.clone()), cx); item } @@ -2448,6 +2485,13 @@ impl Workspace { &self.active_pane } + pub fn adjacent_pane(&mut self, cx: &mut ViewContext) -> View { + self.find_pane_in_direction(SplitDirection::Right, cx) + .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx)) + .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx)) + .clone() + } + pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option> { let weak_pane = self.panes_by_item.get(&handle.item_id())?; weak_pane.upgrade() @@ -2509,6 +2553,10 @@ impl Workspace { }; Ok::<_, anyhow::Error>(()) })??; + if let Some(view) = response.active_view { + Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, &mut cx) + .await?; + } Self::add_views_from_leader( this.clone(), leader_id, @@ -2556,8 +2604,9 @@ impl Workspace { if Some(leader_id) == self.unfollow(&pane, cx) { return; } - self.start_following(leader_id, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = self.start_following(leader_id, cx) { + task.detach_and_log_err(cx) + } } pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext) { @@ -2586,7 +2635,7 @@ impl Workspace { // if they are active in another project, follow there. if let Some(project_id) = other_project_id { let app_state = self.app_state.clone(); - crate::join_remote_project(project_id, remote_participant.user.id, app_state, cx) + crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx) .detach_and_log_err(cx); } @@ -2599,8 +2648,9 @@ impl Workspace { } // Otherwise, follow. - self.start_following(leader_id, cx) - .map(|task| task.detach_and_log_err(cx)); + if let Some(task) = self.start_following(leader_id, cx) { + task.detach_and_log_err(cx) + } } pub fn unfollow(&mut self, pane: &View, cx: &mut ViewContext) -> Option { @@ -2709,7 +2759,7 @@ impl Workspace { .z_index(100) .right_3() .bottom_3() - .w_96() + .w_112() .h_full() .flex() .flex_col() @@ -2726,6 +2776,34 @@ impl Workspace { // RPC handlers + fn active_view_for_follower( + &self, + follower_project_id: Option, + cx: &mut ViewContext, + ) -> Option { + let item = self.active_item(cx)?; + let leader_id = self + .pane_for(&*item) + .and_then(|pane| self.leader_for_pane(&pane)); + + let item_handle = item.to_followable_item_handle(cx)?; + let id = item_handle.remote_id(&self.app_state.client, cx)?; + let variant = item_handle.to_state_proto(cx)?; + + if item_handle.is_project_item(cx) + && (follower_project_id.is_none() + || follower_project_id != self.project.read(cx).remote_id()) + { + return None; + } + + Some(proto::View { + id: Some(id.to_proto()), + leader_id, + variant: Some(variant), + }) + } + fn handle_follow( &mut self, follower_project_id: Option, @@ -2734,17 +2812,14 @@ impl Workspace { let client = &self.app_state.client; let project_id = self.project.read(cx).remote_id(); - let active_view_id = self.active_item(cx).and_then(|i| { - Some( - i.to_followable_item_handle(cx)? - .remote_id(client, cx)? - .to_proto(), - ) - }); + let active_view = self.active_view_for_follower(follower_project_id, cx); + let active_view_id = active_view.as_ref().and_then(|view| view.id.clone()); cx.notify(); proto::FollowResponse { + active_view, + // TODO: once v0.124.0 is retired we can stop sending these active_view_id, views: self .panes() @@ -2802,19 +2877,35 @@ impl Workspace { ) -> Result<()> { match update.variant.ok_or_else(|| anyhow!("invalid update"))? { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { - this.update(cx, |this, _| { - for (_, state) in &mut this.follower_states { - if state.leader_id == leader_id { - state.active_view_id = - if let Some(active_view_id) = update_active_view.id.clone() { - Some(ViewId::from_proto(active_view_id)?) - } else { - None - }; + let panes_missing_view = this.update(cx, |this, _| { + let mut panes = vec![]; + for (pane, state) in &mut this.follower_states { + if state.leader_id != leader_id { + continue; + } + + state.active_view_id = + if let Some(active_view_id) = update_active_view.id.clone() { + Some(ViewId::from_proto(active_view_id)?) + } else { + None + }; + + if state.active_view_id.is_some_and(|view_id| { + !state.items_by_leader_view_id.contains_key(&view_id) + }) { + panes.push(pane.clone()) } } - anyhow::Ok(()) + anyhow::Ok(panes) })??; + + if let Some(view) = update_active_view.view { + for pane in panes_missing_view { + Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, cx) + .await? + } + } } proto::update_followers::Variant::UpdateView(update_view) => { let variant = update_view @@ -2853,6 +2944,56 @@ impl Workspace { Ok(()) } + async fn add_view_from_leader( + this: WeakView, + leader_id: PeerId, + pane: View, + view: &proto::View, + cx: &mut AsyncWindowContext, + ) -> Result<()> { + let this = this.upgrade().context("workspace dropped")?; + + let item_builders = cx.update(|cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + })?; + + let Some(id) = view.id.clone() else { + return Err(anyhow!("no id for view")); + }; + let id = ViewId::from_proto(id)?; + + let mut variant = view.variant.clone(); + if variant.is_none() { + Err(anyhow!("missing view variant"))?; + } + + let task = item_builders.iter().find_map(|build_item| { + cx.update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx)) + .log_err() + .flatten() + }); + let Some(task) = task else { + return Err(anyhow!( + "failed to construct view from leader (maybe from a different version of zed?)" + )); + }; + + let item = task.await?; + + this.update(cx, |this, cx| { + let state = this.follower_states.get_mut(&pane)?; + item.set_leader_peer_id(Some(leader_id), cx); + state.items_by_leader_view_id.insert(id, item); + + Some(()) + })?; + + Ok(()) + } + async fn add_views_from_leader( this: WeakView, leader_id: PeerId, @@ -2874,7 +3015,9 @@ impl Workspace { let mut item_tasks = Vec::new(); let mut leader_view_ids = Vec::new(); for view in &views { - let Some(id) = &view.id else { continue }; + let Some(id) = &view.id else { + continue; + }; let id = ViewId::from_proto(id.clone())?; let mut variant = view.variant.clone(); if variant.is_none() { @@ -2920,13 +3063,31 @@ impl Workspace { if cx.is_window_active() { if let Some(item) = self.active_item(cx) { if item.focus_handle(cx).contains_focused(cx) { + let leader_id = self + .pane_for(&*item) + .and_then(|pane| self.leader_for_pane(&pane)); + if let Some(item) = item.to_followable_item_handle(cx) { - is_project_item = item.is_project_item(cx); - update = proto::UpdateActiveView { - id: item - .remote_id(&self.app_state.client, cx) - .map(|id| id.to_proto()), - leader_id: self.leader_for_pane(&self.active_pane), + let id = item + .remote_id(&self.app_state.client, cx) + .map(|id| id.to_proto()); + + if let Some(id) = id.clone() { + if let Some(variant) = item.to_state_proto(cx) { + let view = Some(proto::View { + id: Some(id.clone()), + leader_id, + variant: Some(variant), + }); + + is_project_item = item.is_project_item(cx); + update = proto::UpdateActiveView { + view, + // TODO: once v0.124.0 is retired we can stop sending these + id: Some(id), + leader_id, + }; + } }; } } @@ -3200,7 +3361,7 @@ impl Workspace { let left_visible = left_dock.is_open(); let left_active_panel = left_dock .visible_panel() - .and_then(|panel| Some(panel.persistent_name().to_string())); + .map(|panel| panel.persistent_name().to_string()); let left_dock_zoom = left_dock .visible_panel() .map(|panel| panel.is_zoomed(cx)) @@ -3210,7 +3371,7 @@ impl Workspace { let right_visible = right_dock.is_open(); let right_active_panel = right_dock .visible_panel() - .and_then(|panel| Some(panel.persistent_name().to_string())); + .map(|panel| panel.persistent_name().to_string()); let right_dock_zoom = right_dock .visible_panel() .map(|panel| panel.is_zoomed(cx)) @@ -3220,7 +3381,7 @@ impl Workspace { let bottom_visible = bottom_dock.is_open(); let bottom_active_panel = bottom_dock .visible_panel() - .and_then(|panel| Some(panel.persistent_name().to_string())); + .map(|panel| panel.persistent_name().to_string()); let bottom_dock_zoom = bottom_dock .visible_panel() .map(|panel| panel.is_zoomed(cx)) @@ -3360,6 +3521,7 @@ impl Workspace { .on_action(cx.listener(Self::close_inactive_items_and_panes)) .on_action(cx.listener(Self::close_all_items_and_panes)) .on_action(cx.listener(Self::save_all)) + .on_action(cx.listener(Self::send_keystrokes)) .on_action(cx.listener(Self::add_folder_to_project)) .on_action(cx.listener(Self::follow_next_collaborator)) .on_action(cx.listener(|workspace, _: &Unfollow, cx| { @@ -3371,6 +3533,11 @@ impl Workspace { .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) .detach_and_log_err(cx); })) + .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| { + workspace + .save_active_item(SaveIntent::SaveWithoutFormat, cx) + .detach_and_log_err(cx); + })) .on_action(cx.listener(|workspace, _: &SaveAs, cx| { workspace .save_active_item(SaveIntent::SaveAs, cx) @@ -3416,7 +3583,6 @@ impl Workspace { workspace.reopen_closed_item(cx).detach(); }), ) - .on_action(|_: &ToggleGraphicsProfiler, cx| cx.toggle_graphics_profiler()) } #[cfg(any(test, feature = "test-support"))] @@ -3558,7 +3724,7 @@ fn open_items( project_paths_to_open .into_iter() .enumerate() - .map(|(i, (abs_path, project_path))| { + .map(|(ix, (abs_path, project_path))| { let workspace = workspace.clone(); cx.spawn(|mut cx| { let fs = app_state.fs.clone(); @@ -3566,7 +3732,7 @@ fn open_items( let file_project_path = project_path?; if fs.is_file(&abs_path).await { Some(( - i, + ix, workspace .update(&mut cx, |workspace, cx| { workspace.open_path(file_project_path, None, true, cx) @@ -3583,11 +3749,9 @@ fn open_items( let tasks = tasks.collect::>(); - let tasks = futures::future::join_all(tasks.into_iter()); - for maybe_opened_path in tasks.await.into_iter() { - if let Some((i, path_open_result)) = maybe_opened_path { - opened_items[i] = Some(path_open_result); - } + let tasks = futures::future::join_all(tasks); + for (ix, path_open_result) in tasks.await.into_iter().flatten() { + opened_items[ix] = Some(path_open_result); } Ok(opened_items) @@ -3600,7 +3764,7 @@ enum ActivateInDirectionTarget { } fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncAppContext) { - const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; + const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; workspace .update(cx, |workspace, cx| { @@ -3635,7 +3799,7 @@ impl Render for Workspace { let theme_settings = ThemeSettings::get_global(cx); ( theme_settings.ui_font.family.clone(), - theme_settings.ui_font_size.clone(), + theme_settings.ui_font_size, ) }; @@ -3789,10 +3953,8 @@ impl WorkspaceStore { pub fn new(client: Arc, cx: &mut ModelContext) -> Self { Self { workspaces: Default::default(), - followers: Default::default(), _subscriptions: vec![ client.add_request_handler(cx.weak_model(), Self::handle_follow), - client.add_message_handler(cx.weak_model(), Self::handle_unfollow), client.add_message_handler(cx.weak_model(), Self::handle_update_followers), ], client, @@ -3807,25 +3969,10 @@ impl WorkspaceStore { ) -> Option<()> { let active_call = ActiveCall::try_global(cx)?; let room_id = active_call.read(cx).room()?.read(cx).id(); - let follower_ids: Vec<_> = self - .followers - .iter() - .filter_map(|follower| { - if follower.project_id == project_id || project_id.is_none() { - Some(follower.peer_id.into()) - } else { - None - } - }) - .collect(); - if follower_ids.is_empty() { - return None; - } self.client .send(proto::UpdateFollowers { room_id, project_id, - follower_ids, variant: Some(update), }) .log_err() @@ -3842,7 +3989,6 @@ impl WorkspaceStore { project_id: envelope.payload.project_id, peer_id: envelope.original_sender_id()?, }; - let active_project = ActiveCall::global(cx).read(cx).location().cloned(); let mut response = proto::FollowResponse::default(); this.workspaces.retain(|workspace| { @@ -3857,41 +4003,27 @@ impl WorkspaceStore { if let Some(active_view_id) = handler_response.active_view_id.clone() { if response.active_view_id.is_none() - || Some(workspace.project.downgrade()) == active_project + || workspace.project.read(cx).remote_id() == follower.project_id { response.active_view_id = Some(active_view_id); } } + + if let Some(active_view) = handler_response.active_view.clone() { + if response.active_view_id.is_none() + || workspace.project.read(cx).remote_id() == follower.project_id + { + response.active_view = Some(active_view) + } + } }) .is_ok() }); - if let Err(ix) = this.followers.binary_search(&follower) { - this.followers.insert(ix, follower); - } - Ok(response) })? } - async fn handle_unfollow( - model: Model, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - model.update(&mut cx, |this, _| { - let follower = Follower { - project_id: envelope.payload.project_id, - peer_id: envelope.original_sender_id()?, - }; - if let Ok(ix) = this.followers.binary_search(&follower) { - this.followers.remove(ix); - } - Ok(()) - })? - } - async fn handle_update_followers( this: Model, envelope: TypedEnvelope, @@ -4000,7 +4132,7 @@ pub async fn last_opened_workspace_paths() -> Option { actions!(collab, [OpenChannelNotes]); async fn join_channel_internal( - channel_id: u64, + channel_id: ChannelId, app_state: &Arc, requesting_window: Option>, active_call: &Model, @@ -4026,7 +4158,7 @@ async fn join_channel_internal( if let Some(room) = open_room { let task = room.update(cx, |room, cx| { if let Some((project, host)) = room.most_active_project(cx) { - return Some(join_remote_project(project, host, app_state.clone(), cx)); + return Some(join_in_room_project(project, host, app_state.clone(), cx)); } None @@ -4077,7 +4209,7 @@ async fn join_channel_internal( Status::SignedOut => return Err(ErrorCode::SignedOut.into()), Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()), Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { - return Err(ErrorCode::Disconnected.into()) + return Err(ErrorCode::Disconnected.into()); } } } @@ -4097,7 +4229,7 @@ async fn join_channel_internal( let task = room.update(cx, |room, cx| { if let Some((project, host)) = room.most_active_project(cx) { - return Some(join_remote_project(project, host, app_state.clone(), cx)); + return Some(join_in_room_project(project, host, app_state.clone(), cx)); } // if you are the first to join a channel, share your project @@ -4140,7 +4272,7 @@ async fn join_channel_internal( } pub fn join_channel( - channel_id: u64, + channel_id: ChannelId, app_state: Arc, requesting_window: Option>, cx: &mut AppContext, @@ -4154,7 +4286,7 @@ pub fn join_channel( &active_call, &mut cx, ) - .await; + .await; // join channel succeeded, and opened a window if matches!(result, Ok(true)) { @@ -4189,16 +4321,16 @@ pub fn join_channel( let detail: SharedString = match err.error_code() { ErrorCode::SignedOut => { "Please sign in to continue.".into() - }, + } ErrorCode::UpgradeRequired => { "Your are running an unsupported version of Zed. Please update to continue.".into() - }, + } ErrorCode::NoSuchChannel => { "No matching channel was found. Please check the link and try again.".into() - }, + } ErrorCode::Forbidden => { "This channel is private, and you do not have access. Please ask someone to add you and try again.".into() - }, + } ErrorCode::Disconnected => "Please check your internet connection and try again.".into(), _ => format!("{}\n\nPlease try again.", err).into(), }; @@ -4270,7 +4402,7 @@ pub fn open_paths( cx.spawn(move |mut cx| async move { if let Some(existing) = existing { Ok(( - existing.clone(), + existing, existing .update(&mut cx, |workspace, cx| { workspace.open_paths(abs_paths, OpenVisible::All, None, cx) @@ -4332,7 +4464,56 @@ pub fn create_and_open_local_file( }) } -pub fn join_remote_project( +pub fn join_hosted_project( + hosted_project_id: HostedProjectId, + app_state: Arc, + cx: &mut AppContext, +) -> Task> { + cx.spawn(|mut cx| async move { + let existing_window = cx.update(|cx| { + cx.windows().into_iter().find_map(|window| { + let workspace = window.downcast::()?; + workspace + .read(cx) + .is_ok_and(|workspace| { + workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id) + }) + .then(|| workspace) + }) + })?; + + let workspace = if let Some(existing_window) = existing_window { + existing_window + } else { + let project = Project::hosted( + hosted_project_id, + app_state.user_store.clone(), + app_state.client.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + cx.clone(), + ) + .await?; + + let window_bounds_override = window_bounds_env_override(&cx); + cx.update(|cx| { + let options = (app_state.build_window_options)(window_bounds_override, None, cx); + cx.open_window(options, |cx| { + cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx)) + }) + })? + }; + + workspace.update(&mut cx, |_, cx| { + cx.activate(true); + cx.activate_window(); + })?; + + Ok(()) + }) +} + +pub fn join_in_room_project( project_id: u64, follow_user_id: u64, app_state: Arc, @@ -4569,7 +4750,7 @@ mod tests { item }); workspace.update(cx, |workspace, cx| { - workspace.add_item(Box::new(item1.clone()), cx); + workspace.add_item_to_active_pane(Box::new(item1.clone()), cx); }); item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0))); @@ -4581,7 +4762,7 @@ mod tests { item }); workspace.update(cx, |workspace, cx| { - workspace.add_item(Box::new(item2.clone()), cx); + workspace.add_item_to_active_pane(Box::new(item2.clone()), cx); }); item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); @@ -4595,7 +4776,7 @@ mod tests { item }); workspace.update(cx, |workspace, cx| { - workspace.add_item(Box::new(item3.clone()), cx); + workspace.add_item_to_active_pane(Box::new(item3.clone()), cx); }); item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); @@ -4638,7 +4819,9 @@ mod tests { }); // Add an item to an empty pane - workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx)); + workspace.update(cx, |workspace, cx| { + workspace.add_item_to_active_pane(Box::new(item1), cx) + }); project.update(cx, |project, cx| { assert_eq!( project.active_entry(), @@ -4650,7 +4833,9 @@ mod tests { assert_eq!(cx.window_title().as_deref(), Some("one.txt β€” root1")); // Add a second item to a non-empty pane - workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx)); + workspace.update(cx, |workspace, cx| { + workspace.add_item_to_active_pane(Box::new(item2), cx) + }); assert_eq!(cx.window_title().as_deref(), Some("two.txt β€” root1")); project.update(cx, |project, cx| { assert_eq!( @@ -4703,7 +4888,9 @@ mod tests { // When there are no dirty items, there's nothing to do. let item1 = cx.new_view(|cx| TestItem::new(cx)); - workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); + workspace.update(cx, |w, cx| { + w.add_item_to_active_pane(Box::new(item1.clone()), cx) + }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); assert!(task.await.unwrap()); @@ -4716,8 +4903,8 @@ mod tests { .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) }); workspace.update(cx, |w, cx| { - w.add_item(Box::new(item2.clone()), cx); - w.add_item(Box::new(item3.clone()), cx); + w.add_item_to_active_pane(Box::new(item2.clone()), cx); + w.add_item_to_active_pane(Box::new(item3.clone()), cx); }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); cx.executor().run_until_parked(); @@ -4761,10 +4948,10 @@ mod tests { .with_project_items(&[TestProjectItem::new_untitled(cx)]) }); let pane = workspace.update(cx, |workspace, cx| { - workspace.add_item(Box::new(item1.clone()), cx); - workspace.add_item(Box::new(item2.clone()), cx); - workspace.add_item(Box::new(item3.clone()), cx); - workspace.add_item(Box::new(item4.clone()), cx); + workspace.add_item_to_active_pane(Box::new(item1.clone()), cx); + workspace.add_item_to_active_pane(Box::new(item2.clone()), cx); + workspace.add_item_to_active_pane(Box::new(item3.clone()), cx); + workspace.add_item_to_active_pane(Box::new(item4.clone()), cx); workspace.active_pane().clone() }); @@ -4886,9 +5073,9 @@ mod tests { // multi-entry items: (3, 4) let left_pane = workspace.update(cx, |workspace, cx| { let left_pane = workspace.active_pane().clone(); - workspace.add_item(Box::new(item_2_3.clone()), cx); + workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), cx); for item in single_entry_items { - workspace.add_item(Box::new(item), cx); + workspace.add_item_to_active_pane(Box::new(item), cx); } left_pane.update(cx, |pane, cx| { pane.activate_item(2, true, true, cx); @@ -4959,7 +5146,7 @@ mod tests { }); let item_id = item.entity_id(); workspace.update(cx, |workspace, cx| { - workspace.add_item(Box::new(item.clone()), cx); + workspace.add_item_to_active_pane(Box::new(item.clone()), cx); }); // Autosave on window change. @@ -5041,7 +5228,7 @@ mod tests { // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. workspace.update(cx, |workspace, cx| { - workspace.add_item(Box::new(item.clone()), cx); + workspace.add_item_to_active_pane(Box::new(item.clone()), cx); }); item.update(cx, |item, cx| { item.project_items[0].update(cx, |item, _| { @@ -5079,7 +5266,7 @@ mod tests { let toolbar_notify_count = Rc::new(RefCell::new(0)); workspace.update(cx, |workspace, cx| { - workspace.add_item(Box::new(item.clone()), cx); + workspace.add_item_to_active_pane(Box::new(item.clone()), cx); let toolbar_notification_count = toolbar_notify_count.clone(); cx.observe(&toolbar, move |_, _, _| { *toolbar_notification_count.borrow_mut() += 1 @@ -5360,7 +5547,7 @@ mod tests { workspace.update(cx, |workspace, cx| { // Since panel_2 was not visible on the right, we don't open the left dock. assert!(!workspace.left_dock().read(cx).is_open()); - // And the right dock is unaffected in it's displaying of panel_1 + // And the right dock is unaffected in its displaying of panel_1 assert!(workspace.right_dock().read(cx).is_open()); assert_eq!( workspace diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a0bbf53fde..2ad94a8bae 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.124.0" +version = "0.126.0" publish = false license = "GPL-3.0-or-later" @@ -17,36 +17,30 @@ path = "src/main.rs" [dependencies] activity_indicator.workspace = true -ai.workspace = true anyhow.workspace = true assets.workspace = true assistant.workspace = true -async-compression.workspace = true -async-recursion = "0.3" -async-tar.workspace = true -async-trait.workspace = true audio.workspace = true auto_update.workspace = true backtrace = "0.3" breadcrumbs.workspace = true call.workspace = true channel.workspace = true -chrono = "0.4" +chrono.workspace = true cli.workspace = true client.workspace = true +clock.workspace = true collab_ui.workspace = true collections.workspace = true command_palette.workspace = true copilot.workspace = true copilot_ui.workspace = true -ctor.workspace = true db.workspace = true diagnostics.workspace = true editor.workspace = true env_logger.workspace = true extension.workspace = true extensions_ui.workspace = true -feature_flags.workspace = true feedback.workspace = true file_finder.workspace = true fs.workspace = true @@ -54,109 +48,42 @@ fsevent.workspace = true futures.workspace = true go_to_line.workspace = true gpui.workspace = true -ignore = "0.4" -image = "0.23" -indexmap = "1.6.2" install_cli.workspace = true isahc.workspace = true -itertools = "0.11" +itertools.workspace = true journal.workspace = true language.workspace = true language_selector.workspace = true language_tools.workspace = true -lazy_static.workspace = true -libc = "0.2" +languages.workspace = true log.workspace = true -lsp.workspace = true markdown_preview.workspace = true menu.workspace = true mimalloc = "0.1" node_runtime.workspace = true notifications.workspace = true -num_cpus = "1.13.0" outline.workspace = true parking_lot.workspace = true -postage.workspace = true +profiling.workspace = true project.workspace = true project_panel.workspace = true project_symbols.workspace = true quick_action_bar.workspace = true -rand.workspace = true recent_projects.workspace = true -regex.workspace = true release_channel.workspace = true rope.workspace = true -rpc.workspace = true -rsa = "0.4" -runnable.workspace = true -runnables_ui.workspace = true -rust-embed.workspace = true -schemars.workspace = true search.workspace = true semantic_index.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true settings.workspace = true -shellexpand = "2.1.0" simplelog = "0.9" -smallvec.workspace = true smol.workspace = true -sum_tree.workspace = true -tempfile.workspace = true +task.workspace = true +tasks_ui.workspace = true terminal_view.workspace = true -text.workspace = true theme.workspace = true theme_selector.workspace = true -thiserror.workspace = true -tiny_http = "0.8" -toml.workspace = true -tree-sitter-astro.workspace = true -tree-sitter-bash.workspace = true -tree-sitter-c-sharp.workspace = true -tree-sitter-c.workspace = true -tree-sitter-clojure.workspace = true -tree-sitter-cpp.workspace = true -tree-sitter-css.workspace = true -tree-sitter-dockerfile.workspace = true -tree-sitter-elixir.workspace = true -tree-sitter-elm.workspace = true -tree-sitter-embedded-template.workspace = true -tree-sitter-erlang.workspace = true -tree-sitter-gitcommit.workspace = true -tree-sitter-gleam.workspace = true -tree-sitter-glsl.workspace = true -tree-sitter-go.workspace = true -tree-sitter-gomod.workspace = true -tree-sitter-gowork.workspace = true -tree-sitter-haskell.workspace = true -tree-sitter-hcl.workspace = true -tree-sitter-heex.workspace = true -tree-sitter-html.workspace = true -tree-sitter-json.workspace = true -tree-sitter-lua.workspace = true -tree-sitter-markdown.workspace = true -tree-sitter-nix.workspace = true -tree-sitter-nu.workspace = true -tree-sitter-ocaml.workspace = true -tree-sitter-php.workspace = true -tree-sitter-prisma-io.workspace = true -tree-sitter-proto.workspace = true -tree-sitter-purescript.workspace = true -tree-sitter-python.workspace = true -tree-sitter-racket.workspace = true -tree-sitter-ruby.workspace = true -tree-sitter-rust.workspace = true -tree-sitter-scheme.workspace = true -tree-sitter-svelte.workspace = true -tree-sitter-toml.workspace = true -tree-sitter-typescript.workspace = true -tree-sitter-uiua.workspace = true -tree-sitter-vue.workspace = true -tree-sitter-yaml.workspace = true -tree-sitter-zig.workspace = true -tree-sitter.workspace = true -url.workspace = true urlencoding = "2.1.2" util.workspace = true uuid.workspace = true @@ -171,8 +98,7 @@ editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } -text = { workspace = true, features = ["test-support"] } -unindent.workspace = true +tree-sitter-rust.workspace = true workspace = { workspace = true, features = ["test-support"] } [package.metadata.bundle-dev] @@ -181,7 +107,7 @@ identifier = "dev.zed.Zed-Dev" name = "Zed Dev" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed-dev"] +osx_url_schemes = ["zed"] [package.metadata.bundle-nightly] icon = ["resources/app-icon-nightly@2x.png", "resources/app-icon-nightly.png"] @@ -189,7 +115,7 @@ identifier = "dev.zed.Zed-Nightly" name = "Zed Nightly" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed-nightly"] +osx_url_schemes = ["zed"] [package.metadata.bundle-preview] icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] @@ -197,7 +123,7 @@ identifier = "dev.zed.Zed-Preview" name = "Zed Preview" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed-preview"] +osx_url_schemes = ["zed"] [package.metadata.bundle-stable] icon = ["resources/app-icon@2x.png", "resources/app-icon.png"] @@ -206,3 +132,6 @@ name = "Zed" osx_minimum_system_version = "10.15.7" osx_info_plist_exts = ["resources/info/*"] osx_url_schemes = ["zed"] + +[package.metadata.cargo-machete] +ignored = ["profiling"] diff --git a/crates/zed/build.rs b/crates/zed/build.rs index 6905d492e1..daea199e21 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -43,4 +43,9 @@ fn main() { } } } + + // todo!("windows"): This is to avoid stack overflow. Remove it when solved. + if std::env::var("CARGO_CFG_TARGET_ENV").ok() == Some("msvc".to_string()) { + println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024); + } } diff --git a/crates/zed/resources/info/Permissions.plist b/crates/zed/resources/info/Permissions.plist index fd608afaa0..bded5a82e2 100644 --- a/crates/zed/resources/info/Permissions.plist +++ b/crates/zed/resources/info/Permissions.plist @@ -22,5 +22,3 @@ An application in Zed wants to use speech recognition. NSRemindersUsageDescription An application in Zed wants to use your reminders. -MetalHudEnabled - diff --git a/crates/zed/src/app_menus.rs b/crates/zed/src/app_menus.rs index 15cc17620b..12a88ed216 100644 --- a/crates/zed/src/app_menus.rs +++ b/crates/zed/src/app_menus.rs @@ -37,7 +37,12 @@ pub fn app_menus() -> Vec> { MenuItem::action("New Window", workspace::NewWindow), MenuItem::separator(), MenuItem::action("Open…", workspace::Open), - MenuItem::action("Open Recent...", recent_projects::OpenRecent), + MenuItem::action( + "Open Recent...", + recent_projects::OpenRecent { + create_new_window: true, + }, + ), MenuItem::separator(), MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject), MenuItem::action("Save", workspace::Save { save_intent: None }), @@ -156,11 +161,6 @@ pub fn app_menus() -> Vec> { MenuItem::action("View Telemetry", crate::OpenTelemetryLog), MenuItem::action("View Dependency Licenses", crate::OpenLicenses), MenuItem::action("Show Welcome", workspace::Welcome), - MenuItem::action( - "Toggle Graphics Profiler", - workspace::ToggleGraphicsProfiler, - ), - MenuItem::separator(), MenuItem::separator(), MenuItem::action( "Documentation", diff --git a/crates/zed/src/languages/language_plugin.rs b/crates/zed/src/languages/language_plugin.rs deleted file mode 100644 index f4367e1f14..0000000000 --- a/crates/zed/src/languages/language_plugin.rs +++ /dev/null @@ -1,168 +0,0 @@ -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use collections::HashMap; -use futures::lock::Mutex; -use gpui::executor::Background; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp2::LanguageServerBinary; -use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn}; -use std::{any::Any, path::PathBuf, sync::Arc}; -use util::ResultExt; - -#[allow(dead_code)] -pub async fn new_json(executor: Arc) -> Result { - let plugin = PluginBuilder::new_default()? - .host_function_async("command", |command: String| async move { - let mut args = command.split(' '); - let command = args.next().unwrap(); - smol::process::Command::new(command) - .args(args) - .output() - .await - .log_err() - .map(|output| output.stdout) - })? - .init(PluginBinary::Precompiled(include_bytes!( - "../../../../plugins/bin/json_language.wasm.pre", - ))) - .await?; - - PluginLspAdapter::new(plugin, executor).await -} - -pub struct PluginLspAdapter { - name: WasiFn<(), String>, - fetch_latest_server_version: WasiFn<(), Option>, - fetch_server_binary: WasiFn<(PathBuf, String), Result>, - cached_server_binary: WasiFn>, - initialization_options: WasiFn<(), String>, - language_ids: WasiFn<(), Vec<(String, String)>>, - executor: Arc, - runtime: Arc>, -} - -impl PluginLspAdapter { - #[allow(unused)] - pub async fn new(mut plugin: Plugin, executor: Arc) -> Result { - Ok(Self { - name: plugin.function("name")?, - fetch_latest_server_version: plugin.function("fetch_latest_server_version")?, - fetch_server_binary: plugin.function("fetch_server_binary")?, - cached_server_binary: plugin.function("cached_server_binary")?, - initialization_options: plugin.function("initialization_options")?, - language_ids: plugin.function("language_ids")?, - executor, - runtime: Arc::new(Mutex::new(plugin)), - }) - } -} - -#[async_trait] -impl LspAdapter for PluginLspAdapter { - async fn name(&self) -> LanguageServerName { - let name: String = self - .runtime - .lock() - .await - .call(&self.name, ()) - .await - .unwrap(); - LanguageServerName(name.into()) - } - - fn short_name(&self) -> &'static str { - "PluginLspAdapter" - } - - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - let runtime = self.runtime.clone(); - let function = self.fetch_latest_server_version; - self.executor - .spawn(async move { - let mut runtime = runtime.lock().await; - let versions: Result> = - runtime.call::<_, Option>(&function, ()).await; - versions - .map_err(|e| anyhow!("{}", e))? - .ok_or_else(|| anyhow!("Could not fetch latest server version")) - .map(|v| Box::new(v) as Box<_>) - }) - .await - } - - async fn fetch_server_binary( - &self, - version: Box, - container_dir: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Result { - let version = *version.downcast::().unwrap(); - let runtime = self.runtime.clone(); - let function = self.fetch_server_binary; - self.executor - .spawn(async move { - let mut runtime = runtime.lock().await; - let handle = runtime.attach_path(&container_dir)?; - let result: Result = - runtime.call(&function, (container_dir, version)).await?; - runtime.remove_resource(handle)?; - result.map_err(|e| anyhow!("{}", e)) - }) - .await - } - - async fn cached_server_binary( - &self, - container_dir: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - let runtime = self.runtime.clone(); - let function = self.cached_server_binary; - - self.executor - .spawn(async move { - let mut runtime = runtime.lock().await; - let handle = runtime.attach_path(&container_dir).ok()?; - let result: Option = - runtime.call(&function, container_dir).await.ok()?; - runtime.remove_resource(handle).ok()?; - result - }) - .await - } - - fn can_be_reinstalled(&self) -> bool { - false - } - - async fn installation_test_binary(&self, _: PathBuf) -> Option { - None - } - - async fn initialization_options(&self) -> Option { - let string: String = self - .runtime - .lock() - .await - .call(&self.initialization_options, ()) - .await - .log_err()?; - - serde_json::from_str(&string).ok() - } - - async fn language_ids(&self) -> HashMap { - self.runtime - .lock() - .await - .call(&self.language_ids, ()) - .await - .log_err() - .unwrap_or_default() - .into_iter() - .collect() - } -} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index aeca532c79..277d4dda03 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -5,7 +5,7 @@ use anyhow::{anyhow, Context as _, Result}; use backtrace::Backtrace; use chrono::Utc; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; -use client::{Client, UserStore}; +use client::{parse_zed_link, Client, UserStore}; use collab_ui::channel_view::ChannelView; use db::kvp::KEY_VALUE_STORE; use editor::Editor; @@ -23,7 +23,7 @@ use assets::Assets; use mimalloc::MiMalloc; use node_runtime::RealNodeRuntime; use parking_lot::Mutex; -use release_channel::{parse_zed_link, AppCommitSha, ReleaseChannel, RELEASE_CHANNEL}; +use release_channel::{AppCommitSha, ReleaseChannel, RELEASE_CHANNEL}; use serde::{Deserialize, Serialize}; use settings::{ default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore, @@ -46,7 +46,7 @@ use std::{ use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, ThemeSettings}; use util::{ async_maybe, - http::{self, HttpClient, ZedHttpClient}, + http::{HttpClient, HttpClientWithUrl}, paths::{self, CRASHES_DIR, CRASHES_RETIRED_DIR}, ResultExt, }; @@ -55,8 +55,7 @@ use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN}; use workspace::{AppState, WorkspaceStore}; use zed::{ app_menus, build_window_options, ensure_only_instance, handle_cli_connection, - handle_keymap_file_changes, initialize_workspace, languages, IsOnlyInstance, OpenListener, - OpenRequest, + handle_keymap_file_changes, initialize_workspace, IsOnlyInstance, OpenListener, OpenRequest, }; #[global_allocator] @@ -107,11 +106,9 @@ fn main() { let (listener, mut open_rx) = OpenListener::new(); let listener = Arc::new(listener); let open_listener = listener.clone(); - app.on_open_urls(move |urls, _| open_listener.open_urls(&urls)); + app.on_open_urls(move |urls, cx| open_listener.open_urls(&urls, cx)); app.on_reopen(move |cx| { - if let Some(app_state) = AppState::try_global(cx) - .map(|app_state| app_state.upgrade()) - .flatten() + if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade()) { workspace::open_new(&app_state, cx, |workspace, cx| { Editor::new_file(workspace, &Default::default(), cx) @@ -140,9 +137,12 @@ fn main() { handle_keymap_file_changes(user_keymap_file_rx, cx); client::init_settings(cx); - let http = http::zed_client(&client::ClientSettings::get_global(cx).server_url); + let clock = Arc::new(clock::RealSystemClock); + let http = Arc::new(HttpClientWithUrl::new( + &client::ClientSettings::get_global(cx).server_url, + )); - let client = client::Client::new(http.clone(), cx); + let client = client::Client::new(clock, http.clone(), cx); let mut languages = LanguageRegistry::new(login_shell_env_loaded); let copilot_language_server_id = languages.next_language_server_id(); languages.set_executor(cx.background_executor().clone()); @@ -176,6 +176,7 @@ fn main() { extension::init( fs.clone(), http.clone(), + node_runtime.clone(), languages.clone(), ThemeRegistry::global(cx), cx, @@ -198,9 +199,8 @@ fn main() { move |cx| { languages.set_theme(cx.theme().clone()); let new_host = &client::ClientSettings::get_global(cx).server_url; - let mut host = http.zed_host.lock(); - if &*host != new_host { - *host = new_host.clone(); + if &http.base_url() != new_host { + http.set_base_url(new_host); if client.status().borrow().is_connected() { client.reconnect(&cx.to_async()); } @@ -244,7 +244,7 @@ fn main() { outline::init(cx); project_symbols::init(cx); project_panel::init(Assets, cx); - runnables_ui::init(cx); + tasks_ui::init(cx); channel::init(&client, user_store.clone(), cx); search::init(cx); semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); @@ -267,13 +267,13 @@ fn main() { initialize_workspace(app_state.clone(), cx); if stdout_is_a_pty() { - //todo!(linux): unblock this + // todo(linux): unblock this #[cfg(not(target_os = "linux"))] upload_panics_and_crashes(http.clone(), cx); cx.activate(true); - let urls = collect_url_args(); + let urls = collect_url_args(cx); if !urls.is_empty() { - listener.open_urls(&urls) + listener.open_urls(&urls, cx) } } else { upload_panics_and_crashes(http.clone(), cx); @@ -282,7 +282,7 @@ fn main() { if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() && !listener.triggered.load(Ordering::Acquire) { - listener.open_urls(&collect_url_args()) + listener.open_urls(&collect_url_args(cx), cx) } } @@ -296,9 +296,9 @@ fn main() { let task = workspace::open_paths(&paths, &app_state, None, cx); cx.spawn(|_| async move { if let Some((_window, results)) = task.await.log_err() { - for result in results { - if let Some(Err(e)) = result { - log::error!("Error opening path: {}", e); + for result in results.into_iter().flatten() { + if let Err(err) = result { + log::error!("Error opening path: {err}",); } } } @@ -322,8 +322,10 @@ fn main() { cx.spawn(|cx| async move { // ignore errors here, we'll show a generic "not signed in" let _ = authenticate(client, &cx).await; - cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))? - .await?; + cx.update(|cx| { + workspace::join_channel(client::ChannelId(channel_id), app_state, None, cx) + })? + .await?; anyhow::Ok(()) }) .detach_and_log_err(cx); @@ -342,7 +344,7 @@ fn main() { workspace::get_any_active_workspace(app_state, cx.clone()).await?; let workspace = workspace_window.root_view(&cx)?; cx.update_window(workspace_window.into(), |_, cx| { - ChannelView::open(channel_id, heading, workspace, cx) + ChannelView::open(client::ChannelId(channel_id), heading, workspace, cx) })? .await?; anyhow::Ok(()) @@ -377,7 +379,12 @@ fn main() { cx.update(|mut cx| { cx.spawn(|cx| async move { cx.update(|cx| { - workspace::join_channel(channel_id, app_state, None, cx) + workspace::join_channel( + client::ChannelId(channel_id), + app_state, + None, + cx, + ) })? .await?; anyhow::Ok(()) @@ -396,7 +403,12 @@ fn main() { workspace::get_any_active_workspace(app_state, cx.clone()).await?; let workspace = workspace_window.root_view(&cx)?; cx.update_window(workspace_window.into(), |_, cx| { - ChannelView::open(channel_id, heading, workspace, cx) + ChannelView::open( + client::ChannelId(channel_id), + heading, + workspace, + cx, + ) })? .await?; anyhow::Ok(()) @@ -480,34 +492,13 @@ fn init_paths() { std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path"); std::fs::create_dir_all(&*util::paths::DB_DIR).expect("could not create database path"); std::fs::create_dir_all(&*util::paths::LOGS_DIR).expect("could not create logs path"); + #[cfg(target_os = "linux")] + std::fs::create_dir_all(&*util::paths::TEMP_DIR).expect("could not create tmp path"); } fn init_logger() { if stdout_is_a_pty() { - Builder::new() - .parse_default_env() - .format(|buf, record| { - use env_logger::fmt::Color; - - let subtle = buf - .style() - .set_color(Color::Black) - .set_intense(true) - .clone(); - write!(buf, "{}", subtle.value("["))?; - write!( - buf, - "{} ", - chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z") - )?; - write!(buf, "{:<5}", buf.default_styled_level(record.level()))?; - if let Some(path) = record.module_path() { - write!(buf, " {}", path)?; - } - write!(buf, "{}", subtle.value("]"))?; - writeln!(buf, " {}", record.args()) - }) - .init(); + init_stdout_logger(); } else { let level = LevelFilter::Info; @@ -520,21 +511,58 @@ fn init_logger() { let _ = std::fs::rename(&*paths::LOG, &*paths::OLD_LOG); } - let log_file = OpenOptions::new() + match OpenOptions::new() .create(true) .append(true) .open(&*paths::LOG) - .expect("could not open logfile"); + { + Ok(log_file) => { + let config = ConfigBuilder::new() + .set_time_format_str("%Y-%m-%dT%T%:z") + .set_time_to_local(true) + .build(); - let config = ConfigBuilder::new() - .set_time_format_str("%Y-%m-%dT%T%:z") - .set_time_to_local(true) - .build(); - - simplelog::WriteLogger::init(level, config, log_file).expect("could not initialize logger"); + simplelog::WriteLogger::init(level, config, log_file) + .expect("could not initialize logger"); + } + Err(err) => { + init_stdout_logger(); + log::error!( + "could not open log file, defaulting to stdout logging: {}", + err + ); + } + } } } +fn init_stdout_logger() { + Builder::new() + .parse_default_env() + .format(|buf, record| { + use env_logger::fmt::Color; + + let subtle = buf + .style() + .set_color(Color::Black) + .set_intense(true) + .clone(); + write!(buf, "{}", subtle.value("["))?; + write!( + buf, + "{} ", + chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z") + )?; + write!(buf, "{:<5}", buf.default_styled_level(record.level()))?; + if let Some(path) = record.module_path() { + write!(buf, " {}", path)?; + } + write!(buf, "{}", subtle.value("]"))?; + writeln!(buf, " {}", record.args()) + }) + .init(); +} + #[derive(Serialize, Deserialize)] struct LocationData { file: String, @@ -634,7 +662,7 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin let panic_data = Panic { thread: thread_name.into(), - payload: payload.into(), + payload, location_data: info.location().map(|location| LocationData { file: location.file().into(), line: location.line(), @@ -677,27 +705,31 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin })); } -fn upload_panics_and_crashes(http: Arc, cx: &mut AppContext) { +fn upload_panics_and_crashes(http: Arc, cx: &mut AppContext) { let telemetry_settings = *client::TelemetrySettings::get_global(cx); cx.background_executor() .spawn(async move { - upload_previous_panics(http.clone(), telemetry_settings) + let most_recent_panic = upload_previous_panics(http.clone(), telemetry_settings) .await - .log_err(); - upload_previous_crashes(http, telemetry_settings) + .log_err() + .flatten(); + upload_previous_crashes(http, most_recent_panic, telemetry_settings) .await .log_err() }) .detach() } -/// upload panics to us (via zed.dev) +/// Uploads panics via `zed.dev`. async fn upload_previous_panics( - http: Arc, + http: Arc, telemetry_settings: client::TelemetrySettings, -) -> Result<()> { - let panic_report_url = http.zed_url("/api/panic"); +) -> Result> { + let panic_report_url = http.build_url("/api/panic"); let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?; + + let mut most_recent_panic = None; + while let Some(child) = children.next().await { let child = child?; let child_path = child.path(); @@ -720,7 +752,7 @@ async fn upload_previous_panics( .await .context("error reading panic file")?; - let panic = serde_json::from_str(&panic_file_content) + let panic: Option = serde_json::from_str(&panic_file_content) .ok() .or_else(|| { panic_file_content @@ -734,6 +766,8 @@ async fn upload_previous_panics( }); if let Some(panic) = panic { + most_recent_panic = Some((panic.panicked_on, panic.payload.clone())); + let body = serde_json::to_string(&PanicRequest { panic }).unwrap(); let request = Request::post(&panic_report_url) @@ -752,7 +786,7 @@ async fn upload_previous_panics( .context("error removing panic") .log_err(); } - Ok::<_, anyhow::Error>(()) + Ok::<_, anyhow::Error>(most_recent_panic) } static LAST_CRASH_UPLOADED: &'static str = "LAST_CRASH_UPLOADED"; @@ -760,7 +794,8 @@ static LAST_CRASH_UPLOADED: &'static str = "LAST_CRASH_UPLOADED"; /// upload crashes from apple's diagnostic reports to our server. /// (only if telemetry is enabled) async fn upload_previous_crashes( - http: Arc, + http: Arc, + most_recent_panic: Option<(i64, String)>, telemetry_settings: client::TelemetrySettings, ) -> Result<()> { if !telemetry_settings.diagnostics { @@ -771,7 +806,7 @@ async fn upload_previous_crashes( .unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this. let mut uploaded = last_uploaded.clone(); - let crash_report_url = http.zed_url("/api/crash"); + let crash_report_url = http.build_zed_api_url("/telemetry/crashes"); for dir in [&*CRASHES_DIR, &*CRASHES_RETIRED_DIR] { let mut children = smol::fs::read_dir(&dir).await?; @@ -797,10 +832,17 @@ async fn upload_previous_crashes( .await .context("error reading crash file")?; - let request = Request::post(&crash_report_url) + let mut request = Request::post(&crash_report_url) .redirect_policy(isahc::config::RedirectPolicy::Follow) - .header("Content-Type", "text/plain") - .body(body.into())?; + .header("Content-Type", "text/plain"); + + if let Some((panicked_on, payload)) = most_recent_panic.as_ref() { + request = request + .header("x-zed-panicked-on", format!("{}", panicked_on)) + .header("x-zed-panic", payload) + } + + let request = request.body(body.into())?; let response = http.send(request).await.context("error sending crash")?; if !response.status().is_success() { @@ -824,8 +866,29 @@ async fn load_login_shell_environment() -> Result<()> { let shell = env::var("SHELL").context( "SHELL environment variable is not assigned so we can't source login environment variables", )?; + + // If possible, we want to `cd` in the user's `$HOME` to trigger programs + // such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook + // into shell's `cd` command (and hooks) to manipulate env. + // We do this so that we get the env a user would have when spawning a shell + // in home directory. + let shell_cmd_prefix = std::env::var_os("HOME") + .and_then(|home| home.into_string().ok()) + .map(|home| format!("cd {home};")); + + // The `exit 0` is the result of hours of debugging, trying to find out + // why running this command here, without `exit 0`, would mess + // up signal process for our process so that `ctrl-c` doesn't work + // anymore. + // We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would + // do that, but it does, and `exit 0` helps. + let shell_cmd = format!( + "{}echo {marker}; /usr/bin/env -0; exit 0;", + shell_cmd_prefix.as_deref().unwrap_or("") + ); + let output = Command::new(&shell) - .args(["-l", "-i", "-c", &format!("echo {marker}; /usr/bin/env -0")]) + .args(["-l", "-i", "-c", &shell_cmd]) .output() .await .context("failed to spawn login shell to source login environment variables")?; @@ -858,13 +921,13 @@ fn stdout_is_a_pty() -> bool { std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal() } -fn collect_url_args() -> Vec { +fn collect_url_args(cx: &AppContext) -> Vec { env::args() .skip(1) .filter_map(|arg| match std::fs::canonicalize(Path::new(&arg)) { Ok(path) => Some(format!("file://{}", path.to_string_lossy())), Err(error) => { - if let Some(_) = parse_zed_link(&arg) { + if let Some(_) = parse_zed_link(&arg, cx) { Some(arg) } else { log::error!("error parsing path argument: {}", error); @@ -933,7 +996,7 @@ fn load_user_themes_in_background(fs: Arc, cx: &mut AppContext) { .detach_and_log_err(cx); } -//todo!(linux): Port fsevents to linux +// todo(linux): Port fsevents to linux /// Spawns a background task to watch the themes directory for changes. #[cfg(target_os = "macos")] fn watch_themes(fs: Arc, cx: &mut AppContext) { diff --git a/crates/zed/src/open_listener.rs b/crates/zed/src/open_listener.rs index d6fec52df8..41dfe7e432 100644 --- a/crates/zed/src/open_listener.rs +++ b/crates/zed/src/open_listener.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Context, Result}; use cli::{ipc, IpcHandshake}; use cli::{ipc::IpcSender, CliRequest, CliResponse}; +use client::parse_zed_link; use collections::HashMap; use editor::scroll::Autoscroll; use editor::Editor; @@ -10,7 +11,6 @@ use futures::{FutureExt, SinkExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Global}; use itertools::Itertools; use language::{Bias, Point}; -use release_channel::parse_zed_link; use std::path::Path; use std::sync::atomic::Ordering; use std::sync::Arc; @@ -66,13 +66,13 @@ impl OpenListener { ) } - pub fn open_urls(&self, urls: &[String]) { + pub fn open_urls(&self, urls: &[String], cx: &AppContext) { self.triggered.store(true, Ordering::Release); let request = if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { self.handle_cli_connection(server_name) - } else if let Some(request_path) = urls.first().and_then(|url| parse_zed_link(url)) { + } else if let Some(request_path) = urls.first().and_then(|url| parse_zed_link(url, cx)) { self.handle_zed_url_scheme(request_path) } else { self.handle_file_urls(urls) @@ -95,10 +95,10 @@ impl OpenListener { } fn handle_zed_url_scheme(&self, request_path: &str) -> Option { - let mut parts = request_path.split("/"); + let mut parts = request_path.split('/'); if parts.next() == Some("channel") { if let Some(slug) = parts.next() { - if let Some(id_str) = slug.split("-").last() { + if let Some(id_str) = slug.split('-').last() { if let Ok(channel_id) = id_str.parse::() { let Some(next) = parts.next() else { return Some(OpenRequest::JoinChannel { channel_id }); @@ -184,7 +184,7 @@ pub async fn handle_cli_connection( } else { paths .into_iter() - .filter_map(|path_with_position_string| { + .map(|path_with_position_string| { let path_with_position = PathLikeWithPosition::parse_str( &path_with_position_string, |path_str| { @@ -203,7 +203,7 @@ pub async fn handle_cli_connection( caret_positions.insert(path.clone(), Point::new(row, col)); } } - Some(path) + path }) .collect() }; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c9205dc8f3..ca28267472 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,47 +1,49 @@ mod app_menus; -pub mod languages; mod only_instance; mod open_listener; pub use app_menus::*; use assistant::AssistantPanel; use breadcrumbs::Breadcrumbs; +use client::ZED_URL_SCHEME; use collections::VecDeque; use editor::{Editor, MultiBuffer}; use gpui::{ - actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions, View, - ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions, + actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, PromptLevel, + TitlebarOptions, View, ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions, }; pub use only_instance::*; pub use open_listener::*; -use anyhow::{anyhow, Context as _}; +use anyhow::Context as _; use assets::Assets; use futures::{channel::mpsc, select_biased, StreamExt}; +use project::TaskSourceKind; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; -use runnable::static_source::StaticSource; use search::project_search::ProjectSearchBar; use settings::{ - initial_local_settings_content, watch_config_file, KeymapFile, Settings, SettingsStore, + initial_local_settings_content, initial_tasks_content, watch_config_file, KeymapFile, Settings, + SettingsStore, DEFAULT_KEYMAP_PATH, }; use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc}; +use task::{oneshot_source::OneshotSource, static_source::StaticSource}; use terminal_view::terminal_panel::{self, TerminalPanel}; use util::{ asset_str, - paths::{self, LOCAL_SETTINGS_RELATIVE_PATH}, + paths::{self, LOCAL_SETTINGS_RELATIVE_PATH, LOCAL_TASKS_RELATIVE_PATH}, ResultExt, }; use uuid::Uuid; use vim::VimModeSetting; use welcome::BaseKeymap; -use workspace::Pane; use workspace::{ create_and_open_local_file, notifications::simple_message_notification::MessageNotification, - open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings, + open_new, AppState, NewFile, NewWindow, Toast, Workspace, WorkspaceSettings, }; +use workspace::{notifications::DetachAndPromptErr, Pane}; use zed_actions::{OpenBrowser, OpenSettings, OpenZedUrl, Quit}; actions!( @@ -57,10 +59,11 @@ actions!( OpenDefaultKeymap, OpenDefaultSettings, OpenKeymap, - OpenRunnables, OpenLicenses, OpenLocalSettings, + OpenLocalTasks, OpenLog, + OpenTasks, OpenTelemetryLog, ResetBufferFontSize, ResetDatabase, @@ -94,7 +97,7 @@ pub fn build_window_options( titlebar: Some(TitlebarOptions { title: None, appears_transparent: true, - traffic_light_position: Some(point(px(8.), px(8.))), + traffic_light_position: Some(point(px(9.5), px(9.5))), }), center: false, focus: false, @@ -142,8 +145,6 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { auto_update::notify_of_any_new_update(cx); - vim::observe_keystrokes(cx); - let handle = cx.view().downgrade(); cx.on_window_should_close(move |cx| { handle @@ -157,16 +158,27 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { let project = workspace.project().clone(); if project.read(cx).is_local() { - let runnables_file_rx = watch_config_file( - &cx.background_executor(), - app_state.fs.clone(), - paths::RUNNABLES.clone(), - ); - let source = StaticSource::new(runnables_file_rx, cx); project.update(cx, |project, cx| { - project - .runnable_inventory() - .update(cx, |inventory, cx| inventory.add_source(source, cx)) + let fs = app_state.fs.clone(); + project.task_inventory().update(cx, |inventory, cx| { + inventory.add_source( + TaskSourceKind::UserInput, + |cx| OneshotSource::new(cx), + cx, + ); + inventory.add_source( + TaskSourceKind::AbsPath(paths::TASKS.clone()), + |cx| { + let tasks_file_rx = watch_config_file( + &cx.background_executor(), + fs, + paths::TASKS.clone(), + ); + StaticSource::new("global_tasks", tasks_file_rx, cx) + }, + cx, + ); + }) }); } cx.spawn(|workspace_handle, mut cx| async move { @@ -221,7 +233,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.toggle_full_screen(); }) .register_action(|_, action: &OpenZedUrl, cx| { - OpenListener::global(cx).open_urls(&[action.url.clone()]) + OpenListener::global(cx).open_urls(&[action.url.clone()], cx) }) .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url)) .register_action(move |_, _: &IncreaseBufferFontSize, cx| { @@ -232,12 +244,50 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }) .register_action(move |_, _: &ResetBufferFontSize, cx| theme::reset_font_size(cx)) .register_action(|_, _: &install_cli::Install, cx| { - cx.spawn(|_, cx| async move { - install_cli::install_cli(cx.deref()) + cx.spawn(|workspace, mut cx| async move { + let path = install_cli::install_cli(cx.deref()) .await - .context("error creating CLI symlink") + .context("error creating CLI symlink")?; + workspace.update(&mut cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + 0, + format!( + "Installed `zed` to {}. You can launch {} from your terminal.", + path.to_string_lossy(), + ReleaseChannel::global(cx).display_name() + ), + ), + cx, + ) + })?; + register_zed_scheme(&cx).await.log_err(); + Ok(()) }) - .detach_and_log_err(cx); + .detach_and_prompt_err("Error installing zed cli", cx, |_, _| None); + }) + .register_action(|_, _: &install_cli::RegisterZedScheme, cx| { + cx.spawn(|workspace, mut cx| async move { + register_zed_scheme(&cx).await?; + workspace.update(&mut cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + 0, + format!( + "zed:// links will now open in {}.", + ReleaseChannel::global(cx).display_name() + ), + ), + cx, + ) + })?; + Ok(()) + }) + .detach_and_prompt_err( + "Error registering zed:// scheme", + cx, + |_, _| None, + ); }) .register_action(|workspace, _: &OpenLog, cx| { open_log_file(workspace, cx); @@ -273,15 +323,16 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }, ) .register_action( - move |_: &mut Workspace, _: &OpenRunnables, cx: &mut ViewContext| { + move |_: &mut Workspace, _: &OpenTasks, cx: &mut ViewContext| { open_settings_file( - &paths::RUNNABLES, - || settings::initial_runnables_content().as_ref().into(), + &paths::TASKS, + || settings::initial_tasks_content().as_ref().into(), cx, ); }, ) .register_action(open_local_settings_file) + .register_action(open_local_tasks_file) .register_action( move |workspace: &mut Workspace, _: &OpenDefaultKeymap, @@ -397,9 +448,9 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View, cx: &mut ViewCo } fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext) { - let app_name = ReleaseChannel::global(cx).display_name(); + let release_channel = ReleaseChannel::global(cx).display_name(); let version = env!("CARGO_PKG_VERSION"); - let message = format!("{app_name} {version}"); + let message = format!("{release_channel} {version}"); let detail = AppCommitSha::try_global(cx).map(|sha| sha.0.clone()); let prompt = cx.prompt(PromptLevel::Info, &message, detail.as_deref(), &["OK"]); @@ -422,8 +473,8 @@ fn quit(_: &Quit, cx: &mut AppContext) { // If multiple windows have unsaved changes, and need a save prompt, // prompt in the active window before switching to a different window. - cx.update(|cx| { - workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); + cx.update(|mut cx| { + workspace_windows.sort_by_key(|window| window.is_active(&mut cx) == Some(false)); }) .log_err(); @@ -474,25 +525,42 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { cx.spawn(|workspace, mut cx| async move { let (old_log, new_log) = futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG)); - - let mut lines = VecDeque::with_capacity(MAX_LINES); - for line in old_log - .iter() - .flat_map(|log| log.lines()) - .chain(new_log.iter().flat_map(|log| log.lines())) - { - if lines.len() == MAX_LINES { - lines.pop_front(); + let log = match (old_log, new_log) { + (Err(_), Err(_)) => None, + (old_log, new_log) => { + let mut lines = VecDeque::with_capacity(MAX_LINES); + for line in old_log + .iter() + .flat_map(|log| log.lines()) + .chain(new_log.iter().flat_map(|log| log.lines())) + { + if lines.len() == MAX_LINES { + lines.pop_front(); + } + lines.push_back(line); + } + Some( + lines + .into_iter() + .flat_map(|line| [line, "\n"]) + .collect::(), + ) } - lines.push_back(line); - } - let log = lines - .into_iter() - .flat_map(|line| [line, "\n"]) - .collect::(); + }; workspace .update(&mut cx, |workspace, cx| { + let Some(log) = log else { + workspace.show_notification(29, cx, |cx| { + cx.new_view(|_| { + MessageNotification::new(format!( + "Unable to access/open log file at path {:?}", + paths::LOG.as_path() + )) + }) + }); + return; + }; let project = workspace.project().clone(); let buffer = project .update(cx, |project, cx| project.create_buffer("", None, cx)) @@ -502,7 +570,7 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { let buffer = cx.new_model(|cx| { MultiBuffer::singleton(buffer, cx).with_title("Log".into()) }); - workspace.add_item( + workspace.add_item_to_active_pane( Box::new( cx.new_view(|cx| { Editor::for_multibuffer(buffer, Some(project), cx) @@ -532,7 +600,7 @@ pub fn handle_keymap_file_changes( let new_base_keymap = *BaseKeymap::get_global(cx); let new_vim_enabled = VimModeSetting::get_global(cx).0; if new_base_keymap != old_base_keymap || new_vim_enabled != old_vim_enabled { - old_base_keymap = new_base_keymap.clone(); + old_base_keymap = new_base_keymap; old_vim_enabled = new_vim_enabled; base_keymap_tx.unbounded_send(()).unwrap(); } @@ -570,7 +638,7 @@ fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) { } pub fn load_default_keymap(cx: &mut AppContext) { - KeymapFile::load_asset("keymaps/default.json", cx).unwrap(); + KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap(); if VimModeSetting::get_global(cx).0 { KeymapFile::load_asset("keymaps/vim.json", cx).unwrap(); } @@ -584,6 +652,33 @@ fn open_local_settings_file( workspace: &mut Workspace, _: &OpenLocalSettings, cx: &mut ViewContext, +) { + open_local_file( + workspace, + &LOCAL_SETTINGS_RELATIVE_PATH, + initial_local_settings_content(), + cx, + ) +} + +fn open_local_tasks_file( + workspace: &mut Workspace, + _: &OpenLocalTasks, + cx: &mut ViewContext, +) { + open_local_file( + workspace, + &LOCAL_TASKS_RELATIVE_PATH, + initial_tasks_content(), + cx, + ) +} + +fn open_local_file( + workspace: &mut Workspace, + settings_relative_path: &'static Path, + initial_contents: Cow<'static, str>, + cx: &mut ViewContext, ) { let project = workspace.project().clone(); let worktree = project @@ -593,9 +688,7 @@ fn open_local_settings_file( if let Some(worktree) = worktree { let tree_id = worktree.read(cx).id(); cx.spawn(|workspace, mut cx| async move { - let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH; - - if let Some(dir_path) = file_path.parent() { + if let Some(dir_path) = settings_relative_path.parent() { if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? { project .update(&mut cx, |project, cx| { @@ -606,10 +699,12 @@ fn open_local_settings_file( } } - if worktree.update(&mut cx, |tree, _| tree.entry_for_path(file_path).is_none())? { + if worktree.update(&mut cx, |tree, _| { + tree.entry_for_path(settings_relative_path).is_none() + })? { project .update(&mut cx, |project, cx| { - project.create_entry((tree_id, file_path), false, cx) + project.create_entry((tree_id, settings_relative_path), false, cx) })? .await .context("worktree was removed")?; @@ -617,11 +712,11 @@ fn open_local_settings_file( let editor = workspace .update(&mut cx, |workspace, cx| { - workspace.open_path((tree_id, file_path), None, true, cx) + workspace.open_path((tree_id, settings_relative_path), None, true, cx) })? .await? .downcast::() - .ok_or_else(|| anyhow!("unexpected item type"))?; + .context("unexpected item type: expected editor item")?; editor .downgrade() @@ -629,7 +724,7 @@ fn open_local_settings_file( if let Some(buffer) = editor.buffer().read(cx).as_singleton() { if buffer.read(cx).is_empty() { buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, initial_local_settings_content())], None, cx) + buffer.edit([(0..0, initial_contents)], None, cx) }); } } @@ -691,7 +786,7 @@ fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext Arc { @@ -2787,10 +2884,10 @@ mod tests { )) } #[track_caller] - fn assert_key_bindings_for<'a>( + fn assert_key_bindings_for( window: AnyWindowHandle, cx: &TestAppContext, - actions: Vec<(&'static str, &'a dyn Action)>, + actions: Vec<(&'static str, &dyn Action)>, line: u32, ) { let available_actions = cx @@ -2823,3 +2920,8 @@ mod tests { } } } + +async fn register_zed_scheme(cx: &AsyncAppContext) -> anyhow::Result<()> { + cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME))? + .await +} diff --git a/docs/how-to-deploy.md b/docs/how-to-deploy.md index c32d3619a6..b1561c701f 100644 --- a/docs/how-to-deploy.md +++ b/docs/how-to-deploy.md @@ -2,9 +2,9 @@ These docs are intendended to replace both docs.zed.dev and introduce people to 1. `cd docs` from repo root 1. Install the vercel cli if you haven't already - - `pnpm i -g vercel` + - `pnpm i -g vercel` 1. `vercel` to deploy if you already have the project linked 1. Otherwise, `vercel login` and `vercel` to link - - Choose Zed Industries as the team, then `zed-app-docs` as the project + - Choose Zed Industries as the team, then `zed-app-docs` as the project Someone can write a script for this when they have time. diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 399fbb8981..0935366be4 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -14,11 +14,14 @@ - [Workflows]() - [Collaboration]() - [Using AI]() +- [Tasks](./tasks.md) # Contributing to Zed - [How to Contribute]() - [Building from Source](./developing_zed__building_zed.md) + - [macOS](./developing_zed__building_zed_macos.md) + - [Linux](./developing_zed__building_zed_linux.md) - [Local Collaboration](./developing_zed__local_collaboration.md) - [Adding Languages](./developing_zed__adding_languages.md) - [Adding UI]() diff --git a/docs/src/configuring_zed.md b/docs/src/configuring_zed.md index 006a448fc9..b2ae9ab86a 100644 --- a/docs/src/configuring_zed.md +++ b/docs/src/configuring_zed.md @@ -245,8 +245,8 @@ To override settings for a language, add an entry for that language server's nam "lsp": { "rust-analyzer": { "initialization_options": { - "checkOnSave": { - "command": "clippy" // rust-analyzer.checkOnSave.command + "check": { + "command": "clippy" // rust-analyzer.check.command (default: "check") } } } @@ -306,7 +306,72 @@ To override settings for a language, add an entry for that language server's nam } ``` +## Code Actions On Format + +- Description: The code actions to perform with the primary language server when formatting the buffer. +- Setting: `code_actions_on_format` +- Default: `{}`, except for Go it's `{ "source.organizeImports": true }` + +**Examples** + +1. Organize imports on format in TypeScript and TSX buffers: + +```json +{ + "languages": { + "TypeScript": { + "code_actions_on_format": { + "source.organizeImports": true + } + }, + "TSX": { + "code_actions_on_format": { + "source.organizeImports": true + } + } + } +} +``` + +2. Run ESLint `fixAll` code action when formatting (requires Zed `0.125.0`): + +```json +{ + "languages": { + "JavaScript": { + "code_actions_on_format": { + "source.fixAll.eslint": true + } + } + } +} +``` + +3. Run only a single ESLint rule when using `fixAll` (requires Zed `0.125.0`): + +```json +{ + "languages": { + "JavaScript": { + "code_actions_on_format": { + "source.fixAll.eslint": true + } + } + }, + "lsp": { + "eslint": { + "settings": { + "codeActionOnSave": { + "rules": ["import/order"] + } + } + } + } +} +``` + ## Auto close + - Description: Whether or not to automatically type closing characters for you. - Setting: `use_autoclose` - Default: `true` @@ -382,7 +447,9 @@ To override settings for a language, add an entry for that language server's nam "enabled": false, "show_type_hints": true, "show_parameter_hints": true, - "show_other_hints": true + "show_other_hints": true, + "edit_debounce_ms": 700, + "scroll_debounce_ms": 50 } ``` @@ -392,93 +459,17 @@ Inlay hints querying consists of two parts: editor (client) and LSP server. With the inlay settings above are changed to enable the hints, editor will start to query certain types of hints and react on LSP hint refresh request from the server. At this point, the server may or may not return hints depending on its implementation, further configuration might be needed, refer to the corresponding LSP server documentation. -Use `lsp` section for the server configuration, below are some examples for well known servers: +The following languages have inlay hints preconfigured by Zed: -### Rust +- [Go](https://docs.zed.dev/languages/go) +- [Rust](https://docs.zed.dev/languages/rust) +- [Svelte](https://docs.zed.dev/languages/svelte) +- [Typescript](https://docs.zed.dev/languages/typescript) -```json -"lsp": { - "rust-analyzer": { - "initialization_options": { - "inlayHints": { - "maxLength": null, - "lifetimeElisionHints": { - "useParameterNames": true, - "enable": "skip_trivial" - }, - "closureReturnTypeHints": { - "enable": "always" - } - } - } - } -} -``` +Use the `lsp` section for the server configuration. Examples are provided in the corresponding language documentation. -### Typescript - -```json -"lsp": { - "typescript-language-server": { - "initialization_options": { - "preferences": { - "includeInlayParameterNameHints": "all", - "includeInlayParameterNameHintsWhenArgumentMatchesName": true, - "includeInlayFunctionParameterTypeHints": true, - "includeInlayVariableTypeHints": true, - "includeInlayVariableTypeHintsWhenTypeMatchesName": false, - "includeInlayPropertyDeclarationTypeHints": true, - "includeInlayFunctionLikeReturnTypeHints": true, - "includeInlayEnumMemberValueHints": true - } - } - } -} -``` - -### Go - -```json -"lsp": { - "gopls": { - "initialization_options": { - "hints": { - "assignVariableTypes": true, - "compositeLiteralFields": true, - "compositeLiteralTypes": true, - "constantValues": true, - "functionTypeParameters": true, - "parameterNames": true, - "rangeVariableTypes": true - } - } - } -} -``` - -### Svelte - -```json -{ - "lsp": { - "typescript-language-server": { - "initialization_options": { - "preferences": { - "includeInlayParameterNameHints": "all", - "includeInlayParameterNameHintsWhenArgumentMatchesName": true, - "includeInlayFunctionParameterTypeHints": true, - "includeInlayVariableTypeHints": true, - "includeInlayVariableTypeHintsWhenTypeMatchesName": false, - "includeInlayPropertyDeclarationTypeHints": true, - "includeInlayFunctionLikeReturnTypeHints": true, - "includeInlayEnumMemberValueHints": true, - "includeInlayEnumMemberDeclarationTypes": true - } - } - } - } -} -``` +Hints are not instantly queried in Zed, two kinds of debounces are used, either may be set to 0 to be disabled. +Settings-related hint updates are not debounced. ## Journal @@ -754,6 +745,9 @@ These values take in the same options as the root-level settings with the same n "font_size": null, "option_as_meta": false, "shell": {}, + "toolbar": { + "title": true + }, "working_directory": "current_project_directory" } ``` @@ -912,6 +906,22 @@ See Buffer Font Features } ``` +## Terminal Toolbar + +- Description: Whether or not to show various elements in the terminal toolbar. It only affects terminals placed in the editor pane. +- Setting: `toolbar` +- Default: + +```json +"toolbar": { + "title": true, +}, +``` + +**Options** + +At the moment, only the `title` option is available, it controls displaying of the terminal title that can be changed via `PROMPT_COMMAND`. If the title is hidden, the terminal toolbar is not displayed. + ### Working Directory - Description: What working directory to use when launching the terminal. @@ -956,10 +966,72 @@ See Buffer Font Features ## Theme -- Description: The name of the Zed theme to use for the UI. +- Description: The theme setting can be specified in two forms - either as the name of a theme or as an object containing the `mode`, `dark`, and `light` themes for the Zed UI. - Setting: `theme` - Default: `One Dark` +### Theme Object + +- Description: Specify the theme using an object that includes the `mode`, `dark`, and `light` themes. +- Setting: `theme` +- Default: + +```json +"theme": { + "mode": "dark", + "dark": "One Dark", + "light": "One Light" +}, +``` + +### Mode + +- Description: Specify theme mode. +- Setting: `mode` +- Default: `dark` + +**Options** + +1. Set the theme to dark mode + +```json +{ + "mode": "dark" +} +``` + +2. Set the theme to light mode + +```json +{ + "mode": "light" +} +``` + +3. Set the theme to system mode + +```json +{ + "mode": "system" +} +``` + +### Dark + +- Description: The name of the dark Zed theme to use for the UI. +- Setting: `dark` +- Default: `One Dark` + +**Options** + +Run the `theme selector: toggle` action in the command palette to see a current list of valid themes names. + +### Light + +- Description: The name of the light Zed theme to use for the UI. +- Setting: `light` +- Default: `One Light` + **Options** Run the `theme selector: toggle` action in the command palette to see a current list of valid themes names. diff --git a/docs/src/configuring_zed__configuring_vim.md b/docs/src/configuring_zed__configuring_vim.md index a4ae1e6be5..4ab0a9da9c 100644 --- a/docs/src/configuring_zed__configuring_vim.md +++ b/docs/src/configuring_zed__configuring_vim.md @@ -144,6 +144,23 @@ Currently supported vim-specific commands (as of Zed 0.106): to sort the current selection (with i, case-insensitively) ``` +## Vim settings + +Some vim settings are available to modify the default vim behavior: + +```json +{ + "vim": { + // "always": use system clipboard + // "never": don't use system clipboard + // "on_yank": use system clipboard for yank operations + "use_system_clipboard": "always", + // Enable multi-line find for `f` and `t` motions + "use_multiline_find": false + } +} +``` + ## Related settings There are a few Zed settings that you may also enjoy if you use vim mode: diff --git a/docs/src/configuring_zed__key_bindings.md b/docs/src/configuring_zed__key_bindings.md index 60f84965c1..186534432c 100644 --- a/docs/src/configuring_zed__key_bindings.md +++ b/docs/src/configuring_zed__key_bindings.md @@ -8,7 +8,6 @@ A selection of base keymaps is available in the welcome screen under the "Choose Additionally, you can change the base keymap from the command palette - `⌘-Shift-P` - by selecting the "welcome: toggle base keymap selector" command. - ## Custom key bindings ### Accessing custom key bindings @@ -39,24 +38,25 @@ You can see more examples in Zed's [`default.json`](https://zed.dev/ref/default. _There are some key bindings that can't be overridden; we are working on an issue surrounding this._ ## Special Keyboard Layouts + Some people have unique and custom keyboard layouts. For example, [@TomPlanche](https://github.com/TomPlanche) having a [French keyboard](https%3A%2F%2Fcdn.shopify.com%2Fs%2Ffiles%2F1%2F0810%2F3669%2Ffiles%2Ffrench-azerty-mac-keyboard-layout-2021-keyshorts.png&f=1&nofb=1&ipt=f53a06c5e60a20b621082410aa699c8cceff269a11ff90b3b5a35c6124dbf827&ipo=images), had to type `Shift-Alt-(` in order to have a simple `[` so he made a simple layout with those 'rules': `ΓΉ -> [`, `backtick -> ]`, `Alt-[ (where [ is the old ΓΉ) -> {`, `Alt-] -> }`. But, it was impossible to take into account the `{` and `}` when he was typing so now, in order to ignore a binding, he can add `null` to the binding: + ```json [ - { - "context": "Editor", - "bindings": { - "alt-[": null, - "alt-]": null - } + { + "context": "Editor", + "bindings": { + "alt-[": null, + "alt-]": null } + } ] ``` - ## All key bindings ### Global diff --git a/docs/src/developing_zed__building_zed.md b/docs/src/developing_zed__building_zed.md index 6eb93ff071..fcb7e231d8 100644 --- a/docs/src/developing_zed__building_zed.md +++ b/docs/src/developing_zed__building_zed.md @@ -1,89 +1,6 @@ -# Building Zed +# Building from Source -## Repository +See the platform-specific instructions for building Zed from source: -After cloning the repository, ensure all git submodules are initialized: - -```shell -git submodule update --init --recursive -``` - -## Dependencies - -- Install [Rust](https://www.rust-lang.org/tools/install) -- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account. - -> Ensure you launch XCode after installing, and install the MacOS components, which is the default option. - -- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) - - ```bash - xcode-select --install - ``` - -- Ensure that the Xcode command line tools are using your newly installed copy of Xcode: - - ``` - sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer - ``` - -* Install the Rust wasm toolchain: - - ```bash - rustup target add wasm32-wasi - ``` - -## Backend Dependencies - -If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server: - -- Install [Postgres](https://postgresapp.com) -- Install [Livekit](https://formulae.brew.sh/formula/livekit) and [Foreman](https://formulae.brew.sh/formula/foreman) - - ```bash - brew install livekit foreman - ``` - -Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose: - -```sh -docker compose up -d -``` - -## Building Zed from Source - -Once you have the dependencies installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/). - -For a debug build: - -``` -cargo run -``` - -For a release build: - -``` -cargo run --release -``` - -And to run the tests: - -``` -cargo test --workspace -``` - -## Troubleshooting - -### Error compiling metal shaders - -``` -error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)`** - -xcrun: error: unable to find utility "metal", not a developer tool or in PATH -``` - -Try `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer` - -### Cargo errors claiming that a dependency is using unstable features - -Try `cargo clean` and `cargo build`. +- [macOS](./developing_zed__building_zed_macos.md) +- [Linux](./developing_zed__building_zed_linux.md) diff --git a/docs/src/developing_zed__building_zed_linux.md b/docs/src/developing_zed__building_zed_linux.md new file mode 100644 index 0000000000..394fe4bd83 --- /dev/null +++ b/docs/src/developing_zed__building_zed_linux.md @@ -0,0 +1,89 @@ +# Building Zed for Linux + +## Repository + +After cloning the repository, ensure all git submodules are initialized: + +```shell +git submodule update --init --recursive +``` + +## Dependencies + +- Install [Rust](https://www.rust-lang.org/tools/install). If it's already installed, make sure it's up-to-date: + + ```bash + rustup update + ``` + +- Install the Rust wasm toolchain: + + ```bash + rustup target add wasm32-wasi + ``` + +- Install the necessary system libraries: + + ```bash + script/linux + ``` + + If you prefer to install the system libraries manually, you can find the list of required packages in the `script/linux` file. + +## Backend dependencies + +> [!WARNING] +> This section is still in development. The instructions are not yet complete. + +If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server: + +- Install [Postgres](https://www.postgresql.org/download/linux/) +- Install [Livekit](https://github.com/livekit/livekit-cli) and [Foreman](https://theforeman.org/manuals/3.9/quickstart_guide.html) + +Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose: + +```sh +docker compose up -d +``` + +## Building from source + +Once you have the dependencies installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/). + +For a debug build: + +``` +cargo run +``` + +For a release build: + +``` +cargo run --release +``` + +And to run the tests: + +``` +cargo test --workspace +``` + +## Wayland & X11 + +Zed has basic support for both modes. The mode is selected at runtime. If you're on wayland and want to run in X11 mode, you can set `WAYLAND_DISPLAY='' cargo run` to do so. + +## Troubleshooting + +### Can't compile zed + +Before reporting the issue, make sure that you have the latest rustc version with `rustup update`. + +### Cargo errors claiming that a dependency is using unstable features + +Try `cargo clean` and `cargo build`. + +### Vulkan/GPU issues + +If Zed crashes at runtime due to GPU or vulkan issues, you can try running [vkcube](https://github.com/krh/vkcube) (usually available as part of the `vulkaninfo` package on various distributions) to try to troubleshoot where the issue is coming from. Try running in both X11 and wayland modes by running `vkcube -m [x11|wayland]`. Some versions of `vkcube` use `vkcube` to run in X11 and `vkcube-wayland` to run in wayland. + +If you have multiple GPUs, you can also try running Zed on a different one (for example, with [vkdevicechooser](https://github.com/jiriks74/vkdevicechooser)) to figure out where the issue comes from. diff --git a/docs/src/developing_zed__building_zed_macos.md b/docs/src/developing_zed__building_zed_macos.md new file mode 100644 index 0000000000..ea3a981e26 --- /dev/null +++ b/docs/src/developing_zed__building_zed_macos.md @@ -0,0 +1,89 @@ +# Building Zed for macOS + +## Repository + +After cloning the repository, ensure all git submodules are initialized: + +```shell +git submodule update --init --recursive +``` + +## Dependencies + +- Install [Rust](https://www.rust-lang.org/tools/install) +- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account. + +> Ensure you launch XCode after installing, and install the MacOS components, which is the default option. + +- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) + + ```bash + xcode-select --install + ``` + +- Ensure that the Xcode command line tools are using your newly installed copy of Xcode: + + ``` + sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer + ``` + +* Install the Rust wasm toolchain: + + ```bash + rustup target add wasm32-wasi + ``` + +## Backend Dependencies + +If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server: + +- Install [Postgres](https://postgresapp.com) +- Install [Livekit](https://formulae.brew.sh/formula/livekit) and [Foreman](https://formulae.brew.sh/formula/foreman) + + ```bash + brew install livekit foreman + ``` + +Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose: + +```sh +docker compose up -d +``` + +## Building Zed from Source + +Once you have the dependencies installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/). + +For a debug build: + +``` +cargo run +``` + +For a release build: + +``` +cargo run --release +``` + +And to run the tests: + +``` +cargo test --workspace +``` + +## Troubleshooting + +### Error compiling metal shaders + +``` +error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)`** + +xcrun: error: unable to find utility "metal", not a developer tool or in PATH +``` + +Try `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer` + +### Cargo errors claiming that a dependency is using unstable features + +Try `cargo clean` and `cargo build`. diff --git a/docs/src/developing_zed__debugging_crashes.md b/docs/src/developing_zed__debugging_crashes.md new file mode 100644 index 0000000000..085f927d24 --- /dev/null +++ b/docs/src/developing_zed__debugging_crashes.md @@ -0,0 +1,27 @@ +## Crashes + +When an app crashes, macOS creates a `.ips` file in `~/Library/Logs/DiagnosticReports`. You can view these using the built in Console app (`cmd-space Console`) under "Crash Reports". + +If you have enabeld Zed's telemetry these will be uploaded to us when you restart the app. They end up in Datadog, and a [Slack channel (internal only)](https://zed-industries.slack.com/archives/C04S6T1T7TQ). + +These crash reports are generated by the crashing binary, and contain a wealth of information; but they are hard to read for a few reasons: + +- They don't contain source files and line numbers +- The symbols are [mangled](https://doc.rust-lang.org/rustc/symbol-mangling/index.html) +- Inlined functions are elided + +To get a better sense of the backtrace of a crash you can download the `.ips` file locally and run: + +``` +./script/symbolicate ~/path/zed-XXX-XXX.ips +``` + +This will download the correct debug symbols from our public [digital ocean bucket](https://zed-debug-symbols.nyc3.digitaloceanspaces.com), and run [symbolicate](https://crates.io/crates/symbolicate) for you. + +The output contains the source file and line number, and the demangled symbol information for every inlined frame. + +## Panics + +When the app panics at the rust level, Zed creates a file in `~/Library/Logs/Zed` with the text of the panic, and a summary of the backtrace. On boot, if you have telemetry enabled, we upload these panics so we can keep track of them. + +A panic is also considered a crash, and so for most panics we get both the crash report and the panic. diff --git a/docs/src/getting_started.md b/docs/src/getting_started.md index f1fbdd163d..cccc25d06b 100644 --- a/docs/src/getting_started.md +++ b/docs/src/getting_started.md @@ -8,7 +8,7 @@ You can obtain the release build via the [download page](https://zed.dev/downloa ## Configure Zed -Use `⌘` + `,` to open your custom settings to set things like fonts, formatting settings, per-language settings and more. You can access the default configuration using the `Zed > Settings > Open Default Settings` menu item. See Configuring Zed for all available settings. +Use `⌘` + `,` to open your custom settings to set things like fonts, formatting settings, per-language settings and more. You can access the default configuration using the `Zed > Settings > Open Default Settings` menu item. See [Configuring Zed](https://zed.dev/docs/configuring-zed) for all available settings. ## Set up your key bindings diff --git a/docs/src/languages/clojure.md b/docs/src/languages/clojure.md index 58ff9790e0..b89e122a56 100644 --- a/docs/src/languages/clojure.md +++ b/docs/src/languages/clojure.md @@ -1,4 +1,4 @@ # Clojure -- Tree Sitter: [tree-sitter-clojure](https://github.com/sogaiu/tree-sitter-clojure) +- Tree Sitter: [tree-sitter-clojure](https://github.com/prcastro/tree-sitter-clojure) - Language Server: [clojure-lsp](https://github.com/clojure-lsp/clojure-lsp) diff --git a/docs/src/languages/go.md b/docs/src/languages/go.md index 79ab5610fb..9dbb322897 100644 --- a/docs/src/languages/go.md +++ b/docs/src/languages/go.md @@ -22,6 +22,7 @@ Zed sets the following initialization options for inlay hints: to make the language server send back inlay hints when Zed has them enabled in the settings. Use + ```json "lsp": { "$LANGUAGE_SERVER_NAME": { @@ -33,6 +34,7 @@ Use } } ``` + to override these settings. See https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md for more information. @@ -49,5 +51,5 @@ TODO: https://github.com/zed-industries/zed/pull/7139 # Go Work - Tree Sitter: -[tree-sitter-go-work](https://github.com/d1y/tree-sitter-go-work) + [tree-sitter-go-work](https://github.com/d1y/tree-sitter-go-work) - Language Server: N/A diff --git a/docs/src/languages/ocaml.md b/docs/src/languages/ocaml.md index 5424803e74..c80ec98963 100644 --- a/docs/src/languages/ocaml.md +++ b/docs/src/languages/ocaml.md @@ -4,14 +4,17 @@ - Language Server: [ocamllsp](https://github.com/ocaml/ocaml-lsp) ## Setup Instructions + If you have the development environment already setup, you can skip to [Launching Zed](#launching-zed) ### Using OPAM -Opam is the official package manager for OCaml and is highly recommended for getting started with OCaml. To get started using Opam, please follow the instructions provided [here](https://ocaml.org/install). + +Opam is the official package manager for OCaml and is highly recommended for getting started with OCaml. To get started using Opam, please follow the instructions provided [here](https://ocaml.org/install). Once you install opam and setup a switch with your development environment as per the instructions, you can proceed. ### Launching Zed + By now you should have `ocamllsp` installed, you can verify so by running ```sh @@ -22,7 +25,7 @@ in your terminal. If you get a help message, you're good to go. If not, please r With that aside, we can now launch Zed. Given how the OCaml package manager works, we require you to run Zed from the terminal, so please make sure you install the [Zed cli](https://zed.dev/features#cli) if you haven't already. -Once you have the cli, simply from a terminal, navigate to your project and run +Once you have the cli, simply from a terminal, navigate to your project and run ```sh $ zed . diff --git a/docs/src/languages/purescript.md b/docs/src/languages/purescript.md index d534f861ac..1c46886e58 100644 --- a/docs/src/languages/purescript.md +++ b/docs/src/languages/purescript.md @@ -1,4 +1,4 @@ # PureScript -- Tree Sitter: [tree-sitter-purescript](https://github.com/ivanmoreau/tree-sitter-purescript) +- Tree Sitter: [tree-sitter-purescript](https://github.com/postsolar/tree-sitter-purescript) - Language Server: [purescript](https://github.com/nwolverson/purescript-language-server) diff --git a/docs/src/languages/rust.md b/docs/src/languages/rust.md index af82a41ab8..6aac53d49a 100644 --- a/docs/src/languages/rust.md +++ b/docs/src/languages/rust.md @@ -3,7 +3,42 @@ - Tree Sitter: [tree-sitter-rust](https://github.com/tree-sitter/tree-sitter-rust) - Language Server: [rust-analyzer](https://github.com/rust-lang/rust-analyzer) -### Target directory +## Inlay Hints + +The following configuration can be used to enable inlay hints for rust: + +```json +"inlayHints": { + "maxLength": null, + "lifetimeElisionHints": { + "useParameterNames": true, + "enable": "skip_trivial" + }, + "closureReturnTypeHints": { + "enable": "always" + } +} +``` + +to make the language server send back inlay hints when Zed has them enabled in the settings. + +Use + +```json +"lsp": { + "$LANGUAGE_SERVER_NAME": { + "initialization_options": { + .... + } + } +} +``` + +to override these settings. + +See https://rust-analyzer.github.io/manual.html#inlay-hints for more information. + +## Target directory The `rust-analyzer` target directory can be set in `initialization_options`: diff --git a/docs/src/languages/svelte.md b/docs/src/languages/svelte.md index d8474b539a..e6b1281893 100644 --- a/docs/src/languages/svelte.md +++ b/docs/src/languages/svelte.md @@ -51,6 +51,7 @@ Use } } } +``` to override these settings. diff --git a/docs/src/languages/terraform.md b/docs/src/languages/terraform.md new file mode 100644 index 0000000000..32bfe9b4b1 --- /dev/null +++ b/docs/src/languages/terraform.md @@ -0,0 +1,24 @@ +# Terraform + +- Tree Sitter: [tree-sitter-hcl](https://github.com/MichaHoffmann/tree-sitter-hcl) +- Language Server: [terraform-ls](https://github.com/hashicorp/terraform-ls) + +### Configuration + +The Terraform language server can be configured in your `settings.json`, e.g.: + +```json +{ + "lsp": { + "terraform-ls": { + "initialization_options": { + "experimentalFeatures": { + "prefillRequiredFields": true + } + } + } + } +} +``` + +See the [full list of server settings here](https://github.com/hashicorp/terraform-ls/blob/main/docs/SETTINGS.md). diff --git a/docs/src/languages/typescript.md b/docs/src/languages/typescript.md index 4bfd15dc16..7210e169e9 100644 --- a/docs/src/languages/typescript.md +++ b/docs/src/languages/typescript.md @@ -23,6 +23,7 @@ Zed sets the following initialization options for inlay hints: to make the language server send back inlay hints when Zed has them enabled in the settings. Use + ```json "lsp": { "$LANGUAGE_SERVER_NAME": { @@ -34,6 +35,7 @@ Use } } ``` + to override these settings. See https://github.com/typescript-language-server/typescript-language-server?tab=readme-ov-file#inlay-hints-textdocumentinlayhint for more information. diff --git a/docs/src/tasks.md b/docs/src/tasks.md new file mode 100644 index 0000000000..cebcf75cf2 --- /dev/null +++ b/docs/src/tasks.md @@ -0,0 +1,23 @@ +# Tasks + +Zed supports ways to spawn (and rerun) commands using its integrated terminal to output the results. + +Currently, two kinds of tasks are supported, but more will be added in the future. + +All tasks are sorted in LRU order and their names can be used (with `menu::UseSelectedQuery`, `shift-enter` by default) as an input text for quicker oneshot task edit-spawn cycle. + +## Static tasks + +Tasks, defined in a config file (`tasks.json` in the Zed config directory) that do not depend on the current editor or its content. + +Config file can be opened with `zed::OpenTasks` action ("zed: open tasks" in the command palette), it will have a configuration example with all options commented. + +Every task from that file can be spawned via the task modal, that is opened with `task::Spawn` action ("tasks: spawn" in the command pane). + +Last task spawned via that modal can be rerun with `task::Rerun` ("tasks: rerun" in the command palette) command. + +## Oneshot tasks + +Same task modal opened via `task::Spawn` supports arbitrary bash-like command execution: type a command inside the modal, and use `cmd-enter` to spawn it. + +Task modal will persist list of those command for current Zed session, `task::Rerun` will also rerun such tasks if they were the last ones spawned. diff --git a/extensions/gleam/Cargo.toml b/extensions/gleam/Cargo.toml new file mode 100644 index 0000000000..b4167dd1ca --- /dev/null +++ b/extensions/gleam/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "zed_gleam" +version = "0.0.1" +edition = "2021" +publish = false +license = "Apache-2.0" + +[dependencies] +zed_extension_api = { path = "../../crates/extension_api" } + +[lib] +path = "src/gleam.rs" +crate-type = ["cdylib"] + +[package.metadata.component] diff --git a/extensions/gleam/LICENSE-APACHE b/extensions/gleam/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/extensions/gleam/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/gleam/extension.toml b/extensions/gleam/extension.toml new file mode 100644 index 0000000000..76cce0f291 --- /dev/null +++ b/extensions/gleam/extension.toml @@ -0,0 +1,13 @@ +id = "gleam" +name = "Gleam" +description = "Gleam support for Zed" +version = "0.0.1" +authors = ["Marshall Bowers "] + +[language_servers.gleam] +name = "Gleam LSP" +language = "Gleam" + +[grammars.gleam] +repository = "https://github.com/gleam-lang/tree-sitter-gleam" +commit = "58b7cac8fc14c92b0677c542610d8738c373fa81" diff --git a/extensions/gleam/languages/gleam/config.toml b/extensions/gleam/languages/gleam/config.toml new file mode 100644 index 0000000000..0a472172ad --- /dev/null +++ b/extensions/gleam/languages/gleam/config.toml @@ -0,0 +1,11 @@ +name = "Gleam" +grammar = "gleam" +path_suffixes = ["gleam"] +line_comments = ["// ", "/// "] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] }, +] diff --git a/extensions/gleam/languages/gleam/highlights.scm b/extensions/gleam/languages/gleam/highlights.scm new file mode 100644 index 0000000000..a95f6cb031 --- /dev/null +++ b/extensions/gleam/languages/gleam/highlights.scm @@ -0,0 +1,130 @@ +; Comments +(module_comment) @comment +(statement_comment) @comment +(comment) @comment + +; Constants +(constant + name: (identifier) @constant) + +; Modules +(module) @module +(import alias: (identifier) @module) +(remote_type_identifier + module: (identifier) @module) +(remote_constructor_name + module: (identifier) @module) +((field_access + record: (identifier) @module + field: (label) @function) + (#is-not? local)) + +; Functions +(unqualified_import (identifier) @function) +(unqualified_import "type" (type_identifier) @type) +(unqualified_import (type_identifier) @constructor) +(function + name: (identifier) @function) +(external_function + name: (identifier) @function) +(function_parameter + name: (identifier) @variable.parameter) +((function_call + function: (identifier) @function) + (#is-not? local)) +((binary_expression + operator: "|>" + right: (identifier) @function) + (#is-not? local)) + +; "Properties" +; Assumed to be intended to refer to a name for a field; something that comes +; before ":" or after "." +; e.g. record field names, tuple indices, names for named arguments, etc +(label) @property +(tuple_access + index: (integer) @property) + +; Attributes +(attribute + "@" @attribute + name: (identifier) @attribute) + +(attribute_value (identifier) @constant) + +; Type names +(remote_type_identifier) @type +(type_identifier) @type + +; Data constructors +(constructor_name) @constructor + +; Literals +(string) @string +((escape_sequence) @warning + ; Deprecated in v0.33.0-rc2: + (#eq? @warning "\\e")) +(escape_sequence) @string.escape +(bit_string_segment_option) @function.builtin +(integer) @number +(float) @number + +; Reserved identifiers +; TODO: when tree-sitter supports `#any-of?` in the Rust bindings, +; refactor this to use `#any-of?` rather than `#match?` +((identifier) @warning + (#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$")) + +; Variables +(identifier) @variable +(discard) @comment.unused + +; Keywords +[ + (visibility_modifier) ; "pub" + (opacity_modifier) ; "opaque" + "as" + "assert" + "case" + "const" + ; DEPRECATED: 'external' was removed in v0.30. + "external" + "fn" + "if" + "import" + "let" + "panic" + "todo" + "type" + "use" +] @keyword + +; Operators +(binary_expression + operator: _ @operator) +(boolean_negation "!" @operator) +(integer_negation "-" @operator) + +; Punctuation +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<<" + ">>" +] @punctuation.bracket +[ + "." + "," + ;; Controversial -- maybe some are operators? + ":" + "#" + "=" + "->" + ".." + "-" + "<-" +] @punctuation.delimiter diff --git a/extensions/gleam/languages/gleam/indents.scm b/extensions/gleam/languages/gleam/indents.scm new file mode 100644 index 0000000000..112b414aa4 --- /dev/null +++ b/extensions/gleam/languages/gleam/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/extensions/gleam/languages/gleam/outline.scm b/extensions/gleam/languages/gleam/outline.scm new file mode 100644 index 0000000000..5df7a6af80 --- /dev/null +++ b/extensions/gleam/languages/gleam/outline.scm @@ -0,0 +1,31 @@ +(external_type + (visibility_modifier)? @context + "type" @context + (type_name) @name) @item + +(type_definition + (visibility_modifier)? @context + (opacity_modifier)? @context + "type" @context + (type_name) @name) @item + +(data_constructor + (constructor_name) @name) @item + +(data_constructor_argument + (label) @name) @item + +(type_alias + (visibility_modifier)? @context + "type" @context + (type_name) @name) @item + +(function + (visibility_modifier)? @context + "fn" @context + name: (_) @name) @item + +(constant + (visibility_modifier)? @context + "const" @context + name: (_) @name) @item diff --git a/extensions/gleam/src/bindings.rs b/extensions/gleam/src/bindings.rs new file mode 100644 index 0000000000..a5e0b040dc --- /dev/null +++ b/extensions/gleam/src/bindings.rs @@ -0,0 +1,11 @@ +// Generated by `wit-bindgen` 0.16.0. DO NOT EDIT! + +#[cfg(target_arch = "wasm32")] +#[link_section = "component-type:zed_gleam"] +#[doc(hidden)] +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 169] = [3, 0, 9, 122, 101, 100, 95, 103, 108, 101, 97, 109, 0, 97, 115, 109, 13, 0, 1, 0, 7, 40, 1, 65, 2, 1, 65, 0, 4, 1, 29, 99, 111, 109, 112, 111, 110, 101, 110, 116, 58, 122, 101, 100, 95, 103, 108, 101, 97, 109, 47, 122, 101, 100, 95, 103, 108, 101, 97, 109, 4, 0, 11, 15, 1, 0, 9, 122, 101, 100, 95, 103, 108, 101, 97, 109, 3, 0, 0, 0, 16, 12, 112, 97, 99, 107, 97, 103, 101, 45, 100, 111, 99, 115, 0, 123, 125, 0, 70, 9, 112, 114, 111, 100, 117, 99, 101, 114, 115, 1, 12, 112, 114, 111, 99, 101, 115, 115, 101, 100, 45, 98, 121, 2, 13, 119, 105, 116, 45, 99, 111, 109, 112, 111, 110, 101, 110, 116, 6, 48, 46, 49, 56, 46, 50, 16, 119, 105, 116, 45, 98, 105, 110, 100, 103, 101, 110, 45, 114, 117, 115, 116, 6, 48, 46, 49, 54, 46, 48]; + +#[inline(never)] +#[doc(hidden)] +#[cfg(target_arch = "wasm32")] +pub fn __link_section() {} diff --git a/extensions/gleam/src/gleam.rs b/extensions/gleam/src/gleam.rs new file mode 100644 index 0000000000..ffc8515802 --- /dev/null +++ b/extensions/gleam/src/gleam.rs @@ -0,0 +1,91 @@ +use zed_extension_api::{self as zed, Result}; + +struct GleamExtension { + cached_binary_path: Option, +} + +impl zed::Extension for GleamExtension { + fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + fn language_server_command( + &mut self, + config: zed::LanguageServerConfig, + _worktree: &zed::Worktree, + ) -> Result { + let binary_path = if let Some(path) = &self.cached_binary_path { + zed::set_language_server_installation_status( + &config.name, + &zed::LanguageServerInstallationStatus::Cached, + ); + + path.clone() + } else { + zed::set_language_server_installation_status( + &config.name, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "gleam-lang/gleam", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (platform, arch) = zed::current_platform(); + let asset_name = format!( + "gleam-{version}-{arch}-{os}.tar.gz", + version = release.version, + arch = match arch { + zed::Architecture::Aarch64 => "aarch64", + zed::Architecture::X86 => "x86", + zed::Architecture::X8664 => "x86_64", + }, + os = match platform { + zed::Os::Mac => "apple-darwin", + zed::Os::Linux => "unknown-linux-musl", + zed::Os::Windows => "pc-windows-msvc", + }, + ); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + zed::set_language_server_installation_status( + &config.name, + &zed::LanguageServerInstallationStatus::Downloading, + ); + let version_dir = format!("gleam-{}", release.version); + zed::download_file( + &asset.download_url, + &version_dir, + zed::DownloadedFileType::GzipTar, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + zed::set_language_server_installation_status( + &config.name, + &zed::LanguageServerInstallationStatus::Downloaded, + ); + + let binary_path = format!("{version_dir}/gleam"); + self.cached_binary_path = Some(binary_path.clone()); + binary_path + }; + + Ok(zed::Command { + command: binary_path, + args: vec!["lsp".to_string()], + env: Default::default(), + }) + } +} + +zed::register_extension!(GleamExtension); diff --git a/plugins/Cargo.lock b/plugins/Cargo.lock deleted file mode 100644 index 1e51dd8085..0000000000 --- a/plugins/Cargo.lock +++ /dev/null @@ -1,129 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "itoa" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" - -[[package]] -name = "json_language" -version = "0.1.0" -dependencies = [ - "plugin", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "plugin" -version = "0.1.0" -dependencies = [ - "bincode", - "plugin_macros", - "serde", - "serde_derive", -] - -[[package]] -name = "plugin_macros" -version = "0.1.0" -dependencies = [ - "bincode", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" - -[[package]] -name = "serde" -version = "1.0.137" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.137" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "syn" -version = "1.0.96" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "test_plugin" -version = "0.1.0" -dependencies = [ - "plugin", -] - -[[package]] -name = "unicode-ident" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" diff --git a/plugins/Cargo.toml b/plugins/Cargo.toml deleted file mode 100644 index 912ad8eea2..0000000000 --- a/plugins/Cargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[workspace] -members = ["./json_language", "./test_plugin"] diff --git a/plugins/json_language/Cargo.toml b/plugins/json_language/Cargo.toml deleted file mode 100644 index 5a5072995f..0000000000 --- a/plugins/json_language/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "json_language" -version = "0.1.0" -edition = "2021" - -[dependencies] -plugin = { path = "../../crates/plugin" } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = "1.0" - -[lib] -crate-type = ["cdylib"] diff --git a/plugins/json_language/src/lib.rs b/plugins/json_language/src/lib.rs deleted file mode 100644 index e18d9ce74b..0000000000 --- a/plugins/json_language/src/lib.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::{fs, path::PathBuf}; - -use plugin::prelude::*; -use serde::Deserialize; - -#[import] -fn command(string: &str) -> Option>; - -const SERVER_PATH: &str = "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; - -#[export] -pub fn name() -> &'static str { - "vscode-json-languageserver" -} - -#[export] -pub fn server_args() -> Vec { - vec!["--stdio".into()] -} - -#[export] -pub fn fetch_latest_server_version() -> Option { - #[derive(Deserialize)] - struct NpmInfo { - versions: Vec, - } - - let output = - command("npm info vscode-json-languageserver --json").expect("could not run command"); - let output = String::from_utf8(output).unwrap(); - - let mut info: NpmInfo = serde_json::from_str(&output).ok()?; - info.versions.pop() -} - -#[export] -pub fn fetch_server_binary(container_dir: PathBuf, version: String) -> Result { - let version_dir = container_dir.join(version.as_str()); - fs::create_dir_all(&version_dir) - .map_err(|_| "failed to create version directory".to_string())?; - let binary_path = version_dir.join(SERVER_PATH); - - if fs::metadata(&binary_path).is_err() { - let output = command(&format!( - "npm install vscode-json-languageserver@{}", - version - )); - let output = output.map(String::from_utf8); - if output.is_none() { - return Err("failed to install vscode-json-languageserver".to_string()); - } - - if let Ok(entries) = fs::read_dir(&container_dir) { - for entry in entries.flatten() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).ok(); - } - } - } - } - - Ok(binary_path) -} - -#[export] -pub fn cached_server_binary(container_dir: PathBuf) -> Option { - let mut last_version_dir = None; - let entries = fs::read_dir(&container_dir).ok()?; - - for entry in entries { - let entry = entry.ok()?; - if entry.file_type().ok()?.is_dir() { - last_version_dir = Some(entry.path()); - } - } - - let last_version_dir = last_version_dir?; - let server_path = last_version_dir.join(SERVER_PATH); - if server_path.exists() { - Some(server_path) - } else { - println!("no binary found"); - None - } -} - -#[export] -pub fn initialization_options() -> Option { - Some("{ \"provideFormatter\": true }".to_string()) -} - -#[export] -pub fn language_ids() -> Vec<(String, String)> { - vec![("JSON".into(), "jsonc".into())] -} diff --git a/plugins/test_plugin/Cargo.toml b/plugins/test_plugin/Cargo.toml deleted file mode 100644 index 850b4a7401..0000000000 --- a/plugins/test_plugin/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "test_plugin" -version = "0.1.0" -edition = "2021" - -[dependencies] -plugin = { path = "../../crates/plugin" } - -[lib] -crate-type = ["cdylib"] diff --git a/plugins/test_plugin/src/lib.rs b/plugins/test_plugin/src/lib.rs deleted file mode 100644 index 769232a26a..0000000000 --- a/plugins/test_plugin/src/lib.rs +++ /dev/null @@ -1,82 +0,0 @@ -use plugin::prelude::*; - -#[export] -pub fn noop() {} - -#[export] -pub fn constant() -> u32 { - 27 -} - -#[export] -pub fn identity(i: u32) -> u32 { - i -} - -#[export] -pub fn add(a: u32, b: u32) -> u32 { - a + b -} - -#[export] -pub fn swap(a: u32, b: u32) -> (u32, u32) { - (b, a) -} - -#[export] -pub fn sort(mut list: Vec) -> Vec { - list.sort(); - list -} - -#[export] -pub fn print(string: String) { - println!("to stdout: {}", string); - eprintln!("to stderr: {}", string); -} - -#[import] -fn mystery_number(input: u32) -> u32; - -#[export] -pub fn and_back(secret: u32) -> u32 { - mystery_number(secret) -} - -#[import] -fn import_noop() -> (); - -#[import] -fn import_identity(i: u32) -> u32; - -#[import] -fn import_swap(a: u32, b: u32) -> (u32, u32); - -#[export] -pub fn imports(x: u32) -> u32 { - let a = import_identity(7); - import_noop(); - let (b, c) = import_swap(a, x); - assert_eq!(a, c); - assert_eq!(x, b); - a + b // should be 7 + x -} - -#[import] -fn import_half(a: u32) -> u32; - -#[export] -pub fn half_async(a: u32) -> u32 { - import_half(a) -} - -#[import] -fn command_async(command: String) -> Option>; - -#[export] -pub fn echo_async(message: String) -> String { - let command = format!("echo {}", message); - let result = command_async(command); - let result = result.expect("Could not run command"); - String::from_utf8_lossy(&result).to_string() -} diff --git a/script/bootstrap b/script/bootstrap index 054daccf42..e23f42e80e 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -3,18 +3,11 @@ echo "installing foreman..." which foreman > /dev/null || brew install foreman -echo "installing minio..." -which minio > /dev/null || brew install minio/stable/minio -mkdir -p .blob_store/the-extensions-bucket - echo "creating database..." script/sqlx database create echo "migrating database..." -(cd crates/collab && cargo run -- migrate) +cargo run -p collab -- migrate echo "seeding database..." script/seed-db - -echo "Linux dependencies..." -script/linux diff --git a/script/bundle b/script/bundle index a6414febcb..d90ed2a014 100755 --- a/script/bundle +++ b/script/bundle @@ -173,6 +173,7 @@ if [ "$local_arch" = false ]; then cp -R target/${local_target_triple}/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" else cp -R target/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" + cp -R target/${target_dir}/cli "${app_path}/Contents/MacOS/" fi # Note: The app identifier for our development builds is the same as the app identifier for nightly. diff --git a/script/clippy b/script/clippy index 69b0bce192..90a121be1c 100755 --- a/script/clippy +++ b/script/clippy @@ -2,8 +2,4 @@ set -euxo pipefail -# clippy.toml is not currently supporting specifying allowed lints -# so specify those here, and disable the rest until Zed's workspace -# will have more fixes & suppression for the standard lint set -cargo clippy --release --workspace --all-features --all-targets -- -A clippy::all -D clippy::dbg_macro -D clippy::todo -cargo clippy -p gpui -- -D warnings +cargo xtask clippy diff --git a/script/generate-licenses b/script/generate-licenses index 107da5a0da..10d85f60dc 100755 --- a/script/generate-licenses +++ b/script/generate-licenses @@ -1,6 +1,6 @@ -#!/bin/bash +#!/usr/bin/env bash -set -e +set -euo pipefail OUTPUT_FILE=$(pwd)/assets/licenses.md diff --git a/script/get-changes-since b/script/get-changes-since index e88505008c..c070616820 100755 --- a/script/get-changes-since +++ b/script/get-changes-since @@ -2,7 +2,7 @@ const { execFileSync } = require("child_process"); const { GITHUB_ACCESS_TOKEN } = process.env; -const PR_REGEX = /#\d+/ // Ex: matches on #4241 +const PR_REGEX = /#\d+/; // Ex: matches on #4241 const FIXES_REGEX = /(fixes|closes|completes) (.+[/#]\d+.*)$/im; main(); @@ -10,7 +10,7 @@ main(); async function main() { // Use form of: YYYY-MM-DD - 2023-01-09 const startDate = new Date(process.argv[2]); - const today = new Date() + const today = new Date(); console.log(`Changes from ${startDate} to ${today}\n`); @@ -32,32 +32,26 @@ async function main() { console.log("*", pullRequest.title); console.log(" PR URL: ", webURL); console.log(" Merged: ", pullRequest.merged_at); - console.log() + console.log(); } } - function getPullRequestNumbers(startDate, endDate) { const sinceDate = startDate.toISOString(); const untilDate = endDate.toISOString(); const pullRequestNumbers = execFileSync( "git", - [ - "log", - `--since=${sinceDate}`, - `--until=${untilDate}`, - "--oneline" - ], - { encoding: "utf8" } + ["log", `--since=${sinceDate}`, `--until=${untilDate}`, "--oneline"], + { encoding: "utf8" }, ) .split("\n") - .filter(line => line.length > 0) - .map(line => { + .filter((line) => line.length > 0) + .map((line) => { const match = line.match(/#(\d+)/); return match ? match[1] : null; }) - .filter(line => line); + .filter((line) => line); return pullRequestNumbers; } diff --git a/script/licenses/zed-licenses.toml b/script/licenses/zed-licenses.toml index d338e7ab0b..c4557a8c20 100644 --- a/script/licenses/zed-licenses.toml +++ b/script/licenses/zed-licenses.toml @@ -12,6 +12,7 @@ accepted = [ "Unicode-DFS-2016", "OpenSSL", "Zlib", + "BSL-1.0" ] workarounds = [ "ring", @@ -35,3 +36,9 @@ license = "BSD-3-Clause" [[fuchsia-cprng.clarify.files]] path = 'LICENSE' checksum = '03b114f53e6587a398931762ee11e2395bfdba252a329940e2c8c9e81813845b' + +[tree-sitter-hcl.clarify] +license = "Apache-2.0" +[[tree-sitter-hcl.clarify.files]] +path = 'LICENSE' +checksum = 'c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4' diff --git a/script/linux b/script/linux index b55d60217a..a35918ce73 100755 --- a/script/linux +++ b/script/linux @@ -1,7 +1,7 @@ #!/usr/bin/bash -e # if sudo is not installed, define an empty alias -maysudo=$(command -v sudo || true) +maysudo=$(command -v sudo || command -v doas || true) # Ubuntu, Debian, etc. # https://packages.ubuntu.com/ @@ -10,9 +10,10 @@ if [[ -n $apt ]]; then deps=( libasound2-dev libfontconfig-dev - vulkan-validationlayers* libwayland-dev libxkbcommon-x11-dev + libssl-dev + libzstd-dev ) $maysudo "$apt" install -y "${deps[@]}" exit 0 @@ -25,14 +26,31 @@ if [[ -n $dnf ]]; then deps=( alsa-lib-devel fontconfig-devel - vulkan-validation-layers wayland-devel libxkbcommon-x11-devel + openssl-devel + libzstd-devel ) $maysudo "$dnf" install -y "${deps[@]}" exit 0 fi +# openSuse +# https://software.opensuse.org/ +zyp=$(command -v zypper || true) +if [[ -n $zyp ]]; then + deps=( + alsa-devel + fontconfig-devel + wayland-devel + libxkbcommon-x11-devel + openssl-devel + libzstd-devel + ) + $maysudo "$zyp" install -y "${deps[@]}" + exit 0 +fi + # Arch, Manjaro, etc. # https://archlinux.org/packages pacman=$(command -v pacman || true) @@ -40,12 +58,29 @@ if [[ -n $pacman ]]; then deps=( alsa-lib fontconfig - vulkan-validation-layers wayland libxkbcommon-x11 + openssl + zstd ) $maysudo "$pacman" -S --needed --noconfirm "${deps[@]}" exit 0 fi +# Void +# https://voidlinux.org/packages/ +xbps=$(command -v xbps-install || true) +if [[ -n $xbps ]]; then + deps=( + alsa-lib-devel + fontconfig-devel + wayland-devel + libxkbcommon-devel + openssl-devel + libzstd-devel + ) + $maysudo "$xbps" -Syu "${deps[@]}" + exit 0 +fi + echo "Unsupported Linux distribution in script/linux" diff --git a/script/randomized-test-minimize b/script/randomized-test-minimize index df003cbf3e..efed3ee501 100755 --- a/script/randomized-test-minimize +++ b/script/randomized-test-minimize @@ -1,131 +1,135 @@ #!/usr/bin/env node --redirect-warnings=/dev/null -const fs = require('fs') -const path = require('path') -const {spawnSync} = require('child_process') +const fs = require("fs"); +const path = require("path"); +const { spawnSync } = require("child_process"); -const FAILING_SEED_REGEX = /failing seed: (\d+)/ig -const CARGO_TEST_ARGS = [ - '--release', - '--lib', - '--package', 'collab', -] +const FAILING_SEED_REGEX = /failing seed: (\d+)/gi; +const CARGO_TEST_ARGS = ["--release", "--lib", "--package", "collab"]; if (require.main === module) { if (process.argv.length < 4) { - process.stderr.write("usage: script/randomized-test-minimize [start-index]\n") - process.exit(1) + process.stderr.write( + "usage: script/randomized-test-minimize [start-index]\n", + ); + process.exit(1); } minimizeTestPlan( process.argv[2], process.argv[3], - parseInt(process.argv[4]) || 0 + parseInt(process.argv[4]) || 0, ); } -function minimizeTestPlan( - inputPlanPath, - outputPlanPath, - startIndex = 0 -) { - const tempPlanPath = inputPlanPath + '.try' +function minimizeTestPlan(inputPlanPath, outputPlanPath, startIndex = 0) { + const tempPlanPath = inputPlanPath + ".try"; - fs.copyFileSync(inputPlanPath, outputPlanPath) - let testPlan = JSON.parse(fs.readFileSync(outputPlanPath, 'utf8')) + fs.copyFileSync(inputPlanPath, outputPlanPath); + let testPlan = JSON.parse(fs.readFileSync(outputPlanPath, "utf8")); - process.stderr.write("minimizing failing test plan...\n") + process.stderr.write("minimizing failing test plan...\n"); for (let ix = startIndex; ix < testPlan.length; ix++) { // Skip 'MutateClients' entries, since they themselves are not single operations. if (testPlan[ix].MutateClients) { - continue + continue; } // Remove a row from the test plan - const newTestPlan = testPlan.slice() - newTestPlan.splice(ix, 1) - fs.writeFileSync(tempPlanPath, serializeTestPlan(newTestPlan), 'utf8'); + const newTestPlan = testPlan.slice(); + newTestPlan.splice(ix, 1); + fs.writeFileSync(tempPlanPath, serializeTestPlan(newTestPlan), "utf8"); - process.stderr.write(`${ix}/${testPlan.length}: ${JSON.stringify(testPlan[ix])}`) + process.stderr.write( + `${ix}/${testPlan.length}: ${JSON.stringify(testPlan[ix])}`, + ); const failingSeed = runTests({ - SEED: '0', + SEED: "0", LOAD_PLAN: tempPlanPath, SAVE_PLAN: tempPlanPath, - ITERATIONS: '500' - }) + ITERATIONS: "500", + }); // If the test failed, keep the test plan with the removed row. Reload the test // plan from the JSON file, since the test itself will remove any operations // which are no longer valid before saving the test plan. if (failingSeed != null) { - process.stderr.write(` - remove. failing seed: ${failingSeed}.\n`) - fs.copyFileSync(tempPlanPath, outputPlanPath) - testPlan = JSON.parse(fs.readFileSync(outputPlanPath, 'utf8')) - ix-- + process.stderr.write(` - remove. failing seed: ${failingSeed}.\n`); + fs.copyFileSync(tempPlanPath, outputPlanPath); + testPlan = JSON.parse(fs.readFileSync(outputPlanPath, "utf8")); + ix--; } else { - process.stderr.write(` - keep.\n`) + process.stderr.write(` - keep.\n`); } } - fs.unlinkSync(tempPlanPath) + fs.unlinkSync(tempPlanPath); // Re-run the final minimized plan to get the correct failing seed. // This is a workaround for the fact that the execution order can // slightly change when replaying a test plan after it has been // saved and loaded. const failingSeed = runTests({ - SEED: '0', - ITERATIONS: '5000', + SEED: "0", + ITERATIONS: "5000", LOAD_PLAN: outputPlanPath, - }) + }); - process.stderr.write(`final test plan: ${outputPlanPath}\n`) - process.stderr.write(`final seed: ${failingSeed}\n`) - return failingSeed + process.stderr.write(`final test plan: ${outputPlanPath}\n`); + process.stderr.write(`final seed: ${failingSeed}\n`); + return failingSeed; } function buildTests() { - const {status} = spawnSync('cargo', ['test', '--no-run', ...CARGO_TEST_ARGS], { - stdio: 'inherit', - encoding: 'utf8', - env: { - ...process.env, - } - }); + const { status } = spawnSync( + "cargo", + ["test", "--no-run", ...CARGO_TEST_ARGS], + { + stdio: "inherit", + encoding: "utf8", + env: { + ...process.env, + }, + }, + ); if (status !== 0) { - throw new Error('build failed') + throw new Error("build failed"); } } function runTests(env) { - const {status, stdout} = spawnSync('cargo', ['test', ...CARGO_TEST_ARGS, 'random_project_collaboration'], { - stdio: 'pipe', - encoding: 'utf8', - env: { - ...process.env, - ...env, - } - }); + const { status, stdout } = spawnSync( + "cargo", + ["test", ...CARGO_TEST_ARGS, "random_project_collaboration"], + { + stdio: "pipe", + encoding: "utf8", + env: { + ...process.env, + ...env, + }, + }, + ); if (status !== 0) { - FAILING_SEED_REGEX.lastIndex = 0 - const match = FAILING_SEED_REGEX.exec(stdout) + FAILING_SEED_REGEX.lastIndex = 0; + const match = FAILING_SEED_REGEX.exec(stdout); if (!match) { - process.stderr.write("test failed, but no failing seed found:\n") - process.stderr.write(stdout) - process.stderr.write('\n') - process.exit(1) + process.stderr.write("test failed, but no failing seed found:\n"); + process.stderr.write(stdout); + process.stderr.write("\n"); + process.exit(1); } - return match[1] + return match[1]; } else { - return null + return null; } } function serializeTestPlan(plan) { - return "[\n" + plan.map(row => JSON.stringify(row)).join(",\n") + "\n]\n" + return "[\n" + plan.map((row) => JSON.stringify(row)).join(",\n") + "\n]\n"; } -exports.buildTests = buildTests -exports.runTests = runTests -exports.minimizeTestPlan = minimizeTestPlan +exports.buildTests = buildTests; +exports.runTests = runTests; +exports.minimizeTestPlan = minimizeTestPlan; diff --git a/script/run-local-minio b/script/run-local-minio new file mode 100755 index 0000000000..8d2d4877ff --- /dev/null +++ b/script/run-local-minio @@ -0,0 +1,9 @@ +#!/bin/bash -e + +which minio > /dev/null || (echo "installing minio..."; brew install minio/stable/minio) +mkdir -p .blob_store/the-extensions-bucket +mkdir -p .blob_store/zed-crash-reports + +export MINIO_ROOT_USER=the-blob-store-access-key +export MINIO_ROOT_PASSWORD=the-blob-store-secret-key +minio server --quiet .blob_store diff --git a/script/seed-db b/script/seed-db index 5079e01955..d3ee89e4f0 100755 --- a/script/seed-db +++ b/script/seed-db @@ -1,4 +1,4 @@ #!/bin/bash set -e -cargo run --quiet --package=collab --features seed-support --bin seed -- $@ +cargo run --quiet --package=collab --bin seed -- $@ diff --git a/script/symbolicate b/script/symbolicate new file mode 100755 index 0000000000..46e89b6f39 --- /dev/null +++ b/script/symbolicate @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +set -eu +if [[ $# -eq 0 ]] || [[ "$1" == "--help" ]]; then + echo "Usage: $(basename $0) " + echo "This script symbolicates the provided .ips file using the appropriate dSYM file from digital ocean" + echo "" + exit 1 +fi + +ips_file=$1; + +version=$(cat $ips_file | head -n 1 | jq -r .app_version) +bundle_id=$(cat $ips_file | head -n 1 | jq -r .bundleID) +cpu_type=$(cat $ips_file | tail -n+2 | jq -r .cpuType) + +which symbolicate >/dev/null || cargo install symbolicate + +arch="x86_64-apple-darwin" +if [[ "$cpu_type" == *ARM-64* ]]; then + arch="aarch64-apple-darwin" +fi + +channel="stable" +if [[ "$bundle_id" == *nightly* ]]; then + channel="nightly" +elif [[ "$bundle_id" == *preview* ]]; then + channel="preview" +fi + +mkdir -p target/dsyms/$channel + +dsym="$channel/Zed-$version-$arch.dwarf" +if [[ ! -f target/dsyms/$dsym ]]; then + echo "Downloading $dsym..." + curl -o target/dsyms/$dsym.gz "https://zed-debug-symbols.nyc3.digitaloceanspaces.com/$channel/Zed-$version-$arch.dwarf.gz" + gunzip target/dsyms/$dsym.gz +fi + +symbolicate $ips_file target/dsyms/$dsym diff --git a/tooling/xtask/Cargo.toml b/tooling/xtask/Cargo.toml new file mode 100644 index 0000000000..fac3b54e5e --- /dev/null +++ b/tooling/xtask/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[dependencies] +anyhow.workspace = true +clap = { workspace = true, features = ["derive"] } diff --git a/tooling/xtask/LICENSE-GPL b/tooling/xtask/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/tooling/xtask/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/tooling/xtask/src/main.rs b/tooling/xtask/src/main.rs new file mode 100644 index 0000000000..44947a4367 --- /dev/null +++ b/tooling/xtask/src/main.rs @@ -0,0 +1,137 @@ +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command(name = "cargo xtask")] +struct Args { + #[command(subcommand)] + command: CliCommand, +} + +#[derive(Subcommand)] +enum CliCommand { + /// Runs `cargo clippy`. + Clippy(ClippyArgs), +} + +fn main() -> Result<()> { + let args = Args::parse(); + + match args.command { + CliCommand::Clippy(args) => run_clippy(args), + } +} + +#[derive(Parser)] +struct ClippyArgs { + /// Automatically apply lint suggestions (`clippy --fix`). + #[arg(long)] + fix: bool, + + /// The package to run Clippy against (`cargo -p clippy`). + #[arg(long, short)] + package: Option, +} + +fn run_clippy(args: ClippyArgs) -> Result<()> { + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + + let mut clippy_command = Command::new(&cargo); + clippy_command.arg("clippy"); + + if let Some(package) = args.package.as_ref() { + clippy_command.args(["--package", package]); + } else { + clippy_command.arg("--workspace"); + } + + clippy_command + .arg("--release") + .arg("--all-targets") + .arg("--all-features"); + + if args.fix { + clippy_command.arg("--fix"); + } + + clippy_command.arg("--"); + + // Deny all warnings. + // We don't do this yet on Windows, as it still has some warnings present. + #[cfg(not(target_os = "windows"))] + clippy_command.args(["--deny", "warnings"]); + + /// These are all of the rules that currently have violations in the Zed + /// codebase. + /// + /// We'll want to drive this list down by either: + /// 1. fixing violations of the rule and begin enforcing it + /// 2. deciding we want to allow the rule permanently, at which point + /// we should codify that separately in this task. + /// + /// This list shouldn't be added to; it should only get shorter. + const MIGRATORY_RULES_TO_ALLOW: &[&str] = &[ + // There are a bunch of rules currently failing in the `style` group, so + // allow all of those, for now. + "clippy::style", + // Individual rules that have violations in the codebase: + "clippy::almost_complete_range", + "clippy::arc_with_non_send_sync", + "clippy::await_holding_lock", + "clippy::borrow_deref_ref", + "clippy::borrowed_box", + "clippy::cast_abs_to_unsigned", + "clippy::cmp_owned", + "clippy::derive_ord_xor_partial_ord", + "clippy::eq_op", + "clippy::implied_bounds_in_impls", + "clippy::let_underscore_future", + "clippy::map_entry", + "clippy::never_loop", + "clippy::non_canonical_clone_impl", + "clippy::non_canonical_partial_ord_impl", + "clippy::reversed_empty_ranges", + "clippy::single_range_in_vec_init", + "clippy::suspicious_to_owned", + "clippy::type_complexity", + "clippy::unnecessary_to_owned", + ]; + + // When fixing violations automatically for a single package we don't care + // about the rules we're already violating, since it may be possible to + // have them fixed automatically. + let ignore_suppressed_rules = args.fix && args.package.is_some(); + if !ignore_suppressed_rules { + for rule in MIGRATORY_RULES_TO_ALLOW { + clippy_command.args(["--allow", rule]); + } + } + + // Deny `dbg!` and `todo!`s. + clippy_command + .args(["--deny", "clippy::dbg_macro"]) + .args(["--deny", "clippy::todo"]); + + eprintln!( + "running: {cargo} {}", + clippy_command + .get_args() + .map(|arg| arg.to_str().unwrap()) + .collect::>() + .join(" ") + ); + + let exit_status = clippy_command + .spawn() + .context("failed to spawn child process")? + .wait() + .context("failed to wait for child process")?; + + if !exit_status.success() { + bail!("clippy failed: {}", exit_status); + } + + Ok(()) +} diff --git a/typos.toml b/typos.toml index b05a1e1b83..91e95e35e9 100644 --- a/typos.toml +++ b/typos.toml @@ -4,7 +4,7 @@ ignore-hidden = false extend-exclude = [ ".git/", # glsl isn't recognized by this tool - "crates/zed/src/languages/glsl/", + "crates/languages/src/glsl/", # File suffixes aren't typos "assets/icons/file_icons/file_types.json", # Not our typos @@ -14,6 +14,8 @@ extend-exclude = [ # Editor and file finder rely on partial typing and custom in-string syntax "crates/file_finder/src/file_finder_tests.rs", "crates/editor/src/editor_tests.rs", + # Clojure uses .edn filename extension, which is not a misspelling of "end" + "crates/languages/src/clojure/config.toml", ] [default] @@ -22,5 +24,9 @@ extend-ignore-re = [ ":ba\\|z", # :/ crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql "COLUMN enviroment", + # Typo in ClickHouse column name. + # crates/collab/src/api/events.rs + "rename = \"sesssion_id\"", + "doas", ] check-filename = true