Merge branch 'main' into kvark-linux

Mikayla created

Change summary

.github/actions/check_style/action.yml                           |  46 
.github/actions/run_tests/action.yml                             |   2 
.github/pull_request_template.md                                 |   6 
.github/workflows/ci.yml                                         | 278 
.github/workflows/danger.yml                                     |  50 
.github/workflows/randomized_tests.yml                           |  52 
.github/workflows/release_nightly.yml                            |   5 
.github/workflows/update_all_top_ranking_issues.yml              |   1 
.github/workflows/update_weekly_top_ranking_issues.yml           |   1 
Cargo.lock                                                       | 137 
Cargo.toml                                                       | 103 
assets/icons/file_icons/elm.svg                                  |   9 
assets/icons/file_icons/file_types.json                          |  20 
assets/icons/file_icons/ocaml.svg                                |   2 
assets/icons/file_icons/prisma.svg                               |   6 
assets/icons/file_icons/yaml.svg                                 |   1 
assets/keymaps/default.json                                      |  22 
assets/keymaps/jetbrains.json                                    |  11 
assets/keymaps/vim.json                                          |  16 
assets/settings/default.json                                     |  28 
assets/themes/andromeda/andromeda.json                           |   2 
assets/themes/atelier/atelier.json                               |  40 
assets/themes/ayu/ayu.json                                       |   6 
assets/themes/gruvbox/gruvbox.json                               |  12 
assets/themes/one/one.json                                       |   4 
assets/themes/rose_pine/rose_pine.json                           |   6 
assets/themes/sandcastle/sandcastle.json                         |   2 
assets/themes/solarized/solarized.json                           |   4 
assets/themes/summercamp/summercamp.json                         |   2 
crates/activity_indicator/Cargo.toml                             |  22 
crates/ai/Cargo.toml                                             |   8 
crates/ai/src/providers/open_ai/embedding.rs                     |   2 
crates/assets/Cargo.toml                                         |   2 
crates/assistant/Cargo.toml                                      |  40 
crates/assistant/src/assistant_panel.rs                          |   2 
crates/audio/Cargo.toml                                          |   6 
crates/auto_update/Cargo.toml                                    |  20 
crates/auto_update/src/update_notification.rs                    |   7 
crates/breadcrumbs/Cargo.toml                                    |  28 
crates/call/Cargo.toml                                           |  38 
crates/call/src/call_settings.rs                                 |   6 
crates/call/src/room.rs                                          |   6 
crates/channel/Cargo.toml                                        |  38 
crates/channel/src/channel_chat.rs                               | 173 
crates/channel/src/channel_store_tests.rs                        |   5 
crates/cli/Cargo.toml                                            |   2 
crates/client/Cargo.toml                                         |  32 
crates/client/src/client.rs                                      |   4 
crates/client/src/telemetry.rs                                   |  19 
crates/collab/Cargo.toml                                         |  60 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql   |   3 
crates/collab/migrations/20240203113741_add_reply_to_message.sql |   1 
crates/collab/src/db.rs                                          |   2 
crates/collab/src/db/queries/messages.rs                         |   3 
crates/collab/src/db/queries/projects.rs                         |   2 
crates/collab/src/db/queries/rooms.rs                            |   4 
crates/collab/src/db/tables/channel_message.rs                   |   1 
crates/collab/src/db/tests/message_tests.rs                      |  50 
crates/collab/src/rpc.rs                                         |   7 
crates/collab/src/tests/channel_message_tests.rs                 |  64 
crates/collab/src/tests/following_tests.rs                       |  81 
crates/collab/src/tests/integration_tests.rs                     |   2 
crates/collab/src/tests/test_server.rs                           |   7 
crates/collab_ui/Cargo.toml                                      |  80 
crates/collab_ui/src/chat_panel.rs                               | 371 
crates/collab_ui/src/chat_panel/message_editor.rs                |  23 
crates/collab_ui/src/collab_panel.rs                             |  18 
crates/collab_ui/src/collab_titlebar_item.rs                     |  25 
crates/color/Cargo.toml                                          |   2 
crates/command_palette/Cargo.toml                                |  44 
crates/copilot/Cargo.toml                                        |  34 
crates/copilot_ui/Cargo.toml                                     |  24 
crates/db/Cargo.toml                                             |  14 
crates/diagnostics/Cargo.toml                                    |  36 
crates/editor/Cargo.toml                                         |  71 
crates/editor/src/actions.rs                                     |  30 
crates/editor/src/display_map.rs                                 |   7 
crates/editor/src/display_map/inlay_map.rs                       |   2 
crates/editor/src/editor.rs                                      | 250 
crates/editor/src/editor_settings.rs                             |  33 
crates/editor/src/element.rs                                     | 202 
crates/editor/src/hover_links.rs                                 | 689 
crates/editor/src/hover_popover.rs                               |   4 
crates/editor/src/inlay_hint_cache.rs                            | 906 +
crates/editor/src/items.rs                                       |  15 
crates/editor/src/movement.rs                                    |  46 
crates/editor/src/scroll.rs                                      |  12 
crates/editor/src/test/editor_test_context.rs                    |  30 
crates/feature_flags/Cargo.toml                                  |   2 
crates/feedback/Cargo.toml                                       |  28 
crates/file_finder/Cargo.toml                                    |  36 
crates/file_finder/src/file_finder.rs                            |  14 
crates/file_finder/src/file_finder_tests.rs                      |   4 
crates/fs/Cargo.toml                                             |  16 
crates/fs/src/repository.rs                                      |   6 
crates/fuzzy/Cargo.toml                                          |   4 
crates/git/Cargo.toml                                            |  10 
crates/go_to_line/Cargo.toml                                     |  20 
crates/gpui/Cargo.toml                                           |  14 
crates/gpui/build.rs                                             |   1 
crates/gpui/src/app.rs                                           |  72 
crates/gpui/src/app/test_context.rs                              |  42 
crates/gpui/src/key_dispatch.rs                                  |  20 
crates/gpui/src/keymap.rs                                        |   7 
crates/gpui/src/platform.rs                                      |  27 
crates/gpui/src/platform/keystroke.rs                            |  30 
crates/gpui/src/platform/mac/dispatcher.rs                       |  11 
crates/gpui/src/platform/mac/metal_renderer.rs                   | 227 
crates/gpui/src/platform/mac/platform.rs                         |  21 
crates/gpui/src/platform/mac/window.rs                           |  91 
crates/gpui/src/platform/test/platform.rs                        |  10 
crates/gpui/src/platform/test/window.rs                          |   8 
crates/gpui/src/scene.rs                                         |   3 
crates/gpui/src/style.rs                                         |  28 
crates/gpui/src/text_system.rs                                   | 163 
crates/gpui/src/text_system/line.rs                              |  53 
crates/gpui/src/text_system/line_wrapper.rs                      |   6 
crates/gpui/src/window.rs                                        | 289 
crates/gpui/src/window/element_cx.rs                             |  53 
crates/install_cli/Cargo.toml                                    |   4 
crates/journal/Cargo.toml                                        |  12 
crates/language/Cargo.toml                                       |  38 
crates/language/src/buffer.rs                                    |   5 
crates/language_selector/Cargo.toml                              |  24 
crates/language_tools/Cargo.toml                                 |  32 
crates/live_kit_client/Cargo.toml                                |  25 
crates/lsp/Cargo.toml                                            |  12 
crates/lsp/src/lsp.rs                                            |  42 
crates/markdown_preview/Cargo.toml                               |  22 
crates/menu/Cargo.toml                                           |   2 
crates/multi_buffer/Cargo.toml                                   |  44 
crates/multi_buffer/src/multi_buffer.rs                          |   6 
crates/node_runtime/Cargo.toml                                   |   2 
crates/notifications/Cargo.toml                                  |  36 
crates/outline/Cargo.toml                                        |  24 
crates/outline/src/outline.rs                                    |   1 
crates/picker/Cargo.toml                                         |  20 
crates/picker/src/picker.rs                                      |  13 
crates/plugin/Cargo.toml                                         |   2 
crates/prettier/Cargo.toml                                       |  22 
crates/project/Cargo.toml                                        |  64 
crates/project/src/project.rs                                    | 166 
crates/project_panel/Cargo.toml                                  |  36 
crates/project_panel/src/project_panel.rs                        |  37 
crates/project_symbols/Cargo.toml                                |  38 
crates/quick_action_bar/Cargo.toml                               |  19 
crates/quick_action_bar/src/quick_action_bar.rs                  |  88 
crates/recent_projects/Cargo.toml                                |  24 
crates/release_channel/Cargo.toml                                |   2 
crates/rich_text/Cargo.toml                                      |  14 
crates/rich_text/src/rich_text.rs                                |  34 
crates/rope/Cargo.toml                                           |   8 
crates/rpc/Cargo.toml                                            |  12 
crates/rpc/proto/zed.proto                                       |   2 
crates/rpc/src/peer.rs                                           |   2 
crates/search/Cargo.toml                                         |  32 
crates/search/src/buffer_search.rs                               |   1 
crates/search/src/project_search.rs                              |   5 
crates/semantic_index/Cargo.toml                                 |  40 
crates/semantic_index/src/parsing.rs                             |   4 
crates/settings/Cargo.toml                                       |  16 
crates/sqlez/Cargo.toml                                          |   3 
crates/sqlez/src/thread_safe_connection.rs                       |   3 
crates/sqlez_macros/Cargo.toml                                   |   2 
crates/story/Cargo.toml                                          |   2 
crates/storybook/Cargo.toml                                      |  26 
crates/terminal/Cargo.toml                                       |  13 
crates/terminal/src/terminal.rs                                  |  31 
crates/terminal/src/terminal_settings.rs                         |   3 
crates/terminal_view/Cargo.toml                                  |  35 
crates/terminal_view/src/terminal_element.rs                     |  18 
crates/terminal_view/src/terminal_view.rs                        | 231 
crates/text/Cargo.toml                                           |  16 
crates/theme/Cargo.toml                                          |  25 
crates/theme/src/default_colors.rs                               |   4 
crates/theme/src/registry.rs                                     |   4 
crates/theme/src/settings.rs                                     | 110 
crates/theme/src/theme.rs                                        |  11 
crates/theme_importer/Cargo.toml                                 |   6 
crates/theme_importer/src/main.rs                                |   6 
crates/theme_selector/Cargo.toml                                 |  26 
crates/theme_selector/src/theme_selector.rs                      |  24 
crates/ui/Cargo.toml                                             |  10 
crates/ui/src/components/label/highlighted_label.rs              |   3 
crates/util/Cargo.toml                                           |   1 
crates/util/src/paths.rs                                         |   2 
crates/util/src/test/marked_text.rs                              |   3 
crates/vcs_menu/Cargo.toml                                       |  14 
crates/vim/Cargo.toml                                            |  46 
crates/vim/src/motion.rs                                         |  62 
crates/vim/src/normal/scroll.rs                                  |  10 
crates/vim/src/test/neovim_backed_test_context.rs                |   6 
crates/vim/src/vim.rs                                            |  26 
crates/vim/test_data/test_next_word_end_newline_last_char.json   |   3 
crates/welcome/Cargo.toml                                        |  34 
crates/workspace/Cargo.toml                                      |  46 
crates/workspace/src/item.rs                                     |   1 
crates/workspace/src/pane.rs                                     |  22 
crates/workspace/src/toolbar.rs                                  |   2 
crates/workspace/src/workspace.rs                                |  95 
crates/zed/Cargo.toml                                            | 135 
crates/zed/src/languages.rs                                      |  14 
crates/zed/src/languages/beancount/config.toml                   |   3 
crates/zed/src/languages/beancount/highlights.scm                |  21 
crates/zed/src/languages/elm.rs                                  |  28 
crates/zed/src/languages/go.rs                                   |   3 
crates/zed/src/languages/go/highlights.scm                       |  17 
crates/zed/src/languages/go/outline.scm                          |  11 
crates/zed/src/languages/haskell/outline.scm                     |  26 
crates/zed/src/languages/hcl/config.toml                         |  13 
crates/zed/src/languages/hcl/highlights.scm                      | 117 
crates/zed/src/languages/hcl/indents.scm                         |  11 
crates/zed/src/languages/hcl/injections.scm                      |   6 
crates/zed/src/languages/ocaml-interface/brackets.scm            |   6 
crates/zed/src/languages/ocaml-interface/config.toml             |  13 
crates/zed/src/languages/ocaml-interface/highlights.scm          |   1 
crates/zed/src/languages/ocaml-interface/indents.scm             |  21 
crates/zed/src/languages/ocaml-interface/outline.scm             |  48 
crates/zed/src/languages/ocaml.rs                                | 317 
crates/zed/src/languages/ocaml/brackets.scm                      |  12 
crates/zed/src/languages/ocaml/config.toml                       |  18 
crates/zed/src/languages/ocaml/highlights.scm                    | 142 
crates/zed/src/languages/ocaml/indents.scm                       |  43 
crates/zed/src/languages/ocaml/outline.scm                       |  59 
crates/zed/src/languages/terraform/config.toml                   |  13 
crates/zed/src/languages/terraform/highlights.scm                | 159 
crates/zed/src/languages/terraform/indents.scm                   |  14 
crates/zed/src/languages/terraform/injections.scm                |   9 
crates/zed/src/main.rs                                           |  57 
crates/zed/src/open_listener.rs                                  |   4 
crates/zed/src/zed.rs                                            |   8 
crates/zed_actions/Cargo.toml                                    |   2 
docs/src/configuring_zed.md                                      |  17 
docs/src/configuring_zed__key_bindings.md                        |  19 
docs/src/developing_zed__building_zed.md                         |   6 
docs/src/languages/elm.md                                        |  21 
docs/src/languages/ocaml.md                                      |  31 
script/zed-local                                                 |   1 
238 files changed, 6,219 insertions(+), 3,149 deletions(-)

Detailed changes

.github/actions/check_style/action.yml πŸ”—

@@ -2,29 +2,29 @@ name: "Check formatting"
 description: "Checks code formatting use cargo fmt"
 
 runs:
-  using: "composite"
-  steps:
-    - name: cargo fmt
-      shell: bash -euxo pipefail {0}
-      run: cargo fmt --all -- --check
+    using: "composite"
+    steps:
+        - name: cargo fmt
+          shell: bash -euxo pipefail {0}
+          run: cargo fmt --all -- --check
 
-    - name: cargo clippy
-      shell: bash -euxo pipefail {0}
-      # 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
-      run: |
-        cargo clippy --release --workspace --all-features --all-targets -- -A clippy::all -D clippy::dbg_macro -D clippy::todo
-        cargo clippy -p gpui
+        - name: cargo clippy
+          shell: bash -euxo pipefail {0}
+          # 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
+          run: |
+              cargo clippy --release --workspace --all-features --all-targets -- -A clippy::all -D clippy::dbg_macro -D clippy::todo
+              cargo clippy -p gpui
 
-    - name: Find modified migrations
-      shell: bash -euxo pipefail {0}
-      run: |
-        export SQUAWK_GITHUB_TOKEN=${{ github.token }}
-        . ./script/squawk
+        - name: Find modified migrations
+          shell: bash -euxo pipefail {0}
+          run: |
+              export SQUAWK_GITHUB_TOKEN=${{ github.token }}
+              . ./script/squawk
 
-    - uses: bufbuild/buf-setup-action@v1
-    - uses: bufbuild/buf-breaking-action@v1
-      with:
-        input: "crates/rpc/proto/"
-        against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=main,subdir=crates/rpc/proto/"
+        - uses: bufbuild/buf-setup-action@v1
+        - uses: bufbuild/buf-breaking-action@v1
+          with:
+              input: "crates/rpc/proto/"
+              against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=main,subdir=crates/rpc/proto/"

.github/pull_request_template.md πŸ”—

@@ -2,4 +2,8 @@
 
 Release Notes:
 
-- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/zed/issues/<public_issue_number_if_exists>)).
+- Added/Fixed/Improved ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/zed/issues/<public_issue_number_if_exists>)).
+
+**or**
+
+- N/A

.github/workflows/ci.yml πŸ”—

@@ -1,149 +1,149 @@
 name: CI
 
 on:
-  push:
-    branches:
-      - main
-      - "v[0-9]+.[0-9]+.x"
-    tags:
-      - "v*"
-  pull_request:
-    branches:
-      - "**"
+    push:
+        branches:
+            - main
+            - "v[0-9]+.[0-9]+.x"
+        tags:
+            - "v*"
+    pull_request:
+        branches:
+            - "**"
 
 concurrency:
-  # Allow only one workflow per any non-`main` branch.
-  group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
-  cancel-in-progress: true
+    # Allow only one workflow per any non-`main` branch.
+    group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
+    cancel-in-progress: true
 
 env:
-  CARGO_TERM_COLOR: always
-  CARGO_INCREMENTAL: 0
-  RUST_BACKTRACE: 1
+    CARGO_TERM_COLOR: always
+    CARGO_INCREMENTAL: 0
+    RUST_BACKTRACE: 1
 
 jobs:
-  style:
-    name: Check formatting, Clippy lints, and spelling
-    runs-on:
-      - self-hosted
-      - test
-    steps:
-      - name: Checkout repo
-        uses: actions/checkout@v4
-        with:
-          clean: false
-          submodules: "recursive"
-          fetch-depth: 0
-
-      - name: Set up default .cargo/config.toml
-        run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
-
-      - name: Check spelling
-        run: |
-          if ! which typos > /dev/null; then
-            cargo install typos-cli
-          fi
-          typos
-
-      - name: Run style checks
-        uses: ./.github/actions/check_style
-
-  tests:
-    name: Run tests
-    runs-on:
-      - self-hosted
-      - test
-    steps:
-      - name: Checkout repo
-        uses: actions/checkout@v4
-        with:
-          clean: false
-          submodules: "recursive"
-
-      - name: Run tests
-        uses: ./.github/actions/run_tests
-
-      - name: Build collab
-        run: cargo build -p collab
-
-      - name: Build other binaries
-        run: cargo build --workspace --bins --all-features
-
-  bundle:
-    name: Bundle app
-    runs-on:
-      - self-hosted
-      - bundle
-    if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
-    needs: tests
-    env:
-      MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
-      MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
-      APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
-      APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
-      ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
-    steps:
-      - name: Install Node
-        uses: actions/setup-node@v3
-        with:
-          node-version: "18"
-
-      - name: Checkout repo
-        uses: actions/checkout@v4
-        with:
-          clean: false
-          submodules: "recursive"
-
-      - name: Limit target directory size
-        run: script/clear-target-dir-if-larger-than 100
-
-      - name: Determine version and release channel
-        if: ${{ startsWith(github.ref, 'refs/tags/v') }}
-        run: |
-          set -eu
-
-          version=$(script/get-crate-version zed)
-          channel=$(cat crates/zed/RELEASE_CHANNEL)
-          echo "Publishing version: ${version} on release channel ${channel}"
-          echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
-
-          expected_tag_name=""
-          case ${channel} in
-            stable)
-              expected_tag_name="v${version}";;
-            preview)
-              expected_tag_name="v${version}-pre";;
-            nightly)
-              expected_tag_name="v${version}-nightly";;
-            *)
-              echo "can't publish a release on channel ${channel}"
-              exit 1;;
-          esac
-          if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
-            echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
-            exit 1
-          fi
-
-      - name: Generate license file
-        run: script/generate-licenses
-
-      - name: Create app bundle
-        run: script/bundle
-
-      - name: Upload app bundle to workflow run if main branch or specific label
-        uses: actions/upload-artifact@v3
-        if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
-        with:
-          name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
-          path: target/release/Zed.dmg
-
-      - uses: softprops/action-gh-release@v1
-        name: Upload app bundle to release
-        if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
-        with:
-          draft: true
-          prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
-          files: target/release/Zed.dmg
-          body: ""
+    style:
+        name: Check formatting, Clippy lints, and spelling
+        runs-on:
+            - self-hosted
+            - test
+        steps:
+            - name: Checkout repo
+              uses: actions/checkout@v4
+              with:
+                  clean: false
+                  submodules: "recursive"
+                  fetch-depth: 0
+
+            - name: Set up default .cargo/config.toml
+              run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
+
+            - name: Check spelling
+              run: |
+                  if ! which typos > /dev/null; then
+                    cargo install typos-cli
+                  fi
+                  typos
+
+            - name: Run style checks
+              uses: ./.github/actions/check_style
+
+    tests:
+        name: Run tests
+        runs-on:
+            - self-hosted
+            - test
+        steps:
+            - name: Checkout repo
+              uses: actions/checkout@v4
+              with:
+                  clean: false
+                  submodules: "recursive"
+
+            - name: Run tests
+              uses: ./.github/actions/run_tests
+
+            - name: Build collab
+              run: cargo build -p collab
+
+            - name: Build other binaries
+              run: cargo build --workspace --bins --all-features
+
+    bundle:
+        name: Bundle app
+        runs-on:
+            - self-hosted
+            - bundle
+        if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
+        needs: tests
         env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+            MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+            MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+            APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
+            APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
+            ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
+        steps:
+            - name: Install Node
+              uses: actions/setup-node@v4
+              with:
+                  node-version: "18"
+
+            - name: Checkout repo
+              uses: actions/checkout@v4
+              with:
+                  clean: false
+                  submodules: "recursive"
+
+            - name: Limit target directory size
+              run: script/clear-target-dir-if-larger-than 100
+
+            - name: Determine version and release channel
+              if: ${{ startsWith(github.ref, 'refs/tags/v') }}
+              run: |
+                  set -eu
+
+                  version=$(script/get-crate-version zed)
+                  channel=$(cat crates/zed/RELEASE_CHANNEL)
+                  echo "Publishing version: ${version} on release channel ${channel}"
+                  echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
+
+                  expected_tag_name=""
+                  case ${channel} in
+                    stable)
+                      expected_tag_name="v${version}";;
+                    preview)
+                      expected_tag_name="v${version}-pre";;
+                    nightly)
+                      expected_tag_name="v${version}-nightly";;
+                    *)
+                      echo "can't publish a release on channel ${channel}"
+                      exit 1;;
+                  esac
+                  if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
+                    echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
+                    exit 1
+                  fi
+
+            - name: Generate license file
+              run: script/generate-licenses
+
+            - name: Create app bundle
+              run: script/bundle
+
+            - name: Upload app bundle to workflow run if main branch or specific label
+              uses: actions/upload-artifact@v3
+              if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
+              with:
+                  name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
+                  path: target/release/Zed.dmg
+
+            - uses: softprops/action-gh-release@v1
+              name: Upload app bundle to release
+              if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
+              with:
+                  draft: true
+                  prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
+                  files: target/release/Zed.dmg
+                  body: ""
+              env:
+                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/danger.yml πŸ”—

@@ -1,35 +1,35 @@
 name: Danger
 
 on:
-  pull_request:
-    branches: [main]
-    types:
-      - opened
-      - synchronize
-      - reopened
-      - edited
+    pull_request:
+        branches: [main]
+        types:
+            - opened
+            - synchronize
+            - reopened
+            - edited
 
 jobs:
-  danger:
-    runs-on: ubuntu-latest
+    danger:
+        runs-on: ubuntu-latest
 
-    steps:
-      - uses: actions/checkout@v4
+        steps:
+            - uses: actions/checkout@v4
 
-      - uses: pnpm/action-setup@v2.2.4
-        with:
-          version: 8
+            - uses: pnpm/action-setup@v2.2.4
+              with:
+                  version: 8
 
-      - name: Setup Node
-        uses: actions/setup-node@v4
-        with:
-          node-version: "20"
-          cache: "pnpm"
-          cache-dependency-path: "script/danger/pnpm-lock.yaml"
+            - name: Setup Node
+              uses: actions/setup-node@v4
+              with:
+                  node-version: "20"
+                  cache: "pnpm"
+                  cache-dependency-path: "script/danger/pnpm-lock.yaml"
 
-      - run: pnpm install --dir script/danger
+            - run: pnpm install --dir script/danger
 
-      - name: Run Danger
-        run: pnpm run --dir script/danger danger ci
-        env:
-          GITHUB_TOKEN: ${{ github.token }}
+            - name: Run Danger
+              run: pnpm run --dir script/danger danger ci
+              env:
+                  GITHUB_TOKEN: ${{ github.token }}

.github/workflows/randomized_tests.yml πŸ”—

@@ -3,35 +3,35 @@ name: Randomized Tests
 concurrency: randomized-tests
 
 on:
-  push:
-    branches:
-      - randomized-tests-runner
-  # schedule:
-  #    - cron: '0 * * * *'
+    push:
+        branches:
+            - randomized-tests-runner
+    # schedule:
+    #    - cron: '0 * * * *'
 
 env:
-  CARGO_TERM_COLOR: always
-  CARGO_INCREMENTAL: 0
-  RUST_BACKTRACE: 1
-  ZED_SERVER_URL: https://zed.dev
+    CARGO_TERM_COLOR: always
+    CARGO_INCREMENTAL: 0
+    RUST_BACKTRACE: 1
+    ZED_SERVER_URL: https://zed.dev
 
 jobs:
-  tests:
-    name: Run randomized tests
-    runs-on:
-      - self-hosted
-      - randomized-tests
-    steps:
-      - name: Install Node
-        uses: actions/setup-node@v3
-        with:
-          node-version: "18"
+    tests:
+        name: Run randomized tests
+        runs-on:
+            - self-hosted
+            - randomized-tests
+        steps:
+            - name: Install Node
+              uses: actions/setup-node@v4
+              with:
+                  node-version: "18"
 
-      - name: Checkout repo
-        uses: actions/checkout@v4
-        with:
-          clean: false
-          submodules: "recursive"
+            - name: Checkout repo
+              uses: actions/checkout@v4
+              with:
+                  clean: false
+                  submodules: "recursive"
 
-      - name: Run randomized tests
-        run: script/randomized-test-ci
+            - name: Run randomized tests
+              run: script/randomized-test-ci

.github/workflows/release_nightly.yml πŸ”—

@@ -16,6 +16,7 @@ env:
 jobs:
     style:
         name: Check formatting and Clippy lints
+        if: github.repository_owner == 'zed-industries'
         runs-on:
             - self-hosted
             - test
@@ -32,6 +33,7 @@ jobs:
 
     tests:
         name: Run tests
+        if: github.repository_owner == 'zed-industries'
         runs-on:
             - self-hosted
             - test
@@ -48,6 +50,7 @@ jobs:
 
     bundle:
         name: Bundle app
+        if: github.repository_owner == 'zed-industries'
         runs-on:
             - self-hosted
             - bundle
@@ -62,7 +65,7 @@ jobs:
             ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
         steps:
             - name: Install Node
-              uses: actions/setup-node@v3
+              uses: actions/setup-node@v4
               with:
                   node-version: "18"
 

Cargo.lock πŸ”—

@@ -103,9 +103,9 @@ dependencies = [
 
 [[package]]
 name = "alacritty_terminal"
-version = "0.21.0"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35229555d7cc7e83392dfc27c96bec560b1076d756184893296cd60125f4a264"
+checksum = "cc7ceabf6fc76511f616ca216b51398a2511f19ba9f71bcbd977999edff1b0d1"
 dependencies = [
  "base64 0.21.4",
  "bitflags 2.4.1",
@@ -467,6 +467,18 @@ dependencies = [
  "event-listener",
 ]
 
+[[package]]
+name = "async-native-tls"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e9e7a929bd34c68a82d58a4de7f86fffdaf97fb2af850162a7bb19dd7269b33"
+dependencies = [
+ "async-std",
+ "native-tls",
+ "thiserror",
+ "url",
+]
+
 [[package]]
 name = "async-net"
 version = "1.7.0"
@@ -596,19 +608,6 @@ version = "4.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
 
-[[package]]
-name = "async-tls"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f23d769dbf1838d5df5156e7b1ad404f4c463d1ac2c6aeb6cd943630f8a8400"
-dependencies = [
- "futures-core",
- "futures-io",
- "rustls 0.19.1",
- "webpki",
- "webpki-roots 0.21.1",
-]
-
 [[package]]
 name = "async-trait"
 version = "0.1.73"
@@ -626,7 +625,8 @@ version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5682ea0913e5c20780fe5785abacb85a411e7437bf52a1bedb93ddb3972cb8dd"
 dependencies = [
- "async-tls",
+ "async-native-tls",
+ "async-std",
  "futures-io",
  "futures-util",
  "log",
@@ -2507,6 +2507,7 @@ dependencies = [
  "itertools 0.10.5",
  "language",
  "lazy_static",
+ "linkify",
  "log",
  "lsp",
  "multi_buffer",
@@ -4026,12 +4027,6 @@ dependencies = [
  "wasm-bindgen",
 ]
 
-[[package]]
-name = "json_comments"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9dbbfed4e59ba9750e15ba154fdfd9329cee16ff3df539c2666b70f58cc32105"
-
 [[package]]
 name = "jwt"
 version = "0.16.0"
@@ -4310,6 +4305,15 @@ dependencies = [
  "safemem",
 ]
 
+[[package]]
+name = "linkify"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "linkme"
 version = "0.3.17"
@@ -6283,6 +6287,7 @@ dependencies = [
  "editor",
  "gpui",
  "search",
+ "settings",
  "ui",
  "workspace",
 ]
@@ -6965,19 +6970,6 @@ dependencies = [
  "rustix 0.38.30",
 ]
 
-[[package]]
-name = "rustls"
-version = "0.19.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
-dependencies = [
- "base64 0.13.1",
- "log",
- "ring",
- "sct 0.6.1",
- "webpki",
-]
-
 [[package]]
 name = "rustls"
 version = "0.21.7"
@@ -6986,7 +6978,7 @@ checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
 dependencies = [
  "ring",
  "rustls-webpki",
- "sct 0.7.0",
+ "sct",
 ]
 
 [[package]]
@@ -7129,16 +7121,6 @@ dependencies = [
  "sha2 0.9.9",
 ]
 
-[[package]]
-name = "sct"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
-dependencies = [
- "ring",
- "untrusted",
-]
-
 [[package]]
 name = "sct"
 version = "0.7.0"
@@ -7792,6 +7774,7 @@ name = "sqlez"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "collections",
  "futures 0.3.28",
  "indoc",
  "lazy_static",
@@ -7870,7 +7853,7 @@ dependencies = [
  "paste",
  "percent-encoding",
  "rust_decimal",
- "rustls 0.21.7",
+ "rustls",
  "rustls-pemfile",
  "serde",
  "serde_json",
@@ -7884,7 +7867,7 @@ dependencies = [
  "tracing",
  "url",
  "uuid 1.4.1",
- "webpki-roots 0.24.0",
+ "webpki-roots",
 ]
 
 [[package]]
@@ -8367,6 +8350,7 @@ version = "0.1.0"
 dependencies = [
  "alacritty_terminal",
  "anyhow",
+ "collections",
  "db",
  "dirs 4.0.0",
  "futures 0.3.28",
@@ -8397,6 +8381,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "client",
+ "collections",
  "db",
  "dirs 4.0.0",
  "editor",
@@ -8461,6 +8446,7 @@ name = "theme"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "collections",
  "color",
  "derive_more",
  "fs",
@@ -8495,7 +8481,6 @@ dependencies = [
  "gpui",
  "indexmap 1.9.3",
  "indoc",
- "json_comments",
  "log",
  "palette",
  "pathfinder_color",
@@ -8503,6 +8488,7 @@ dependencies = [
  "schemars",
  "serde",
  "serde_json",
+ "serde_json_lenient",
  "simplelog",
  "strum",
  "theme",
@@ -8991,6 +8977,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-beancount"
+version = "2.2.0"
+source = "git+https://github.com/polarmutex/tree-sitter-beancount?rev=da1bf8c6eb0ae7a97588affde7227630bcd678b6#da1bf8c6eb0ae7a97588affde7227630bcd678b6"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-c"
 version = "0.20.6"
@@ -9129,6 +9124,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-hcl"
+version = "0.0.1"
+source = "git+https://github.com/MichaHoffmann/tree-sitter-hcl?rev=v1.1.0#636dbe70301ecbab8f353c8c78b3406fe4f185f5"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-heex"
 version = "0.0.1"
@@ -9204,6 +9208,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-ocaml"
+version = "0.20.4"
+source = "git+https://github.com/tree-sitter/tree-sitter-ocaml?rev=4abfdc1c7af2c6c77a370aee974627be1c285b3b#4abfdc1c7af2c6c77a370aee974627be1c285b3b"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-php"
 version = "0.21.1"
@@ -9373,6 +9386,7 @@ dependencies = [
  "http",
  "httparse",
  "log",
+ "native-tls",
  "rand 0.8.5",
  "sha-1 0.9.8",
  "thiserror",
@@ -9582,6 +9596,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "backtrace",
+ "collections",
  "dirs 3.0.2",
  "futures 0.3.28",
  "git2",
@@ -10086,25 +10101,6 @@ dependencies = [
  "wasm-bindgen",
 ]
 
-[[package]]
-name = "webpki"
-version = "0.21.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
-dependencies = [
- "ring",
- "untrusted",
-]
-
-[[package]]
-name = "webpki-roots"
-version = "0.21.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
-dependencies = [
- "webpki",
-]
-
 [[package]]
 name = "webpki-roots"
 version = "0.24.0"
@@ -10563,7 +10559,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.122.0"
+version = "0.123.0"
 dependencies = [
  "activity_indicator",
  "ai",
@@ -10658,6 +10654,7 @@ dependencies = [
  "toml",
  "tree-sitter",
  "tree-sitter-bash",
+ "tree-sitter-beancount",
  "tree-sitter-c",
  "tree-sitter-c-sharp",
  "tree-sitter-cpp",
@@ -10673,6 +10670,7 @@ dependencies = [
  "tree-sitter-gomod",
  "tree-sitter-gowork",
  "tree-sitter-haskell",
+ "tree-sitter-hcl",
  "tree-sitter-heex",
  "tree-sitter-html",
  "tree-sitter-json 0.20.0",
@@ -10680,6 +10678,7 @@ dependencies = [
  "tree-sitter-markdown",
  "tree-sitter-nix",
  "tree-sitter-nu",
+ "tree-sitter-ocaml",
  "tree-sitter-php",
  "tree-sitter-proto",
  "tree-sitter-purescript",

Cargo.toml πŸ”—

@@ -92,6 +92,90 @@ default-members = ["crates/zed"]
 resolver = "2"
 
 [workspace.dependencies]
+activity_indicator = { path = "crates/activity_indicator" }
+ai = { path = "crates/ai" }
+assets = { path = "crates/assets" }
+assistant = { path = "crates/assistant" }
+audio = { path = "crates/audio" }
+auto_update = { path = "crates/auto_update" }
+breadcrumbs = { path = "crates/breadcrumbs" }
+call = { path = "crates/call" }
+channel = { path = "crates/channel" }
+cli = { path = "crates/cli" }
+client = { path = "crates/client" }
+clock = { path = "crates/clock" }
+collab = { path = "crates/collab" }
+collab_ui = { path = "crates/collab_ui" }
+collections = { path = "crates/collections" }
+color = { path = "crates/color" }
+command_palette = { path = "crates/command_palette" }
+copilot = { path = "crates/copilot" }
+copilot_ui = { path = "crates/copilot_ui" }
+db = { path = "crates/db" }
+diagnostics = { path = "crates/diagnostics" }
+editor = { path = "crates/editor" }
+feature_flags = { path = "crates/feature_flags" }
+feedback = { path = "crates/feedback" }
+file_finder = { path = "crates/file_finder" }
+fs = { path = "crates/fs" }
+fsevent = { path = "crates/fsevent" }
+fuzzy = { path = "crates/fuzzy" }
+git = { path = "crates/git" }
+go_to_line = { path = "crates/go_to_line" }
+gpui = { path = "crates/gpui" }
+gpui_macros = { path = "crates/gpui_macros" }
+install_cli = { path = "crates/install_cli" }
+journal = { path = "crates/journal" }
+language = { path = "crates/language" }
+language_selector = { path = "crates/language_selector" }
+language_tools = { path = "crates/language_tools" }
+live_kit_client = { path = "crates/live_kit_client" }
+live_kit_server = { path = "crates/live_kit_server" }
+lsp = { path = "crates/lsp" }
+markdown_preview = { path = "crates/markdown_preview" }
+media = { path = "crates/media" }
+menu = { path = "crates/menu" }
+multi_buffer = { path = "crates/multi_buffer" }
+node_runtime = { path = "crates/node_runtime" }
+notifications = { path = "crates/notifications" }
+outline = { path = "crates/outline" }
+picker = { path = "crates/picker" }
+plugin = { path = "crates/plugin" }
+plugin_macros = { path = "crates/plugin_macros" }
+prettier = { path = "crates/prettier" }
+project = { path = "crates/project" }
+project_panel = { path = "crates/project_panel" }
+project_symbols = { path = "crates/project_symbols" }
+quick_action_bar = { path = "crates/quick_action_bar" }
+recent_projects = { path = "crates/recent_projects" }
+release_channel = { path = "crates/release_channel" }
+rich_text = { path = "crates/rich_text" }
+rope = { path = "crates/rope" }
+rpc = { path = "crates/rpc" }
+search = { path = "crates/search" }
+semantic_index = { path = "crates/semantic_index" }
+settings = { path = "crates/settings" }
+snippet = { path = "crates/snippet" }
+sqlez = { path = "crates/sqlez" }
+sqlez_macros = { path = "crates/sqlez_macros" }
+story = { path = "crates/story" }
+storybook = { path = "crates/storybook" }
+sum_tree = { path = "crates/sum_tree" }
+terminal = { path = "crates/terminal" }
+terminal_view = { path = "crates/terminal_view" }
+text = { path = "crates/text" }
+theme = { path = "crates/theme" }
+theme_importer = { path = "crates/theme_importer" }
+theme_selector = { path = "crates/theme_selector" }
+ui = { path = "crates/ui" }
+util = { path = "crates/util" }
+vcs_menu = { path = "crates/vcs_menu" }
+vim = { path = "crates/vim" }
+welcome = { path = "crates/welcome" }
+workspace = { path = "crates/workspace" }
+zed = { path = "crates/zed" }
+zed_actions = { path = "crates/zed_actions" }
+
 anyhow = "1.0.57"
 async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
 async-trait = "0.1"
@@ -100,11 +184,14 @@ ctor = "0.2.6"
 derive_more = "0.99.17"
 env_logger = "0.9"
 futures = "0.3"
-git2 = { version = "0.15", default-features = false}
+git2 = { version = "0.15", default-features = false }
 globset = "0.4"
 indoc = "1"
 # We explicitly disable a http2 support in isahc.
-isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
+isahc = { version = "1.7.2", default-features = false, features = [
+    "static-curl",
+    "text-decoding",
+] }
 lazy_static = "1.4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 ordered-float = "2.1.1"
@@ -122,7 +209,10 @@ schemars = "0.8"
 serde = { version = "1.0", features = ["derive", "rc"] }
 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_json_lenient = { version = "0.1", features = [
+    "preserve_order",
+    "raw_value",
+] }
 serde_repr = "0.1"
 smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"
@@ -135,9 +225,10 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] }
 toml = "0.5"
 tree-sitter = { version = "0.20", features = ["wasm"] }
 tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" }
+tree-sitter-beancount = { git = "https://github.com/polarmutex/tree-sitter-beancount", rev = "da1bf8c6eb0ae7a97588affde7227630bcd678b6" }
 tree-sitter-c = "0.20.1"
 tree-sitter-c-sharp = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "dd5e59721a5f8dae34604060833902b882023aaf" }
-tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
+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-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" }
@@ -150,6 +241,7 @@ tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev =
 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-hcl = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0" }
 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" }
@@ -157,8 +249,9 @@ 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-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "4abfdc1c7af2c6c77a370aee974627be1c285b3b" }
 tree-sitter-php = "0.21.1"
-tree-sitter-proto = {git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
+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-python = "0.20.2"
 tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" }

assets/icons/file_icons/elm.svg πŸ”—

@@ -0,0 +1,9 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.7295 12.1981L7.17677 7.64544C7.07914 7.5478 6.92085 7.5478 6.82322 7.64544L2.27053 12.1981C2.11304 12.3556 2.22458 12.6249 2.4473 12.6249H11.5527C11.7754 12.6249 11.887 12.3556 11.7295 12.1981Z" fill="black" fill-opacity="0.9"/>
+<path d="M1.80178 11.7294L6.35447 7.17668C6.4521 7.07905 6.4521 6.92076 6.35447 6.82312L1.80178 2.27043C1.64429 2.11294 1.375 2.22448 1.375 2.44721L1.375 11.5526C1.375 11.7753 1.64428 11.8869 1.80178 11.7294Z" fill="black" fill-opacity="0.9"/>
+<path d="M9.98928 9.16694L11.9794 7.17751C12.0771 7.0799 12.0771 6.92161 11.9795 6.82396L9.98928 4.833C9.89165 4.73534 9.73333 4.73532 9.63569 4.83297L7.64553 6.82313C7.5479 6.92076 7.5479 7.07905 7.64553 7.17668L9.63575 9.16691C9.73337 9.26453 9.89164 9.26455 9.98928 9.16694Z" fill="black" fill-opacity="0.9"/>
+<path d="M7.89553 1.80168L12.1982 6.10438C12.3557 6.26187 12.625 6.15033 12.625 5.9276V2.37491C12.625 1.82262 12.1773 1.37491 11.625 1.37491H8.0723C7.84958 1.37491 7.73804 1.64419 7.89553 1.80168Z" fill="black" fill-opacity="0.6"/>
+<path d="M8.73976 4.18772L5.25981 4.1895C5.03708 4.18962 4.92567 4.45896 5.08325 4.61637L6.82322 6.35456C6.92087 6.45211 7.07909 6.45207 7.17669 6.35447L8.91666 4.61449C9.0742 4.45696 8.96255 4.1876 8.73976 4.18772Z" fill="black" fill-opacity="0.6"/>
+<path d="M8.1147 3.55936L4.13431 3.55936C4.06801 3.55936 4.00442 3.53302 3.95753 3.48614L2.27057 1.79918C2.11308 1.64169 2.22462 1.37241 2.44735 1.37241L6.42774 1.37241C6.49405 1.37241 6.55763 1.39874 6.60452 1.44563L8.29148 3.13258C8.44897 3.29007 8.33743 3.55936 8.1147 3.55936Z" fill="black" fill-opacity="0.6"/>
+<path d="M12.625 8.07221V11.5526C12.625 11.7753 12.3557 11.8869 12.1982 11.7294L10.458 9.98918C10.3604 9.89155 10.3604 9.73326 10.458 9.63563L12.1982 7.89544C12.3557 7.73794 12.625 7.84949 12.625 8.07221Z" fill="black" fill-opacity="0.6"/>
+</svg>

assets/icons/file_icons/file_types.json πŸ”—

@@ -25,6 +25,7 @@
         "doc": "document",
         "docx": "document",
         "eex": "elixir",
+        "elm": "elm",
         "erl": "erlang",
         "escript": "erlang",
         "eslintrc": "eslint",
@@ -69,6 +70,8 @@
         "mdb": "storage",
         "mdf": "storage",
         "mdx": "document",
+        "ml": "ocaml",
+        "mli": "ocaml",
         "mp3": "audio",
         "mp4": "video",
         "myd": "storage",
@@ -85,6 +88,7 @@
         "pptx": "document",
         "prettierignore": "prettier",
         "prettierrc": "prettier",
+        "prisma": "prisma",
         "profile": "terminal",
         "ps1": "terminal",
         "psd": "image",
@@ -116,8 +120,8 @@
         "xlsx": "document",
         "xml": "template",
         "xrl": "erlang",
-        "yaml": "yaml",
-        "yml": "yaml",
+        "yaml": "settings",
+        "yml": "settings",
         "yrl": "erlang",
         "zlogin": "terminal",
         "zsh": "terminal",
@@ -152,6 +156,9 @@
         "elixir": {
             "icon": "icons/file_icons/elixir.svg"
         },
+        "elm": {
+            "icon": "icons/file_icons/elm.svg"
+        },
         "erlang": {
             "icon": "icons/file_icons/erlang.svg"
         },
@@ -179,18 +186,21 @@
         "log": {
             "icon": "icons/file_icons/info.svg"
         },
+        "ocaml": {
+          "icon": "icons/file_icons/ocaml.svg"
+        },
         "phoenix": {
             "icon": "icons/file_icons/phoenix.svg"
         },
         "php": {
             "icon": "icons/file_icons/php.svg"
         },
-        "yaml": {
-            "icon": "icons/file_icons/yaml.svg"
-        },
         "prettier": {
             "icon": "icons/file_icons/prettier.svg"
         },
+        "prisma": {
+            "icon": "icons/file_icons/prisma.svg"
+        },
         "python": {
             "icon": "icons/file_icons/python.svg"
         },

assets/icons/file_icons/ocaml.svg πŸ”—

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.73843 11.709C6.70584 11.6334 6.60683 11.4367 6.55712 11.3736C6.44917 11.2362 6.42396 11.2258 6.39221 11.0523C6.33703 10.7501 6.19094 10.202 6.01879 9.82381C5.92987 9.62863 5.78201 9.46467 5.64665 9.32312C5.52847 9.19895 5.26214 8.99002 5.2157 9.00037C4.7807 9.09487 4.64576 9.55902 4.44115 9.92673C4.32795 10.1301 4.208 10.3031 4.1188 10.5195C4.03641 10.7184 4.04373 10.9387 3.90268 11.1095C3.75801 11.2849 3.66398 11.4715 3.59311 11.6981C3.57968 11.7412 3.54148 12.1939 3.5 12.3006L4.14649 12.2511C4.74896 12.2958 4.57496 12.547 5.51526 12.4922L7 12.4423C6.95398 12.2942 6.89056 12.1228 6.86613 12.067C6.82472 11.9732 6.77267 11.7897 6.73843 11.709Z" fill="black"/>

assets/icons/file_icons/prisma.svg πŸ”—

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="
+0 0 425 512" xmlns="http://www.w3.org/2000/svg" fill="#000000
+">
+    <path
+        d="m381.38934 405.88714-229.67062 67.92744c-7.01651 2.07778-13.74132-3.99173-12.2669-11.07217l82.04834-392.9335c1.53436-7.352147 11.69152-8.514905 14.89609-1.710173l151.9177 322.59543c2.86494 6.08949-.40357 13.26702-6.92461 15.19297zm39.38512-16.02808-175.89887-373.53306c-11.59465-21.691431-39.0351-20.904032-49.75484-2.749064l-190.77231 308.99c-5.9096786 9.63371-5.7938027 21.50903.3356409 31.01887l93.252489 144.4589c9.615412 11.46292 18.506512 16.87006 33.692012 12.37878l270.68561-80.05849c18.03265-5.40039 26.72265-22.82202 18.46027-40.50593z" />
+</svg>

assets/icons/file_icons/yaml.svg πŸ”—

@@ -1 +0,0 @@
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="457px" height="512px"><polygon points="342.0159302,0 457,0 114.9831009,512 0,512 171.0082092,256 0,0 114.9831009,0 228.4997559,169.9342041 "/></svg>

assets/keymaps/default.json πŸ”—

@@ -415,7 +415,15 @@
       "cmd-?": "assistant::ToggleFocus",
       "cmd-alt-s": "workspace::SaveAll",
       "cmd-k m": "language_selector::Toggle",
-      "escape": "workspace::Unfollow"
+      "escape": "workspace::Unfollow",
+      "cmd-k cmd-left": ["workspace::ActivatePaneInDirection", "Left"],
+      "cmd-k cmd-right": ["workspace::ActivatePaneInDirection", "Right"],
+      "cmd-k cmd-up": ["workspace::ActivatePaneInDirection", "Up"],
+      "cmd-k cmd-down": ["workspace::ActivatePaneInDirection", "Down"],
+      "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"]
     }
   },
   // Bindings from Sublime Text
@@ -441,18 +449,6 @@
       "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
     }
   },
-  {
-    "bindings": {
-      "cmd-k cmd-left": ["workspace::ActivatePaneInDirection", "Left"],
-      "cmd-k cmd-right": ["workspace::ActivatePaneInDirection", "Right"],
-      "cmd-k cmd-up": ["workspace::ActivatePaneInDirection", "Up"],
-      "cmd-k cmd-down": ["workspace::ActivatePaneInDirection", "Down"],
-      "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"]
-    }
-  },
   // Bindings from Atom
   {
     "context": "Pane",

assets/keymaps/jetbrains.json πŸ”—

@@ -42,6 +42,7 @@
       "shift-alt-up": "editor::MoveLineUp",
       "shift-alt-down": "editor::MoveLineDown",
       "cmd-alt-l": "editor::Format",
+      "shift-f6": "editor::Rename",
       "cmd-[": "pane::GoBack",
       "cmd-]": "pane::GoForward",
       "alt-f7": "editor::FindAllReferences",
@@ -80,10 +81,18 @@
       "cmd-6": "diagnostics::Deploy"
     }
   },
+  {
+    "context": "Pane",
+    "bindings": {
+      "cmd-alt-left": "pane::GoBack",
+      "cmd-alt-right": "pane::GoForward"
+    }
+  },
   {
     "context": "ProjectPanel",
     "bindings": {
-      "enter": "project_panel::Open"
+      "enter": "project_panel::Open",
+      "shift-f6": "project_panel::Rename"
     }
   }
 ]

assets/keymaps/vim.json πŸ”—

@@ -284,7 +284,8 @@
       "ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
       "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
       "ctrl-w n": ["workspace::NewFileInDirection", "Up"],
-      "ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"]
+      "ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"],
+      "-": "pane::RevealInProjectPanel"
     }
   },
   {
@@ -502,18 +503,5 @@
       "enter": "vim::SearchSubmit",
       "escape": "buffer_search::Dismiss"
     }
-  },
-  {
-    "context": "Dock",
-    "bindings": {
-      "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
-      "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
-      "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
-      "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
-      "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
-      "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
-      "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
-      "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"]
-    }
   }
 ]

assets/settings/default.json πŸ”—

@@ -104,8 +104,17 @@
   "show_whitespaces": "selection",
   // Settings related to calls in Zed
   "calls": {
-    // Join calls with the microphone muted by default
-    "mute_on_join": false
+    // Join calls with the microphone live by default
+    "mute_on_join": false,
+    // Share your project when you are the first to join a channel
+    "share_on_join": true
+  },
+  // Toolbar related settings
+  "toolbar": {
+    // Whether to show breadcrumbs.
+    "breadcrumbs": true,
+    // Whether to show quick action buttons.
+    "quick_actions": true
   },
   // Scrollbar related settings
   "scrollbar": {
@@ -127,8 +136,12 @@
     // Whether to show selections in the scrollbar.
     "selections": true,
     // Whether to show symbols selections in the scrollbar.
-    "symbols_selections": true
+    "symbols_selections": true,
+    // Whether to show diagnostic indicators in the scrollbar.
+    "diagnostics": true
   },
+  // The number of lines to keep above/below the cursor when scrolling.
+  "vertical_scroll_margin": 3,
   "relative_line_numbers": false,
   // When to populate a new search's query based on the text under the cursor.
   // This setting can take the following three values:
@@ -485,6 +498,9 @@
     "JavaScript": {
       "tab_size": 2
     },
+    "Terraform": {
+      "tab_size": 2
+    },
     "TypeScript": {
       "tab_size": 2
     },
@@ -496,6 +512,12 @@
     },
     "JSON": {
       "tab_size": 2
+    },
+    "OCaml": {
+      "tab_size": 2
+    },
+    "OCaml Interface": {
+      "tab_size": 2
     }
   },
   // Zed's Prettier integration settings.

assets/themes/andromeda/andromeda.json πŸ”—

@@ -42,7 +42,7 @@
         "tab_bar.background": "#21242bff",
         "tab.inactive_background": "#21242bff",
         "tab.active_background": "#1e2025ff",
-        "search.match_background": null,
+        "search.match_background": "#11a79366",
         "panel.background": "#21242bff",
         "panel.focused_border": null,
         "pane.focused_border": null,

assets/themes/atelier/atelier.json πŸ”—

@@ -42,7 +42,7 @@
         "tab_bar.background": "#221f26ff",
         "tab.inactive_background": "#221f26ff",
         "tab.active_background": "#19171cff",
-        "search.match_background": null,
+        "search.match_background": "#576dda66",
         "panel.background": "#221f26ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -426,7 +426,7 @@
         "tab_bar.background": "#e6e3ebff",
         "tab.inactive_background": "#e6e3ebff",
         "tab.active_background": "#efecf4ff",
-        "search.match_background": null,
+        "search.match_background": "#586dda66",
         "panel.background": "#e6e3ebff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -810,7 +810,7 @@
         "tab_bar.background": "#262622ff",
         "tab.inactive_background": "#262622ff",
         "tab.active_background": "#20201dff",
-        "search.match_background": null,
+        "search.match_background": "#6684e066",
         "panel.background": "#262622ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -1194,7 +1194,7 @@
         "tab_bar.background": "#eeebd7ff",
         "tab.inactive_background": "#eeebd7ff",
         "tab.active_background": "#fefbecff",
-        "search.match_background": null,
+        "search.match_background": "#6784e066",
         "panel.background": "#eeebd7ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -1578,7 +1578,7 @@
         "tab_bar.background": "#2c2b23ff",
         "tab.inactive_background": "#2c2b23ff",
         "tab.active_background": "#22221bff",
-        "search.match_background": null,
+        "search.match_background": "#37a16666",
         "panel.background": "#2c2b23ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -1962,7 +1962,7 @@
         "tab_bar.background": "#ebeae3ff",
         "tab.inactive_background": "#ebeae3ff",
         "tab.active_background": "#f4f3ecff",
-        "search.match_background": null,
+        "search.match_background": "#38a16666",
         "panel.background": "#ebeae3ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -2346,7 +2346,7 @@
         "tab_bar.background": "#27211eff",
         "tab.inactive_background": "#27211eff",
         "tab.active_background": "#1b1918ff",
-        "search.match_background": null,
+        "search.match_background": "#417ee666",
         "panel.background": "#27211eff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -2730,7 +2730,7 @@
         "tab_bar.background": "#e9e6e4ff",
         "tab.inactive_background": "#e9e6e4ff",
         "tab.active_background": "#f0eeedff",
-        "search.match_background": null,
+        "search.match_background": "#417ee666",
         "panel.background": "#e9e6e4ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -3114,7 +3114,7 @@
         "tab_bar.background": "#252025ff",
         "tab.inactive_background": "#252025ff",
         "tab.active_background": "#1b181bff",
-        "search.match_background": null,
+        "search.match_background": "#526aeb66",
         "panel.background": "#252025ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -3498,7 +3498,7 @@
         "tab_bar.background": "#e0d5e0ff",
         "tab.inactive_background": "#e0d5e0ff",
         "tab.active_background": "#f7f3f7ff",
-        "search.match_background": null,
+        "search.match_background": "#526aeb66",
         "panel.background": "#e0d5e0ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -3882,7 +3882,7 @@
         "tab_bar.background": "#1c2529ff",
         "tab.inactive_background": "#1c2529ff",
         "tab.active_background": "#161b1dff",
-        "search.match_background": null,
+        "search.match_background": "#277fad66",
         "panel.background": "#1c2529ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -4266,7 +4266,7 @@
         "tab_bar.background": "#cdeaf9ff",
         "tab.inactive_background": "#cdeaf9ff",
         "tab.active_background": "#ebf8ffff",
-        "search.match_background": null,
+        "search.match_background": "#277fad66",
         "panel.background": "#cdeaf9ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -4650,7 +4650,7 @@
         "tab_bar.background": "#252020ff",
         "tab.inactive_background": "#252020ff",
         "tab.active_background": "#1b1818ff",
-        "search.match_background": null,
+        "search.match_background": "#7272ca66",
         "panel.background": "#252020ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -5034,7 +5034,7 @@
         "tab_bar.background": "#ebe3e3ff",
         "tab.inactive_background": "#ebe3e3ff",
         "tab.active_background": "#f4ececff",
-        "search.match_background": null,
+        "search.match_background": "#7372ca66",
         "panel.background": "#ebe3e3ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -5418,7 +5418,7 @@
         "tab_bar.background": "#1f2621ff",
         "tab.inactive_background": "#1f2621ff",
         "tab.active_background": "#171c19ff",
-        "search.match_background": null,
+        "search.match_background": "#478c9066",
         "panel.background": "#1f2621ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -5802,7 +5802,7 @@
         "tab_bar.background": "#e3ebe6ff",
         "tab.inactive_background": "#e3ebe6ff",
         "tab.active_background": "#ecf4eeff",
-        "search.match_background": null,
+        "search.match_background": "#488c9066",
         "panel.background": "#e3ebe6ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -6186,7 +6186,7 @@
         "tab_bar.background": "#1f231fff",
         "tab.inactive_background": "#1f231fff",
         "tab.active_background": "#131513ff",
-        "search.match_background": null,
+        "search.match_background": "#3e62f466",
         "panel.background": "#1f231fff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -6570,7 +6570,7 @@
         "tab_bar.background": "#daeedaff",
         "tab.inactive_background": "#daeedaff",
         "tab.active_background": "#f3faf3ff",
-        "search.match_background": null,
+        "search.match_background": "#3f62f466",
         "panel.background": "#daeedaff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -6954,7 +6954,7 @@
         "tab_bar.background": "#262f51ff",
         "tab.inactive_background": "#262f51ff",
         "tab.active_background": "#202646ff",
-        "search.match_background": null,
+        "search.match_background": "#3e8fd066",
         "panel.background": "#262f51ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -7338,7 +7338,7 @@
         "tab_bar.background": "#e5e8f5ff",
         "tab.inactive_background": "#e5e8f5ff",
         "tab.active_background": "#f5f7ffff",
-        "search.match_background": null,
+        "search.match_background": "#3f8fd066",
         "panel.background": "#e5e8f5ff",
         "panel.focused_border": null,
         "pane.focused_border": null,

assets/themes/ayu/ayu.json πŸ”—

@@ -42,7 +42,7 @@
         "tab_bar.background": "#1f2127ff",
         "tab.inactive_background": "#1f2127ff",
         "tab.active_background": "#0d1016ff",
-        "search.match_background": null,
+        "search.match_background": "#5ac2fe66",
         "panel.background": "#1f2127ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -411,7 +411,7 @@
         "tab_bar.background": "#ececedff",
         "tab.inactive_background": "#ececedff",
         "tab.active_background": "#fcfcfcff",
-        "search.match_background": null,
+        "search.match_background": "#3b9ee566",
         "panel.background": "#ececedff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -780,7 +780,7 @@
         "tab_bar.background": "#353944ff",
         "tab.inactive_background": "#353944ff",
         "tab.active_background": "#242835ff",
-        "search.match_background": null,
+        "search.match_background": "#73cffe66",
         "panel.background": "#353944ff",
         "panel.focused_border": null,
         "pane.focused_border": null,

assets/themes/gruvbox/gruvbox.json πŸ”—

@@ -42,7 +42,7 @@
         "tab_bar.background": "#3a3735ff",
         "tab.inactive_background": "#3a3735ff",
         "tab.active_background": "#282828ff",
-        "search.match_background": null,
+        "search.match_background": "#83a59866",
         "panel.background": "#3a3735ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -416,7 +416,7 @@
         "tab_bar.background": "#393634ff",
         "tab.inactive_background": "#393634ff",
         "tab.active_background": "#1d2021ff",
-        "search.match_background": null,
+        "search.match_background": "#83a59866",
         "panel.background": "#393634ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -790,7 +790,7 @@
         "tab_bar.background": "#3b3735ff",
         "tab.inactive_background": "#3b3735ff",
         "tab.active_background": "#32302fff",
-        "search.match_background": null,
+        "search.match_background": "#83a59866",
         "panel.background": "#3b3735ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -1164,7 +1164,7 @@
         "tab_bar.background": "#ecddb4ff",
         "tab.inactive_background": "#ecddb4ff",
         "tab.active_background": "#fbf1c7ff",
-        "search.match_background": null,
+        "search.match_background": "#0b667866",
         "panel.background": "#ecddb4ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -1538,7 +1538,7 @@
         "tab_bar.background": "#ecddb5ff",
         "tab.inactive_background": "#ecddb5ff",
         "tab.active_background": "#f9f5d7ff",
-        "search.match_background": null,
+        "search.match_background": "#0b667866",
         "panel.background": "#ecddb5ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -1912,7 +1912,7 @@
         "tab_bar.background": "#ecdcb3ff",
         "tab.inactive_background": "#ecdcb3ff",
         "tab.active_background": "#f2e5bcff",
-        "search.match_background": null,
+        "search.match_background": "#0b667866",
         "panel.background": "#ecdcb3ff",
         "panel.focused_border": null,
         "pane.focused_border": null,

assets/themes/one/one.json πŸ”—

@@ -42,7 +42,7 @@
         "tab_bar.background": "#2f343eff",
         "tab.inactive_background": "#2f343eff",
         "tab.active_background": "#282c33ff",
-        "search.match_background": null,
+        "search.match_background": "#74ade866",
         "panel.background": "#2f343eff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -416,7 +416,7 @@
         "tab_bar.background": "#ebebecff",
         "tab.inactive_background": "#ebebecff",
         "tab.active_background": "#fafafaff",
-        "search.match_background": null,
+        "search.match_background": "#5c79e266",
         "panel.background": "#ebebecff",
         "panel.focused_border": null,
         "pane.focused_border": null,

assets/themes/rose_pine/rose_pine.json πŸ”—

@@ -42,7 +42,7 @@
         "tab_bar.background": "#1c1b2aff",
         "tab.inactive_background": "#1c1b2aff",
         "tab.active_background": "#191724ff",
-        "search.match_background": null,
+        "search.match_background": "#57949f66",
         "panel.background": "#1c1b2aff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -421,7 +421,7 @@
         "tab_bar.background": "#fef9f2ff",
         "tab.inactive_background": "#fef9f2ff",
         "tab.active_background": "#faf4edff",
-        "search.match_background": null,
+        "search.match_background": "#9cced766",
         "panel.background": "#fef9f2ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -800,7 +800,7 @@
         "tab_bar.background": "#28253cff",
         "tab.inactive_background": "#28253cff",
         "tab.active_background": "#232136ff",
-        "search.match_background": null,
+        "search.match_background": "#9cced766",
         "panel.background": "#28253cff",
         "panel.focused_border": null,
         "pane.focused_border": null,

assets/themes/sandcastle/sandcastle.json πŸ”—

@@ -42,7 +42,7 @@
         "tab_bar.background": "#2b3038ff",
         "tab.inactive_background": "#2b3038ff",
         "tab.active_background": "#282c33ff",
-        "search.match_background": null,
+        "search.match_background": "#528b8b66",
         "panel.background": "#2b3038ff",
         "panel.focused_border": null,
         "pane.focused_border": null,

assets/themes/solarized/solarized.json πŸ”—

@@ -42,7 +42,7 @@
         "tab_bar.background": "#04313bff",
         "tab.inactive_background": "#04313bff",
         "tab.active_background": "#002a35ff",
-        "search.match_background": null,
+        "search.match_background": "#288bd166",
         "panel.background": "#04313bff",
         "panel.focused_border": null,
         "pane.focused_border": null,
@@ -411,7 +411,7 @@
         "tab_bar.background": "#f3eddaff",
         "tab.inactive_background": "#f3eddaff",
         "tab.active_background": "#fdf6e3ff",
-        "search.match_background": null,
+        "search.match_background": "#298bd166",
         "panel.background": "#f3eddaff",
         "panel.focused_border": null,
         "pane.focused_border": null,

assets/themes/summercamp/summercamp.json πŸ”—

@@ -42,7 +42,7 @@
         "tab_bar.background": "#231f16ff",
         "tab.inactive_background": "#231f16ff",
         "tab.active_background": "#1b1810ff",
-        "search.match_background": null,
+        "search.match_background": "#499bef66",
         "panel.background": "#231f16ff",
         "panel.focused_border": null,
         "pane.focused_border": null,

crates/activity_indicator/Cargo.toml πŸ”—

@@ -11,18 +11,18 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-auto_update = { path = "../auto_update" }
-editor = { path = "../editor" }
+auto_update.workspace = true
+editor.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
-language = { path = "../language" }
-project = { path = "../project" }
-settings = { path = "../settings" }
+gpui.workspace = true
+language.workspace = true
+project.workspace = true
+settings.workspace = true
 smallvec.workspace = true
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace", package = "workspace" }
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }

crates/ai/Cargo.toml πŸ”—

@@ -17,9 +17,9 @@ anyhow.workspace = true
 async-trait.workspace = true
 bincode = "1.3.3"
 futures.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 isahc.workspace = true
-language = { path = "../language" }
+language.workspace = true
 lazy_static.workspace = true
 log.workspace = true
 matrixmultiply = "0.3.7"
@@ -33,7 +33,7 @@ rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
 serde.workspace = true
 serde_json.workspace = true
 tiktoken-rs.workspace = true
-util = { path = "../util" }
+util.workspace = true
 
 [dev-dependencies]
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }

crates/ai/src/providers/open_ai/embedding.rs πŸ”—

@@ -134,7 +134,7 @@ impl OpenAiEmbeddingProvider {
         spans: Vec<&str>,
         request_timeout: u64,
     ) -> Result<Response<AsyncBody>> {
-        let request = Request::post("https://api.openai.com/v1/embeddings")
+        let request = Request::post(format!("{OPEN_AI_API_URL}/embeddings"))
             .redirect_policy(isahc::config::RedirectPolicy::Follow)
             .timeout(Duration::from_secs(request_timeout))
             .header("Content-Type", "application/json")

crates/assets/Cargo.toml πŸ”—

@@ -7,5 +7,5 @@ license = "GPL-3.0-or-later"
 
 [dependencies]
 anyhow.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 rust-embed.workspace = true

crates/assistant/Cargo.toml πŸ”—

@@ -10,44 +10,44 @@ path = "src/assistant.rs"
 doctest = false
 
 [dependencies]
-ai = { path = "../ai" }
+ai.workspace = true
 anyhow.workspace = true
 chrono.workspace = true
-client = { path = "../client" }
-collections = { path = "../collections" }
-editor = { path = "../editor" }
-fs = { path = "../fs" }
+client.workspace = true
+collections.workspace = true
+editor.workspace = true
+fs.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 indoc.workspace = true
 isahc.workspace = true
-language = { path = "../language" }
+language.workspace = true
 log.workspace = true
-menu = { path = "../menu" }
-multi_buffer = { path = "../multi_buffer" }
+menu.workspace = true
+multi_buffer.workspace = true
 ordered-float.workspace = true
 parking_lot.workspace = true
-project = { path = "../project" }
+project.workspace = true
 regex.workspace = true
 schemars.workspace = true
-search = { path = "../search" }
-semantic_index = { path = "../semantic_index" }
+search.workspace = true
+semantic_index.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smol.workspace = true
-theme = { path = "../theme" }
+theme.workspace = true
 tiktoken-rs.workspace = true
-ui = { path = "../ui" }
-util = { path = "../util" }
+ui.workspace = true
+util.workspace = true
 uuid.workspace = true
-workspace = { path = "../workspace" }
+workspace.workspace = true
 
 [dev-dependencies]
-ai = { path = "../ai", features = ["test-support"] }
+ai = { workspace = true, features = ["test-support"] }
 ctor.workspace = true
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
 log.workspace = true
-project = { path = "../project", features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
 rand.workspace = true

crates/assistant/src/assistant_panel.rs πŸ”—

@@ -962,6 +962,7 @@ impl AssistantPanel {
             line_height: relative(1.3).into(),
             background_color: None,
             underline: None,
+            strikethrough: None,
             white_space: WhiteSpace::Normal,
         };
         EditorElement::new(
@@ -3166,6 +3167,7 @@ impl InlineAssistant {
             line_height: relative(1.3).into(),
             background_color: None,
             underline: None,
+            strikethrough: None,
             white_space: WhiteSpace::Normal,
         };
         EditorElement::new(

crates/audio/Cargo.toml πŸ”—

@@ -11,11 +11,11 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-collections = { path = "../collections" }
+collections.workspace = true
 derive_more.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 rodio = { version = "0.17.1", default-features = false, features = ["wav"] }
-util = { path = "../util" }
+util.workspace = true

crates/auto_update/Cargo.toml πŸ”—

@@ -11,22 +11,22 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-client = { path = "../client" }
-db = { path = "../db" }
-gpui = { path = "../gpui" }
+client.workspace = true
+db.workspace = true
+gpui.workspace = true
 isahc.workspace = true
 lazy_static.workspace = true
 log.workspace = true
-menu = { path = "../menu" }
-project = { path = "../project" }
-release_channel = { path = "../release_channel" }
+menu.workspace = true
+project.workspace = true
+release_channel.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smol.workspace = true
 tempfile.workspace = true
-theme = { path = "../theme" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+theme.workspace = true
+util.workspace = true
+workspace.workspace = true

crates/auto_update/src/update_notification.rs πŸ”—

@@ -40,10 +40,11 @@ impl Render for UpdateNotification {
                     .id("notes")
                     .child(Label::new("View the release notes"))
                     .cursor_pointer()
-                    .on_click(|_, cx| {
+                    .on_click(cx.listener(|this, _, cx| {
                         crate::view_release_notes(&Default::default(), cx);
-                    }),
-            )
+                        this.dismiss(&menu::Cancel, cx)
+                    })),
+            );
     }
 }
 

crates/breadcrumbs/Cargo.toml πŸ”—

@@ -10,20 +10,20 @@ path = "src/breadcrumbs.rs"
 doctest = false
 
 [dependencies]
-collections = { path = "../collections" }
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
+collections.workspace = true
+editor.workspace = true
+gpui.workspace = true
 itertools = "0.10"
-language = { path = "../language" }
-outline = { path = "../outline" }
-project = { path = "../project" }
-search = { path = "../search" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-workspace = { path = "../workspace" }
+language.workspace = true
+outline.workspace = true
+project.workspace = true
+search.workspace = true
+settings.workspace = true
+theme.workspace = true
+ui.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/call/Cargo.toml πŸ”—

@@ -22,33 +22,33 @@ test-support = [
 [dependencies]
 anyhow.workspace = true
 async-broadcast = "0.4"
-audio = { path = "../audio" }
-client = { path = "../client" }
-collections = { path = "../collections" }
-fs = { path = "../fs" }
+audio.workspace = true
+client.workspace = true
+collections.workspace = true
+fs.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 image = "0.23"
-language = { path = "../language" }
-live_kit_client = { path = "../live_kit_client" }
+language.workspace = true
+live_kit_client.workspace = true
 log.workspace = true
-media = { path = "../media" }
+media.workspace = true
 postage.workspace = true
-project = { path = "../project" }
+project.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smallvec.workspace = true
-util = { path = "../util" }
+util.workspace = true
 
 [dev-dependencies]
-client = { path = "../client", features = ["test-support"] }
-collections = { path = "../collections", features = ["test-support"] }
-fs = { path = "../fs", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
-live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
-util = { path = "../util", features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
+fs = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+live_kit_client = { workspace = true, features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/call/src/call_settings.rs πŸ”—

@@ -7,6 +7,7 @@ use settings::Settings;
 #[derive(Deserialize, Debug)]
 pub struct CallSettings {
     pub mute_on_join: bool,
+    pub share_on_join: bool,
 }
 
 /// Configuration of voice calls in Zed.
@@ -16,6 +17,11 @@ pub struct CallSettingsContent {
     ///
     /// Default: false
     pub mute_on_join: Option<bool>,
+
+    /// Whether your current project should be shared when joining an empty channel.
+    ///
+    /// Default: true
+    pub share_on_join: Option<bool>,
 }
 
 impl Settings for CallSettings {

crates/call/src/room.rs πŸ”—

@@ -617,6 +617,10 @@ impl Room {
         self.local_participant.role == proto::ChannelRole::Admin
     }
 
+    pub fn local_participant_is_guest(&self) -> bool {
+        self.local_participant.role == proto::ChannelRole::Guest
+    }
+
     pub fn set_participant_role(
         &mut self,
         user_id: u64,
@@ -1202,7 +1206,7 @@ impl Room {
         })
     }
 
-    pub(crate) fn share_project(
+    pub fn share_project(
         &mut self,
         project: Model<Project>,
         cx: &mut ModelContext<Self>,

crates/channel/Cargo.toml πŸ”—

@@ -14,42 +14,42 @@ test-support = ["collections/test-support", "gpui/test-support", "rpc/test-suppo
 
 [dependencies]
 anyhow.workspace = true
-client = { path = "../client" }
-clock = { path = "../clock" }
-collections = { path = "../collections" }
-db = { path = "../db" }
-feature_flags = { path = "../feature_flags" }
+client.workspace = true
+clock.workspace = true
+collections.workspace = true
+db.workspace = true
+feature_flags.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 image = "0.23"
-language = { path = "../language" }
+language.workspace = true
 lazy_static.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 rand.workspace = true
-release_channel = { path = "../release_channel" }
-rpc = { path = "../rpc" }
+release_channel.workspace = true
+rpc.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smallvec.workspace = true
 smol.workspace = true
-sum_tree = { path = "../sum_tree" }
+sum_tree.workspace = true
 tempfile.workspace = true
-text = { path = "../text" }
+text.workspace = true
 thiserror.workspace = true
 time.workspace = true
 tiny_http = "0.8"
 url.workspace = true
-util = { path = "../util" }
+util.workspace = true
 uuid.workspace = true
 
 [dev-dependencies]
-collections = { path = "../collections", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-rpc = { path = "../rpc", features = ["test-support"] }
-client = { path = "../client", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
-util = { path = "../util", features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+rpc = { workspace = true, features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/channel/src/channel_chat.rs πŸ”—

@@ -5,12 +5,13 @@ use client::{
     user::{User, UserStore},
     Client, Subscription, TypedEnvelope, UserId,
 };
+use collections::HashSet;
 use futures::lock::Mutex;
-use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
+use gpui::{
+    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
+};
 use rand::prelude::*;
 use std::{
-    collections::HashSet,
-    mem,
     ops::{ControlFlow, Range},
     sync::Arc,
 };
@@ -26,6 +27,7 @@ pub struct ChannelChat {
     loaded_all_messages: bool,
     last_acknowledged_id: Option<u64>,
     next_pending_message_id: usize,
+    first_loaded_message_id: Option<u64>,
     user_store: Model<UserStore>,
     rpc: Arc<Client>,
     outgoing_messages_lock: Arc<Mutex<()>>,
@@ -37,6 +39,7 @@ pub struct ChannelChat {
 pub struct MessageParams {
     pub text: String,
     pub mentions: Vec<(Range<usize>, UserId)>,
+    pub reply_to_message_id: Option<u64>,
 }
 
 #[derive(Clone, Debug)]
@@ -47,6 +50,7 @@ pub struct ChannelMessage {
     pub sender: Arc<User>,
     pub nonce: u128,
     pub mentions: Vec<(Range<usize>, UserId)>,
+    pub reply_to_message_id: Option<u64>,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -55,6 +59,15 @@ pub enum ChannelMessageId {
     Pending(usize),
 }
 
+impl Into<Option<u64>> for ChannelMessageId {
+    fn into(self) -> Option<u64> {
+        match self {
+            ChannelMessageId::Saved(id) => Some(id),
+            ChannelMessageId::Pending(_) => None,
+        }
+    }
+}
+
 #[derive(Clone, Debug, Default)]
 pub struct ChannelMessageSummary {
     max_id: ChannelMessageId,
@@ -96,28 +109,35 @@ impl ChannelChat {
         let response = client
             .request(proto::JoinChannelChat { channel_id })
             .await?;
-        let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
-        let loaded_all_messages = response.done;
 
-        Ok(cx.new_model(|cx| {
+        let handle = cx.new_model(|cx| {
             cx.on_release(Self::release).detach();
-            let mut this = Self {
+            Self {
                 channel_id: channel.id,
-                user_store,
+                user_store: user_store.clone(),
                 channel_store,
-                rpc: client,
+                rpc: client.clone(),
                 outgoing_messages_lock: Default::default(),
                 messages: Default::default(),
                 acknowledged_message_ids: Default::default(),
-                loaded_all_messages,
+                loaded_all_messages: false,
                 next_pending_message_id: 0,
                 last_acknowledged_id: None,
                 rng: StdRng::from_entropy(),
+                first_loaded_message_id: None,
                 _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
-            };
-            this.insert_messages(messages, cx);
-            this
-        })?)
+            }
+        })?;
+        Self::handle_loaded_messages(
+            handle.downgrade(),
+            user_store,
+            client,
+            response.messages,
+            response.done,
+            &mut cx,
+        )
+        .await?;
+        Ok(handle)
     }
 
     fn release(&mut self, _: &mut AppContext) {
@@ -166,6 +186,7 @@ impl ChannelChat {
                     timestamp: OffsetDateTime::now_utc(),
                     mentions: message.mentions.clone(),
                     nonce,
+                    reply_to_message_id: message.reply_to_message_id,
                 },
                 &(),
             ),
@@ -183,6 +204,7 @@ impl ChannelChat {
                 body: message.text,
                 nonce: Some(nonce.into()),
                 mentions: mentions_to_proto(&message.mentions),
+                reply_to_message_id: message.reply_to_message_id,
             });
             let response = request.await?;
             drop(outgoing_message_guard);
@@ -227,12 +249,16 @@ impl ChannelChat {
                         before_message_id,
                     })
                     .await?;
-                let loaded_all_messages = response.done;
-                let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
-                this.update(&mut cx, |this, cx| {
-                    this.loaded_all_messages = loaded_all_messages;
-                    this.insert_messages(messages, cx);
-                })?;
+                Self::handle_loaded_messages(
+                    this,
+                    user_store,
+                    rpc,
+                    response.messages,
+                    response.done,
+                    &mut cx,
+                )
+                .await?;
+
                 anyhow::Ok(())
             }
             .log_err()
@@ -240,9 +266,14 @@ impl ChannelChat {
     }
 
     pub fn first_loaded_message_id(&mut self) -> Option<u64> {
-        self.messages.first().and_then(|message| match message.id {
-            ChannelMessageId::Saved(id) => Some(id),
-            ChannelMessageId::Pending(_) => None,
+        self.first_loaded_message_id
+    }
+
+    /// Load a message by its id, if it's already stored locally.
+    pub fn find_loaded_message(&self, id: u64) -> Option<&ChannelMessage> {
+        self.messages.iter().find(|message| match message.id {
+            ChannelMessageId::Saved(message_id) => message_id == id,
+            ChannelMessageId::Pending(_) => false,
         })
     }
 
@@ -304,6 +335,66 @@ impl ChannelChat {
         }
     }
 
+    async fn handle_loaded_messages(
+        this: WeakModel<Self>,
+        user_store: Model<UserStore>,
+        rpc: Arc<Client>,
+        proto_messages: Vec<proto::ChannelMessage>,
+        loaded_all_messages: bool,
+        cx: &mut AsyncAppContext,
+    ) -> Result<()> {
+        let loaded_messages = messages_from_proto(proto_messages, &user_store, cx).await?;
+
+        let first_loaded_message_id = loaded_messages.first().map(|m| m.id);
+        let loaded_message_ids = this.update(cx, |this, _| {
+            let mut loaded_message_ids: HashSet<u64> = HashSet::default();
+            for message in loaded_messages.iter() {
+                if let Some(saved_message_id) = message.id.into() {
+                    loaded_message_ids.insert(saved_message_id);
+                }
+            }
+            for message in this.messages.iter() {
+                if let Some(saved_message_id) = message.id.into() {
+                    loaded_message_ids.insert(saved_message_id);
+                }
+            }
+            loaded_message_ids
+        })?;
+
+        let missing_ancestors = loaded_messages
+            .iter()
+            .filter_map(|message| {
+                if let Some(ancestor_id) = message.reply_to_message_id {
+                    if !loaded_message_ids.contains(&ancestor_id) {
+                        return Some(ancestor_id);
+                    }
+                }
+                None
+            })
+            .collect::<Vec<_>>();
+
+        let loaded_ancestors = if missing_ancestors.is_empty() {
+            None
+        } else {
+            let response = rpc
+                .request(proto::GetChannelMessagesById {
+                    message_ids: missing_ancestors,
+                })
+                .await?;
+            Some(messages_from_proto(response.messages, &user_store, cx).await?)
+        };
+        this.update(cx, |this, cx| {
+            this.first_loaded_message_id = first_loaded_message_id.and_then(|msg_id| msg_id.into());
+            this.loaded_all_messages = loaded_all_messages;
+            this.insert_messages(loaded_messages, cx);
+            if let Some(loaded_ancestors) = loaded_ancestors {
+                this.insert_messages(loaded_ancestors, cx);
+            }
+        })?;
+
+        Ok(())
+    }
+
     pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
         let user_store = self.user_store.clone();
         let rpc = self.rpc.clone();
@@ -311,28 +402,17 @@ impl ChannelChat {
         cx.spawn(move |this, mut cx| {
             async move {
                 let response = rpc.request(proto::JoinChannelChat { channel_id }).await?;
-                let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
-                let loaded_all_messages = response.done;
-
-                let pending_messages = this.update(&mut cx, |this, cx| {
-                    if let Some((first_new_message, last_old_message)) =
-                        messages.first().zip(this.messages.last())
-                    {
-                        if first_new_message.id > last_old_message.id {
-                            let old_messages = mem::take(&mut this.messages);
-                            cx.emit(ChannelChatEvent::MessagesUpdated {
-                                old_range: 0..old_messages.summary().count,
-                                new_count: 0,
-                            });
-                            this.loaded_all_messages = loaded_all_messages;
-                        }
-                    }
-
-                    this.insert_messages(messages, cx);
-                    if loaded_all_messages {
-                        this.loaded_all_messages = loaded_all_messages;
-                    }
-
+                Self::handle_loaded_messages(
+                    this.clone(),
+                    user_store.clone(),
+                    rpc.clone(),
+                    response.messages,
+                    response.done,
+                    &mut cx,
+                )
+                .await?;
+
+                let pending_messages = this.update(&mut cx, |this, _| {
                     this.pending_messages().cloned().collect::<Vec<_>>()
                 })?;
 
@@ -342,6 +422,7 @@ impl ChannelChat {
                         body: pending_message.body,
                         mentions: mentions_to_proto(&pending_message.mentions),
                         nonce: Some(pending_message.nonce.into()),
+                        reply_to_message_id: pending_message.reply_to_message_id,
                     });
                     let response = request.await?;
                     let message = ChannelMessage::from_proto(
@@ -553,6 +634,7 @@ impl ChannelMessage {
                 .nonce
                 .ok_or_else(|| anyhow!("nonce is required"))?
                 .into(),
+            reply_to_message_id: message.reply_to_message_id,
         })
     }
 
@@ -642,6 +724,7 @@ impl<'a> From<&'a str> for MessageParams {
         Self {
             text: value.into(),
             mentions: Vec::new(),
+            reply_to_message_id: None,
         }
     }
 }

crates/channel/src/channel_store_tests.rs πŸ”—

@@ -184,6 +184,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     sender_id: 5,
                     mentions: vec![],
                     nonce: Some(1.into()),
+                    reply_to_message_id: None,
                 },
                 proto::ChannelMessage {
                     id: 11,
@@ -192,6 +193,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     sender_id: 6,
                     mentions: vec![],
                     nonce: Some(2.into()),
+                    reply_to_message_id: None,
                 },
             ],
             done: false,
@@ -239,6 +241,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
             sender_id: 7,
             mentions: vec![],
             nonce: Some(3.into()),
+            reply_to_message_id: None,
         }),
     });
 
@@ -292,6 +295,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     sender_id: 5,
                     nonce: Some(4.into()),
                     mentions: vec![],
+                    reply_to_message_id: None,
                 },
                 proto::ChannelMessage {
                     id: 9,
@@ -300,6 +304,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     sender_id: 6,
                     nonce: Some(5.into()),
                     mentions: vec![],
+                    reply_to_message_id: None,
                 },
             ],
         },

crates/cli/Cargo.toml πŸ”—

@@ -20,7 +20,7 @@ dirs = "3.0"
 ipc-channel = "0.16"
 serde.workspace = true
 serde_derive.workspace = true
-util = { path = "../util" }
+util.workspace = true
 
 [target.'cfg(target_os = "macos")'.dependencies]
 core-foundation = "0.9"

crates/client/Cargo.toml πŸ”—

@@ -14,20 +14,20 @@ test-support = ["collections/test-support", "gpui/test-support", "rpc/test-suppo
 
 [dependencies]
 chrono = { version = "0.4", features = ["serde"] }
-collections = { path = "../collections" }
-db = { path = "../db" }
-gpui = { path = "../gpui" }
-util = { path = "../util" }
-release_channel = { path = "../release_channel" }
-rpc = { path = "../rpc" }
-text = { path = "../text" }
-settings = { path = "../settings" }
-feature_flags = { path = "../feature_flags" }
-sum_tree = { path = "../sum_tree" }
+collections.workspace = true
+db.workspace = true
+gpui.workspace = true
+util.workspace = true
+release_channel.workspace = true
+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-tls"] }
+async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] }
 futures.workspace = true
 image = "0.23"
 lazy_static.workspace = true
@@ -51,8 +51,8 @@ uuid.workspace = true
 url.workspace = true
 
 [dev-dependencies]
-collections = { path = "../collections", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-rpc = { path = "../rpc", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
-util = { path = "../util", features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+rpc = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/client/src/client.rs πŸ”—

@@ -10,6 +10,7 @@ use async_tungstenite::tungstenite::{
     error::Error as WebsocketError,
     http::{Request, StatusCode},
 };
+use collections::HashMap;
 use futures::{
     channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt,
     TryFutureExt as _, TryStreamExt,
@@ -29,7 +30,6 @@ use serde_json;
 use settings::{Settings, SettingsStore};
 use std::{
     any::TypeId,
-    collections::HashMap,
     convert::TryFrom,
     fmt::Write as _,
     future::Future,
@@ -1040,7 +1040,7 @@ impl Client {
                     rpc_url.set_scheme("wss").unwrap();
                     let request = request.uri(rpc_url.as_str()).body(())?;
                     let (stream, _) =
-                        async_tungstenite::async_tls::client_async_tls(request, stream).await?;
+                        async_tungstenite::async_std::client_async_tls(request, stream).await?;
                     Ok(Connection::new(
                         stream
                             .map_err(|error| anyhow!(error))

crates/client/src/telemetry.rs πŸ”—

@@ -145,11 +145,14 @@ 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<Vec<u8>> = Lazy::new(|| {
+static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
     option_env!("ZED_CLIENT_CHECKSUM_SEED")
-        .unwrap_or("development-checksum-seed")
-        .as_bytes()
-        .into()
+        .map(|s| s.as_bytes().into())
+        .or_else(|| {
+            env::var("ZED_CLIENT_CHECKSUM_SEED")
+                .ok()
+                .map(|s| s.as_bytes().into())
+        })
 });
 
 impl Telemetry {
@@ -510,6 +513,10 @@ impl Telemetry {
             return;
         }
 
+        let Some(checksum_seed) = &*ZED_CLIENT_CHECKSUM_SEED else {
+            return;
+        };
+
         let this = self.clone();
         self.executor
             .spawn(
@@ -551,9 +558,9 @@ impl Telemetry {
                     }
 
                     let mut summer = Sha256::new();
-                    summer.update(&*ZED_CLIENT_CHECKSUM_SEED);
+                    summer.update(checksum_seed);
                     summer.update(&json_bytes);
-                    summer.update(&*ZED_CLIENT_CHECKSUM_SEED);
+                    summer.update(checksum_seed);
                     let mut checksum = String::new();
                     for byte in summer.finalize().as_slice() {
                         use std::fmt::Write;

crates/collab/Cargo.toml πŸ”—

@@ -22,15 +22,15 @@ axum-extra = { version = "0.3", features = ["erased-json"] }
 base64 = "0.13"
 chrono.workspace = true
 clap = { version = "3.1", features = ["derive"], optional = true }
-clock = { path = "../clock" }
-collections = { path = "../collections" }
+clock.workspace = true
+collections.workspace = true
 dashmap = "5.4"
 envy = "0.4.2"
 futures.workspace = true
 hyper = "0.14"
 lazy_static.workspace = true
 lipsum = { version = "0.8", optional = true }
-live_kit_server = { path = "../live_kit_server" }
+live_kit_server.workspace = true
 log.workspace = true
 nanoid = "0.4"
 parking_lot.workspace = true
@@ -38,7 +38,7 @@ prometheus = "0.13"
 prost.workspace = true
 rand.workspace = true
 reqwest = { version = "0.11", features = ["json"], optional = true }
-rpc = { path = "../rpc" }
+rpc.workspace = true
 scrypt = "0.7"
 sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
 serde.workspace = true
@@ -47,7 +47,7 @@ serde_json.workspace = true
 sha-1 = "0.9"
 smallvec.workspace = true
 sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
-text = { path = "../text" }
+text.workspace = true
 time.workspace = true
 tokio = { version = "1", features = ["full"] }
 tokio-tungstenite = "0.17"
@@ -57,44 +57,44 @@ tower = "0.4"
 tracing = "0.1.34"
 tracing-log = "0.1.3"
 tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
-util = { path = "../util" }
+util.workspace = true
 uuid.workspace = true
 
 [dev-dependencies]
-release_channel = { path = "../release_channel" }
+release_channel.workspace = true
 async-trait.workspace = true
-audio = { path = "../audio" }
-call = { path = "../call", features = ["test-support"] }
-channel = { path = "../channel" }
-client = { path = "../client", features = ["test-support"] }
-collab_ui = { path = "../collab_ui", features = ["test-support"] }
-collections = { path = "../collections", features = ["test-support"] }
+audio.workspace = true
+call = { workspace = true, features = ["test-support"] }
+channel.workspace = true
+client = { workspace = true, features = ["test-support"] }
+collab_ui = { workspace = true, features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
 ctor.workspace = true
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
-file_finder = { path = "../file_finder" }
-fs = { path = "../fs", features = ["test-support"] }
-git = { path = "../git", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
+file_finder.workspace = true
+fs = { workspace = true, features = ["test-support"] }
+git = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 indoc.workspace = true
-language = { path = "../language", features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
 lazy_static.workspace = true
-live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
-lsp = { path = "../lsp", features = ["test-support"] }
-menu = { path = "../menu" }
-node_runtime = { path = "../node_runtime" }
-notifications = { path = "../notifications", features = ["test-support"] }
+live_kit_client = { workspace = true, features = ["test-support"] }
+lsp = { workspace = true, features = ["test-support"] }
+menu.workspace = true
+node_runtime.workspace = true
+notifications = { workspace = true, features = ["test-support"] }
 pretty_assertions.workspace = true
-project = { path = "../project", features = ["test-support"] }
-rpc = { path = "../rpc", features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
+rpc = { workspace = true, features = ["test-support"] }
 sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
 serde_json.workspace = true
-settings = { path = "../settings", features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
 sqlx = { version = "0.7", features = ["sqlite"] }
-theme = { path = "../theme" }
+theme.workspace = true
 unindent.workspace = true
-util = { path = "../util" }
-workspace = { path = "../workspace", features = ["test-support"] }
+util.workspace = true
+workspace = { workspace = true, features = ["test-support"] }
 
 [features]
 seed-support = ["clap", "lipsum", "reqwest"]

crates/collab/migrations.sqlite/20221109000000_test_schema.sql πŸ”—

@@ -217,7 +217,8 @@ CREATE TABLE IF NOT EXISTS "channel_messages" (
     "sender_id" INTEGER NOT NULL REFERENCES users (id),
     "body" TEXT NOT NULL,
     "sent_at" TIMESTAMP,
-    "nonce" BLOB NOT NULL
+    "nonce" BLOB NOT NULL,
+    "reply_to_message_id" INTEGER DEFAULT NULL
 );
 CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id");
 CREATE UNIQUE INDEX "index_channel_messages_on_sender_id_nonce" ON "channel_messages" ("sender_id", "nonce");

crates/collab/src/db.rs πŸ”—

@@ -692,7 +692,7 @@ impl ProjectCollaborator {
 pub struct LeftProject {
     pub id: ProjectId,
     pub host_user_id: UserId,
-    pub host_connection_id: ConnectionId,
+    pub host_connection_id: Option<ConnectionId>,
     pub connection_ids: Vec<ConnectionId>,
 }
 

crates/collab/src/db/queries/messages.rs πŸ”—

@@ -161,6 +161,7 @@ impl Database {
                         upper_half: nonce.0,
                         lower_half: nonce.1,
                     }),
+                    reply_to_message_id: row.reply_to_message_id.map(|id| id.to_proto()),
                 }
             })
             .collect::<Vec<_>>();
@@ -207,6 +208,7 @@ impl Database {
         mentions: &[proto::ChatMention],
         timestamp: OffsetDateTime,
         nonce: u128,
+        reply_to_message_id: Option<MessageId>,
     ) -> Result<CreatedChannelMessage> {
         self.transaction(|tx| async move {
             let channel = self.get_channel_internal(channel_id, &*tx).await?;
@@ -245,6 +247,7 @@ impl Database {
                 sent_at: ActiveValue::Set(timestamp),
                 nonce: ActiveValue::Set(Uuid::from_u128(nonce)),
                 id: ActiveValue::NotSet,
+                reply_to_message_id: ActiveValue::Set(reply_to_message_id),
             })
             .on_conflict(
                 OnConflict::columns([

crates/collab/src/db/queries/projects.rs πŸ”—

@@ -778,7 +778,7 @@ impl Database {
             let left_project = LeftProject {
                 id: project_id,
                 host_user_id: project.host_user_id,
-                host_connection_id: project.host_connection()?,
+                host_connection_id: Some(project.host_connection()?),
                 connection_ids,
             };
             Ok((room, left_project))

crates/collab/src/db/queries/rooms.rs πŸ”—

@@ -862,7 +862,7 @@ impl Database {
                                 id: collaborator.project_id,
                                 host_user_id: Default::default(),
                                 connection_ids: Default::default(),
-                                host_connection_id: Default::default(),
+                                host_connection_id: None,
                             });
 
                     let collaborator_connection_id = collaborator.connection();
@@ -872,7 +872,7 @@ impl Database {
 
                     if collaborator.is_host {
                         left_project.host_user_id = collaborator.user_id;
-                        left_project.host_connection_id = collaborator_connection_id;
+                        left_project.host_connection_id = Some(collaborator_connection_id);
                     }
                 }
                 drop(collaborators);

crates/collab/src/db/tests/message_tests.rs πŸ”—

@@ -32,6 +32,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
                 &[],
                 OffsetDateTime::now_utc(),
                 i,
+                None,
             )
             .await
             .unwrap()
@@ -106,6 +107,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
             &mentions_to_proto(&[(3..10, user_b.to_proto())]),
             OffsetDateTime::now_utc(),
             100,
+            None,
         )
         .await
         .unwrap()
@@ -118,6 +120,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
             &mentions_to_proto(&[]),
             OffsetDateTime::now_utc(),
             200,
+            None,
         )
         .await
         .unwrap()
@@ -130,6 +133,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
             &mentions_to_proto(&[(4..11, user_c.to_proto())]),
             OffsetDateTime::now_utc(),
             100,
+            None,
         )
         .await
         .unwrap()
@@ -142,6 +146,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
             &mentions_to_proto(&[]),
             OffsetDateTime::now_utc(),
             200,
+            None,
         )
         .await
         .unwrap()
@@ -157,6 +162,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
             &mentions_to_proto(&[(4..11, user_a.to_proto())]),
             OffsetDateTime::now_utc(),
             100,
+            None,
         )
         .await
         .unwrap()
@@ -231,17 +237,41 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
         .unwrap();
 
     let _ = db
-        .create_channel_message(channel_1, user, "1_1", &[], OffsetDateTime::now_utc(), 1)
+        .create_channel_message(
+            channel_1,
+            user,
+            "1_1",
+            &[],
+            OffsetDateTime::now_utc(),
+            1,
+            None,
+        )
         .await
         .unwrap();
 
     let _ = db
-        .create_channel_message(channel_1, user, "1_2", &[], OffsetDateTime::now_utc(), 2)
+        .create_channel_message(
+            channel_1,
+            user,
+            "1_2",
+            &[],
+            OffsetDateTime::now_utc(),
+            2,
+            None,
+        )
         .await
         .unwrap();
 
     let third_message = db
-        .create_channel_message(channel_1, user, "1_3", &[], OffsetDateTime::now_utc(), 3)
+        .create_channel_message(
+            channel_1,
+            user,
+            "1_3",
+            &[],
+            OffsetDateTime::now_utc(),
+            3,
+            None,
+        )
         .await
         .unwrap()
         .message_id;
@@ -251,7 +281,15 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
         .unwrap();
 
     let fourth_message = db
-        .create_channel_message(channel_2, user, "2_1", &[], OffsetDateTime::now_utc(), 4)
+        .create_channel_message(
+            channel_2,
+            user,
+            "2_1",
+            &[],
+            OffsetDateTime::now_utc(),
+            4,
+            None,
+        )
         .await
         .unwrap()
         .message_id;
@@ -317,6 +355,7 @@ async fn test_channel_message_mentions(db: &Arc<Database>) {
         &mentions_to_proto(&[(3..10, user_b.to_proto()), (15..22, user_c.to_proto())]),
         OffsetDateTime::now_utc(),
         1,
+        None,
     )
     .await
     .unwrap();
@@ -327,6 +366,7 @@ async fn test_channel_message_mentions(db: &Arc<Database>) {
         &mentions_to_proto(&[(4..11, user_c.to_proto())]),
         OffsetDateTime::now_utc(),
         2,
+        None,
     )
     .await
     .unwrap();
@@ -337,6 +377,7 @@ async fn test_channel_message_mentions(db: &Arc<Database>) {
         &mentions_to_proto(&[]),
         OffsetDateTime::now_utc(),
         3,
+        None,
     )
     .await
     .unwrap();
@@ -347,6 +388,7 @@ async fn test_channel_message_mentions(db: &Arc<Database>) {
         &mentions_to_proto(&[(0..7, user_b.to_proto())]),
         OffsetDateTime::now_utc(),
         4,
+        None,
     )
     .await
     .unwrap();

crates/collab/src/rpc.rs πŸ”—

@@ -1691,7 +1691,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
     tracing::info!(
         %project_id,
         host_user_id = %project.host_user_id,
-        host_connection_id = %project.host_connection_id,
+        host_connection_id = ?project.host_connection_id,
         "leave project"
     );
 
@@ -3019,6 +3019,10 @@ async fn send_channel_message(
             &request.mentions,
             timestamp,
             nonce.clone().into(),
+            match request.reply_to_message_id {
+                Some(reply_to_message_id) => Some(MessageId::from_proto(reply_to_message_id)),
+                None => None,
+            },
         )
         .await?;
     let message = proto::ChannelMessage {
@@ -3028,6 +3032,7 @@ async fn send_channel_message(
         mentions: request.mentions,
         timestamp: timestamp.unix_timestamp() as u64,
         nonce: Some(nonce),
+        reply_to_message_id: request.reply_to_message_id,
     };
     broadcast(
         Some(session.connection_id),

crates/collab/src/tests/channel_message_tests.rs πŸ”—

@@ -43,6 +43,7 @@ async fn test_basic_channel_messages(
                 MessageParams {
                     text: "hi @user_c!".into(),
                     mentions: vec![(3..10, client_c.id())],
+                    reply_to_message_id: None,
                 },
                 cx,
             )
@@ -402,3 +403,66 @@ async fn test_channel_message_changes(
 
     assert!(b_has_messages);
 }
+
+#[gpui::test]
+async fn test_chat_replies(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    let mut server = TestServer::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;
+
+    let channel_id = server
+        .make_channel(
+            "the-channel",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b)],
+        )
+        .await;
+
+    // Client A sends a message, client B should see that there is a new message.
+    let channel_chat_a = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
+        .await
+        .unwrap();
+
+    let channel_chat_b = client_b
+        .channel_store()
+        .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
+        .await
+        .unwrap();
+
+    let msg_id = channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
+        .await
+        .unwrap();
+
+    cx_a.run_until_parked();
+
+    let reply_id = channel_chat_b
+        .update(cx_b, |c, cx| {
+            c.send_message(
+                MessageParams {
+                    text: "reply".into(),
+                    reply_to_message_id: Some(msg_id),
+                    mentions: Vec::new(),
+                },
+                cx,
+            )
+            .unwrap()
+        })
+        .await
+        .unwrap();
+
+    cx_a.run_until_parked();
+
+    channel_chat_a.update(cx_a, |channel_chat, _| {
+        assert_eq!(
+            channel_chat
+                .find_loaded_message(reply_id)
+                .unwrap()
+                .reply_to_message_id,
+            Some(msg_id),
+        )
+    });
+}

crates/collab/src/tests/following_tests.rs πŸ”—

@@ -22,6 +22,8 @@ use workspace::{
     SplitDirection, Workspace,
 };
 
+use super::TestClient;
+
 #[gpui::test(iterations = 10)]
 async fn test_basic_following(
     cx_a: &mut TestAppContext,
@@ -1996,3 +1998,82 @@ async fn test_following_to_channel_notes_without_a_shared_project(
         );
     });
 }
+
+async fn join_channel(
+    channel_id: u64,
+    client: &TestClient,
+    cx: &mut TestAppContext,
+) -> anyhow::Result<()> {
+    cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
+        .await
+}
+
+async fn share_workspace(
+    workspace: &View<Workspace>,
+    cx: &mut VisualTestContext,
+) -> anyhow::Result<u64> {
+    let project = workspace.update(cx, |workspace, _| workspace.project().clone());
+    cx.read(ActiveCall::global)
+        .update(cx, |call, cx| call.share_project(project, cx))
+        .await
+}
+
+#[gpui::test]
+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 mut cx_a2 = cx_a.clone();
+    let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
+    join_channel(channel, &client_a, cx_a).await.unwrap();
+    share_workspace(&workspace_a, cx_a).await.unwrap();
+
+    // a opens 1.txt
+    cx_a.simulate_keystrokes("cmd-p 1 enter");
+    cx_a.run_until_parked();
+    workspace_a.update(cx_a, |workspace, cx| {
+        let editor = workspace.active_item(cx).unwrap();
+        assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
+    });
+
+    // b joins channel and is following a
+    join_channel(channel, &client_b, cx_b).await.unwrap();
+    cx_b.run_until_parked();
+    let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
+    workspace_b.update(cx_b, |workspace, cx| {
+        let editor = workspace.active_item(cx).unwrap();
+        assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
+    });
+
+    // a opens a second workspace and the channel notes
+    let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
+    cx_a2.update(|cx| cx.activate_window());
+    cx_a2
+        .update(|cx| ChannelView::open(channel, None, workspace_a2, cx))
+        .await
+        .unwrap();
+    cx_a2.run_until_parked();
+
+    // b should follow a to the channel notes
+    workspace_b.update(cx_b, |workspace, cx| {
+        let editor = workspace.active_item_as::<ChannelView>(cx).unwrap();
+        assert_eq!(editor.read(cx).channel(cx).unwrap().id, channel);
+    });
+
+    // a returns to the shared project
+    cx_a.update(|cx| cx.activate_window());
+    cx_a.run_until_parked();
+
+    workspace_a.update(cx_a, |workspace, cx| {
+        let editor = workspace.active_item(cx).unwrap();
+        assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
+    });
+
+    // b should follow a back
+    workspace_b.update(cx_b, |workspace, cx| {
+        let editor = workspace.active_item_as::<Editor>(cx).unwrap();
+        assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
+    });
+}

crates/collab/src/tests/integration_tests.rs πŸ”—

@@ -5967,6 +5967,6 @@ async fn test_cmd_k_left(cx: &mut TestAppContext) {
     cx.executor().advance_clock(Duration::from_secs(2));
     cx.simulate_keystrokes("left");
     workspace.update(cx, |workspace, cx| {
-        assert!(workspace.items(cx).collect::<Vec<_>>().len() == 3);
+        assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
     });
 }

crates/collab/src/tests/test_server.rs πŸ”—

@@ -123,7 +123,12 @@ impl TestServer {
         let client_a = server.create_client(cx_a, "user_a").await;
         let client_b = server.create_client(cx_b, "user_b").await;
         let channel_id = server
-            .make_channel("a", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
+            .make_channel(
+                "test-channel",
+                None,
+                (&client_a, cx_a),
+                &mut [(&client_b, cx_b)],
+            )
             .await;
         cx_a.run_until_parked();
 

crates/collab_ui/Cargo.toml πŸ”—

@@ -26,58 +26,58 @@ test-support = [
 
 [dependencies]
 anyhow.workspace = true
-auto_update = { path = "../auto_update" }
-call = { path = "../call" }
-channel = { path = "../channel" }
-client = { path = "../client" }
-clock = { path = "../clock" }
-collections = { path = "../collections" }
-db = { path = "../db" }
-editor = { path = "../editor" }
-feature_flags = { path = "../feature_flags" }
-feedback = { path = "../feedback" }
+auto_update.workspace = true
+call.workspace = true
+channel.workspace = true
+client.workspace = true
+clock.workspace = true
+collections.workspace = true
+db.workspace = true
+editor.workspace = true
+feature_flags.workspace = true
+feedback.workspace = true
 futures.workspace = true
-fuzzy = { path = "../fuzzy" }
-gpui = { path = "../gpui" }
-language = { path = "../language" }
+fuzzy.workspace = true
+gpui.workspace = true
+language.workspace = true
 lazy_static.workspace = true
 log.workspace = true
-menu = { path = "../menu" }
-notifications = { path = "../notifications" }
+menu.workspace = true
+notifications.workspace = true
 parking_lot.workspace = true
-picker = { path = "../picker" }
+picker.workspace = true
 postage.workspace = true
-project = { path = "../project" }
-recent_projects = { path = "../recent_projects" }
-rich_text = { path = "../rich_text" }
-rpc = { path = "../rpc" }
+project.workspace = true
+recent_projects.workspace = true
+rich_text.workspace = true
+rpc.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smallvec.workspace = true
-story = { path = "../story", optional = true }
-theme = { path = "../theme" }
-theme_selector = { path = "../theme_selector" }
+story = { workspace = true, optional = true }
+theme.workspace = true
+theme_selector.workspace = true
 time.workspace = true
-ui = { path = "../ui" }
-util = { path = "../util" }
-vcs_menu = { path = "../vcs_menu" }
-workspace = { path = "../workspace" }
-zed_actions = { path = "../zed_actions" }
+ui.workspace = true
+util.workspace = true
+vcs_menu.workspace = true
+workspace.workspace = true
+zed_actions.workspace = true
 
 [dev-dependencies]
-call = { path = "../call", features = ["test-support"] }
-client = { path = "../client", features = ["test-support"] }
-collections = { path = "../collections", features = ["test-support"] }
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-notifications = { path = "../notifications", features = ["test-support"] }
+call = { workspace = true, features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+notifications = { workspace = true, features = ["test-support"] }
 pretty_assertions.workspace = true
-project = { path = "../project", features = ["test-support"] }
-rpc = { path = "../rpc", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
+rpc = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
 tree-sitter-markdown.workspace = true
-util = { path = "../util", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/collab_ui/src/chat_panel.rs πŸ”—

@@ -1,16 +1,16 @@
 use crate::{collab_panel, ChatPanelSettings};
 use anyhow::Result;
 use call::{room, ActiveCall};
-use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
+use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore};
 use client::Client;
 use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
 use gpui::{
-    actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, DismissEvent,
-    ElementId, EventEmitter, Fill, FocusHandle, FocusableView, FontWeight, ListOffset,
-    ListScrollEvent, ListState, Model, Render, Subscription, Task, View, ViewContext,
-    VisualContext, WeakView,
+    actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, CursorStyle,
+    DismissEvent, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight,
+    HighlightStyle, ListOffset, ListScrollEvent, ListState, Model, Render, StyledText,
+    Subscription, Task, View, ViewContext, VisualContext, WeakView,
 };
 use language::LanguageRegistry;
 use menu::Confirm;
@@ -23,7 +23,7 @@ use std::{sync::Arc, time::Duration};
 use time::{OffsetDateTime, UtcOffset};
 use ui::{
     popover_menu, prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label,
-    TabBar,
+    TabBar, Tooltip,
 };
 use util::{ResultExt, TryFutureExt};
 use workspace::{
@@ -62,6 +62,7 @@ pub struct ChatPanel {
     markdown_data: HashMap<ChannelMessageId, RichText>,
     focus_handle: FocusHandle,
     open_context_menu: Option<(u64, Subscription)>,
+    highlighted_message: Option<(u64, Task<()>)>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -124,6 +125,7 @@ impl ChatPanel {
                 markdown_data: Default::default(),
                 focus_handle: cx.focus_handle(),
                 open_context_menu: None,
+                highlighted_message: None,
             };
 
             if let Some(channel_id) = ActiveCall::global(cx)
@@ -236,6 +238,7 @@ impl ChatPanel {
                 let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
                 self.message_editor.update(cx, |editor, cx| {
                     editor.set_channel(channel_id, channel_name, cx);
+                    editor.clear_reply_to_message_id();
                 });
             };
             let subscription = cx.subscribe(&chat, Self::channel_did_change);
@@ -285,6 +288,99 @@ impl ChatPanel {
         }
     }
 
+    fn render_replied_to_message(
+        &mut self,
+        message_id: Option<ChannelMessageId>,
+        reply_to_message: &ChannelMessage,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        let body_element_id: ElementId = match message_id {
+            Some(ChannelMessageId::Saved(id)) => ("reply-to-saved-message", id).into(),
+            Some(ChannelMessageId::Pending(id)) => ("reply-to-pending-message", id).into(), // This should never happen
+            None => ("composing-reply").into(),
+        };
+
+        let message_element_id: ElementId = match message_id {
+            Some(ChannelMessageId::Saved(id)) => ("reply-to-saved-message-container", id).into(),
+            Some(ChannelMessageId::Pending(id)) => {
+                ("reply-to-pending-message-container", id).into()
+            } // This should never happen
+            None => ("composing-reply-container").into(),
+        };
+
+        let current_channel_id = self.channel_id(cx);
+        let reply_to_message_id = reply_to_message.id;
+
+        let reply_to_message_body = self
+            .markdown_data
+            .entry(reply_to_message.id)
+            .or_insert_with(|| {
+                Self::render_markdown_with_mentions(
+                    &self.languages,
+                    self.client.id(),
+                    reply_to_message,
+                )
+            });
+
+        const REPLY_TO_PREFIX: &str = "Reply to @";
+
+        div().flex_grow().child(
+            v_flex()
+                .id(message_element_id)
+                .text_ui_xs()
+                .child(
+                    h_flex()
+                        .gap_x_1()
+                        .items_center()
+                        .justify_start()
+                        .overflow_x_hidden()
+                        .whitespace_nowrap()
+                        .child(
+                            StyledText::new(format!(
+                                "{}{}",
+                                REPLY_TO_PREFIX,
+                                reply_to_message.sender.github_login.clone()
+                            ))
+                            .with_highlights(
+                                &cx.text_style(),
+                                vec![(
+                                    (REPLY_TO_PREFIX.len() - 1)
+                                        ..(reply_to_message.sender.github_login.len()
+                                            + REPLY_TO_PREFIX.len()),
+                                    HighlightStyle {
+                                        font_weight: Some(FontWeight::BOLD),
+                                        ..Default::default()
+                                    },
+                                )],
+                            ),
+                        ),
+                )
+                .child(
+                    div()
+                        .border_l_2()
+                        .border_color(cx.theme().colors().border)
+                        .px_1()
+                        .py_0p5()
+                        .mb_1()
+                        .overflow_hidden()
+                        .child(
+                            div()
+                                .max_h_12()
+                                .child(reply_to_message_body.element(body_element_id, cx)),
+                        ),
+                )
+                .cursor(CursorStyle::PointingHand)
+                .tooltip(|cx| Tooltip::text("Go to message", cx))
+                .on_click(cx.listener(move |chat_panel, _, cx| {
+                    if let Some(channel_id) = current_channel_id {
+                        chat_panel
+                            .select_channel(channel_id, reply_to_message_id.into(), cx)
+                            .detach_and_log_err(cx)
+                    }
+                })),
+        )
+    }
+
     fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let active_chat = &self.active_chat.as_ref().unwrap().0;
         let (message, is_continuation_from_previous, is_admin) =
@@ -317,18 +413,9 @@ impl ChatPanel {
             });
 
         let _is_pending = message.is_pending();
-        let text = self.markdown_data.entry(message.id).or_insert_with(|| {
-            Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
-        });
 
         let belongs_to_user = Some(message.sender.id) == self.client.user_id();
-        let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
-            (message.id, belongs_to_user || is_admin)
-        {
-            Some(id)
-        } else {
-            None
-        };
+        let can_delete_message = belongs_to_user || is_admin;
 
         let element_id: ElementId = match message.id {
             ChannelMessageId::Saved(id) => ("saved-message", id).into(),
@@ -341,19 +428,41 @@ impl ChatPanel {
             .iter()
             .any(|m| Some(m.1) == self.client.user_id());
 
+        let message_id = match message.id {
+            ChannelMessageId::Saved(id) => Some(id),
+            ChannelMessageId::Pending(_) => None,
+        };
+
+        let reply_to_message = message
+            .reply_to_message_id
+            .map(|id| active_chat.read(cx).find_loaded_message(id))
+            .flatten()
+            .cloned();
+
+        let replied_to_you =
+            reply_to_message.as_ref().map(|m| m.sender.id) == self.client.user_id();
+
+        let is_highlighted_message = self
+            .highlighted_message
+            .as_ref()
+            .is_some_and(|(id, _)| Some(id) == message_id.as_ref());
+        let background = if is_highlighted_message {
+            cx.theme().status().info_background
+        } else if mentioning_you || replied_to_you {
+            cx.theme().colors().background
+        } else {
+            cx.theme().colors().panel_background
+        };
+
         v_flex().w_full().relative().child(
             div()
-                .bg(if mentioning_you {
-                    Fill::from(cx.theme().colors().background)
-                } else {
-                    Fill::default()
-                })
+                .bg(background)
                 .rounded_md()
                 .overflow_hidden()
                 .px_1()
                 .py_0p5()
                 .when(!is_continuation_from_previous, |this| {
-                    this.mt_1().child(
+                    this.mt_2().child(
                         h_flex()
                             .text_ui_sm()
                             .child(div().absolute().child(
@@ -377,36 +486,86 @@ impl ChatPanel {
                             ),
                     )
                 })
-                .when(mentioning_you, |this| this.mt_1())
-                .child(
-                    v_flex()
-                        .w_full()
-                        .text_ui_sm()
-                        .id(element_id)
-                        .group("")
-                        .child(text.element("body".into(), cx))
-                        .child(
+                .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()
-                                .absolute()
-                                .z_index(1)
-                                .right_0()
-                                .w_6()
-                                .bg(cx.theme().colors().panel_background)
-                                .when(!self.has_open_menu(message_id_to_remove), |el| {
-                                    el.visible_on_hover("")
-                                })
-                                .children(message_id_to_remove.map(|message_id| {
-                                    popover_menu(("menu", message_id))
-                                        .trigger(IconButton::new(
-                                            ("trigger", message_id),
-                                            IconName::Ellipsis,
-                                        ))
-                                        .menu(move |cx| {
-                                            Some(Self::render_message_menu(&this, message_id, cx))
-                                        })
-                                })),
-                        ),
-                ),
+                                .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,
+                                                    ))
+                                                }),
+                                        )
+                                    }),
+                            ),
+                    )
+                }),
         )
     }
 
@@ -420,13 +579,27 @@ impl ChatPanel {
     fn render_message_menu(
         this: &View<Self>,
         message_id: u64,
+        can_delete_message: bool,
         cx: &mut WindowContext,
     ) -> View<ContextMenu> {
         let menu = {
-            let this = this.clone();
-            ContextMenu::build(cx, move |menu, _| {
-                menu.entry("Delete message", None, move |cx| {
-                    this.update(cx, |this, cx| this.remove_message(message_id, cx))
+            ContextMenu::build(cx, move |menu, cx| {
+                menu.entry(
+                    "Reply to message",
+                    None,
+                    cx.handler_for(&this, move |this, cx| {
+                        this.message_editor.update(cx, |editor, cx| {
+                            editor.set_reply_to_message_id(message_id);
+                            editor.focus_handle(cx).focus(cx);
+                        })
+                    }),
+                )
+                .when(can_delete_message, move |menu| {
+                    menu.entry(
+                        "Delete message",
+                        None,
+                        cx.handler_for(&this, move |this, cx| this.remove_message(message_id, cx)),
+                    )
                 })
             })
         };
@@ -517,7 +690,21 @@ 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 this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
                             this.message_list.scroll_to(ListOffset {
                                 item_ix,
@@ -536,6 +723,8 @@ impl ChatPanel {
 
 impl Render for ChatPanel {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let reply_to_message_id = self.message_editor.read(cx).reply_to_message_id();
+
         v_flex()
             .track_focus(&self.focus_handle)
             .full()
@@ -558,7 +747,7 @@ impl Render for ChatPanel {
                     ),
                 ),
             )
-            .child(div().flex_grow().px_2().pt_1().map(|this| {
+            .child(div().flex_grow().px_2().map(|this| {
                 if self.active_chat.is_some() {
                     this.child(list(self.message_list.clone()).full())
                 } else {
@@ -589,25 +778,58 @@ impl Render for ChatPanel {
                     )
                 }
             }))
-            .child(
-                h_flex()
-                    .when(!self.is_scrolled_to_bottom, |el| {
-                        el.border_t_1().border_color(cx.theme().colors().border)
+            .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
+                            }
+                        })
                     })
-                    .p_2()
-                    .map(|el| {
-                        if self.active_chat.is_some() {
-                            el.child(self.message_editor.clone())
-                        } else {
-                            el.child(
-                                div()
-                                    .rounded_md()
-                                    .h_6()
-                                    .w_full()
-                                    .bg(cx.theme().colors().editor_background),
-                            )
-                        }
-                    }),
+                    .flatten()
+                    .cloned();
+
+                el.when_some(reply_message, |el, reply_message| {
+                    el.child(
+                        div()
+                            .when(!self.is_scrolled_to_bottom, |el| {
+                                el.border_t_1().border_color(cx.theme().colors().border)
+                            })
+                            .flex()
+                            .w_full()
+                            .items_start()
+                            .overflow_hidden()
+                            .py_1()
+                            .px_2()
+                            .bg(cx.theme().colors().background)
+                            .child(self.render_replied_to_message(None, &reply_message, cx))
+                            .child(
+                                IconButton::new("close-reply-preview", IconName::Close)
+                                    .shape(ui::IconButtonShape::Square)
+                                    .on_click(cx.listener(move |this, _, cx| {
+                                        this.message_editor.update(cx, |editor, _| {
+                                            editor.clear_reply_to_message_id()
+                                        });
+                                    })),
+                            ),
+                    )
+                })
+            })
+            .children(
+                Some(
+                    h_flex()
+                        .when(
+                            !self.is_scrolled_to_bottom && reply_to_message_id.is_none(),
+                            |el| el.border_t_1().border_color(cx.theme().colors().border),
+                        )
+                        .p_2()
+                        .map(|el| el.child(self.message_editor.clone())),
+                )
+                .filter(|_| self.active_chat.is_some()),
             )
             .into_any()
     }
@@ -747,6 +969,7 @@ mod tests {
             }),
             nonce: 5,
             mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+            reply_to_message_id: None,
         };
 
         let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);

crates/collab_ui/src/chat_panel/message_editor.rs πŸ”—

@@ -34,6 +34,7 @@ pub struct MessageEditor {
     mentions: Vec<UserId>,
     mentions_task: Option<Task<()>>,
     channel_id: Option<ChannelId>,
+    reply_to_message_id: Option<u64>,
 }
 
 struct MessageEditorCompletionProvider(WeakView<MessageEditor>);
@@ -112,9 +113,22 @@ impl MessageEditor {
             channel_id: None,
             mentions: Vec::new(),
             mentions_task: None,
+            reply_to_message_id: None,
         }
     }
 
+    pub fn reply_to_message_id(&self) -> Option<u64> {
+        self.reply_to_message_id
+    }
+
+    pub fn set_reply_to_message_id(&mut self, reply_to_message_id: u64) {
+        self.reply_to_message_id = Some(reply_to_message_id);
+    }
+
+    pub fn clear_reply_to_message_id(&mut self) {
+        self.reply_to_message_id = None;
+    }
+
     pub fn set_channel(
         &mut self,
         channel_id: u64,
@@ -172,8 +186,13 @@ impl MessageEditor {
 
             editor.clear(cx);
             self.mentions.clear();
+            let reply_to_message_id = std::mem::take(&mut self.reply_to_message_id);
 
-            MessageParams { text, mentions }
+            MessageParams {
+                text,
+                mentions,
+                reply_to_message_id,
+            }
         })
     }
 
@@ -341,6 +360,7 @@ impl Render for MessageEditor {
             line_height: relative(1.3).into(),
             background_color: None,
             underline: None,
+            strikethrough: None,
             white_space: WhiteSpace::Normal,
         };
 
@@ -424,6 +444,7 @@ mod tests {
                 MessageParams {
                     text,
                     mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+                    reply_to_message_id: None
                 }
             );
         });

crates/collab_ui/src/collab_panel.rs πŸ”—

@@ -40,7 +40,7 @@ use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
     notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
-    Workspace,
+    OpenChannelNotes, Workspace,
 };
 
 actions!(
@@ -69,6 +69,19 @@ pub fn init(cx: &mut AppContext) {
         workspace.register_action(|workspace, _: &ToggleFocus, cx| {
             workspace.toggle_panel_focus::<CollabPanel>(cx);
         });
+        workspace.register_action(|_, _: &OpenChannelNotes, cx| {
+            let channel_id = ActiveCall::global(cx)
+                .read(cx)
+                .room()
+                .and_then(|room| room.read(cx).channel_id());
+
+            if let Some(channel_id) = channel_id {
+                let workspace = cx.view().clone();
+                cx.window_context().defer(move |cx| {
+                    ChannelView::open(channel_id, None, workspace, cx).detach_and_log_err(cx)
+                });
+            }
+        });
     })
     .detach();
 }
@@ -957,7 +970,7 @@ impl CollabPanel {
                     .child(render_tree_branch(false, true, cx))
                     .child(IconButton::new(0, IconName::File)),
             )
-            .child(div().h_7().w_full().child(Label::new("notes")))
+            .child(Label::new("notes"))
             .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
     }
 
@@ -2055,6 +2068,7 @@ impl CollabPanel {
             line_height: relative(1.3).into(),
             background_color: None,
             underline: None,
+            strikethrough: None,
             white_space: WhiteSpace::Normal,
         };
 

crates/collab_ui/src/collab_titlebar_item.rs πŸ”—

@@ -562,14 +562,23 @@ impl CollabTitlebarItem {
     }
 
     fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
-        let project = if cx.is_window_active() {
-            Some(self.project.clone())
-        } else {
-            None
-        };
-        ActiveCall::global(cx)
-            .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
-            .detach_and_log_err(cx);
+        if cx.is_window_active() {
+            ActiveCall::global(cx)
+                .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
+                .detach_and_log_err(cx);
+            return;
+        }
+
+        if cx.active_window().is_none() {
+            ActiveCall::global(cx)
+                .update(cx, |call, cx| call.set_location(None, cx))
+                .detach_and_log_err(cx);
+        }
+        self.workspace
+            .update(cx, |workspace, cx| {
+                workspace.update_active_view_for_followers(cx);
+            })
+            .ok();
     }
 
     fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {

crates/color/Cargo.toml πŸ”—

@@ -16,4 +16,4 @@ doctest = true
 [dependencies]
 itertools = { version = "0.11.0", optional = true }
 palette = "0.7.3"
-story = { path = "../story", optional = true }
+story = { workspace = true, optional = true }

crates/command_palette/Cargo.toml πŸ”—

@@ -11,32 +11,32 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-client = { path = "../client" }
-collections = { path = "../collections" }
+client.workspace = true
+collections.workspace = true
 # HACK: We're only depending on `copilot` here for `CommandPaletteFilter`.  See the attached comment on that type.
-copilot = { path = "../copilot" }
-editor = { path = "../editor" }
-fuzzy = {  path = "../fuzzy" }
-gpui = { path = "../gpui" }
-picker = { path = "../picker" }
-project = { path = "../project" }
-release_channel = { path = "../release_channel" }
+copilot.workspace = true
+editor.workspace = true
+fuzzy.workspace = true
+gpui.workspace = true
+picker.workspace = true
+project.workspace = true
+release_channel.workspace = true
 serde.workspace = true
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
-zed_actions = { path = "../zed_actions" }
+settings.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
+zed_actions.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
-go_to_line = { path = "../go_to_line" }
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
-menu = { path = "../menu" }
-project = { path = "../project", features = ["test-support"] }
+go_to_line.workspace = true
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+menu.workspace = true
+project = { workspace = true, features = ["test-support"] }
 serde_json.workspace = true
-workspace = { path = "../workspace", features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/copilot/Cargo.toml πŸ”—

@@ -23,28 +23,28 @@ test-support = [
 anyhow.workspace = true
 async-compression.workspace = true
 async-tar = "0.4.2"
-collections = { path = "../collections" }
+collections.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
-language = { path = "../language" }
+gpui.workspace = true
+language.workspace = true
 log.workspace = true
-lsp = { path = "../lsp" }
-node_runtime = { path = "../node_runtime" }
+lsp.workspace = true
+node_runtime.workspace = true
 parking_lot.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smol.workspace = true
-theme = { path = "../theme" }
-util = { path = "../util" }
+theme.workspace = true
+util.workspace = true
 
 [dev-dependencies]
-clock = { path = "../clock" }
-collections = { path = "../collections", features = ["test-support"] }
-fs = { path = "../fs", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
-lsp = { path = "../lsp", features = ["test-support"] }
-rpc = { path = "../rpc", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
-util = { path = "../util", features = ["test-support"] }
+clock.workspace = true
+collections = { workspace = true, features = ["test-support"] }
+fs = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+lsp = { workspace = true, features = ["test-support"] }
+rpc = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/copilot_ui/Cargo.toml πŸ”—

@@ -11,19 +11,19 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-copilot = { path = "../copilot" }
-editor = { path = "../editor" }
-fs = { path = "../fs" }
+copilot.workspace = true
+editor.workspace = true
+fs.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
-language = { path = "../language" }
-settings = { path = "../settings" }
+gpui.workspace = true
+language.workspace = true
+settings.workspace = true
 smol.workspace = true
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
-zed_actions = { path = "../zed_actions" }
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
+zed_actions.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }

crates/db/Cargo.toml πŸ”—

@@ -15,21 +15,21 @@ test-support = []
 [dependencies]
 anyhow.workspace = true
 async-trait.workspace = true
-collections = { path = "../collections" }
-gpui = { path = "../gpui" }
+collections.workspace = true
+gpui.workspace = true
 indoc.workspace = true
 lazy_static.workspace = true
 log.workspace = true
 parking_lot.workspace = true
-release_channel = { path = "../release_channel" }
+release_channel.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 smol.workspace = true
-sqlez = { path = "../sqlez" }
-sqlez_macros = { path = "../sqlez_macros" }
-util = { path = "../util" }
+sqlez.workspace = true
+sqlez_macros.workspace = true
+util.workspace = true
 
 [dev-dependencies]
 env_logger.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 tempfile.workspace = true

crates/diagnostics/Cargo.toml πŸ”—

@@ -11,32 +11,32 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-collections = { path = "../collections" }
-editor = { path = "../editor" }
+collections.workspace = true
+editor.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
-language = { path = "../language" }
+gpui.workspace = true
+language.workspace = true
 log.workspace = true
-lsp = { path = "../lsp" }
+lsp.workspace = true
 postage.workspace = true
-project = { path = "../project" }
+project.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smallvec.workspace = true
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-client = { path = "../client", features = ["test-support"] }
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
-lsp = { path = "../lsp", features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+lsp = { workspace = true, features = ["test-support"] }
 serde_json.workspace = true
-theme = { path = "../theme", features = ["test-support"] }
+theme = { workspace = true, features = ["test-support"] }
 unindent.workspace = true
-workspace = { path = "../workspace", features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/editor/Cargo.toml πŸ”—

@@ -20,73 +20,74 @@ test-support = [
     "util/test-support",
     "workspace/test-support",
     "tree-sitter-rust",
-    "tree-sitter-typescript"
+    "tree-sitter-typescript",
 ]
 
 [dependencies]
 aho-corasick = "1.1"
 anyhow.workspace = true
-client = { path = "../client" }
-clock = { path = "../clock" }
-collections = { path = "../collections" }
+client.workspace = true
+clock.workspace = true
+collections.workspace = true
 convert_case = "0.6.0"
-copilot = { path = "../copilot" }
-db = { path = "../db" }
+copilot.workspace = true
+db.workspace = true
 futures.workspace = true
-fuzzy = {  path = "../fuzzy" }
-git = { path = "../git" }
-gpui = { path = "../gpui" }
+fuzzy.workspace = true
+git.workspace = true
+gpui.workspace = true
 indoc = "1.0.4"
 itertools = "0.10"
-language = { path = "../language" }
+language.workspace = true
 lazy_static.workspace = true
+linkify = "0.10.0"
 log.workspace = true
-lsp = { path = "../lsp" }
-multi_buffer = { path = "../multi_buffer" }
+lsp.workspace = true
+multi_buffer.workspace = true
 ordered-float.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
-project = { path = "../project" }
+project.workspace = true
 rand.workspace = true
-rich_text = { path = "../rich_text" }
-rpc = { path = "../rpc" }
+rich_text.workspace = true
+rpc.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smallvec.workspace = true
 smol.workspace = true
-snippet = { path = "../snippet" }
-sqlez = { path = "../sqlez" }
-sum_tree = { path = "../sum_tree" }
-text = { path = "../text" }
-theme = { path = "../theme" }
+snippet.workspace = true
+sqlez.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 }
-ui = { path = "../ui" }
+ui.workspace = true
 url.workspace = true
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-copilot = { path = "../copilot", features = ["test-support"] }
+copilot = { workspace = true, features = ["test-support"] }
 ctor.workspace = true
 env_logger.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
-lsp = { path = "../lsp", features = ["test-support"] }
-multi_buffer = { path = "../multi_buffer", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
-release_channel = { path = "../release_channel" }
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+lsp = { workspace = true, features = ["test-support"] }
+multi_buffer = { workspace = true, features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
+release_channel.workspace = true
 rand.workspace = true
-settings = { path = "../settings", features = ["test-support"] }
-text = { path = "../text", features = ["test-support"] }
+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 = { path = "../util", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/editor/src/actions.rs πŸ”—

@@ -70,6 +70,30 @@ pub struct FoldAt {
 pub struct UnfoldAt {
     pub buffer_row: u32,
 }
+
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct MoveUpByLines {
+    #[serde(default)]
+    pub(super) lines: u32,
+}
+
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct MoveDownByLines {
+    #[serde(default)]
+    pub(super) lines: u32,
+}
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct SelectUpByLines {
+    #[serde(default)]
+    pub(super) lines: u32,
+}
+
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct SelectDownByLines {
+    #[serde(default)]
+    pub(super) lines: u32,
+}
+
 impl_actions!(
     editor,
     [
@@ -84,7 +108,11 @@ impl_actions!(
         ConfirmCodeAction,
         ToggleComments,
         FoldAt,
-        UnfoldAt
+        UnfoldAt,
+        MoveUpByLines,
+        MoveDownByLines,
+        SelectUpByLines,
+        SelectDownByLines
     ]
 );
 

crates/editor/src/display_map.rs πŸ”—

@@ -25,8 +25,8 @@ mod wrap_map;
 
 use crate::EditorStyle;
 use crate::{
-    link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
-    InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+    hover_links::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt, InlayId,
+    MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
 };
 pub use block_map::{BlockMap, BlockPoint};
 use collections::{BTreeMap, HashMap, HashSet};
@@ -586,8 +586,9 @@ impl DisplaySnapshot {
             text_system,
             editor_style,
             rem_size,
-            anchor: _,
+            scroll_anchor: _,
             visible_rows: _,
+            vertical_scroll_margin: _,
         }: &TextLayoutDetails,
     ) -> Arc<LineLayout> {
         let mut runs = Vec::new();

crates/editor/src/display_map/inlay_map.rs πŸ”—

@@ -1168,7 +1168,7 @@ mod tests {
     use super::*;
     use crate::{
         display_map::{InlayHighlights, TextHighlights},
-        link_go_to_definition::InlayHighlight,
+        hover_links::InlayHighlight,
         InlayId, MultiBuffer,
     };
     use gpui::AppContext;

crates/editor/src/editor.rs πŸ”—

@@ -22,9 +22,9 @@ mod inlay_hint_cache;
 mod debounced_delay;
 mod git;
 mod highlight_matching_bracket;
+mod hover_links;
 mod hover_popover;
 pub mod items;
-mod link_go_to_definition;
 mod mouse_context_menu;
 pub mod movement;
 mod persistence;
@@ -61,8 +61,8 @@ use gpui::{
     DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle,
     FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton,
     ParentElement, Pixels, Render, SharedString, Styled, StyledText, Subscription, Task, TextStyle,
-    UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakView,
-    WhiteSpace, WindowContext,
+    UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
+    WeakView, WhiteSpace, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -77,7 +77,7 @@ use language::{
     Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
 };
 
-use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
+use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
 use lsp::{DiagnosticSeverity, LanguageServerId};
 use mouse_context_menu::MouseContextMenu;
 use movement::TextLayoutDetails;
@@ -120,6 +120,7 @@ use ui::{
     Tooltip,
 };
 use util::{maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
+use workspace::Toast;
 use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace};
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@@ -374,6 +375,7 @@ pub struct Editor {
     hovered_cursors: HashMap<HoveredCursor, Task<()>>,
     pub show_local_selections: bool,
     mode: EditorMode,
+    show_breadcrumbs: bool,
     show_gutter: bool,
     show_wrap_guides: Option<bool>,
     placeholder_text: Option<Arc<str>>,
@@ -402,7 +404,7 @@ pub struct Editor {
     remote_id: Option<ViewId>,
     hover_state: HoverState,
     gutter_hovered: bool,
-    link_go_to_definition_state: LinkGoToDefinitionState,
+    hovered_link_state: Option<HoveredLinkState>,
     copilot_state: CopilotState,
     inlay_hint_cache: InlayHintCache,
     next_inlay_id: usize,
@@ -851,15 +853,21 @@ impl CompletionsMenu {
         let selected_item = self.selected_item;
         let style = style.clone();
 
-        let multiline_docs = {
+        let multiline_docs = if show_completion_documentation {
             let mat = &self.matches[selected_item];
             let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation {
                 Some(Documentation::MultiLinePlainText(text)) => {
                     Some(div().child(SharedString::from(text.clone())))
                 }
-                Some(Documentation::MultiLineMarkdown(parsed)) => Some(div().child(
-                    render_parsed_markdown("completions_markdown", parsed, &style, workspace, cx),
-                )),
+                Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
+                    Some(div().child(render_parsed_markdown(
+                        "completions_markdown",
+                        parsed,
+                        &style,
+                        workspace,
+                        cx,
+                    )))
+                }
                 _ => None,
             };
             multiline_docs.map(|div| {
@@ -876,6 +884,8 @@ impl CompletionsMenu {
                     // because that would move the cursor.
                     .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
             })
+        } else {
+            None
         };
 
         let list = uniform_list(
@@ -1424,7 +1434,7 @@ impl Editor {
             buffer: buffer.clone(),
             display_map: display_map.clone(),
             selections,
-            scroll_manager: ScrollManager::new(),
+            scroll_manager: ScrollManager::new(cx),
             columnar_selection_tail: None,
             add_selections_state: None,
             select_next_state: None,
@@ -1442,6 +1452,7 @@ impl Editor {
             blink_manager: blink_manager.clone(),
             show_local_selections: true,
             mode,
+            show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
             show_gutter: mode == EditorMode::Full,
             show_wrap_guides: None,
             placeholder_text: None,
@@ -1471,7 +1482,7 @@ impl Editor {
             leader_peer_id: None,
             remote_id: None,
             hover_state: Default::default(),
-            link_go_to_definition_state: Default::default(),
+            hovered_link_state: Default::default(),
             copilot_state: Default::default(),
             inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
             gutter_hovered: false,
@@ -3080,8 +3091,9 @@ impl Editor {
             text_system: cx.text_system().clone(),
             editor_style: self.style.clone().unwrap(),
             rem_size: cx.rem_size(),
-            anchor: self.scroll_manager.anchor().anchor,
+            scroll_anchor: self.scroll_manager.anchor(),
             visible_rows: self.visible_line_count(),
+            vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin,
         }
     }
 
@@ -5373,6 +5385,86 @@ impl Editor {
         })
     }
 
+    pub fn move_up_by_lines(&mut self, action: &MoveUpByLines, cx: &mut ViewContext<Self>) {
+        if self.take_rename(true, cx).is_some() {
+            return;
+        }
+
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate();
+            return;
+        }
+
+        let text_layout_details = &self.text_layout_details(cx);
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            let line_mode = s.line_mode;
+            s.move_with(|map, selection| {
+                if !selection.is_empty() && !line_mode {
+                    selection.goal = SelectionGoal::None;
+                }
+                let (cursor, goal) = movement::up_by_rows(
+                    map,
+                    selection.start,
+                    action.lines,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
+                selection.collapse_to(cursor, goal);
+            });
+        })
+    }
+
+    pub fn move_down_by_lines(&mut self, action: &MoveDownByLines, cx: &mut ViewContext<Self>) {
+        if self.take_rename(true, cx).is_some() {
+            return;
+        }
+
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate();
+            return;
+        }
+
+        let text_layout_details = &self.text_layout_details(cx);
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            let line_mode = s.line_mode;
+            s.move_with(|map, selection| {
+                if !selection.is_empty() && !line_mode {
+                    selection.goal = SelectionGoal::None;
+                }
+                let (cursor, goal) = movement::down_by_rows(
+                    map,
+                    selection.start,
+                    action.lines,
+                    selection.goal,
+                    false,
+                    &text_layout_details,
+                );
+                selection.collapse_to(cursor, goal);
+            });
+        })
+    }
+
+    pub fn select_down_by_lines(&mut self, action: &SelectDownByLines, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, goal| {
+                movement::down_by_rows(map, head, action.lines, goal, false, &text_layout_details)
+            })
+        })
+    }
+
+    pub fn select_up_by_lines(&mut self, action: &SelectUpByLines, cx: &mut ViewContext<Self>) {
+        let text_layout_details = &self.text_layout_details(cx);
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, goal| {
+                movement::up_by_rows(map, head, action.lines, goal, false, &text_layout_details)
+            })
+        })
+    }
+
     pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext<Self>) {
         if self.take_rename(true, cx).is_some() {
             return;
@@ -7156,11 +7248,8 @@ impl Editor {
         cx.spawn(|editor, mut cx| async move {
             let definitions = definitions.await?;
             editor.update(&mut cx, |editor, cx| {
-                editor.navigate_to_definitions(
-                    definitions
-                        .into_iter()
-                        .map(GoToDefinitionLink::Text)
-                        .collect(),
+                editor.navigate_to_hover_links(
+                    definitions.into_iter().map(HoverLink::Text).collect(),
                     split,
                     cx,
                 );
@@ -7170,29 +7259,34 @@ impl Editor {
         .detach_and_log_err(cx);
     }
 
-    pub fn navigate_to_definitions(
+    pub fn navigate_to_hover_links(
         &mut self,
-        mut definitions: Vec<GoToDefinitionLink>,
+        mut definitions: Vec<HoverLink>,
         split: bool,
         cx: &mut ViewContext<Editor>,
     ) {
-        let Some(workspace) = self.workspace() else {
-            return;
-        };
-        let pane = workspace.read(cx).active_pane().clone();
         // If there is one definition, just open it directly
         if definitions.len() == 1 {
             let definition = definitions.pop().unwrap();
             let target_task = match definition {
-                GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
-                GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
+                HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
+                HoverLink::InlayHint(lsp_location, server_id) => {
                     self.compute_target_location(lsp_location, server_id, cx)
                 }
+                HoverLink::Url(url) => {
+                    cx.open_url(&url);
+                    Task::ready(Ok(None))
+                }
             };
             cx.spawn(|editor, mut cx| async move {
                 let target = target_task.await.context("target resolution task")?;
                 if let Some(target) = target {
                     editor.update(&mut cx, |editor, cx| {
+                        let Some(workspace) = editor.workspace() else {
+                            return;
+                        };
+                        let pane = workspace.read(cx).active_pane().clone();
+
                         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() {
@@ -7233,37 +7327,35 @@ impl Editor {
         } else if !definitions.is_empty() {
             let replica_id = self.replica_id(cx);
             cx.spawn(|editor, mut cx| async move {
-                let (title, location_tasks) = editor
+                let (title, location_tasks, workspace) = editor
                     .update(&mut cx, |editor, cx| {
                         let title = definitions
                             .iter()
                             .find_map(|definition| match definition {
-                                GoToDefinitionLink::Text(link) => {
-                                    link.origin.as_ref().map(|origin| {
-                                        let buffer = origin.buffer.read(cx);
-                                        format!(
-                                            "Definitions for {}",
-                                            buffer
-                                                .text_for_range(origin.range.clone())
-                                                .collect::<String>()
-                                        )
-                                    })
-                                }
-                                GoToDefinitionLink::InlayHint(_, _) => None,
+                                HoverLink::Text(link) => link.origin.as_ref().map(|origin| {
+                                    let buffer = origin.buffer.read(cx);
+                                    format!(
+                                        "Definitions for {}",
+                                        buffer
+                                            .text_for_range(origin.range.clone())
+                                            .collect::<String>()
+                                    )
+                                }),
+                                HoverLink::InlayHint(_, _) => None,
+                                HoverLink::Url(_) => None,
                             })
                             .unwrap_or("Definitions".to_string());
                         let location_tasks = definitions
                             .into_iter()
                             .map(|definition| match definition {
-                                GoToDefinitionLink::Text(link) => {
-                                    Task::Ready(Some(Ok(Some(link.target))))
-                                }
-                                GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
+                                HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
+                                HoverLink::InlayHint(lsp_location, server_id) => {
                                     editor.compute_target_location(lsp_location, server_id, cx)
                                 }
+                                HoverLink::Url(_) => Task::ready(Ok(None)),
                             })
                             .collect::<Vec<_>>();
-                        (title, location_tasks)
+                        (title, location_tasks, editor.workspace().clone())
                     })
                     .context("location tasks preparation")?;
 
@@ -7273,6 +7365,10 @@ impl Editor {
                     .filter_map(|location| location.transpose())
                     .collect::<Result<_>>()
                     .context("location tasks")?;
+
+                let Some(workspace) = workspace else {
+                    return Ok(());
+                };
                 workspace
                     .update(&mut cx, |workspace, cx| {
                         Self::open_locations_in_multibuffer(
@@ -7811,7 +7907,7 @@ impl Editor {
                 .insert_blocks(
                     diagnostic_group.iter().map(|entry| {
                         let diagnostic = entry.diagnostic.clone();
-                        let message_height = diagnostic.message.lines().count() as u8;
+                        let message_height = diagnostic.message.matches('\n').count() as u8 + 1;
                         BlockProperties {
                             style: BlockStyle::Fixed,
                             position: buffer.anchor_after(entry.range.start),
@@ -8259,21 +8355,37 @@ impl Editor {
         use git::permalink::{build_permalink, BuildPermalinkParams};
 
         let permalink = maybe!({
-            let project = self.project.clone()?;
+            let project = self.project.clone().ok_or_else(|| anyhow!("no project"))?;
             let project = project.read(cx);
 
-            let worktree = project.visible_worktrees(cx).next()?;
+            let worktree = project
+                .visible_worktrees(cx)
+                .next()
+                .ok_or_else(|| anyhow!("no worktree"))?;
 
             let mut cwd = worktree.read(cx).abs_path().to_path_buf();
             cwd.push(".git");
 
-            let repo = project.fs().open_repo(&cwd)?;
-            let origin_url = repo.lock().remote_url("origin")?;
-            let sha = repo.lock().head_sha()?;
-
-            let buffer = self.buffer().read(cx).as_singleton()?;
-            let file = buffer.read(cx).file().and_then(|f| f.as_local())?;
-            let path = file.path().to_str().map(|path| path.to_string())?;
+            const REMOTE_NAME: &'static str = "origin";
+            let repo = project
+                .fs()
+                .open_repo(&cwd)
+                .ok_or_else(|| anyhow!("no Git repo"))?;
+            let origin_url = repo
+                .lock()
+                .remote_url(REMOTE_NAME)
+                .ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?;
+            let sha = repo
+                .lock()
+                .head_sha()
+                .ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
+
+            let path = maybe!({
+                let buffer = self.buffer().read(cx).as_singleton()?;
+                let file = buffer.read(cx).file().and_then(|f| f.as_local())?;
+                file.path().to_str().map(|path| path.to_string())
+            })
+            .ok_or_else(|| anyhow!("failed to determine file path"))?;
 
             let selections = self.selections.all::<Point>(cx);
             let selection = selections.iter().peekable().next();
@@ -8284,11 +8396,23 @@ impl Editor {
                 path: &path,
                 selection: selection.map(|selection| selection.range()),
             })
-            .log_err()
         });
 
-        if let Some(permalink) = permalink {
-            cx.write_to_clipboard(ClipboardItem::new(permalink.to_string()));
+        match permalink {
+            Ok(permalink) => {
+                cx.write_to_clipboard(ClipboardItem::new(permalink.to_string()));
+            }
+            Err(err) => {
+                let message = format!("Failed to copy permalink: {err}");
+
+                Err::<(), anyhow::Error>(err).log_err();
+
+                if let Some(workspace) = self.workspace() {
+                    workspace.update(cx, |workspace, cx| {
+                        workspace.show_toast(Toast::new(0x156a5f9ee, message), cx)
+                    })
+                }
+            }
         }
     }
 
@@ -8676,6 +8800,9 @@ impl Editor {
             )),
             cx,
         );
+        let editor_settings = EditorSettings::get_global(cx);
+        self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
+        self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
         cx.notify();
     }
 
@@ -9368,6 +9495,7 @@ impl Render for Editor {
                 line_height: relative(settings.buffer_line_height.value()),
                 background_color: None,
                 underline: None,
+                strikethrough: None,
                 white_space: WhiteSpace::Normal,
             },
 
@@ -9381,6 +9509,7 @@ impl Render for Editor {
                 line_height: relative(settings.buffer_line_height.value()),
                 background_color: None,
                 underline: None,
+                strikethrough: None,
                 white_space: WhiteSpace::Normal,
             },
         };
@@ -9587,7 +9716,14 @@ impl ViewInputHandler for Editor {
             } else {
                 this.highlight_text::<InputComposition>(
                     marked_ranges.clone(),
-                    HighlightStyle::default(), // todo!() this.style(cx).composition_mark,
+                    HighlightStyle {
+                        underline: Some(UnderlineStyle {
+                            thickness: px(1.),
+                            color: None,
+                            wavy: false,
+                        }),
+                        ..Default::default()
+                    },
                     cx,
                 );
             }

crates/editor/src/editor_settings.rs πŸ”—

@@ -10,7 +10,9 @@ pub struct EditorSettings {
     pub show_completion_documentation: bool,
     pub completion_documentation_secondary_query_debounce: u64,
     pub use_on_type_format: bool,
+    pub toolbar: Toolbar,
     pub scrollbar: Scrollbar,
+    pub vertical_scroll_margin: f32,
     pub relative_line_numbers: bool,
     pub seed_search_query_from_cursor: SeedQuerySetting,
     pub redact_private_values: bool,
@@ -28,12 +30,19 @@ pub enum SeedQuerySetting {
     Never,
 }
 
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct Toolbar {
+    pub breadcrumbs: bool,
+    pub quick_actions: bool,
+}
+
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 pub struct Scrollbar {
     pub show: ShowScrollbar,
     pub git_diff: bool,
     pub selections: bool,
     pub symbols_selections: bool,
+    pub diagnostics: bool,
 }
 
 /// When to show the scrollbar in the editor.
@@ -84,8 +93,15 @@ pub struct EditorSettingsContent {
     ///
     /// Default: true
     pub use_on_type_format: Option<bool>,
+    /// Toolbar related settings
+    pub toolbar: Option<ToolbarContent>,
     /// Scrollbar related settings
     pub scrollbar: Option<ScrollbarContent>,
+
+    /// The number of lines to keep above/below the cursor when auto-scrolling.
+    ///
+    /// Default: 3.
+    pub vertical_scroll_margin: Option<f32>,
     /// Whether the line numbers on editors gutter are relative or not.
     ///
     /// Default: false
@@ -103,6 +119,19 @@ pub struct EditorSettingsContent {
     pub redact_private_values: Option<bool>,
 }
 
+// Toolbar related settings
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct ToolbarContent {
+    /// Whether to display breadcrumbs in the editor toolbar.
+    ///
+    /// Default: true
+    pub breadcrumbs: Option<bool>,
+    /// Whether to display quik action buttons in the editor toolbar.
+    ///
+    /// Default: true
+    pub quick_actions: Option<bool>,
+}
+
 /// Scrollbar related settings
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 pub struct ScrollbarContent {
@@ -122,6 +151,10 @@ pub struct ScrollbarContent {
     ///
     /// Default: true
     pub symbols_selections: Option<bool>,
+    /// Whether to show diagnostic indicators in the scrollbar.
+    ///
+    /// Default: true
+    pub diagnostics: Option<bool>,
 }
 
 impl Settings for EditorSettings {

crates/editor/src/element.rs πŸ”—

@@ -9,11 +9,6 @@ use crate::{
         self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
     },
     items::BufferSearchHighlights,
-    link_go_to_definition::{
-        go_to_fetched_definition, go_to_fetched_type_definition, show_link_definition,
-        update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger,
-        LinkGoToDefinitionState,
-    },
     mouse_context_menu,
     scroll::scroll_amount::ScrollAmount,
     CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
@@ -35,6 +30,7 @@ use gpui::{
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
+use lsp::DiagnosticSeverity;
 use multi_buffer::Anchor;
 use project::{
     project_settings::{GitGutterSetting, ProjectSettings},
@@ -146,7 +142,11 @@ impl EditorElement {
         register_action(view, cx, Editor::move_left);
         register_action(view, cx, Editor::move_right);
         register_action(view, cx, Editor::move_down);
+        register_action(view, cx, Editor::move_down_by_lines);
+        register_action(view, cx, Editor::select_down_by_lines);
         register_action(view, cx, Editor::move_up);
+        register_action(view, cx, Editor::move_up_by_lines);
+        register_action(view, cx, Editor::select_up_by_lines);
         register_action(view, cx, Editor::cancel);
         register_action(view, cx, Editor::newline);
         register_action(view, cx, Editor::newline_above);
@@ -332,7 +332,14 @@ impl EditorElement {
         register_action(view, cx, Editor::display_cursor_names);
     }
 
-    fn register_key_listeners(&self, cx: &mut ElementContext) {
+    fn register_key_listeners(
+        &self,
+        cx: &mut ElementContext,
+        text_bounds: Bounds<Pixels>,
+        layout: &LayoutState,
+    ) {
+        let position_map = layout.position_map.clone();
+        let stacking_order = cx.stacking_order().clone();
         cx.on_key_event({
             let editor = self.editor.clone();
             move |event: &ModifiersChangedEvent, phase, cx| {
@@ -340,46 +347,41 @@ impl EditorElement {
                     return;
                 }
 
-                if editor.update(cx, |editor, cx| Self::modifiers_changed(editor, event, cx)) {
-                    cx.stop_propagation();
-                }
+                editor.update(cx, |editor, cx| {
+                    Self::modifiers_changed(
+                        editor,
+                        event,
+                        &position_map,
+                        text_bounds,
+                        &stacking_order,
+                        cx,
+                    )
+                })
             }
         });
     }
 
-    pub(crate) fn modifiers_changed(
+    fn modifiers_changed(
         editor: &mut Editor,
         event: &ModifiersChangedEvent,
+        position_map: &PositionMap,
+        text_bounds: Bounds<Pixels>,
+        stacking_order: &StackingOrder,
         cx: &mut ViewContext<Editor>,
-    ) -> bool {
-        let pending_selection = editor.has_pending_selection();
-
-        if let Some(point) = &editor.link_go_to_definition_state.last_trigger_point {
-            if event.command && !pending_selection {
-                let point = point.clone();
-                let snapshot = editor.snapshot(cx);
-                let kind = point.definition_kind(event.shift);
-
-                show_link_definition(kind, editor, point, snapshot, cx);
-                return false;
-            }
-        }
-
+    ) {
+        let mouse_position = cx.mouse_position();
+        if !text_bounds.contains(&mouse_position)
+            || !cx.was_top_layer(&mouse_position, stacking_order)
         {
-            if editor.link_go_to_definition_state.symbol_range.is_some()
-                || !editor.link_go_to_definition_state.definitions.is_empty()
-            {
-                editor.link_go_to_definition_state.symbol_range.take();
-                editor.link_go_to_definition_state.definitions.clear();
-                cx.notify();
-            }
-
-            editor.link_go_to_definition_state.task = None;
-
-            editor.clear_highlights::<LinkGoToDefinitionState>(cx);
+            return;
         }
 
-        false
+        editor.update_hovered_link(
+            position_map.point_for_position(text_bounds, mouse_position),
+            &position_map.snapshot,
+            event.modifiers,
+            cx,
+        )
     }
 
     fn mouse_left_down(
@@ -480,13 +482,7 @@ impl EditorElement {
             && cx.was_top_layer(&event.position, stacking_order)
         {
             let point = position_map.point_for_position(text_bounds, event.position);
-            let could_be_inlay = point.as_valid().is_none();
-            let split = event.modifiers.alt;
-            if event.modifiers.shift || could_be_inlay {
-                go_to_fetched_type_definition(editor, point, split, cx);
-            } else {
-                go_to_fetched_definition(editor, point, split, cx);
-            }
+            editor.handle_click_hovered_link(point, event.modifiers, cx);
 
             cx.stop_propagation();
         } else if end_selection {
@@ -559,31 +555,14 @@ impl EditorElement {
         if text_hovered && was_top {
             let point_for_position = position_map.point_for_position(text_bounds, event.position);
 
-            match point_for_position.as_valid() {
-                Some(point) => {
-                    update_go_to_definition_link(
-                        editor,
-                        Some(GoToDefinitionTrigger::Text(point)),
-                        modifiers.command,
-                        modifiers.shift,
-                        cx,
-                    );
-                    hover_at(editor, Some(point), cx);
-                    Self::update_visible_cursor(editor, point, position_map, cx);
-                }
-                None => {
-                    update_inlay_link_and_hover_points(
-                        &position_map.snapshot,
-                        point_for_position,
-                        editor,
-                        modifiers.command,
-                        modifiers.shift,
-                        cx,
-                    );
-                }
+            editor.update_hovered_link(point_for_position, &position_map.snapshot, modifiers, cx);
+
+            if let Some(point) = point_for_position.as_valid() {
+                hover_at(editor, Some(point), cx);
+                Self::update_visible_cursor(editor, point, position_map, cx);
             }
         } else {
-            update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx);
+            editor.hide_hovered_link(cx);
             hover_at(editor, None, cx);
             if gutter_hovered && was_top {
                 cx.stop_propagation();
@@ -925,13 +904,13 @@ impl EditorElement {
                     if self
                         .editor
                         .read(cx)
-                        .link_go_to_definition_state
-                        .definitions
-                        .is_empty()
+                        .hovered_link_state
+                        .as_ref()
+                        .is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
                     {
-                        cx.set_cursor_style(CursorStyle::IBeam);
-                    } else {
                         cx.set_cursor_style(CursorStyle::PointingHand);
+                    } else {
+                        cx.set_cursor_style(CursorStyle::IBeam);
                     }
                 }
 
@@ -1094,6 +1073,7 @@ impl EditorElement {
                                                         font: self.style.text.font(),
                                                         color: self.style.background,
                                                         background_color: None,
+                                                        strikethrough: None,
                                                         underline: None,
                                                     }],
                                                 )
@@ -1477,6 +1457,64 @@ impl EditorElement {
                 }
             }
 
+            if layout.is_singleton && scrollbar_settings.diagnostics {
+                let max_point = layout
+                    .position_map
+                    .snapshot
+                    .display_snapshot
+                    .buffer_snapshot
+                    .max_point();
+
+                let diagnostics = layout
+                    .position_map
+                    .snapshot
+                    .buffer_snapshot
+                    .diagnostics_in_range::<_, Point>(Point::zero()..max_point, false)
+                    // We want to sort by severity, in order to paint the most severe diagnostics last.
+                    .sorted_by_key(|diagnostic| std::cmp::Reverse(diagnostic.diagnostic.severity));
+
+                for diagnostic in diagnostics {
+                    let start_display = diagnostic
+                        .range
+                        .start
+                        .to_display_point(&layout.position_map.snapshot.display_snapshot);
+                    let end_display = diagnostic
+                        .range
+                        .end
+                        .to_display_point(&layout.position_map.snapshot.display_snapshot);
+                    let start_y = y_for_row(start_display.row() as f32);
+                    let mut end_y = if diagnostic.range.start == diagnostic.range.end {
+                        y_for_row((end_display.row() + 1) as f32)
+                    } else {
+                        y_for_row((end_display.row()) as f32)
+                    };
+
+                    if end_y - start_y < px(1.) {
+                        end_y = start_y + px(1.);
+                    }
+                    let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
+
+                    let color = match diagnostic.diagnostic.severity {
+                        DiagnosticSeverity::ERROR => cx.theme().status().error,
+                        DiagnosticSeverity::WARNING => cx.theme().status().warning,
+                        DiagnosticSeverity::INFORMATION => cx.theme().status().info,
+                        _ => cx.theme().status().hint,
+                    };
+                    cx.paint_quad(quad(
+                        bounds,
+                        Corners::default(),
+                        color,
+                        Edges {
+                            top: Pixels::ZERO,
+                            right: px(1.),
+                            bottom: Pixels::ZERO,
+                            left: px(1.),
+                        },
+                        cx.theme().colors().scrollbar_thumb_border,
+                    ));
+                }
+            }
+
             cx.paint_quad(quad(
                 thumb_bounds,
                 Corners::default(),
@@ -1676,6 +1714,7 @@ impl EditorElement {
                     color: Hsla::default(),
                     background_color: None,
                     underline: None,
+                    strikethrough: None,
                 }],
             )
             .unwrap();
@@ -1812,6 +1851,7 @@ impl EditorElement {
                         color,
                         background_color: None,
                         underline: None,
+                        strikethrough: None,
                     };
                     let shaped_line = cx
                         .text_system()
@@ -1869,6 +1909,7 @@ impl EditorElement {
                         color: placeholder_color,
                         background_color: None,
                         underline: Default::default(),
+                        strikethrough: None,
                     };
                     cx.text_system()
                         .shape_line(line.to_string().into(), font_size, &[run])
@@ -2106,6 +2147,9 @@ impl EditorElement {
                     // Symbols Selections
                     (is_singleton && scrollbar_settings.symbols_selections && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
                     ||
+                    // Diagnostics
+                    (is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics())
+                    ||
                     // Scrollmanager
                     editor.scroll_manager.scrollbars_visible()
                 }
@@ -2281,6 +2325,7 @@ impl EditorElement {
                         color: cx.theme().colors().editor_invisible,
                         background_color: None,
                         underline: None,
+                        strikethrough: None,
                     }],
                 )
                 .unwrap();
@@ -2295,6 +2340,7 @@ impl EditorElement {
                         color: cx.theme().colors().editor_invisible,
                         background_color: None,
                         underline: None,
+                        strikethrough: None,
                     }],
                 )
                 .unwrap();
@@ -2828,6 +2874,7 @@ impl LineWithInvisibles {
                         color: text_style.color,
                         background_color: text_style.background_color,
                         underline: text_style.underline,
+                        strikethrough: text_style.strikethrough,
                     });
 
                     if editor_mode == EditorMode::Full {
@@ -3039,9 +3086,9 @@ impl Element for EditorElement {
                     let key_context = self.editor.read(cx).key_context(cx);
                     cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| {
                         self.register_actions(cx);
-                        self.register_key_listeners(cx);
 
                         cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
+                            self.register_key_listeners(cx, text_bounds, &layout);
                             cx.handle_input(
                                 &focus_handle,
                                 ElementInputHandler::new(bounds, self.editor.clone()),
@@ -3158,16 +3205,6 @@ pub struct PointForPosition {
 }
 
 impl PointForPosition {
-    #[cfg(test)]
-    pub fn valid(valid: DisplayPoint) -> Self {
-        Self {
-            previous_valid: valid,
-            next_valid: valid,
-            exact_unclipped: valid,
-            column_overshoot_after_line_end: 0,
-        }
-    }
-
     pub fn as_valid(&self) -> Option<DisplayPoint> {
         if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
             Some(self.previous_valid)
@@ -3251,6 +3288,7 @@ fn layout_line(
             color: Hsla::default(),
             background_color: None,
             underline: None,
+            strikethrough: None,
         }],
     )
 }

crates/editor/src/link_go_to_definition.rs β†’ crates/editor/src/hover_links.rs πŸ”—

@@ -1,12 +1,11 @@
 use crate::{
-    display_map::DisplaySnapshot,
     element::PointForPosition,
     hover_popover::{self, InlayHover},
-    Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId,
-    SelectPhase,
+    Anchor, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId, SelectPhase,
 };
-use gpui::{px, Task, ViewContext};
+use gpui::{px, AsyncWindowContext, Model, Modifiers, Task, ViewContext};
 use language::{Bias, ToOffset};
+use linkify::{LinkFinder, LinkKind};
 use lsp::LanguageServerId;
 use project::{
     HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink,
@@ -16,12 +15,12 @@ use std::ops::Range;
 use theme::ActiveTheme as _;
 use util::TryFutureExt;
 
-#[derive(Debug, Default)]
-pub struct LinkGoToDefinitionState {
-    pub last_trigger_point: Option<TriggerPoint>,
+#[derive(Debug)]
+pub struct HoveredLinkState {
+    pub last_trigger_point: TriggerPoint,
+    pub preferred_kind: LinkDefinitionKind,
     pub symbol_range: Option<RangeInEditor>,
-    pub kind: Option<LinkDefinitionKind>,
-    pub definitions: Vec<GoToDefinitionLink>,
+    pub links: Vec<HoverLink>,
     pub task: Option<Task<Option<()>>>,
 }
 
@@ -56,14 +55,9 @@ impl RangeInEditor {
     }
 }
 
-#[derive(Debug)]
-pub enum GoToDefinitionTrigger {
-    Text(DisplayPoint),
-    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
-}
-
 #[derive(Debug, Clone)]
-pub enum GoToDefinitionLink {
+pub enum HoverLink {
+    Url(String),
     Text(LocationLink),
     InlayHint(lsp::Location, LanguageServerId),
 }
@@ -75,26 +69,13 @@ pub(crate) struct InlayHighlight {
     pub range: Range<usize>,
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub enum TriggerPoint {
     Text(Anchor),
     InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
 }
 
 impl TriggerPoint {
-    pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind {
-        match self {
-            TriggerPoint::Text(_) => {
-                if shift {
-                    LinkDefinitionKind::Type
-                } else {
-                    LinkDefinitionKind::Symbol
-                }
-            }
-            TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type,
-        }
-    }
-
     fn anchor(&self) -> &Anchor {
         match self {
             TriggerPoint::Text(anchor) => anchor,
@@ -103,69 +84,88 @@ impl TriggerPoint {
     }
 }
 
-pub fn update_go_to_definition_link(
-    editor: &mut Editor,
-    origin: Option<GoToDefinitionTrigger>,
-    cmd_held: bool,
-    shift_held: bool,
-    cx: &mut ViewContext<Editor>,
-) {
-    let pending_nonempty_selection = editor.has_pending_nonempty_selection();
-
-    // Store new mouse point as an anchor
-    let snapshot = editor.snapshot(cx);
-    let trigger_point = match origin {
-        Some(GoToDefinitionTrigger::Text(p)) => {
-            Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before(
-                p.to_offset(&snapshot.display_snapshot, Bias::Left),
-            )))
-        }
-        Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => {
-            Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id))
+impl Editor {
+    pub(crate) fn update_hovered_link(
+        &mut self,
+        point_for_position: PointForPosition,
+        snapshot: &EditorSnapshot,
+        modifiers: Modifiers,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if !modifiers.command || self.has_pending_selection() {
+            self.hide_hovered_link(cx);
+            return;
         }
-        None => None,
-    };
 
-    // If the new point is the same as the previously stored one, return early
-    if let (Some(a), Some(b)) = (
-        &trigger_point,
-        &editor.link_go_to_definition_state.last_trigger_point,
-    ) {
-        match (a, b) {
-            (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => {
-                if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() {
-                    return;
-                }
+        match point_for_position.as_valid() {
+            Some(point) => {
+                let trigger_point = TriggerPoint::Text(
+                    snapshot
+                        .buffer_snapshot
+                        .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left)),
+                );
+
+                show_link_definition(modifiers.shift, self, trigger_point, snapshot, cx);
             }
-            (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => {
-                if range_a == range_b {
-                    return;
-                }
+            None => {
+                update_inlay_link_and_hover_points(
+                    &snapshot,
+                    point_for_position,
+                    self,
+                    modifiers.command,
+                    modifiers.shift,
+                    cx,
+                );
             }
-            _ => {}
         }
     }
 
-    editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone();
-
-    if pending_nonempty_selection {
-        hide_link_definition(editor, cx);
-        return;
+    pub(crate) fn hide_hovered_link(&mut self, cx: &mut ViewContext<Self>) {
+        self.hovered_link_state.take();
+        self.clear_highlights::<HoveredLinkState>(cx);
     }
 
-    if cmd_held {
-        if let Some(trigger_point) = trigger_point {
-            let kind = trigger_point.definition_kind(shift_held);
-            show_link_definition(kind, editor, trigger_point, snapshot, cx);
-            return;
+    pub(crate) fn handle_click_hovered_link(
+        &mut self,
+        point: PointForPosition,
+        modifiers: Modifiers,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        if let Some(hovered_link_state) = self.hovered_link_state.take() {
+            self.hide_hovered_link(cx);
+            if !hovered_link_state.links.is_empty() {
+                if !self.focus_handle.is_focused(cx) {
+                    cx.focus(&self.focus_handle);
+                }
+
+                self.navigate_to_hover_links(hovered_link_state.links, modifiers.alt, cx);
+                return;
+            }
         }
-    }
 
-    hide_link_definition(editor, cx);
+        // We don't have the correct kind of link cached, set the selection on
+        // click and immediately trigger GoToDefinition.
+        self.select(
+            SelectPhase::Begin {
+                position: point.next_valid,
+                add: false,
+                click_count: 1,
+            },
+            cx,
+        );
+
+        if point.as_valid().is_some() {
+            if modifiers.shift {
+                self.go_to_type_definition(&GoToTypeDefinition, cx)
+            } else {
+                self.go_to_definition(&GoToDefinition, cx)
+            }
+        }
+    }
 }
 
 pub fn update_inlay_link_and_hover_points(
-    snapshot: &DisplaySnapshot,
+    snapshot: &EditorSnapshot,
     point_for_position: PointForPosition,
     editor: &mut Editor,
     cmd_held: bool,
@@ -306,18 +306,20 @@ pub fn update_inlay_link_and_hover_points(
                                     if let Some((language_server_id, location)) =
                                         hovered_hint_part.location
                                     {
-                                        go_to_definition_updated = true;
-                                        update_go_to_definition_link(
-                                            editor,
-                                            Some(GoToDefinitionTrigger::InlayHint(
-                                                highlight,
-                                                location,
-                                                language_server_id,
-                                            )),
-                                            cmd_held,
-                                            shift_held,
-                                            cx,
-                                        );
+                                        if cmd_held && !editor.has_pending_nonempty_selection() {
+                                            go_to_definition_updated = true;
+                                            show_link_definition(
+                                                shift_held,
+                                                editor,
+                                                TriggerPoint::InlayHint(
+                                                    highlight,
+                                                    location,
+                                                    language_server_id,
+                                                ),
+                                                snapshot,
+                                                cx,
+                                            );
+                                        }
                                     }
                                 }
                             }
@@ -330,7 +332,7 @@ pub fn update_inlay_link_and_hover_points(
     }
 
     if !go_to_definition_updated {
-        update_go_to_definition_link(editor, None, cmd_held, shift_held, cx);
+        editor.hide_hovered_link(cx)
     }
     if !hover_updated {
         hover_popover::hover_at(editor, None, cx);
@@ -344,113 +346,148 @@ pub enum LinkDefinitionKind {
 }
 
 pub fn show_link_definition(
-    definition_kind: LinkDefinitionKind,
+    shift_held: bool,
     editor: &mut Editor,
     trigger_point: TriggerPoint,
-    snapshot: EditorSnapshot,
+    snapshot: &EditorSnapshot,
     cx: &mut ViewContext<Editor>,
 ) {
-    let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind);
-    if !same_kind {
-        hide_link_definition(editor, cx);
-    }
+    let preferred_kind = match trigger_point {
+        TriggerPoint::Text(_) if !shift_held => LinkDefinitionKind::Symbol,
+        _ => LinkDefinitionKind::Type,
+    };
+
+    let (mut hovered_link_state, is_cached) =
+        if let Some(existing) = editor.hovered_link_state.take() {
+            (existing, true)
+        } else {
+            (
+                HoveredLinkState {
+                    last_trigger_point: trigger_point.clone(),
+                    symbol_range: None,
+                    preferred_kind,
+                    links: vec![],
+                    task: None,
+                },
+                false,
+            )
+        };
 
     if editor.pending_rename.is_some() {
         return;
     }
 
     let trigger_anchor = trigger_point.anchor();
-    let (buffer, buffer_position) = if let Some(output) = editor
+    let Some((buffer, buffer_position)) = editor
         .buffer
         .read(cx)
         .text_anchor_for_position(trigger_anchor.clone(), cx)
-    {
-        output
-    } else {
+    else {
         return;
     };
 
-    let excerpt_id = if let Some((excerpt_id, _, _)) = editor
+    let Some((excerpt_id, _, _)) = editor
         .buffer()
         .read(cx)
         .excerpt_containing(trigger_anchor.clone(), cx)
-    {
-        excerpt_id
-    } else {
-        return;
-    };
-
-    let project = if let Some(project) = editor.project.clone() {
-        project
-    } else {
+    else {
         return;
     };
 
-    // Don't request again if the location is within the symbol region of a previous request with the same kind
-    if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range {
-        if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) {
+    let same_kind = hovered_link_state.preferred_kind == preferred_kind
+        || hovered_link_state
+            .links
+            .first()
+            .is_some_and(|d| matches!(d, HoverLink::Url(_)));
+
+    if same_kind {
+        if is_cached && (&hovered_link_state.last_trigger_point == &trigger_point)
+            || hovered_link_state
+                .symbol_range
+                .as_ref()
+                .is_some_and(|symbol_range| {
+                    symbol_range.point_within_range(&trigger_point, &snapshot)
+                })
+        {
+            editor.hovered_link_state = Some(hovered_link_state);
             return;
         }
+    } else {
+        editor.hide_hovered_link(cx)
     }
+    let project = editor.project.clone();
 
-    let task = cx.spawn(|this, mut cx| {
+    let snapshot = snapshot.buffer_snapshot.clone();
+    hovered_link_state.task = Some(cx.spawn(|this, mut cx| {
         async move {
             let result = match &trigger_point {
                 TriggerPoint::Text(_) => {
-                    // query the LSP for definition info
-                    project
-                        .update(&mut cx, |project, cx| match definition_kind {
-                            LinkDefinitionKind::Symbol => {
-                                project.definition(&buffer, buffer_position, cx)
-                            }
-
-                            LinkDefinitionKind::Type => {
-                                project.type_definition(&buffer, buffer_position, cx)
-                            }
-                        })?
-                        .await
-                        .ok()
-                        .map(|definition_result| {
+                    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);
                             (
-                                definition_result.iter().find_map(|link| {
-                                    link.origin.as_ref().map(|origin| {
-                                        let start = snapshot.buffer_snapshot.anchor_in_excerpt(
-                                            excerpt_id.clone(),
-                                            origin.range.start,
-                                        );
-                                        let end = snapshot.buffer_snapshot.anchor_in_excerpt(
-                                            excerpt_id.clone(),
-                                            origin.range.end,
-                                        );
-                                        RangeInEditor::Text(start..end)
-                                    })
-                                }),
-                                definition_result
-                                    .into_iter()
-                                    .map(GoToDefinitionLink::Text)
-                                    .collect(),
+                                Some(RangeInEditor::Text(start..end)),
+                                vec![HoverLink::Url(url)],
                             )
                         })
+                        .ok()
+                    } else if let Some(project) = project {
+                        // query the LSP for definition info
+                        project
+                            .update(&mut cx, |project, cx| match preferred_kind {
+                                LinkDefinitionKind::Symbol => {
+                                    project.definition(&buffer, buffer_position, cx)
+                                }
+
+                                LinkDefinitionKind::Type => {
+                                    project.type_definition(&buffer, buffer_position, cx)
+                                }
+                            })?
+                            .await
+                            .ok()
+                            .map(|definition_result| {
+                                (
+                                    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,
+                                            );
+                                            RangeInEditor::Text(start..end)
+                                        })
+                                    }),
+                                    definition_result.into_iter().map(HoverLink::Text).collect(),
+                                )
+                            })
+                    } else {
+                        None
+                    }
                 }
                 TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
                     Some(RangeInEditor::Inlay(highlight.clone())),
-                    vec![GoToDefinitionLink::InlayHint(
-                        lsp_location.clone(),
-                        *server_id,
-                    )],
+                    vec![HoverLink::InlayHint(lsp_location.clone(), *server_id)],
                 )),
             };
 
             this.update(&mut cx, |this, cx| {
                 // Clear any existing highlights
-                this.clear_highlights::<LinkGoToDefinitionState>(cx);
-                this.link_go_to_definition_state.kind = Some(definition_kind);
-                this.link_go_to_definition_state.symbol_range = result
+                this.clear_highlights::<HoveredLinkState>(cx);
+                let Some(hovered_link_state) = this.hovered_link_state.as_mut() else {
+                    return;
+                };
+                hovered_link_state.preferred_kind = preferred_kind;
+                hovered_link_state.symbol_range = result
                     .as_ref()
                     .and_then(|(symbol_range, _)| symbol_range.clone());
 
                 if let Some((symbol_range, definitions)) = result {
-                    this.link_go_to_definition_state.definitions = definitions.clone();
+                    hovered_link_state.links = definitions.clone();
 
                     let buffer_snapshot = buffer.read(cx).snapshot();
 
@@ -459,7 +496,7 @@ pub fn show_link_definition(
                     let any_definition_does_not_contain_current_location =
                         definitions.iter().any(|definition| {
                             match &definition {
-                                GoToDefinitionLink::Text(link) => {
+                                HoverLink::Text(link) => {
                                     if link.target.buffer == buffer {
                                         let range = &link.target.range;
                                         // Expand range by one character as lsp definition ranges include positions adjacent
@@ -481,7 +518,8 @@ pub fn show_link_definition(
                                         true
                                     }
                                 }
-                                GoToDefinitionLink::InlayHint(_, _) => true,
+                                HoverLink::InlayHint(_, _) => true,
+                                HoverLink::Url(_) => true,
                             }
                         });
 
@@ -497,7 +535,6 @@ pub fn show_link_definition(
                         let highlight_range =
                             symbol_range.unwrap_or_else(|| match &trigger_point {
                                 TriggerPoint::Text(trigger_anchor) => {
-                                    let snapshot = &snapshot.buffer_snapshot;
                                     // If no symbol range returned from language server, use the surrounding word.
                                     let (offset_range, _) =
                                         snapshot.surrounding_word(*trigger_anchor);
@@ -512,21 +549,14 @@ pub fn show_link_definition(
                             });
 
                         match highlight_range {
-                            RangeInEditor::Text(text_range) => this
-                                .highlight_text::<LinkGoToDefinitionState>(
-                                    vec![text_range],
-                                    style,
-                                    cx,
-                                ),
+                            RangeInEditor::Text(text_range) => {
+                                this.highlight_text::<HoveredLinkState>(vec![text_range], style, cx)
+                            }
                             RangeInEditor::Inlay(highlight) => this
-                                .highlight_inlays::<LinkGoToDefinitionState>(
-                                    vec![highlight],
-                                    style,
-                                    cx,
-                                ),
+                                .highlight_inlays::<HoveredLinkState>(vec![highlight], style, cx),
                         }
                     } else {
-                        hide_link_definition(this, cx);
+                        this.hide_hovered_link(cx);
                     }
                 }
             })?;
@@ -534,78 +564,68 @@ pub fn show_link_definition(
             Ok::<_, anyhow::Error>(())
         }
         .log_err()
-    });
+    }));
 
-    editor.link_go_to_definition_state.task = Some(task);
+    editor.hovered_link_state = Some(hovered_link_state);
 }
 
-pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
-    if editor.link_go_to_definition_state.symbol_range.is_some()
-        || !editor.link_go_to_definition_state.definitions.is_empty()
-    {
-        editor.link_go_to_definition_state.symbol_range.take();
-        editor.link_go_to_definition_state.definitions.clear();
-        cx.notify();
-    }
-
-    editor.link_go_to_definition_state.task = None;
-
-    editor.clear_highlights::<LinkGoToDefinitionState>(cx);
-}
+fn find_url(
+    buffer: &Model<language::Buffer>,
+    position: text::Anchor,
+    mut cx: AsyncWindowContext,
+) -> Option<(Range<text::Anchor>, String)> {
+    const LIMIT: usize = 2048;
 
-pub fn go_to_fetched_definition(
-    editor: &mut Editor,
-    point: PointForPosition,
-    split: bool,
-    cx: &mut ViewContext<Editor>,
-) {
-    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, split, cx);
-}
+    let Ok(snapshot) = buffer.update(&mut cx, |buffer, _| buffer.snapshot()) else {
+        return None;
+    };
 
-pub fn go_to_fetched_type_definition(
-    editor: &mut Editor,
-    point: PointForPosition,
-    split: bool,
-    cx: &mut ViewContext<Editor>,
-) {
-    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, split, cx);
-}
+    let offset = position.to_offset(&snapshot);
+    let mut token_start = offset;
+    let mut token_end = offset;
+    let mut found_start = false;
+    let mut found_end = false;
 
-fn go_to_fetched_definition_of_kind(
-    kind: LinkDefinitionKind,
-    editor: &mut Editor,
-    point: PointForPosition,
-    split: bool,
-    cx: &mut ViewContext<Editor>,
-) {
-    let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
-    hide_link_definition(editor, cx);
-    let cached_definitions_kind = editor.link_go_to_definition_state.kind;
-
-    let is_correct_kind = cached_definitions_kind == Some(kind);
-    if !cached_definitions.is_empty() && is_correct_kind {
-        if !editor.focus_handle.is_focused(cx) {
-            cx.focus(&editor.focus_handle);
+    for ch in snapshot.reversed_chars_at(offset).take(LIMIT) {
+        if ch.is_whitespace() {
+            found_start = true;
+            break;
         }
+        token_start -= ch.len_utf8();
+    }
+    if !found_start {
+        return None;
+    }
 
-        editor.navigate_to_definitions(cached_definitions, split, cx);
-    } else {
-        editor.select(
-            SelectPhase::Begin {
-                position: point.next_valid,
-                add: false,
-                click_count: 1,
-            },
-            cx,
-        );
+    for ch in snapshot
+        .chars_at(offset)
+        .take(LIMIT - (offset - token_start))
+    {
+        if ch.is_whitespace() {
+            found_end = true;
+            break;
+        }
+        token_end += ch.len_utf8();
+    }
+    if !found_end {
+        return None;
+    }
 
-        if point.as_valid().is_some() {
-            match kind {
-                LinkDefinitionKind::Symbol => editor.go_to_definition(&GoToDefinition, cx),
-                LinkDefinitionKind::Type => editor.go_to_type_definition(&GoToTypeDefinition, cx),
-            }
+    let mut finder = LinkFinder::new();
+    finder.kinds(&[LinkKind::Url]);
+    let input = snapshot
+        .text_for_range(token_start..token_end)
+        .collect::<String>();
+
+    let relative_offset = offset - token_start;
+    for link in finder.links(&input) {
+        if link.start() <= relative_offset && link.end() >= relative_offset {
+            let range = snapshot.anchor_before(token_start + link.start())
+                ..snapshot.anchor_after(token_start + link.end());
+            return Some((range, link.as_str().to_string()));
         }
     }
+    None
 }
 
 #[cfg(test)]
@@ -616,16 +636,18 @@ mod tests {
         editor_tests::init_test,
         inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
         test::editor_lsp_test_context::EditorLspTestContext,
+        DisplayPoint,
     };
     use futures::StreamExt;
-    use gpui::{Modifiers, ModifiersChangedEvent};
+    use gpui::Modifiers;
     use indoc::indoc;
     use language::language_settings::InlayHintSettings;
     use lsp::request::{GotoDefinition, GotoTypeDefinition};
     use util::assert_set_eq;
+    use workspace::item::Item;
 
     #[gpui::test]
-    async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
+    async fn test_hover_type_links(cx: &mut gpui::TestAppContext) {
         init_test(cx, |_| {});
 
         let mut cx = EditorLspTestContext::new_rust(
@@ -642,12 +664,9 @@ mod tests {
             struct A;
             let vˇariable = A;
         "});
+        let screen_coord = cx.editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 
         // Basic hold cmd+shift, expect highlight in region if response contains type definition
-        let hover_point = cx.display_point(indoc! {"
-            struct A;
-            let vˇariable = A;
-        "});
         let symbol_range = cx.lsp_range(indoc! {"
             struct A;
             let Β«variableΒ» = A;
@@ -657,6 +676,8 @@ mod tests {
             let variable = A;
         "});
 
+        cx.run_until_parked();
+
         let mut requests =
             cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
                 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
@@ -669,70 +690,28 @@ mod tests {
                 ])))
             });
 
-        // Press cmd+shift to trigger highlight
-        cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                Some(GoToDefinitionTrigger::Text(hover_point)),
-                true,
-                true,
-                cx,
-            );
-        });
+        cx.cx
+            .cx
+            .simulate_mouse_move(screen_coord.unwrap(), Modifiers::command_shift());
+
         requests.next().await;
-        cx.background_executor.run_until_parked();
-        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+        cx.run_until_parked();
+        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
             struct A;
             let Β«variableΒ» = A;
         "});
 
-        // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
-        cx.update_editor(|editor, cx| {
-            crate::element::EditorElement::modifiers_changed(
-                editor,
-                &ModifiersChangedEvent {
-                    modifiers: Modifiers {
-                        command: true,
-                        ..Default::default()
-                    },
-                    ..Default::default()
-                },
-                cx,
-            );
-        });
+        cx.simulate_modifiers_change(Modifiers::command());
+        cx.run_until_parked();
         // Assert no link highlights
-        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-            struct A;
-            let variable = A;
-        "});
-
-        // Cmd+shift click without existing definition requests and jumps
-        let hover_point = cx.display_point(indoc! {"
+        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
             struct A;
-            let vˇariable = A;
-        "});
-        let target_range = cx.lsp_range(indoc! {"
-            struct Β«AΒ»;
             let variable = A;
         "});
 
-        let mut requests =
-            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
-                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
-                    lsp::LocationLink {
-                        origin_selection_range: None,
-                        target_uri: url,
-                        target_range,
-                        target_selection_range: target_range,
-                    },
-                ])))
-            });
-
-        cx.update_editor(|editor, cx| {
-            go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
-        });
-        requests.next().await;
-        cx.background_executor.run_until_parked();
+        cx.cx
+            .cx
+            .simulate_click(screen_coord.unwrap(), Modifiers::command_shift());
 
         cx.assert_editor_state(indoc! {"
             struct «Aˇ»;
@@ -741,7 +720,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
+    async fn test_hover_links(cx: &mut gpui::TestAppContext) {
         init_test(cx, |_| {});
 
         let mut cx = EditorLspTestContext::new_rust(
@@ -759,7 +738,7 @@ mod tests {
             "});
 
         // Basic hold cmd, expect highlight in region if response contains definition
-        let hover_point = cx.display_point(indoc! {"
+        let hover_point = cx.pixel_position(indoc! {"
                 fn test() { do_wˇork(); }
                 fn do_work() { test(); }
             "});
@@ -783,65 +762,42 @@ mod tests {
             ])))
         });
 
-        cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                Some(GoToDefinitionTrigger::Text(hover_point)),
-                true,
-                false,
-                cx,
-            );
-        });
+        cx.simulate_mouse_move(hover_point, Modifiers::command());
         requests.next().await;
         cx.background_executor.run_until_parked();
-        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                 fn test() { Β«do_workΒ»(); }
                 fn do_work() { test(); }
             "});
 
         // Unpress cmd causes highlight to go away
-        cx.update_editor(|editor, cx| {
-            crate::element::EditorElement::modifiers_changed(editor, &Default::default(), cx);
-        });
-
-        // Assert no link highlights
-        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+        cx.simulate_modifiers_change(Modifiers::none());
+        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                 fn test() { do_work(); }
                 fn do_work() { test(); }
             "});
 
-        // Response without source range still highlights word
-        cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
         let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
             Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
                 lsp::LocationLink {
-                    // No origin range
-                    origin_selection_range: None,
+                    origin_selection_range: Some(symbol_range),
                     target_uri: url.clone(),
                     target_range,
                     target_selection_range: target_range,
                 },
             ])))
         });
-        cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                Some(GoToDefinitionTrigger::Text(hover_point)),
-                true,
-                false,
-                cx,
-            );
-        });
+
+        cx.simulate_mouse_move(hover_point, Modifiers::command());
         requests.next().await;
         cx.background_executor.run_until_parked();
-
-        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                 fn test() { Β«do_workΒ»(); }
                 fn do_work() { test(); }
             "});
 
         // Moving mouse to location with no response dismisses highlight
-        let hover_point = cx.display_point(indoc! {"
+        let hover_point = cx.pixel_position(indoc! {"
                 fˇn test() { do_work(); }
                 fn do_work() { test(); }
             "});
@@ -851,42 +807,26 @@ mod tests {
                 // No definitions returned
                 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
             });
-        cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                Some(GoToDefinitionTrigger::Text(hover_point)),
-                true,
-                false,
-                cx,
-            );
-        });
+        cx.simulate_mouse_move(hover_point, Modifiers::command());
+
         requests.next().await;
         cx.background_executor.run_until_parked();
 
         // Assert no link highlights
-        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                 fn test() { do_work(); }
                 fn do_work() { test(); }
             "});
 
-        // Move mouse without cmd and then pressing cmd triggers highlight
-        let hover_point = cx.display_point(indoc! {"
+        // // Move mouse without cmd and then pressing cmd triggers highlight
+        let hover_point = cx.pixel_position(indoc! {"
                 fn test() { do_work(); }
                 fn do_work() { teˇst(); }
             "});
-        cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                Some(GoToDefinitionTrigger::Text(hover_point)),
-                false,
-                false,
-                cx,
-            );
-        });
-        cx.background_executor.run_until_parked();
+        cx.simulate_mouse_move(hover_point, Modifiers::none());
 
         // Assert no link highlights
-        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                 fn test() { do_work(); }
                 fn do_work() { test(); }
             "});
@@ -910,73 +850,44 @@ mod tests {
                 },
             ])))
         });
-        cx.update_editor(|editor, cx| {
-            crate::element::EditorElement::modifiers_changed(
-                editor,
-                &ModifiersChangedEvent {
-                    modifiers: Modifiers {
-                        command: true,
-                        ..Default::default()
-                    },
-                },
-                cx,
-            );
-        });
+
+        cx.simulate_modifiers_change(Modifiers::command());
+
         requests.next().await;
         cx.background_executor.run_until_parked();
 
-        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                 fn test() { do_work(); }
                 fn do_work() { Β«testΒ»(); }
             "});
 
-        cx.cx.cx.deactivate_window();
-        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+        cx.deactivate_window();
+        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                 fn test() { do_work(); }
                 fn do_work() { test(); }
             "});
 
-        // Moving the mouse restores the highlights.
-        cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                Some(GoToDefinitionTrigger::Text(hover_point)),
-                true,
-                false,
-                cx,
-            );
-        });
+        cx.simulate_mouse_move(hover_point, Modifiers::command());
         cx.background_executor.run_until_parked();
-        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                 fn test() { do_work(); }
                 fn do_work() { Β«testΒ»(); }
             "});
 
         // Moving again within the same symbol range doesn't re-request
-        let hover_point = cx.display_point(indoc! {"
+        let hover_point = cx.pixel_position(indoc! {"
                 fn test() { do_work(); }
                 fn do_work() { tesˇt(); }
             "});
-        cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(
-                editor,
-                Some(GoToDefinitionTrigger::Text(hover_point)),
-                true,
-                false,
-                cx,
-            );
-        });
+        cx.simulate_mouse_move(hover_point, Modifiers::command());
         cx.background_executor.run_until_parked();
-        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                 fn test() { do_work(); }
                 fn do_work() { Β«testΒ»(); }
             "});
 
         // Cmd click with existing definition doesn't re-request and dismisses highlight
-        cx.update_editor(|editor, cx| {
-            go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
-        });
-        // Assert selection moved to to definition
+        cx.simulate_click(hover_point, Modifiers::command());
         cx.lsp
             .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
                 // Empty definition response to make sure we aren't hitting the lsp and using

crates/editor/src/hover_popover.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{
     display_map::{InlayOffset, ToDisplayPoint},
-    link_go_to_definition::{InlayHighlight, RangeInEditor},
+    hover_links::{InlayHighlight, RangeInEditor},
     Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
     ExcerptId, Hover, RangeToAnchorExt,
 };
@@ -605,8 +605,8 @@ mod tests {
     use crate::{
         editor_tests::init_test,
         element::PointForPosition,
+        hover_links::update_inlay_link_and_hover_points,
         inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
-        link_go_to_definition::update_inlay_link_and_hover_points,
         test::editor_lsp_test_context::EditorLspTestContext,
         InlayId,
     };

crates/editor/src/inlay_hint_cache.rs πŸ”—

@@ -19,7 +19,7 @@ use crate::{
 use anyhow::Context;
 use clock::Global;
 use futures::future;
-use gpui::{Model, ModelContext, Task, ViewContext};
+use gpui::{AsyncWindowContext, Model, ModelContext, Task, ViewContext};
 use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
 use parking_lot::RwLock;
 use project::{InlayHint, ResolveState};
@@ -29,7 +29,7 @@ use language::language_settings::InlayHintSettings;
 use smol::lock::Semaphore;
 use sum_tree::Bias;
 use text::{BufferId, ToOffset, ToPoint};
-use util::post_inc;
+use util::{post_inc, ResultExt};
 
 pub struct InlayHintCache {
     hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
@@ -78,6 +78,7 @@ pub(super) enum InvalidationStrategy {
 /// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes.
 /// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead.
 /// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.
+#[derive(Debug, Default)]
 pub(super) struct InlaySplice {
     pub to_remove: Vec<InlayId>,
     pub to_insert: Vec<Inlay>,
@@ -86,7 +87,7 @@ pub(super) struct InlaySplice {
 #[derive(Debug)]
 struct ExcerptHintsUpdate {
     excerpt_id: ExcerptId,
-    remove_from_visible: Vec<InlayId>,
+    remove_from_visible: HashSet<InlayId>,
     remove_from_cache: HashSet<InlayId>,
     add_to_cache: Vec<InlayHint>,
 }
@@ -619,7 +620,6 @@ fn spawn_new_update_tasks(
     update_cache_version: usize,
     cx: &mut ViewContext<'_, Editor>,
 ) {
-    let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
     for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in
         excerpts_to_query
     {
@@ -636,8 +636,7 @@ fn spawn_new_update_tasks(
             continue;
         }
 
-        let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
-        if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+        if let Some(cached_excerpt_hints) = editor.inlay_hint_cache.hints.get(&excerpt_id) {
             let cached_excerpt_hints = cached_excerpt_hints.read();
             let cached_buffer_version = &cached_excerpt_hints.buffer_version;
             if cached_excerpt_hints.version > update_cache_version
@@ -647,20 +646,15 @@ fn spawn_new_update_tasks(
             }
         };
 
-        let (multi_buffer_snapshot, Some(query_ranges)) =
-            editor.buffer.update(cx, |multi_buffer, cx| {
-                (
-                    multi_buffer.snapshot(cx),
-                    determine_query_ranges(
-                        multi_buffer,
-                        excerpt_id,
-                        &excerpt_buffer,
-                        excerpt_visible_range,
-                        cx,
-                    ),
-                )
-            })
-        else {
+        let Some(query_ranges) = editor.buffer.update(cx, |multi_buffer, cx| {
+            determine_query_ranges(
+                multi_buffer,
+                excerpt_id,
+                &excerpt_buffer,
+                excerpt_visible_range,
+                cx,
+            )
+        }) else {
             return;
         };
         let query = ExcerptQuery {
@@ -671,18 +665,8 @@ fn spawn_new_update_tasks(
             reason,
         };
 
-        let new_update_task = |query_ranges| {
-            new_update_task(
-                query,
-                query_ranges,
-                multi_buffer_snapshot,
-                buffer_snapshot.clone(),
-                Arc::clone(&visible_hints),
-                cached_excerpt_hints,
-                Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter),
-                cx,
-            )
-        };
+        let mut new_update_task =
+            |query_ranges| new_update_task(query, query_ranges, excerpt_buffer.clone(), cx);
 
         match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
             hash_map::Entry::Occupied(mut o) => {
@@ -790,62 +774,55 @@ const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400;
 fn new_update_task(
     query: ExcerptQuery,
     query_ranges: QueryRanges,
-    multi_buffer_snapshot: MultiBufferSnapshot,
-    buffer_snapshot: BufferSnapshot,
-    visible_hints: Arc<Vec<Inlay>>,
-    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
-    lsp_request_limiter: Arc<Semaphore>,
+    excerpt_buffer: Model<Buffer>,
     cx: &mut ViewContext<'_, Editor>,
 ) -> Task<()> {
-    cx.spawn(|editor, mut cx| async move {
-        let closure_cx = cx.clone();
-        let fetch_and_update_hints = |invalidate, range| {
-            fetch_and_update_hints(
-                editor.clone(),
-                multi_buffer_snapshot.clone(),
-                buffer_snapshot.clone(),
-                Arc::clone(&visible_hints),
-                cached_excerpt_hints.as_ref().map(Arc::clone),
-                query,
-                invalidate,
-                range,
-                Arc::clone(&lsp_request_limiter),
-                closure_cx.clone(),
-            )
-        };
-        let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map(
-            |visible_range| async move {
-                (
-                    visible_range.clone(),
-                    fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range)
-                        .await,
-                )
-            },
-        ))
+    cx.spawn(move |editor, mut cx| async move {
+        let visible_range_update_results = future::join_all(
+            query_ranges
+                .visible
+                .into_iter()
+                .filter_map(|visible_range| {
+                    let fetch_task = editor
+                        .update(&mut cx, |_, cx| {
+                            fetch_and_update_hints(
+                                excerpt_buffer.clone(),
+                                query,
+                                visible_range.clone(),
+                                query.invalidate.should_invalidate(),
+                                cx,
+                            )
+                        })
+                        .log_err()?;
+                    Some(async move { (visible_range, fetch_task.await) })
+                }),
+        )
         .await;
 
         let hint_delay = cx.background_executor().timer(Duration::from_millis(
             INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS,
         ));
 
-        let mut query_range_failed = |range: &Range<language::Anchor>, e: anyhow::Error| {
-            log::error!("inlay hint update task for range {range:?} failed: {e:#}");
-            editor
-                .update(&mut cx, |editor, _| {
-                    if let Some(task_ranges) = editor
-                        .inlay_hint_cache
-                        .update_tasks
-                        .get_mut(&query.excerpt_id)
-                    {
-                        task_ranges.invalidate_range(&buffer_snapshot, &range);
-                    }
-                })
-                .ok()
-        };
+        let query_range_failed =
+            |range: &Range<language::Anchor>, e: anyhow::Error, cx: &mut AsyncWindowContext| {
+                log::error!("inlay hint update task for range {range:?} failed: {e:#}");
+                editor
+                    .update(cx, |editor, cx| {
+                        if let Some(task_ranges) = editor
+                            .inlay_hint_cache
+                            .update_tasks
+                            .get_mut(&query.excerpt_id)
+                        {
+                            let buffer_snapshot = excerpt_buffer.read(cx).snapshot();
+                            task_ranges.invalidate_range(&buffer_snapshot, &range);
+                        }
+                    })
+                    .ok()
+            };
 
         for (range, result) in visible_range_update_results {
             if let Err(e) = result {
-                query_range_failed(&range, e);
+                query_range_failed(&range, e, &mut cx);
             }
         }
 
@@ -855,149 +832,171 @@ fn new_update_task(
                 .before_visible
                 .into_iter()
                 .chain(query_ranges.after_visible.into_iter())
-                .map(|invisible_range| async move {
-                    (
-                        invisible_range.clone(),
-                        fetch_and_update_hints(false, invisible_range).await,
-                    )
+                .filter_map(|invisible_range| {
+                    let fetch_task = editor
+                        .update(&mut cx, |_, cx| {
+                            fetch_and_update_hints(
+                                excerpt_buffer.clone(),
+                                query,
+                                invisible_range.clone(),
+                                false, // visible screen request already invalidated the entries
+                                cx,
+                            )
+                        })
+                        .log_err()?;
+                    Some(async move { (invisible_range, fetch_task.await) })
                 }),
         )
         .await;
         for (range, result) in invisible_range_update_results {
             if let Err(e) = result {
-                query_range_failed(&range, e);
+                query_range_failed(&range, e, &mut cx);
             }
         }
     })
 }
 
-async fn fetch_and_update_hints(
-    editor: gpui::WeakView<Editor>,
-    multi_buffer_snapshot: MultiBufferSnapshot,
-    buffer_snapshot: BufferSnapshot,
-    visible_hints: Arc<Vec<Inlay>>,
-    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+fn fetch_and_update_hints(
+    excerpt_buffer: Model<Buffer>,
     query: ExcerptQuery,
-    invalidate: bool,
     fetch_range: Range<language::Anchor>,
-    lsp_request_limiter: Arc<Semaphore>,
-    mut cx: gpui::AsyncWindowContext,
-) -> anyhow::Result<()> {
-    let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
-        (None, false)
-    } else {
-        match lsp_request_limiter.try_acquire() {
-            Some(guard) => (Some(guard), false),
-            None => (Some(lsp_request_limiter.acquire().await), true),
-        }
-    };
-    let fetch_range_to_log =
-        fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot);
-    let inlay_hints_fetch_task = editor
-        .update(&mut cx, |editor, cx| {
-            if got_throttled {
-                let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
-                    Some((_, _, current_visible_range)) => {
-                        let visible_offset_length = current_visible_range.len();
-                        let double_visible_range = current_visible_range
-                            .start
-                            .saturating_sub(visible_offset_length)
-                            ..current_visible_range
-                                .end
-                                .saturating_add(visible_offset_length)
-                                .min(buffer_snapshot.len());
-                        !double_visible_range
-                            .contains(&fetch_range.start.to_offset(&buffer_snapshot))
-                            && !double_visible_range
-                                .contains(&fetch_range.end.to_offset(&buffer_snapshot))
-                    },
-                    None => true,
-                };
-                if query_not_around_visible_range {
-                    log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
-                    if let Some(task_ranges) = editor
-                        .inlay_hint_cache
-                        .update_tasks
-                        .get_mut(&query.excerpt_id)
-                    {
-                        task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
+    invalidate: bool,
+    cx: &mut ViewContext<Editor>,
+) -> Task<anyhow::Result<()>> {
+    cx.spawn(|editor, mut cx| async move {
+        let buffer_snapshot = excerpt_buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
+        let (lsp_request_limiter, multi_buffer_snapshot) = editor.update(&mut cx, |editor, cx| {
+            let multi_buffer_snapshot  = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+            let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter);
+            (lsp_request_limiter, multi_buffer_snapshot)
+        })?;
+
+        let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
+            (None, false)
+        } else {
+            match lsp_request_limiter.try_acquire() {
+                Some(guard) => (Some(guard), false),
+                None => (Some(lsp_request_limiter.acquire().await), true),
+            }
+        };
+        let fetch_range_to_log =
+            fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot);
+        let inlay_hints_fetch_task = editor
+            .update(&mut cx, |editor, cx| {
+                if got_throttled {
+                    let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
+                        Some((_, _, current_visible_range)) => {
+                            let visible_offset_length = current_visible_range.len();
+                            let double_visible_range = current_visible_range
+                                .start
+                                .saturating_sub(visible_offset_length)
+                                ..current_visible_range
+                                    .end
+                                    .saturating_add(visible_offset_length)
+                                    .min(buffer_snapshot.len());
+                            !double_visible_range
+                                .contains(&fetch_range.start.to_offset(&buffer_snapshot))
+                                && !double_visible_range
+                                    .contains(&fetch_range.end.to_offset(&buffer_snapshot))
+                        },
+                        None => true,
+                    };
+                    if query_not_around_visible_range {
+                        log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
+                        if let Some(task_ranges) = editor
+                            .inlay_hint_cache
+                            .update_tasks
+                            .get_mut(&query.excerpt_id)
+                        {
+                            task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
+                        }
+                        return None;
                     }
-                    return None;
                 }
-            }
+                editor
+                    .buffer()
+                    .read(cx)
+                    .buffer(query.buffer_id)
+                    .and_then(|buffer| {
+                        let project = editor.project.as_ref()?;
+                        Some(project.update(cx, |project, cx| {
+                            project.inlay_hints(buffer, fetch_range.clone(), cx)
+                        }))
+                    })
+            })
+            .ok()
+            .flatten();
+
+        let cached_excerpt_hints = editor.update(&mut cx, |editor, _| {
             editor
-                .buffer()
-                .read(cx)
-                .buffer(query.buffer_id)
-                .and_then(|buffer| {
-                    let project = editor.project.as_ref()?;
-                    Some(project.update(cx, |project, cx| {
-                        project.inlay_hints(buffer, fetch_range.clone(), cx)
-                    }))
-                })
-        })
-        .ok()
-        .flatten();
-    let new_hints = match inlay_hints_fetch_task {
-        Some(fetch_task) => {
-            log::debug!(
-                "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
-                query_reason = query.reason,
-            );
-            log::trace!(
-                "Currently visible hints: {visible_hints:?}, cached hints present: {}",
-                cached_excerpt_hints.is_some(),
-            );
-            fetch_task.await.context("inlay hint fetch task")?
-        }
-        None => return Ok(()),
-    };
-    drop(lsp_request_guard);
-    log::debug!(
-        "Fetched {} hints for range {fetch_range_to_log:?}",
-        new_hints.len()
-    );
-    log::trace!("Fetched hints: {new_hints:?}");
-
-    let background_task_buffer_snapshot = buffer_snapshot.clone();
-    let background_fetch_range = fetch_range.clone();
-    let new_update = cx
-        .background_executor()
-        .spawn(async move {
-            calculate_hint_updates(
-                query.excerpt_id,
-                invalidate,
-                background_fetch_range,
-                new_hints,
-                &background_task_buffer_snapshot,
-                cached_excerpt_hints,
-                &visible_hints,
-            )
-        })
-        .await;
-    if let Some(new_update) = new_update {
+                .inlay_hint_cache
+                .hints
+                .get(&query.excerpt_id)
+                .cloned()
+        })?;
+
+        let visible_hints = editor.update(&mut cx, |editor, cx| editor.visible_inlay_hints(cx))?;
+        let new_hints = match inlay_hints_fetch_task {
+            Some(fetch_task) => {
+                log::debug!(
+                    "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
+                    query_reason = query.reason,
+                );
+                log::trace!(
+                    "Currently visible hints: {visible_hints:?}, cached hints present: {}",
+                    cached_excerpt_hints.is_some(),
+                );
+                fetch_task.await.context("inlay hint fetch task")?
+            }
+            None => return Ok(()),
+        };
+        drop(lsp_request_guard);
         log::debug!(
-            "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
-            new_update.remove_from_visible.len(),
-            new_update.remove_from_cache.len(),
-            new_update.add_to_cache.len()
+            "Fetched {} hints for range {fetch_range_to_log:?}",
+            new_hints.len()
         );
-        log::trace!("New update: {new_update:?}");
-        editor
-            .update(&mut cx, |editor, cx| {
-                apply_hint_update(
-                    editor,
-                    new_update,
-                    query,
+        log::trace!("Fetched hints: {new_hints:?}");
+
+        let background_task_buffer_snapshot = buffer_snapshot.clone();
+        let background_fetch_range = fetch_range.clone();
+        let new_update = cx
+            .background_executor()
+            .spawn(async move {
+                calculate_hint_updates(
+                    query.excerpt_id,
                     invalidate,
-                    buffer_snapshot,
-                    multi_buffer_snapshot,
-                    cx,
-                );
+                    background_fetch_range,
+                    new_hints,
+                    &background_task_buffer_snapshot,
+                    cached_excerpt_hints,
+                    &visible_hints,
+                )
             })
-            .ok();
-    }
-    Ok(())
+            .await;
+        if let Some(new_update) = new_update {
+            log::debug!(
+                "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
+                new_update.remove_from_visible.len(),
+                new_update.remove_from_cache.len(),
+                new_update.add_to_cache.len()
+            );
+            log::trace!("New update: {new_update:?}");
+            editor
+                .update(&mut cx, |editor, cx| {
+                    apply_hint_update(
+                        editor,
+                        new_update,
+                        query,
+                        invalidate,
+                        buffer_snapshot,
+                        multi_buffer_snapshot,
+                        cx,
+                    );
+                })
+                .ok();
+        }
+        anyhow::Ok(())
+    })
 }
 
 fn calculate_hint_updates(
@@ -1053,7 +1052,7 @@ fn calculate_hint_updates(
         }
     }
 
-    let mut remove_from_visible = Vec::new();
+    let mut remove_from_visible = HashSet::default();
     let mut remove_from_cache = HashSet::default();
     if invalidate {
         remove_from_visible.extend(
@@ -1075,6 +1074,7 @@ fn calculate_hint_updates(
                     })
                     .copied(),
             );
+            remove_from_visible.extend(remove_from_cache.iter().cloned());
         }
     }
 
@@ -1136,10 +1136,8 @@ fn apply_hint_update(
     cached_excerpt_hints
         .hints_by_id
         .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
-    let mut splice = InlaySplice {
-        to_remove: new_update.remove_from_visible,
-        to_insert: Vec::new(),
-    };
+    let mut splice = InlaySplice::default();
+    splice.to_remove.extend(new_update.remove_from_visible);
     for new_hint in new_update.add_to_cache {
         let insert_position = match cached_excerpt_hints
             .ordered_hints
@@ -1308,49 +1306,55 @@ pub mod tests {
         cx.executor().run_until_parked();
 
         let mut edits_made = 1;
-        _ = editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["0".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "Should get its first hints when opening the editor"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(
-                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-                "Cache should use editor settings to get the allowed hint kinds"
-            );
-            assert_eq!(
-                inlay_cache.version, edits_made,
-                "The editor update the cache version after every cache/view change"
-            );
-        });
+        editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["0".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "Should get its first hints when opening the editor"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                let inlay_cache = editor.inlay_hint_cache();
+                assert_eq!(
+                    inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                    "Cache should use editor settings to get the allowed hint kinds"
+                );
+                assert_eq!(
+                    inlay_cache.version, edits_made,
+                    "The editor update the cache version after every cache/view change"
+                );
+            })
+            .unwrap();
 
-        _ = editor.update(cx, |editor, cx| {
-            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-            editor.handle_input("some change", cx);
-            edits_made += 1;
-        });
+        editor
+            .update(cx, |editor, cx| {
+                editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                editor.handle_input("some change", cx);
+                edits_made += 1;
+            })
+            .unwrap();
         cx.executor().run_until_parked();
-        _ = editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["0".to_string(), "1".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "Should get new hints after an edit"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(
-                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-                "Cache should use editor settings to get the allowed hint kinds"
-            );
-            assert_eq!(
-                inlay_cache.version, edits_made,
-                "The editor update the cache version after every cache/view change"
-            );
-        });
+        editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["0".to_string(), "1".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "Should get new hints after an edit"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                let inlay_cache = editor.inlay_hint_cache();
+                assert_eq!(
+                    inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                    "Cache should use editor settings to get the allowed hint kinds"
+                );
+                assert_eq!(
+                    inlay_cache.version, edits_made,
+                    "The editor update the cache version after every cache/view change"
+                );
+            })
+            .unwrap();
 
         fake_server
             .request::<lsp::request::InlayHintRefreshRequest>(())
@@ -1358,24 +1362,26 @@ pub mod tests {
             .expect("inlay refresh request failed");
         edits_made += 1;
         cx.executor().run_until_parked();
-        _ = editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "Should get new hints after hint refresh/ request"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(
-                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-                "Cache should use editor settings to get the allowed hint kinds"
-            );
-            assert_eq!(
-                inlay_cache.version, edits_made,
-                "The editor update the cache version after every cache/view change"
-            );
-        });
+        editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "Should get new hints after hint refresh/ request"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                let inlay_cache = editor.inlay_hint_cache();
+                assert_eq!(
+                    inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                    "Cache should use editor settings to get the allowed hint kinds"
+                );
+                assert_eq!(
+                    inlay_cache.version, edits_made,
+                    "The editor update the cache version after every cache/view change"
+                );
+            })
+            .unwrap();
     }
 
     #[gpui::test]
@@ -1418,20 +1424,22 @@ pub mod tests {
         cx.executor().run_until_parked();
 
         let mut edits_made = 1;
-        _ = editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["0".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "Should get its first hints when opening the editor"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(
-                editor.inlay_hint_cache().version,
-                edits_made,
-                "The editor update the cache version after every cache/view change"
-            );
-        });
+        editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["0".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "Should get its first hints when opening the editor"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(
+                    editor.inlay_hint_cache().version,
+                    edits_made,
+                    "The editor update the cache version after every cache/view change"
+                );
+            })
+            .unwrap();
 
         let progress_token = "test_progress_token";
         fake_server
@@ -1449,20 +1457,22 @@ pub mod tests {
         });
         cx.executor().run_until_parked();
 
-        _ = editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["0".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "Should not update hints while the work task is running"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(
-                editor.inlay_hint_cache().version,
-                edits_made,
-                "Should not update the cache while the work task is running"
-            );
-        });
+        editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["0".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "Should not update hints while the work task is running"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(
+                    editor.inlay_hint_cache().version,
+                    edits_made,
+                    "Should not update the cache while the work task is running"
+                );
+            })
+            .unwrap();
 
         fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
             token: lsp::ProgressToken::String(progress_token.to_string()),
@@ -1473,20 +1483,22 @@ pub mod tests {
         cx.executor().run_until_parked();
 
         edits_made += 1;
-        _ = editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["1".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "New hints should be queried after the work task is done"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(
-                editor.inlay_hint_cache().version,
-                edits_made,
-                "Cache version should update once after the work task is done"
-            );
-        });
+        editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["1".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "New hints should be queried after the work task is done"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(
+                    editor.inlay_hint_cache().version,
+                    edits_made,
+                    "Cache version should update once after the work task is done"
+                );
+            })
+            .unwrap();
     }
 
     #[gpui::test]
@@ -1578,20 +1590,22 @@ pub mod tests {
             .next()
             .await;
         cx.executor().run_until_parked();
-        _ = rs_editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["0".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "Should get its first hints when opening the editor"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(
-                editor.inlay_hint_cache().version,
-                1,
-                "Rust editor update the cache version after every cache/view change"
-            );
-        });
+        rs_editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["0".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "Should get its first hints when opening the editor"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(
+                    editor.inlay_hint_cache().version,
+                    1,
+                    "Rust editor update the cache version after every cache/view change"
+                );
+            })
+            .unwrap();
 
         cx.executor().run_until_parked();
         let md_buffer = project
@@ -1629,72 +1643,86 @@ pub mod tests {
             .next()
             .await;
         cx.executor().run_until_parked();
-        _ = md_editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["0".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "Markdown editor should have a separate version, repeating Rust editor rules"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(editor.inlay_hint_cache().version, 1);
-        });
+        md_editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["0".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "Markdown editor should have a separate version, repeating Rust editor rules"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(editor.inlay_hint_cache().version, 1);
+            })
+            .unwrap();
 
-        _ = rs_editor.update(cx, |editor, cx| {
-            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-            editor.handle_input("some rs change", cx);
-        });
+        rs_editor
+            .update(cx, |editor, cx| {
+                editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                editor.handle_input("some rs change", cx);
+            })
+            .unwrap();
         cx.executor().run_until_parked();
-        _ = rs_editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["1".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "Rust inlay cache should change after the edit"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(
-                editor.inlay_hint_cache().version,
-                2,
-                "Every time hint cache changes, cache version should be incremented"
-            );
-        });
-        _ = md_editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["0".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "Markdown editor should not be affected by Rust editor changes"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(editor.inlay_hint_cache().version, 1);
-        });
+        rs_editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["1".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "Rust inlay cache should change after the edit"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(
+                    editor.inlay_hint_cache().version,
+                    2,
+                    "Every time hint cache changes, cache version should be incremented"
+                );
+            })
+            .unwrap();
+        md_editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["0".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "Markdown editor should not be affected by Rust editor changes"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(editor.inlay_hint_cache().version, 1);
+            })
+            .unwrap();
 
-        _ = md_editor.update(cx, |editor, cx| {
-            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-            editor.handle_input("some md change", cx);
-        });
+        md_editor
+            .update(cx, |editor, cx| {
+                editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                editor.handle_input("some md change", cx);
+            })
+            .unwrap();
         cx.executor().run_until_parked();
-        _ = md_editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["1".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "Rust editor should not be affected by Markdown editor changes"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(editor.inlay_hint_cache().version, 2);
-        });
-        _ = rs_editor.update(cx, |editor, cx| {
-            let expected_hints = vec!["1".to_string()];
-            assert_eq!(
-                expected_hints,
-                cached_hint_labels(editor),
-                "Markdown editor should also change independently"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(editor.inlay_hint_cache().version, 2);
-        });
+        md_editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["1".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "Rust editor should not be affected by Markdown editor changes"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(editor.inlay_hint_cache().version, 2);
+            })
+            .unwrap();
+        rs_editor
+            .update(cx, |editor, cx| {
+                let expected_hints = vec!["1".to_string()];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "Markdown editor should also change independently"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(editor.inlay_hint_cache().version, 2);
+            })
+            .unwrap();
     }
 
     #[gpui::test]
@@ -1760,66 +1788,70 @@ pub mod tests {
         cx.executor().run_until_parked();
 
         let mut edits_made = 1;
-        _ = editor.update(cx, |editor, cx| {
-            assert_eq!(
-                lsp_request_count.load(Ordering::Relaxed),
-                1,
-                "Should query new hints once"
-            );
-            assert_eq!(
-                vec![
-                    "other hint".to_string(),
-                    "parameter hint".to_string(),
-                    "type hint".to_string(),
-                ],
-                cached_hint_labels(editor),
-                "Should get its first hints when opening the editor"
-            );
-            assert_eq!(
-                vec!["other hint".to_string(), "type hint".to_string()],
-                visible_hint_labels(editor, cx)
-            );
-            let inlay_cache = editor.inlay_hint_cache();
-            assert_eq!(
-                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
-                "Cache should use editor settings to get the allowed hint kinds"
-            );
-            assert_eq!(
-                inlay_cache.version, edits_made,
-                "The editor update the cache version after every cache/view change"
-            );
-        });
+        editor
+            .update(cx, |editor, cx| {
+                assert_eq!(
+                    lsp_request_count.load(Ordering::Relaxed),
+                    1,
+                    "Should query new hints once"
+                );
+                assert_eq!(
+                    vec![
+                        "other hint".to_string(),
+                        "parameter hint".to_string(),
+                        "type hint".to_string(),
+                    ],
+                    cached_hint_labels(editor),
+                    "Should get its first hints when opening the editor"
+                );
+                assert_eq!(
+                    vec!["other hint".to_string(), "type hint".to_string()],
+                    visible_hint_labels(editor, cx)
+                );
+                let inlay_cache = editor.inlay_hint_cache();
+                assert_eq!(
+                    inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                    "Cache should use editor settings to get the allowed hint kinds"
+                );
+                assert_eq!(
+                    inlay_cache.version, edits_made,
+                    "The editor update the cache version after every cache/view change"
+                );
+            })
+            .unwrap();
 
         fake_server
             .request::<lsp::request::InlayHintRefreshRequest>(())
             .await
             .expect("inlay refresh request failed");
         cx.executor().run_until_parked();
-        _ = editor.update(cx, |editor, cx| {
-            assert_eq!(
-                lsp_request_count.load(Ordering::Relaxed),
-                2,
-                "Should load new hints twice"
-            );
-            assert_eq!(
-                vec![
-                    "other hint".to_string(),
-                    "parameter hint".to_string(),
-                    "type hint".to_string(),
-                ],
-                cached_hint_labels(editor),
-                "Cached hints should not change due to allowed hint kinds settings update"
-            );
-            assert_eq!(
-                vec!["other hint".to_string(), "type hint".to_string()],
-                visible_hint_labels(editor, cx)
-            );
-            assert_eq!(
-                editor.inlay_hint_cache().version,
-                edits_made,
-                "Should not update cache version due to new loaded hints being the same"
-            );
-        });
+        editor
+            .update(cx, |editor, cx| {
+                assert_eq!(
+                    lsp_request_count.load(Ordering::Relaxed),
+                    2,
+                    "Should load new hints twice"
+                );
+                assert_eq!(
+                    vec![
+                        "other hint".to_string(),
+                        "parameter hint".to_string(),
+                        "type hint".to_string(),
+                    ],
+                    cached_hint_labels(editor),
+                    "Cached hints should not change due to allowed hint kinds settings update"
+                );
+                assert_eq!(
+                    vec!["other hint".to_string(), "type hint".to_string()],
+                    visible_hint_labels(editor, cx)
+                );
+                assert_eq!(
+                    editor.inlay_hint_cache().version,
+                    edits_made,
+                    "Should not update cache version due to new loaded hints being the same"
+                );
+            })
+            .unwrap();
 
         for (new_allowed_hint_kinds, expected_visible_hints) in [
             (HashSet::from_iter([None]), vec!["other hint".to_string()]),

crates/editor/src/items.rs πŸ”—

@@ -1,7 +1,7 @@
 use crate::{
-    editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition,
-    persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, EditorEvent, EditorSettings,
-    ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
+    editor_settings::SeedQuerySetting, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll,
+    Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
+    NavigationData, ToPoint as _,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::HashSet;
@@ -682,8 +682,7 @@ impl Item for Editor {
     }
 
     fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
-        hide_link_definition(self, cx);
-        self.link_go_to_definition_state.last_trigger_point = None;
+        self.hide_hovered_link(cx);
     }
 
     fn is_dirty(&self, cx: &AppContext) -> bool {
@@ -801,7 +800,11 @@ impl Item for Editor {
     }
 
     fn breadcrumb_location(&self) -> ToolbarItemLocation {
-        ToolbarItemLocation::PrimaryLeft
+        if self.show_breadcrumbs {
+            ToolbarItemLocation::PrimaryLeft
+        } else {
+            ToolbarItemLocation::Hidden
+        }
     }
 
     fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {

crates/editor/src/movement.rs πŸ”—

@@ -2,14 +2,12 @@
 //! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate.
 
 use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
-use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
-use gpui::{px, Pixels, TextSystem};
+use crate::{char_kind, scroll::ScrollAnchor, CharKind, EditorStyle, ToOffset, ToPoint};
+use gpui::{px, Pixels, WindowTextSystem};
 use language::Point;
 
 use std::{ops::Range, sync::Arc};
 
-use multi_buffer::Anchor;
-
 /// 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.
@@ -22,11 +20,12 @@ pub enum FindRange {
 /// TextLayoutDetails encompasses everything we need to move vertically
 /// taking into account variable width characters.
 pub struct TextLayoutDetails {
-    pub(crate) text_system: Arc<TextSystem>,
+    pub(crate) text_system: Arc<WindowTextSystem>,
     pub(crate) editor_style: EditorStyle,
     pub(crate) rem_size: Pixels,
-    pub anchor: Anchor,
+    pub scroll_anchor: ScrollAnchor,
     pub visible_rows: Option<f32>,
+    pub vertical_scroll_margin: f32,
 }
 
 /// Returns a column to the left of the current point, wrapping
@@ -396,14 +395,17 @@ pub fn find_preceding_boundary(
 /// Scans for a boundary following the given start point until a boundary is found, indicated by the
 /// given predicate returning true. The predicate is called with the character to the left and right
 /// of the candidate boundary location, and will be called with `\n` characters indicating the start
-/// or end of a line.
-pub fn find_boundary(
+/// or end of a line. The function supports optionally returning the point just before the boundary
+/// is found via return_point_before_boundary.
+pub fn find_boundary_point(
     map: &DisplaySnapshot,
     from: DisplayPoint,
     find_range: FindRange,
     mut is_boundary: impl FnMut(char, char) -> bool,
+    return_point_before_boundary: bool,
 ) -> DisplayPoint {
     let mut offset = from.to_offset(&map, Bias::Right);
+    let mut prev_offset = offset;
     let mut prev_ch = None;
 
     for ch in map.buffer_snapshot.chars_at(offset) {
@@ -412,16 +414,38 @@ pub fn find_boundary(
         }
         if let Some(prev_ch) = prev_ch {
             if is_boundary(prev_ch, ch) {
-                break;
+                if return_point_before_boundary {
+                    return map.clip_point(prev_offset.to_display_point(map), Bias::Right);
+                } else {
+                    break;
+                }
             }
         }
-
+        prev_offset = offset;
         offset += ch.len_utf8();
         prev_ch = Some(ch);
     }
     map.clip_point(offset.to_display_point(map), Bias::Right)
 }
 
+pub fn find_boundary(
+    map: &DisplaySnapshot,
+    from: DisplayPoint,
+    find_range: FindRange,
+    is_boundary: impl FnMut(char, char) -> bool,
+) -> DisplayPoint {
+    return find_boundary_point(map, from, find_range, is_boundary, false);
+}
+
+pub fn find_boundary_exclusive(
+    map: &DisplaySnapshot,
+    from: DisplayPoint,
+    find_range: FindRange,
+    is_boundary: impl FnMut(char, char) -> bool,
+) -> DisplayPoint {
+    return find_boundary_point(map, from, find_range, is_boundary, true);
+}
+
 /// Returns an iterator over the characters following a given offset in the [`DisplaySnapshot`].
 /// The returned value also contains a range of the start/end of a returned character in
 /// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
@@ -764,7 +788,7 @@ mod tests {
                     &snapshot,
                     display_points[0],
                     FindRange::MultiLine,
-                    is_boundary
+                    is_boundary,
                 ),
                 display_points[1]
             );

crates/editor/src/scroll.rs πŸ”—

@@ -6,13 +6,14 @@ use crate::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
     hover_popover::hide_hover,
     persistence::DB,
-    Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason,
+    Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, EditorSettings, InlayHintRefreshReason,
     MultiBufferSnapshot, ToPoint,
 };
 pub use autoscroll::{Autoscroll, AutoscrollStrategy};
-use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext};
+use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext, WindowContext};
 use language::{Bias, Point};
 pub use scroll_amount::ScrollAmount;
+use settings::Settings;
 use std::{
     cmp::Ordering,
     time::{Duration, Instant},
@@ -21,7 +22,6 @@ use util::ResultExt;
 use workspace::{ItemId, WorkspaceId};
 
 pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
-pub const VERTICAL_SCROLL_MARGIN: f32 = 3.;
 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 
 #[derive(Default)]
@@ -128,7 +128,7 @@ impl OngoingScroll {
 }
 
 pub struct ScrollManager {
-    vertical_scroll_margin: f32,
+    pub(crate) vertical_scroll_margin: f32,
     anchor: ScrollAnchor,
     ongoing: OngoingScroll,
     autoscroll_request: Option<(Autoscroll, bool)>,
@@ -140,9 +140,9 @@ pub struct ScrollManager {
 }
 
 impl ScrollManager {
-    pub fn new() -> Self {
+    pub fn new(cx: &mut WindowContext) -> Self {
         ScrollManager {
-            vertical_scroll_margin: VERTICAL_SCROLL_MARGIN,
+            vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin,
             anchor: ScrollAnchor::new(),
             ongoing: OngoingScroll::new(),
             autoscroll_request: None,

crates/editor/src/test/editor_test_context.rs πŸ”—

@@ -4,7 +4,8 @@ use crate::{
 use collections::BTreeMap;
 use futures::Future;
 use gpui::{
-    AnyWindowHandle, AppContext, Keystroke, ModelContext, View, ViewContext, VisualTestContext,
+    AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View, ViewContext,
+    VisualTestContext,
 };
 use indoc::indoc;
 use itertools::Itertools;
@@ -187,6 +188,31 @@ impl EditorTestContext {
         ranges[0].start.to_display_point(&snapshot)
     }
 
+    pub fn pixel_position(&mut self, marked_text: &str) -> Point<Pixels> {
+        let display_point = self.display_point(marked_text);
+        self.pixel_position_for(display_point)
+    }
+
+    pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
+        self.update_editor(|editor, cx| {
+            let newest_point = editor.selections.newest_display(cx).head();
+            let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
+            let line_height = editor
+                .style()
+                .unwrap()
+                .text
+                .line_height_in_pixels(cx.rem_size());
+            let snapshot = editor.snapshot(cx);
+            let details = editor.text_layout_details(cx);
+
+            let y = pixel_position.y
+                + line_height * (display_point.row() as f32 - newest_point.row() as f32);
+            let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
+                - snapshot.x_for_display_point(newest_point, &details);
+            Point::new(x, y)
+        })
+    }
+
     // Returns anchors for the current buffer using `Β«` and `Β»`
     pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
         let ranges = self.ranges(marked_text);
@@ -343,7 +369,7 @@ impl EditorTestContext {
 }
 
 impl Deref for EditorTestContext {
-    type Target = gpui::TestAppContext;
+    type Target = gpui::VisualTestContext;
 
     fn deref(&self) -> &Self::Target {
         &self.cx

crates/feedback/Cargo.toml πŸ”—

@@ -14,34 +14,34 @@ test-support = []
 [dependencies]
 anyhow.workspace = true
 bitflags = "2.4.1"
-client = { path = "../client" }
-db = { path = "../db" }
-editor = { path = "../editor" }
+client.workspace = true
+db.workspace = true
+editor.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 human_bytes = "0.4.1"
 isahc.workspace = true
-language = { path = "../language" }
+language.workspace = true
 lazy_static.workspace = true
 log.workspace = true
-menu = { path = "../menu" }
+menu.workspace = true
 postage.workspace = true
-project = { path = "../project" }
+project.workspace = true
 regex.workspace = true
-release_channel = { path = "../release_channel" }
+release_channel.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smallvec.workspace = true
 smol.workspace = true
 sysinfo.workspace = true
-theme = { path = "../theme" }
+theme.workspace = true
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
-ui = { path = "../ui" }
+ui.workspace = true
 urlencoding = "2.1.2"
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }

crates/file_finder/Cargo.toml πŸ”—

@@ -11,29 +11,29 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-collections = { path = "../collections" }
-editor = { path = "../editor" }
-fuzzy = {  path = "../fuzzy" }
-gpui = { path = "../gpui" }
+collections.workspace = true
+editor.workspace = true
+fuzzy.workspace = true
+gpui.workspace = true
 itertools = "0.11"
-menu = { path = "../menu" }
-picker = { path = "../picker" }
+menu.workspace = true
+picker.workspace = true
 postage.workspace = true
-project = { path = "../project" }
+project.workspace = true
 serde.workspace = true
-settings = { path = "../settings" }
-text = { path = "../text" }
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+settings.workspace = true
+text.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
 serde_json.workspace = true
-theme = { path = "../theme", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+theme = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/file_finder/src/file_finder.rs πŸ”—

@@ -566,7 +566,7 @@ impl FileFinderDelegate {
         let path = &path_match.path;
         let path_string = path.to_string_lossy();
         let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
-        let path_positions = path_match.positions.clone();
+        let mut path_positions = path_match.positions.clone();
 
         let file_name = path.file_name().map_or_else(
             || path_match.path_prefix.to_string(),
@@ -584,6 +584,9 @@ impl FileFinderDelegate {
             })
             .collect();
 
+        let full_path = full_path.trim_end_matches(&file_name).to_string();
+        path_positions.retain(|idx| *idx < full_path.len());
+
         (file_name, file_name_positions, full_path, path_positions)
     }
 
@@ -868,9 +871,14 @@ impl PickerDelegate for FileFinderDelegate {
                 .inset(true)
                 .selected(selected)
                 .child(
-                    v_flex()
+                    h_flex()
+                        .gap_2()
                         .child(HighlightedLabel::new(file_name, file_name_positions))
-                        .child(HighlightedLabel::new(full_path, full_path_positions)),
+                        .child(
+                            HighlightedLabel::new(full_path, full_path_positions)
+                                .size(LabelSize::Small)
+                                .color(Color::Muted),
+                        ),
                 ),
         )
     }

crates/file_finder/src/file_finder_tests.rs πŸ”—

@@ -490,8 +490,8 @@ async fn test_single_file_worktrees(cx: &mut TestAppContext) {
             delegate.labels_for_path_match(&matches[0].0);
         assert_eq!(file_name, "the-file");
         assert_eq!(file_name_positions, &[0, 1, 4]);
-        assert_eq!(full_path, "the-file");
-        assert_eq!(full_path_positions, &[0, 1, 4]);
+        assert_eq!(full_path, "");
+        assert_eq!(full_path_positions, &[0; 0]);
     });
 
     // Since the worktree root is a file, searching for its name followed by a slash does

crates/fs/Cargo.toml πŸ”—

@@ -9,11 +9,11 @@ license = "GPL-3.0-or-later"
 path = "src/fs.rs"
 
 [dependencies]
-collections = { path = "../collections" }
-rope = { path = "../rope" }
-text = { path = "../text" }
-util = { path = "../util" }
-sum_tree = { path = "../sum_tree" }
+collections.workspace = true
+rope.workspace = true
+text.workspace = true
+util.workspace = true
+sum_tree.workspace = true
 
 anyhow.workspace = true
 async-trait.workspace = true
@@ -31,16 +31,16 @@ log.workspace = true
 libc = "0.2"
 time.workspace = true
 
-gpui = { path = "../gpui", optional = true}
+gpui = { workspace = true, optional = true}
 
 [target.'cfg(target_os = "macos")'.dependencies]
-fsevent = { path = "../fsevent" }
+fsevent.workspace = true
 
 [target.'cfg(not(target_os = "macos"))'.dependencies]
 notify = "6.1.1"
 
 [dev-dependencies]
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 
 [features]
 test-support = ["gpui/test-support"]

crates/fs/src/repository.rs πŸ”—

@@ -5,14 +5,12 @@ use parking_lot::Mutex;
 use serde_derive::{Deserialize, Serialize};
 use std::{
     cmp::Ordering,
-    ffi::OsStr,
-    os::unix::prelude::OsStrExt,
     path::{Component, Path, PathBuf},
     sync::Arc,
     time::SystemTime,
 };
 use sum_tree::{MapSeekTarget, TreeMap};
-use util::ResultExt;
+use util::{paths::PathExt, ResultExt};
 
 pub use git2::Repository as LibGitRepository;
 
@@ -119,7 +117,7 @@ impl GitRepository for LibGitRepository {
 
         if let Some(statuses) = self.statuses(Some(&mut options)).log_err() {
             for status in statuses.iter() {
-                let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes())));
+                let path = RepoPath(PathBuf::try_from_bytes(status.path_bytes()).unwrap());
                 let status = status.status();
                 if !status.contains(git2::Status::IGNORED) {
                     if let Some(status) = read_status(status) {

crates/fuzzy/Cargo.toml πŸ”—

@@ -10,5 +10,5 @@ path = "src/fuzzy.rs"
 doctest = false
 
 [dependencies]
-gpui = { path = "../gpui" }
-util = { path = "../util" }
+gpui.workspace = true
+util.workspace = true

crates/git/Cargo.toml πŸ”—

@@ -11,17 +11,17 @@ path = "src/git.rs"
 [dependencies]
 anyhow.workspace = true
 async-trait.workspace = true
-clock = { path = "../clock" }
-collections = { path = "../collections" }
+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 = { path = "../sum_tree" }
-text = { path = "../text" }
-util = { path = "../util" }
+sum_tree.workspace = true
+text.workspace = true
+util.workspace = true
 
 [dev-dependencies]
 unindent.workspace = true

crates/go_to_line/Cargo.toml πŸ”—

@@ -10,17 +10,17 @@ path = "src/go_to_line.rs"
 doctest = false
 
 [dependencies]
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
-menu = { path = "../menu" }
+editor.workspace = true
+gpui.workspace = true
+menu.workspace = true
 postage.workspace = true
 serde.workspace = true
-settings = { path = "../settings" }
-text = { path = "../text" }
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+settings.workspace = true
+text.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }

crates/gpui/Cargo.toml πŸ”—

@@ -26,15 +26,15 @@ anyhow.workspace = true
 async-task = "4.7"
 backtrace = { version = "0.3", optional = true }
 bitflags = "2.4.0"
-collections = { path = "../collections" }
+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
-gpui_macros = { path = "../gpui_macros" }
 font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "d97147f" }
+gpui_macros.workspace = true
 image = "0.23"
 itertools = "0.10"
 lazy_static.workspace = true
@@ -59,24 +59,24 @@ serde_json.workspace = true
 slotmap = "1.0.6"
 smallvec.workspace = true
 smol.workspace = true
-sum_tree = { path = "../sum_tree" }
+sum_tree.workspace = true
 taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "1876f72bee5e376023eaa518aa7b8a34c769bd1b" }
 thiserror.workspace = true
 time.workspace = true
 tiny-skia = "0.5"
 usvg = { version = "0.14", features = [] }
-util = { path = "../util" }
+util.workspace = true
 uuid = { version = "1.1.2", features = ["v4"] }
 waker-fn = "1.1.0"
 
 [dev-dependencies]
 backtrace = "0.3"
-collections = { path = "../collections", features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
 dhat = "0.3"
 env_logger.workspace = true
 png = "0.16"
 simplelog = "0.9"
-util = { path = "../util", features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }
 
 [build-dependencies]
 bindgen = "0.65.1"
@@ -90,7 +90,7 @@ core-graphics = "0.22.3"
 core-text = "19.2"
 foreign-types = "0.3"
 log.workspace = true
-media = { path = "../media" }
+media.workspace = true
 metal = "0.21.0"
 objc = "0.2"
 

crates/gpui/build.rs πŸ”—

@@ -28,6 +28,7 @@ fn generate_dispatch_bindings() {
         .header("src/platform/mac/dispatch.h")
         .allowlist_var("_dispatch_main_q")
         .allowlist_var("DISPATCH_QUEUE_PRIORITY_DEFAULT")
+        .allowlist_var("DISPATCH_QUEUE_PRIORITY_HIGH")
         .allowlist_var("DISPATCH_TIME_NOW")
         .allowlist_function("dispatch_get_global_queue")
         .allowlist_function("dispatch_async_f")

crates/gpui/src/app.rs πŸ”—

@@ -14,6 +14,7 @@ use smol::future::FutureExt;
 pub use test_context::*;
 use time::UtcOffset;
 
+use crate::WindowAppearance;
 use crate::{
     current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
     AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
@@ -380,6 +381,11 @@ impl AppContext {
         })
     }
 
+    pub(crate) fn new_observer(&mut self, key: EntityId, value: Handler) -> Subscription {
+        let (subscription, activate) = self.observers.insert(key, value);
+        self.defer(move |_| activate());
+        subscription
+    }
     pub(crate) fn observe_internal<W, E>(
         &mut self,
         entity: &E,
@@ -391,7 +397,7 @@ impl AppContext {
     {
         let entity_id = entity.entity_id();
         let handle = entity.downgrade();
-        let (subscription, activate) = self.observers.insert(
+        self.new_observer(
             entity_id,
             Box::new(move |cx| {
                 if let Some(handle) = E::upgrade_from(&handle) {
@@ -400,9 +406,7 @@ impl AppContext {
                     false
                 }
             }),
-        );
-        self.defer(move |_| activate());
-        subscription
+        )
     }
 
     /// Arrange for the given callback to be invoked whenever the given model or view emits an event of a given type.
@@ -423,6 +427,15 @@ impl AppContext {
         })
     }
 
+    pub(crate) fn new_subscription(
+        &mut self,
+        key: EntityId,
+        value: (TypeId, Listener),
+    ) -> Subscription {
+        let (subscription, activate) = self.event_listeners.insert(key, value);
+        self.defer(move |_| activate());
+        subscription
+    }
     pub(crate) fn subscribe_internal<T, E, Evt>(
         &mut self,
         entity: &E,
@@ -435,7 +448,7 @@ impl AppContext {
     {
         let entity_id = entity.entity_id();
         let entity = entity.downgrade();
-        let (subscription, activate) = self.event_listeners.insert(
+        self.new_subscription(
             entity_id,
             (
                 TypeId::of::<Evt>(),
@@ -448,9 +461,7 @@ impl AppContext {
                     }
                 }),
             ),
-        );
-        self.defer(move |_| activate());
-        subscription
+        )
     }
 
     /// Returns handles to all open windows in the application.
@@ -512,6 +523,11 @@ impl AppContext {
         self.platform.displays()
     }
 
+    /// Returns the appearance of the application's windows.
+    pub fn window_appearance(&self) -> WindowAppearance {
+        self.platform.window_appearance()
+    }
+
     /// Writes data to the platform clipboard.
     pub fn write_to_clipboard(&self, item: ClipboardItem) {
         self.platform.write_to_clipboard(item)
@@ -652,27 +668,20 @@ impl AppContext {
                     }
                 }
             } else {
-                for window in self.windows.values() {
-                    if let Some(window) = window.as_ref() {
-                        if window.dirty {
-                            window.platform_window.invalidate();
-                        }
-                    }
-                }
-
                 #[cfg(any(test, feature = "test-support"))]
                 for window in self
                     .windows
                     .values()
                     .filter_map(|window| {
                         let window = window.as_ref()?;
-                        (window.dirty || window.focus_invalidated).then_some(window.handle)
+                        window.dirty.get().then_some(window.handle)
                     })
                     .collect::<Vec<_>>()
                 {
                     self.update_window(window, |_, cx| cx.draw()).unwrap();
                 }
 
+                #[allow(clippy::collapsible_else_if)]
                 if self.pending_effects.is_empty() {
                     break;
                 }
@@ -749,7 +758,7 @@ impl AppContext {
     fn apply_refresh_effect(&mut self) {
         for window in self.windows.values_mut() {
             if let Some(window) = window.as_mut() {
-                window.dirty = true;
+                window.dirty.set(true);
             }
         }
     }
@@ -937,13 +946,22 @@ impl AppContext {
         self.globals_by_type.insert(global_type, lease.global);
     }
 
+    pub(crate) fn new_view_observer(
+        &mut self,
+        key: TypeId,
+        value: NewViewListener,
+    ) -> Subscription {
+        let (subscription, activate) = self.new_view_observers.insert(key, value);
+        activate();
+        subscription
+    }
     /// Arrange for the given function to be invoked whenever a view of the specified type is created.
     /// The function will be passed a mutable reference to the view along with an appropriate context.
     pub fn observe_new_views<V: 'static>(
         &mut self,
         on_new: impl 'static + Fn(&mut V, &mut ViewContext<V>),
     ) -> Subscription {
-        let (subscription, activate) = self.new_view_observers.insert(
+        self.new_view_observer(
             TypeId::of::<V>(),
             Box::new(move |any_view: AnyView, cx: &mut WindowContext| {
                 any_view
@@ -953,9 +971,7 @@ impl AppContext {
                         on_new(view_state, cx);
                     })
             }),
-        );
-        activate();
-        subscription
+        )
     }
 
     /// Observe the release of a model or view. The callback is invoked after the model or view
@@ -987,9 +1003,15 @@ impl AppContext {
         &mut self,
         f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static,
     ) -> Subscription {
-        let (subscription, activate) = self.keystroke_observers.insert((), Box::new(f));
-        activate();
-        subscription
+        fn inner(
+            keystroke_observers: &mut SubscriberSet<(), KeystrokeObserver>,
+            handler: KeystrokeObserver,
+        ) -> Subscription {
+            let (subscription, activate) = keystroke_observers.insert((), handler);
+            activate();
+            subscription
+        }
+        inner(&mut self.keystroke_observers, Box::new(f))
     }
 
     pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {

crates/gpui/src/app/test_context.rs πŸ”—

@@ -1,9 +1,10 @@
 use crate::{
     Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
     AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter,
-    ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, Pixels, Platform,
-    Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View,
-    ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
+    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};
 use futures::{Stream, StreamExt};
@@ -236,6 +237,11 @@ impl TestAppContext {
         self.test_platform.has_pending_prompt()
     }
 
+    /// All the urls that have been opened with cx.open_url() during this test.
+    pub fn opened_url(&self) -> Option<String> {
+        self.test_platform.opened_url.borrow().clone()
+    }
+
     /// Simulates the user resizing the window to the new size.
     pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
         self.test_window(window_handle).simulate_resize(size);
@@ -625,6 +631,36 @@ impl<'a> VisualTestContext {
         self.cx.simulate_input(self.window, input)
     }
 
+    /// Simulate a mouse move event to the given point
+    pub fn simulate_mouse_move(&mut self, position: Point<Pixels>, modifiers: Modifiers) {
+        self.simulate_event(MouseMoveEvent {
+            position,
+            modifiers,
+            pressed_button: None,
+        })
+    }
+
+    /// Simulate a primary mouse click at the given point
+    pub fn simulate_click(&mut self, position: Point<Pixels>, modifiers: Modifiers) {
+        self.simulate_event(MouseDownEvent {
+            position,
+            modifiers,
+            button: MouseButton::Left,
+            click_count: 1,
+        });
+        self.simulate_event(MouseUpEvent {
+            position,
+            modifiers,
+            button: MouseButton::Left,
+            click_count: 1,
+        });
+    }
+
+    /// Simulate a modifiers changed event
+    pub fn simulate_modifiers_change(&mut self, modifiers: Modifiers) {
+        self.simulate_event(ModifiersChangedEvent { modifiers })
+    }
+
     /// Simulates the user resizing the window to the new size.
     pub fn simulate_resize(&self, size: Size<Pixels>) {
         self.simulate_window_resize(self.window, size)

crates/gpui/src/key_dispatch.rs πŸ”—

@@ -62,16 +62,6 @@ use std::{
     rc::Rc,
 };
 
-/// KeymatchMode controls how keybindings are resolved in the case of conflicting pending keystrokes.
-/// When `Sequenced`, gpui will wait for 1s for sequences to complete.
-/// When `Immediate`, gpui will immediately resolve the keybinding.
-#[derive(Default, PartialEq)]
-pub enum KeymatchMode {
-    #[default]
-    Sequenced,
-    Immediate,
-}
-
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
 pub(crate) struct DispatchNodeId(usize);
 
@@ -84,7 +74,6 @@ pub(crate) struct DispatchTree {
     keystroke_matchers: FxHashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
     keymap: Rc<RefCell<Keymap>>,
     action_registry: Rc<ActionRegistry>,
-    pub(crate) keymatch_mode: KeymatchMode,
 }
 
 #[derive(Default)]
@@ -116,7 +105,6 @@ impl DispatchTree {
             keystroke_matchers: FxHashMap::default(),
             keymap,
             action_registry,
-            keymatch_mode: KeymatchMode::Sequenced,
         }
     }
 
@@ -127,7 +115,6 @@ impl DispatchTree {
         self.focusable_node_ids.clear();
         self.view_node_ids.clear();
         self.keystroke_matchers.clear();
-        self.keymatch_mode = KeymatchMode::Sequenced;
     }
 
     pub fn push_node(
@@ -335,7 +322,7 @@ impl DispatchTree {
             .collect()
     }
 
-    // dispatch_key pushses the next keystroke into any key binding matchers.
+    // dispatch_key pushes the next keystroke into any key binding matchers.
     // any matching bindings are returned in the order that they should be dispatched:
     // * First by length of binding (so if you have a binding for "b" and "ab", the "ab" binding fires first)
     // * Secondly by depth in the tree (so if Editor has a binding for "b" and workspace a
@@ -364,6 +351,11 @@ impl DispatchTree {
                 .or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone()));
 
             let result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
+            if result.pending && !pending && !bindings.is_empty() {
+                context_stack.pop();
+                continue;
+            }
+
             pending = result.pending || pending;
             for new_binding in result.bindings {
                 match bindings

crates/gpui/src/keymap.rs πŸ”—

@@ -7,12 +7,9 @@ pub use context::*;
 pub(crate) use matcher::*;
 
 use crate::{Action, Keystroke, NoAction};
-use collections::HashSet;
+use collections::{HashMap, HashSet};
 use smallvec::SmallVec;
-use std::{
-    any::{Any, TypeId},
-    collections::HashMap,
-};
+use std::any::{Any, TypeId};
 
 /// An opaque identifier of which version of the keymap is currently active.
 /// The keymap's version is changed whenever bindings are added or removed.

crates/gpui/src/platform.rs πŸ”—

@@ -75,6 +75,9 @@ pub(crate) trait Platform: 'static {
         options: WindowOptions,
     ) -> Box<dyn PlatformWindow>;
 
+    /// Returns the appearance of the application's windows.
+    fn window_appearance(&self) -> WindowAppearance;
+
     fn set_display_link_output_callback(
         &self,
         display_id: DisplayId,
@@ -183,7 +186,6 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn on_close(&self, callback: Box<dyn FnOnce()>);
     fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
     fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
-    fn invalidate(&self);
     fn draw(&self, scene: &Scene);
 
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
@@ -569,29 +571,30 @@ pub enum WindowBounds {
     Fixed(Bounds<GlobalPixels>),
 }
 
-/// The appearance of the window, as defined by the operating system
-/// On macOS, this corresponds to named [NSAppearance](https://developer.apple.com/documentation/appkit/nsappearance)
-/// values
+/// The appearance of the window, as defined by the operating system.
+///
+/// On macOS, this corresponds to named [`NSAppearance`](https://developer.apple.com/documentation/appkit/nsappearance)
+/// values.
 #[derive(Copy, Clone, Debug)]
 pub enum WindowAppearance {
-    /// A light appearance
+    /// A light appearance.
     ///
-    /// on macOS, this corresponds to the `aqua` appearance
+    /// On macOS, this corresponds to the `aqua` appearance.
     Light,
 
-    /// A light appearance with vibrant colors
+    /// A light appearance with vibrant colors.
     ///
-    /// on macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance
+    /// On macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance.
     VibrantLight,
 
-    /// A dark appearance
+    /// A dark appearance.
     ///
-    /// on macOS, this corresponds to the `darkAqua` appearance
+    /// On macOS, this corresponds to the `darkAqua` appearance.
     Dark,
 
-    /// A dark appearance with vibrant colors
+    /// A dark appearance with vibrant colors.
     ///
-    /// on macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance
+    /// On macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance.
     VibrantDark,
 }
 

crates/gpui/src/platform/keystroke.rs πŸ”—

@@ -170,4 +170,34 @@ impl Modifiers {
     pub fn modified(&self) -> bool {
         self.control || self.alt || self.shift || self.command || self.function
     }
+
+    /// helper method for Modifiers with no modifiers
+    pub fn none() -> Modifiers {
+        Default::default()
+    }
+
+    /// helper method for Modifiers with just command
+    pub fn command() -> Modifiers {
+        Modifiers {
+            command: true,
+            ..Default::default()
+        }
+    }
+
+    /// helper method for Modifiers with just shift
+    pub fn shift() -> Modifiers {
+        Modifiers {
+            shift: true,
+            ..Default::default()
+        }
+    }
+
+    /// helper method for Modifiers with command + shift
+    pub fn command_shift() -> Modifiers {
+        Modifiers {
+            shift: true,
+            command: true,
+            ..Default::default()
+        }
+    }
 }

crates/gpui/src/platform/mac/dispatcher.rs πŸ”—

@@ -11,7 +11,12 @@ use objc::{
 };
 use parking::{Parker, Unparker};
 use parking_lot::Mutex;
-use std::{ffi::c_void, ptr::NonNull, sync::Arc, time::Duration};
+use std::{
+    ffi::c_void,
+    ptr::{addr_of, NonNull},
+    sync::Arc,
+    time::Duration,
+};
 
 /// All items in the generated file are marked as pub, so we're gonna wrap it in a separate mod to prevent
 /// these pub items from leaking into public API.
@@ -21,7 +26,7 @@ pub(crate) mod dispatch_sys {
 
 use dispatch_sys::*;
 pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t {
-    unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t }
+    unsafe { addr_of!(_dispatch_main_q) as *const _ as dispatch_queue_t }
 }
 
 pub(crate) struct MacDispatcher {
@@ -51,7 +56,7 @@ impl PlatformDispatcher for MacDispatcher {
     fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
         unsafe {
             dispatch_async_f(
-                dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0),
+                dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0),
                 runnable.into_raw().as_ptr() as *mut c_void,
                 Some(trampoline),
             );

crates/gpui/src/platform/mac/metal_renderer.rs πŸ”—

@@ -3,6 +3,7 @@ use crate::{
     Hsla, MetalAtlas, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch,
     Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
 };
+use block::ConcreteBlock;
 use cocoa::{
     base::{NO, YES},
     foundation::NSUInteger,
@@ -14,17 +15,19 @@ use foreign_types::ForeignType;
 use media::core_video::CVMetalTextureCache;
 use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
 use objc::{self, msg_send, sel, sel_impl};
+use parking_lot::Mutex;
 use smallvec::SmallVec;
-use std::{ffi::c_void, mem, ptr, sync::Arc};
+use std::{cell::Cell, ffi::c_void, mem, ptr, sync::Arc};
 
 #[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 INSTANCE_BUFFER_SIZE: usize = 32 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...)
+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(crate) struct MetalRenderer {
+    device: metal::Device,
     layer: metal::MetalLayer,
     command_queue: CommandQueue,
     paths_rasterization_pipeline_state: metal::RenderPipelineState,
@@ -36,13 +39,14 @@ pub(crate) struct MetalRenderer {
     polychrome_sprites_pipeline_state: metal::RenderPipelineState,
     surfaces_pipeline_state: metal::RenderPipelineState,
     unit_vertices: metal::Buffer,
-    instances: metal::Buffer,
+    #[allow(clippy::arc_with_non_send_sync)]
+    instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
     sprite_atlas: Arc<MetalAtlas>,
     core_video_texture_cache: CVMetalTextureCache,
 }
 
 impl MetalRenderer {
-    pub fn new(is_opaque: bool) -> Self {
+    pub fn new(instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>) -> Self {
         let device: metal::Device = if let Some(device) = metal::Device::system_default() {
             device
         } else {
@@ -54,7 +58,7 @@ impl MetalRenderer {
         layer.set_device(&device);
         layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
         layer.set_presents_with_transaction(true);
-        layer.set_opaque(is_opaque);
+        layer.set_opaque(true);
         unsafe {
             let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
             let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
@@ -93,10 +97,6 @@ impl MetalRenderer {
             mem::size_of_val(&unit_vertices) as u64,
             MTLResourceOptions::StorageModeManaged,
         );
-        let instances = device.new_buffer(
-            INSTANCE_BUFFER_SIZE as u64,
-            MTLResourceOptions::StorageModeManaged,
-        );
 
         let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state(
             &device,
@@ -165,8 +165,11 @@ impl MetalRenderer {
 
         let command_queue = device.new_command_queue();
         let sprite_atlas = Arc::new(MetalAtlas::new(device.clone()));
+        let core_video_texture_cache =
+            unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() };
 
         Self {
+            device,
             layer,
             command_queue,
             paths_rasterization_pipeline_state,
@@ -178,9 +181,9 @@ impl MetalRenderer {
             polychrome_sprites_pipeline_state,
             surfaces_pipeline_state,
             unit_vertices,
-            instances,
+            instance_buffer_pool,
             sprite_atlas,
-            core_video_texture_cache: unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() },
+            core_video_texture_cache,
         }
     }
 
@@ -208,14 +211,24 @@ impl MetalRenderer {
             );
             return;
         };
+        let mut instance_buffer = self.instance_buffer_pool.lock().pop().unwrap_or_else(|| {
+            self.device.new_buffer(
+                INSTANCE_BUFFER_SIZE as u64,
+                MTLResourceOptions::StorageModeManaged,
+            )
+        });
         let command_queue = self.command_queue.clone();
         let command_buffer = command_queue.new_command_buffer();
         let mut instance_offset = 0;
 
-        let Some(path_tiles) =
-            self.rasterize_paths(scene.paths(), &mut instance_offset, command_buffer)
-        else {
-            panic!("failed to rasterize {} paths", scene.paths().len());
+        let Some(path_tiles) = self.rasterize_paths(
+            scene.paths(),
+            &mut instance_buffer,
+            &mut instance_offset,
+            command_buffer,
+        ) else {
+            log::error!("failed to rasterize {} paths", scene.paths().len());
+            return;
         };
 
         let render_pass_descriptor = metal::RenderPassDescriptor::new();
@@ -243,22 +256,29 @@ impl MetalRenderer {
             let ok = match batch {
                 PrimitiveBatch::Shadows(shadows) => self.draw_shadows(
                     shadows,
+                    &mut instance_buffer,
+                    &mut instance_offset,
+                    viewport_size,
+                    command_encoder,
+                ),
+                PrimitiveBatch::Quads(quads) => self.draw_quads(
+                    quads,
+                    &mut instance_buffer,
                     &mut instance_offset,
                     viewport_size,
                     command_encoder,
                 ),
-                PrimitiveBatch::Quads(quads) => {
-                    self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder)
-                }
                 PrimitiveBatch::Paths(paths) => self.draw_paths(
                     paths,
                     &path_tiles,
+                    &mut instance_buffer,
                     &mut instance_offset,
                     viewport_size,
                     command_encoder,
                 ),
                 PrimitiveBatch::Underlines(underlines) => self.draw_underlines(
                     underlines,
+                    &mut instance_buffer,
                     &mut instance_offset,
                     viewport_size,
                     command_encoder,
@@ -269,6 +289,7 @@ impl MetalRenderer {
                 } => self.draw_monochrome_sprites(
                     texture_id,
                     sprites,
+                    &mut instance_buffer,
                     &mut instance_offset,
                     viewport_size,
                     command_encoder,
@@ -279,12 +300,14 @@ impl MetalRenderer {
                 } => self.draw_polychrome_sprites(
                     texture_id,
                     sprites,
+                    &mut instance_buffer,
                     &mut instance_offset,
                     viewport_size,
                     command_encoder,
                 ),
                 PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(
                     surfaces,
+                    &mut instance_buffer,
                     &mut instance_offset,
                     viewport_size,
                     command_encoder,
@@ -292,7 +315,7 @@ impl MetalRenderer {
             };
 
             if !ok {
-                panic!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
+                log::error!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
                     scene.paths.len(),
                     scene.shadows.len(),
                     scene.quads.len(),
@@ -300,17 +323,27 @@ impl MetalRenderer {
                     scene.monochrome_sprites.len(),
                     scene.polychrome_sprites.len(),
                     scene.surfaces.len(),
-                )
+                );
+                break;
             }
         }
 
         command_encoder.end_encoding();
 
-        self.instances.did_modify_range(NSRange {
+        instance_buffer.did_modify_range(NSRange {
             location: 0,
             length: instance_offset as NSUInteger,
         });
 
+        let instance_buffer_pool = self.instance_buffer_pool.clone();
+        let instance_buffer = Cell::new(Some(instance_buffer));
+        let block = ConcreteBlock::new(move |_| {
+            if let Some(instance_buffer) = instance_buffer.take() {
+                instance_buffer_pool.lock().push(instance_buffer);
+            }
+        });
+        let block = block.copy();
+        command_buffer.add_completed_handler(&block);
         command_buffer.commit();
         self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
 
@@ -321,7 +354,8 @@ impl MetalRenderer {
     fn rasterize_paths(
         &mut self,
         paths: &[Path<ScaledPixels>],
-        offset: &mut usize,
+        instance_buffer: &mut metal::Buffer,
+        instance_offset: &mut usize,
         command_buffer: &metal::CommandBufferRef,
     ) -> Option<HashMap<PathId, AtlasTile>> {
         let mut tiles = HashMap::default();
@@ -347,9 +381,9 @@ impl MetalRenderer {
         }
 
         for (texture_id, vertices) in vertices_by_texture_id {
-            align_offset(offset);
+            align_offset(instance_offset);
             let vertices_bytes_len = mem::size_of_val(vertices.as_slice());
-            let next_offset = *offset + vertices_bytes_len;
+            let next_offset = *instance_offset + vertices_bytes_len;
             if next_offset > INSTANCE_BUFFER_SIZE {
                 return None;
             }
@@ -369,8 +403,8 @@ impl MetalRenderer {
             command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
             command_encoder.set_vertex_buffer(
                 PathRasterizationInputIndex::Vertices as u64,
-                Some(&self.instances),
-                *offset as u64,
+                Some(instance_buffer),
+                *instance_offset as u64,
             );
             let texture_size = Size {
                 width: DevicePixels::from(texture.width()),
@@ -382,7 +416,8 @@ impl MetalRenderer {
                 &texture_size as *const Size<DevicePixels> as *const _,
             );
 
-            let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+            let buffer_contents =
+                unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
             unsafe {
                 ptr::copy_nonoverlapping(
                     vertices.as_ptr() as *const u8,
@@ -397,7 +432,7 @@ impl MetalRenderer {
                 vertices.len() as u64,
             );
             command_encoder.end_encoding();
-            *offset = next_offset;
+            *instance_offset = next_offset;
         }
 
         Some(tiles)
@@ -406,14 +441,15 @@ impl MetalRenderer {
     fn draw_shadows(
         &mut self,
         shadows: &[Shadow],
-        offset: &mut usize,
+        instance_buffer: &mut metal::Buffer,
+        instance_offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
     ) -> bool {
         if shadows.is_empty() {
             return true;
         }
-        align_offset(offset);
+        align_offset(instance_offset);
 
         command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state);
         command_encoder.set_vertex_buffer(
@@ -423,13 +459,13 @@ impl MetalRenderer {
         );
         command_encoder.set_vertex_buffer(
             ShadowInputIndex::Shadows as u64,
-            Some(&self.instances),
-            *offset as u64,
+            Some(instance_buffer),
+            *instance_offset as u64,
         );
         command_encoder.set_fragment_buffer(
             ShadowInputIndex::Shadows as u64,
-            Some(&self.instances),
-            *offset as u64,
+            Some(instance_buffer),
+            *instance_offset as u64,
         );
 
         command_encoder.set_vertex_bytes(
@@ -439,9 +475,10 @@ impl MetalRenderer {
         );
 
         let shadow_bytes_len = mem::size_of_val(shadows);
-        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        let buffer_contents =
+            unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
 
-        let next_offset = *offset + shadow_bytes_len;
+        let next_offset = *instance_offset + shadow_bytes_len;
         if next_offset > INSTANCE_BUFFER_SIZE {
             return false;
         }
@@ -460,21 +497,22 @@ impl MetalRenderer {
             6,
             shadows.len() as u64,
         );
-        *offset = next_offset;
+        *instance_offset = next_offset;
         true
     }
 
     fn draw_quads(
         &mut self,
         quads: &[Quad],
-        offset: &mut usize,
+        instance_buffer: &mut metal::Buffer,
+        instance_offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
     ) -> bool {
         if quads.is_empty() {
             return true;
         }
-        align_offset(offset);
+        align_offset(instance_offset);
 
         command_encoder.set_render_pipeline_state(&self.quads_pipeline_state);
         command_encoder.set_vertex_buffer(
@@ -484,13 +522,13 @@ impl MetalRenderer {
         );
         command_encoder.set_vertex_buffer(
             QuadInputIndex::Quads as u64,
-            Some(&self.instances),
-            *offset as u64,
+            Some(instance_buffer),
+            *instance_offset as u64,
         );
         command_encoder.set_fragment_buffer(
             QuadInputIndex::Quads as u64,
-            Some(&self.instances),
-            *offset as u64,
+            Some(instance_buffer),
+            *instance_offset as u64,
         );
 
         command_encoder.set_vertex_bytes(
@@ -500,9 +538,10 @@ impl MetalRenderer {
         );
 
         let quad_bytes_len = mem::size_of_val(quads);
-        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        let buffer_contents =
+            unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
 
-        let next_offset = *offset + quad_bytes_len;
+        let next_offset = *instance_offset + quad_bytes_len;
         if next_offset > INSTANCE_BUFFER_SIZE {
             return false;
         }
@@ -517,7 +556,7 @@ impl MetalRenderer {
             6,
             quads.len() as u64,
         );
-        *offset = next_offset;
+        *instance_offset = next_offset;
         true
     }
 
@@ -525,7 +564,8 @@ impl MetalRenderer {
         &mut self,
         paths: &[Path<ScaledPixels>],
         tiles_by_path_id: &HashMap<PathId, AtlasTile>,
-        offset: &mut usize,
+        instance_buffer: &mut metal::Buffer,
+        instance_offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
     ) -> bool {
@@ -573,7 +613,7 @@ impl MetalRenderer {
             if sprites.is_empty() {
                 break;
             } else {
-                align_offset(offset);
+                align_offset(instance_offset);
                 let texture_id = prev_texture_id.take().unwrap();
                 let texture: metal::Texture = self.sprite_atlas.metal_texture(texture_id);
                 let texture_size = size(
@@ -583,8 +623,8 @@ impl MetalRenderer {
 
                 command_encoder.set_vertex_buffer(
                     SpriteInputIndex::Sprites as u64,
-                    Some(&self.instances),
-                    *offset as u64,
+                    Some(instance_buffer),
+                    *instance_offset as u64,
                 );
                 command_encoder.set_vertex_bytes(
                     SpriteInputIndex::AtlasTextureSize as u64,
@@ -593,20 +633,20 @@ impl MetalRenderer {
                 );
                 command_encoder.set_fragment_buffer(
                     SpriteInputIndex::Sprites as u64,
-                    Some(&self.instances),
-                    *offset as u64,
+                    Some(instance_buffer),
+                    *instance_offset as u64,
                 );
                 command_encoder
                     .set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
 
                 let sprite_bytes_len = mem::size_of_val(sprites.as_slice());
-                let next_offset = *offset + sprite_bytes_len;
+                let next_offset = *instance_offset + sprite_bytes_len;
                 if next_offset > INSTANCE_BUFFER_SIZE {
                     return false;
                 }
 
                 let buffer_contents =
-                    unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+                    unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
 
                 unsafe {
                     ptr::copy_nonoverlapping(
@@ -622,7 +662,7 @@ impl MetalRenderer {
                     6,
                     sprites.len() as u64,
                 );
-                *offset = next_offset;
+                *instance_offset = next_offset;
                 sprites.clear();
             }
         }
@@ -632,14 +672,15 @@ impl MetalRenderer {
     fn draw_underlines(
         &mut self,
         underlines: &[Underline],
-        offset: &mut usize,
+        instance_buffer: &mut metal::Buffer,
+        instance_offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
     ) -> bool {
         if underlines.is_empty() {
             return true;
         }
-        align_offset(offset);
+        align_offset(instance_offset);
 
         command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state);
         command_encoder.set_vertex_buffer(
@@ -649,13 +690,13 @@ impl MetalRenderer {
         );
         command_encoder.set_vertex_buffer(
             UnderlineInputIndex::Underlines as u64,
-            Some(&self.instances),
-            *offset as u64,
+            Some(instance_buffer),
+            *instance_offset as u64,
         );
         command_encoder.set_fragment_buffer(
             UnderlineInputIndex::Underlines as u64,
-            Some(&self.instances),
-            *offset as u64,
+            Some(instance_buffer),
+            *instance_offset as u64,
         );
 
         command_encoder.set_vertex_bytes(
@@ -665,9 +706,10 @@ impl MetalRenderer {
         );
 
         let underline_bytes_len = mem::size_of_val(underlines);
-        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        let buffer_contents =
+            unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
 
-        let next_offset = *offset + underline_bytes_len;
+        let next_offset = *instance_offset + underline_bytes_len;
         if next_offset > INSTANCE_BUFFER_SIZE {
             return false;
         }
@@ -686,7 +728,7 @@ impl MetalRenderer {
             6,
             underlines.len() as u64,
         );
-        *offset = next_offset;
+        *instance_offset = next_offset;
         true
     }
 
@@ -694,14 +736,15 @@ impl MetalRenderer {
         &mut self,
         texture_id: AtlasTextureId,
         sprites: &[MonochromeSprite],
-        offset: &mut usize,
+        instance_buffer: &mut metal::Buffer,
+        instance_offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
     ) -> bool {
         if sprites.is_empty() {
             return true;
         }
-        align_offset(offset);
+        align_offset(instance_offset);
 
         let texture = self.sprite_atlas.metal_texture(texture_id);
         let texture_size = size(
@@ -716,8 +759,8 @@ impl MetalRenderer {
         );
         command_encoder.set_vertex_buffer(
             SpriteInputIndex::Sprites as u64,
-            Some(&self.instances),
-            *offset as u64,
+            Some(instance_buffer),
+            *instance_offset as u64,
         );
         command_encoder.set_vertex_bytes(
             SpriteInputIndex::ViewportSize as u64,
@@ -731,15 +774,16 @@ impl MetalRenderer {
         );
         command_encoder.set_fragment_buffer(
             SpriteInputIndex::Sprites as u64,
-            Some(&self.instances),
-            *offset as u64,
+            Some(instance_buffer),
+            *instance_offset as u64,
         );
         command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
 
         let sprite_bytes_len = mem::size_of_val(sprites);
-        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        let buffer_contents =
+            unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
 
-        let next_offset = *offset + sprite_bytes_len;
+        let next_offset = *instance_offset + sprite_bytes_len;
         if next_offset > INSTANCE_BUFFER_SIZE {
             return false;
         }
@@ -758,7 +802,7 @@ impl MetalRenderer {
             6,
             sprites.len() as u64,
         );
-        *offset = next_offset;
+        *instance_offset = next_offset;
         true
     }
 
@@ -766,14 +810,15 @@ impl MetalRenderer {
         &mut self,
         texture_id: AtlasTextureId,
         sprites: &[PolychromeSprite],
-        offset: &mut usize,
+        instance_buffer: &mut metal::Buffer,
+        instance_offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
     ) -> bool {
         if sprites.is_empty() {
             return true;
         }
-        align_offset(offset);
+        align_offset(instance_offset);
 
         let texture = self.sprite_atlas.metal_texture(texture_id);
         let texture_size = size(
@@ -788,8 +833,8 @@ impl MetalRenderer {
         );
         command_encoder.set_vertex_buffer(
             SpriteInputIndex::Sprites as u64,
-            Some(&self.instances),
-            *offset as u64,
+            Some(instance_buffer),
+            *instance_offset as u64,
         );
         command_encoder.set_vertex_bytes(
             SpriteInputIndex::ViewportSize as u64,
@@ -803,15 +848,16 @@ impl MetalRenderer {
         );
         command_encoder.set_fragment_buffer(
             SpriteInputIndex::Sprites as u64,
-            Some(&self.instances),
-            *offset as u64,
+            Some(instance_buffer),
+            *instance_offset as u64,
         );
         command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
 
         let sprite_bytes_len = mem::size_of_val(sprites);
-        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        let buffer_contents =
+            unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
 
-        let next_offset = *offset + sprite_bytes_len;
+        let next_offset = *instance_offset + sprite_bytes_len;
         if next_offset > INSTANCE_BUFFER_SIZE {
             return false;
         }
@@ -830,14 +876,15 @@ impl MetalRenderer {
             6,
             sprites.len() as u64,
         );
-        *offset = next_offset;
+        *instance_offset = next_offset;
         true
     }
 
     fn draw_surfaces(
         &mut self,
         surfaces: &[Surface],
-        offset: &mut usize,
+        instance_buffer: &mut metal::Buffer,
+        instance_offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
     ) -> bool {
@@ -889,16 +936,16 @@ impl MetalRenderer {
                     .unwrap()
             };
 
-            align_offset(offset);
-            let next_offset = *offset + mem::size_of::<Surface>();
+            align_offset(instance_offset);
+            let next_offset = *instance_offset + mem::size_of::<Surface>();
             if next_offset > INSTANCE_BUFFER_SIZE {
                 return false;
             }
 
             command_encoder.set_vertex_buffer(
                 SurfaceInputIndex::Surfaces as u64,
-                Some(&self.instances),
-                *offset as u64,
+                Some(instance_buffer),
+                *instance_offset as u64,
             );
             command_encoder.set_vertex_bytes(
                 SurfaceInputIndex::TextureSize as u64,
@@ -915,8 +962,8 @@ impl MetalRenderer {
             );
 
             unsafe {
-                let buffer_contents =
-                    (self.instances.contents() as *mut u8).add(*offset) as *mut SurfaceBounds;
+                let buffer_contents = (instance_buffer.contents() as *mut u8).add(*instance_offset)
+                    as *mut SurfaceBounds;
                 ptr::write(
                     buffer_contents,
                     SurfaceBounds {
@@ -927,7 +974,7 @@ impl MetalRenderer {
             }
 
             command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6);
-            *offset = next_offset;
+            *instance_offset = next_offset;
         }
         true
     }

crates/gpui/src/platform/mac/platform.rs πŸ”—

@@ -3,7 +3,8 @@ use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
     ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem,
     MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput,
-    PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowOptions,
+    PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowAppearance,
+    WindowOptions,
 };
 use anyhow::anyhow;
 use block::ConcreteBlock;
@@ -146,6 +147,7 @@ pub(crate) struct MacPlatformState {
     foreground_executor: ForegroundExecutor,
     text_system: Arc<MacTextSystem>,
     display_linker: MacDisplayLinker,
+    instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
     pasteboard: id,
     text_hash_pasteboard_type: id,
     metadata_pasteboard_type: id,
@@ -176,6 +178,7 @@ impl MacPlatform {
             foreground_executor: ForegroundExecutor::new(dispatcher),
             text_system: Arc::new(MacTextSystem::new()),
             display_linker: MacDisplayLinker::new(),
+            instance_buffer_pool: Arc::default(),
             pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
             text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
             metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
@@ -494,7 +497,21 @@ impl Platform for MacPlatform {
         handle: AnyWindowHandle,
         options: WindowOptions,
     ) -> Box<dyn PlatformWindow> {
-        Box::new(MacWindow::open(handle, options, self.foreground_executor()))
+        let instance_buffer_pool = self.0.lock().instance_buffer_pool.clone();
+        Box::new(MacWindow::open(
+            handle,
+            options,
+            self.foreground_executor(),
+            instance_buffer_pool,
+        ))
+    }
+
+    fn window_appearance(&self) -> WindowAppearance {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let appearance: id = msg_send![app, effectiveAppearance];
+            WindowAppearance::from_native(appearance)
+        }
     }
 
     fn set_display_link_output_callback(

crates/gpui/src/platform/mac/window.rs πŸ”—

@@ -12,12 +12,12 @@ use cocoa::{
         CGPoint, NSApplication, NSBackingStoreBuffered, NSEventModifierFlags,
         NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable,
         NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
-        NSWindowStyleMask, NSWindowTitleVisibility,
+        NSWindowOcclusionState, NSWindowStyleMask, NSWindowTitleVisibility,
     },
     base::{id, nil},
     foundation::{
-        NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSPoint, NSRect,
-        NSSize, NSString, NSUInteger,
+        NSArray, NSAutoreleasePool, NSDefaultRunLoopMode, NSDictionary, NSFastEnumeration,
+        NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger,
     },
 };
 use core_graphics::display::CGRect;
@@ -168,6 +168,7 @@ unsafe fn build_classes() {
             sel!(displayLayer:),
             display_layer as extern "C" fn(&Object, Sel, id),
         );
+        decl.add_method(sel!(step:), step as extern "C" fn(&Object, Sel, id));
 
         decl.add_protocol(Protocol::get("NSTextInputClient").unwrap());
         decl.add_method(
@@ -248,6 +249,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
         sel!(windowDidResize:),
         window_did_resize as extern "C" fn(&Object, Sel, id),
     );
+    decl.add_method(
+        sel!(windowDidChangeOcclusionState:),
+        window_did_change_occlusion_state as extern "C" fn(&Object, Sel, id),
+    );
     decl.add_method(
         sel!(windowWillEnterFullScreen:),
         window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
@@ -260,6 +265,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
         sel!(windowDidMove:),
         window_did_move as extern "C" fn(&Object, Sel, id),
     );
+    decl.add_method(
+        sel!(windowDidChangeScreen:),
+        window_did_change_screen as extern "C" fn(&Object, Sel, id),
+    );
     decl.add_method(
         sel!(windowDidBecomeKey:),
         window_did_change_key_status as extern "C" fn(&Object, Sel, id),
@@ -320,7 +329,8 @@ struct MacWindowState {
     handle: AnyWindowHandle,
     executor: ForegroundExecutor,
     native_window: id,
-    native_view: NonNull<id>,
+    native_view: NonNull<Object>,
+    display_link: id,
     renderer: MetalRenderer,
     kind: WindowKind,
     request_frame_callback: Option<Box<dyn FnMut()>>,
@@ -458,6 +468,7 @@ impl MacWindow {
         handle: AnyWindowHandle,
         options: WindowOptions,
         executor: ForegroundExecutor,
+        instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
     ) -> Self {
         unsafe {
             let pool = NSAutoreleasePool::new(nil);
@@ -521,15 +532,15 @@ impl MacWindow {
 
             let native_view: id = msg_send![VIEW_CLASS, alloc];
             let native_view = NSView::init(native_view);
-
             assert!(!native_view.is_null());
 
             let window = Self(Arc::new(Mutex::new(MacWindowState {
                 handle,
                 executor,
                 native_window,
-                native_view: NonNull::new_unchecked(native_view as *mut _),
-                renderer: MetalRenderer::new(true),
+                native_view: NonNull::new_unchecked(native_view),
+                display_link: nil,
+                renderer: MetalRenderer::new(instance_buffer_pool),
                 kind: options.kind,
                 request_frame_callback: None,
                 event_callback: None,
@@ -663,6 +674,7 @@ impl MacWindow {
             }
 
             window.0.lock().move_traffic_light();
+
             pool.drain();
 
             window
@@ -683,10 +695,19 @@ impl MacWindow {
     }
 }
 
+unsafe fn start_display_link(native_screen: id, native_view: id) -> id {
+    let display_link: id =
+        msg_send![native_screen, displayLinkWithTarget: native_view selector: sel!(step:)];
+    let main_run_loop: id = msg_send![class!(NSRunLoop), mainRunLoop];
+    let _: () = msg_send![display_link, addToRunLoop: main_run_loop forMode: NSDefaultRunLoopMode];
+    display_link
+}
+
 impl Drop for MacWindow {
     fn drop(&mut self) {
-        let this = self.0.lock();
+        let mut this = self.0.lock();
         let window = this.native_window;
+        this.display_link = nil;
         this.executor
             .spawn(async move {
                 unsafe {
@@ -1000,13 +1021,6 @@ impl PlatformWindow for MacWindow {
         }
     }
 
-    fn invalidate(&self) {
-        let this = self.0.lock();
-        unsafe {
-            let _: () = msg_send![this.native_window.contentView(), setNeedsDisplay: YES];
-        }
-    }
-
     fn draw(&self, scene: &crate::Scene) {
         let mut this = self.0.lock();
         this.renderer.draw(scene);
@@ -1320,6 +1334,23 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
     }
 }
 
+extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) {
+    let window_state = unsafe { get_window_state(this) };
+    let mut lock = window_state.lock();
+    unsafe {
+        if lock
+            .native_window
+            .occlusionState()
+            .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible)
+        {
+            lock.display_link =
+                start_display_link(lock.native_window.screen(), lock.native_view.as_ptr());
+        } else {
+            lock.display_link = nil;
+        }
+    }
+}
+
 extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
     let window_state = unsafe { get_window_state(this) };
     window_state.as_ref().lock().move_traffic_light();
@@ -1353,6 +1384,19 @@ extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
     }
 }
 
+extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) {
+    let window_state = unsafe { get_window_state(this) };
+    let mut lock = window_state.as_ref().lock();
+    unsafe {
+        let screen = lock.native_window.screen();
+        if screen == nil {
+            lock.display_link = nil;
+        } else {
+            lock.display_link = start_display_link(screen, lock.native_view.as_ptr());
+        }
+    }
+}
+
 extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
     let window_state = unsafe { get_window_state(this) };
     let lock = window_state.lock();
@@ -1502,6 +1546,23 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
     }
 }
 
+extern "C" fn step(this: &Object, _: Sel, display_link: id) {
+    let window_state = unsafe { get_window_state(this) };
+    let mut lock = window_state.lock();
+
+    if lock.display_link == display_link {
+        if let Some(mut callback) = lock.request_frame_callback.take() {
+            drop(lock);
+            callback();
+            window_state.lock().request_frame_callback = Some(callback);
+        }
+    } else {
+        unsafe {
+            let _: () = msg_send![display_link, invalidate];
+        }
+    }
+}
+
 extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
     unsafe { msg_send![class!(NSArray), array] }
 }

crates/gpui/src/platform/test/platform.rs πŸ”—

@@ -25,6 +25,7 @@ pub(crate) struct TestPlatform {
     active_cursor: Mutex<CursorStyle>,
     current_clipboard_item: Mutex<Option<ClipboardItem>>,
     pub(crate) prompts: RefCell<TestPrompts>,
+    pub opened_url: RefCell<Option<String>>,
     weak: Weak<Self>,
 }
 
@@ -45,6 +46,7 @@ impl TestPlatform {
             active_window: Default::default(),
             current_clipboard_item: Mutex::new(None),
             weak: weak.clone(),
+            opened_url: Default::default(),
         })
     }
 
@@ -180,6 +182,10 @@ impl Platform for TestPlatform {
         Box::new(window)
     }
 
+    fn window_appearance(&self) -> WindowAppearance {
+        WindowAppearance::Light
+    }
+
     fn set_display_link_output_callback(
         &self,
         _display_id: DisplayId,
@@ -192,8 +198,8 @@ impl Platform for TestPlatform {
 
     fn stop_display_link(&self, _display_id: DisplayId) {}
 
-    fn open_url(&self, _url: &str) {
-        unimplemented!()
+    fn open_url(&self, url: &str) {
+        *self.opened_url.borrow_mut() = Some(url.to_string())
     }
 
     fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {

crates/gpui/src/platform/test/window.rs πŸ”—

@@ -171,7 +171,7 @@ impl PlatformWindow for TestWindow {
     }
 
     fn appearance(&self) -> WindowAppearance {
-        unimplemented!()
+        WindowAppearance::Light
     }
 
     fn display(&self) -> std::rc::Rc<dyn crate::PlatformDisplay> {
@@ -276,16 +276,12 @@ impl PlatformWindow for TestWindow {
         unimplemented!()
     }
 
-    fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {
-        unimplemented!()
-    }
+    fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {}
 
     fn is_topmost_for_position(&self, _position: crate::Point<Pixels>) -> bool {
         unimplemented!()
     }
 
-    fn invalidate(&self) {}
-
     fn draw(&self, _scene: &crate::Scene) {}
 
     fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {

crates/gpui/src/scene.rs πŸ”—

@@ -866,6 +866,3 @@ impl PathVertex<Pixels> {
         }
     }
 }
-
-#[derive(Copy, Clone, Debug)]
-pub(crate) struct AtlasId(pub(crate) usize);

crates/gpui/src/style.rs πŸ”—

@@ -197,6 +197,9 @@ pub struct TextStyle {
     /// The underline style of the text
     pub underline: Option<UnderlineStyle>,
 
+    /// The strikethrough style of the text
+    pub strikethrough: Option<StrikethroughStyle>,
+
     /// How to handle whitespace in the text
     pub white_space: WhiteSpace,
 }
@@ -214,6 +217,7 @@ impl Default for TextStyle {
             font_style: FontStyle::default(),
             background_color: None,
             underline: None,
+            strikethrough: None,
             white_space: WhiteSpace::Normal,
         }
     }
@@ -246,6 +250,10 @@ impl TextStyle {
             self.underline = Some(underline);
         }
 
+        if let Some(strikethrough) = style.strikethrough {
+            self.strikethrough = Some(strikethrough);
+        }
+
         self
     }
 
@@ -277,6 +285,7 @@ impl TextStyle {
             color: self.color,
             background_color: self.background_color,
             underline: self.underline,
+            strikethrough: self.strikethrough,
         }
     }
 }
@@ -300,6 +309,9 @@ pub struct HighlightStyle {
     /// The underline style of the text
     pub underline: Option<UnderlineStyle>,
 
+    /// The underline style of the text
+    pub strikethrough: Option<StrikethroughStyle>,
+
     /// Similar to the CSS `opacity` property, this will cause the text to be less vibrant.
     pub fade_out: Option<f32>,
 }
@@ -553,6 +565,17 @@ pub struct UnderlineStyle {
     pub wavy: bool,
 }
 
+/// The properties that can be applied to a strikethrough.
+#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Eq)]
+#[refineable(Debug)]
+pub struct StrikethroughStyle {
+    /// The thickness of the strikethrough.
+    pub thickness: Pixels,
+
+    /// The color of the strikethrough.
+    pub color: Option<Hsla>,
+}
+
 /// The kinds of fill that can be applied to a shape.
 #[derive(Clone, Debug)]
 pub enum Fill {
@@ -601,6 +624,7 @@ impl From<&TextStyle> for HighlightStyle {
             font_style: Some(other.font_style),
             background_color: other.background_color,
             underline: other.underline,
+            strikethrough: other.strikethrough,
             fade_out: None,
         }
     }
@@ -636,6 +660,10 @@ impl HighlightStyle {
             self.underline = other.underline;
         }
 
+        if other.strikethrough.is_some() {
+            self.strikethrough = other.strikethrough;
+        }
+
         match (other.fade_out, self.fade_out) {
             (Some(source_fade), None) => self.fade_out = Some(source_fade),
             (Some(source_fade), Some(dest_fade)) => {

crates/gpui/src/text_system.rs πŸ”—

@@ -10,11 +10,12 @@ pub use line_wrapper::*;
 
 use crate::{
     px, Bounds, DevicePixels, EntityId, Hsla, Pixels, PlatformTextSystem, Point, Result,
-    SharedString, Size, UnderlineStyle,
+    SharedString, Size, StrikethroughStyle, UnderlineStyle,
 };
 use anyhow::anyhow;
 use collections::{BTreeSet, FxHashMap, FxHashSet};
 use core::fmt;
+use derive_more::Deref;
 use itertools::Itertools;
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
 use smallvec::{smallvec, SmallVec};
@@ -38,9 +39,8 @@ pub struct FontFamilyId(pub usize);
 
 pub(crate) const SUBPIXEL_VARIANTS: u8 = 4;
 
-/// The GPUI text layout and rendering sub system.
+/// The GPUI text rendering sub system.
 pub struct TextSystem {
-    line_layout_cache: Arc<LineLayoutCache>,
     platform_text_system: Arc<dyn PlatformTextSystem>,
     font_ids_by_font: RwLock<FxHashMap<Font, Result<FontId>>>,
     font_metrics: RwLock<FxHashMap<FontId, FontMetrics>>,
@@ -53,7 +53,6 @@ pub struct TextSystem {
 impl TextSystem {
     pub(crate) fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
         TextSystem {
-            line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
             platform_text_system,
             font_metrics: RwLock::default(),
             raster_bounds: RwLock::default(),
@@ -234,43 +233,66 @@ impl TextSystem {
         }
     }
 
-    pub(crate) fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
-        self.line_layout_cache.with_view(view_id, f)
+    /// Returns a handle to a line wrapper, for the given font and font size.
+    pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
+        let lock = &mut self.wrapper_pool.lock();
+        let font_id = self.resolve_font(&font);
+        let wrappers = lock
+            .entry(FontIdWithSize { font_id, font_size })
+            .or_default();
+        let wrapper = wrappers.pop().unwrap_or_else(|| {
+            LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
+        });
+
+        LineWrapperHandle {
+            wrapper: Some(wrapper),
+            text_system: self.clone(),
+        }
     }
 
-    /// Layout the given line of text, at the given font_size.
-    /// Subsets of the line can be styled independently with the `runs` parameter.
-    /// Generally, you should prefer to use `TextLayout::shape_line` instead, which
-    /// can be painted directly.
-    pub fn layout_line(
-        &self,
-        text: &str,
-        font_size: Pixels,
-        runs: &[TextRun],
-    ) -> Result<Arc<LineLayout>> {
-        let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
-        for run in runs.iter() {
-            let font_id = self.resolve_font(&run.font);
-            if let Some(last_run) = font_runs.last_mut() {
-                if last_run.font_id == font_id {
-                    last_run.len += run.len;
-                    continue;
-                }
-            }
-            font_runs.push(FontRun {
-                len: run.len,
-                font_id,
-            });
+    /// Get the rasterized size and location of a specific, rendered glyph.
+    pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
+        let raster_bounds = self.raster_bounds.upgradable_read();
+        if let Some(bounds) = raster_bounds.get(params) {
+            Ok(*bounds)
+        } else {
+            let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
+            let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
+            raster_bounds.insert(params.clone(), bounds);
+            Ok(bounds)
         }
+    }
 
-        let layout = self
-            .line_layout_cache
-            .layout_line(text, font_size, &font_runs);
+    pub(crate) fn rasterize_glyph(
+        &self,
+        params: &RenderGlyphParams,
+    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
+        let raster_bounds = self.raster_bounds(params)?;
+        self.platform_text_system
+            .rasterize_glyph(params, raster_bounds)
+    }
+}
 
-        font_runs.clear();
-        self.font_runs_pool.lock().push(font_runs);
+/// The GPUI text layout subsystem.
+#[derive(Deref)]
+pub struct WindowTextSystem {
+    line_layout_cache: Arc<LineLayoutCache>,
+    #[deref]
+    text_system: Arc<TextSystem>,
+}
 
-        Ok(layout)
+impl WindowTextSystem {
+    pub(crate) fn new(text_system: Arc<TextSystem>) -> Self {
+        Self {
+            line_layout_cache: Arc::new(LineLayoutCache::new(
+                text_system.platform_text_system.clone(),
+            )),
+            text_system,
+        }
+    }
+
+    pub(crate) fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
+        self.line_layout_cache.with_view(view_id, f)
     }
 
     /// Shape the given line, at the given font_size, for painting to the screen.
@@ -295,6 +317,7 @@ impl TextSystem {
             if let Some(last_run) = decoration_runs.last_mut() {
                 if last_run.color == run.color
                     && last_run.underline == run.underline
+                    && last_run.strikethrough == run.strikethrough
                     && last_run.background_color == run.background_color
                 {
                     last_run.len += run.len as u32;
@@ -306,6 +329,7 @@ impl TextSystem {
                 color: run.color,
                 background_color: run.background_color,
                 underline: run.underline,
+                strikethrough: run.strikethrough,
             });
         }
 
@@ -360,6 +384,7 @@ impl TextSystem {
                 if decoration_runs.last().map_or(false, |last_run| {
                     last_run.color == run.color
                         && last_run.underline == run.underline
+                        && last_run.strikethrough == run.strikethrough
                         && last_run.background_color == run.background_color
                 }) {
                     decoration_runs.last_mut().unwrap().len += run_len_within_line as u32;
@@ -369,6 +394,7 @@ impl TextSystem {
                         color: run.color,
                         background_color: run.background_color,
                         underline: run.underline,
+                        strikethrough: run.strikethrough,
                     });
                 }
 
@@ -384,6 +410,7 @@ impl TextSystem {
             let layout = self
                 .line_layout_cache
                 .layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width);
+
             lines.push(WrappedLine {
                 layout,
                 decoration_runs,
@@ -429,43 +456,39 @@ impl TextSystem {
         self.line_layout_cache.finish_frame(reused_views)
     }
 
-    /// Returns a handle to a line wrapper, for the given font and font size.
-    pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
-        let lock = &mut self.wrapper_pool.lock();
-        let font_id = self.resolve_font(&font);
-        let wrappers = lock
-            .entry(FontIdWithSize { font_id, font_size })
-            .or_default();
-        let wrapper = wrappers.pop().unwrap_or_else(|| {
-            LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
-        });
-
-        LineWrapperHandle {
-            wrapper: Some(wrapper),
-            text_system: self.clone(),
+    /// Layout the given line of text, at the given font_size.
+    /// Subsets of the line can be styled independently with the `runs` parameter.
+    /// Generally, you should prefer to use `TextLayout::shape_line` instead, which
+    /// can be painted directly.
+    pub fn layout_line(
+        &self,
+        text: &str,
+        font_size: Pixels,
+        runs: &[TextRun],
+    ) -> Result<Arc<LineLayout>> {
+        let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
+        for run in runs.iter() {
+            let font_id = self.resolve_font(&run.font);
+            if let Some(last_run) = font_runs.last_mut() {
+                if last_run.font_id == font_id {
+                    last_run.len += run.len;
+                    continue;
+                }
+            }
+            font_runs.push(FontRun {
+                len: run.len,
+                font_id,
+            });
         }
-    }
 
-    /// Get the rasterized size and location of a specific, rendered glyph.
-    pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
-        let raster_bounds = self.raster_bounds.upgradable_read();
-        if let Some(bounds) = raster_bounds.get(params) {
-            Ok(*bounds)
-        } else {
-            let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
-            let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
-            raster_bounds.insert(params.clone(), bounds);
-            Ok(bounds)
-        }
-    }
+        let layout = self
+            .line_layout_cache
+            .layout_line(text, font_size, &font_runs);
 
-    pub(crate) fn rasterize_glyph(
-        &self,
-        params: &RenderGlyphParams,
-    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
-        let raster_bounds = self.raster_bounds(params)?;
-        self.platform_text_system
-            .rasterize_glyph(params, raster_bounds)
+        font_runs.clear();
+        self.font_runs_pool.lock().push(font_runs);
+
+        Ok(layout)
     }
 }
 
@@ -581,6 +604,8 @@ pub struct TextRun {
     pub background_color: Option<Hsla>,
     /// The underline style (if any)
     pub underline: Option<UnderlineStyle>,
+    /// The strikethrough style (if any)
+    pub strikethrough: Option<StrikethroughStyle>,
 }
 
 /// An identifier for a specific glyph, as returned by [`TextSystem::layout_line`].

crates/gpui/src/text_system/line.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{
     black, fill, point, px, size, Bounds, ElementContext, Hsla, LineLayout, Pixels, Point, Result,
-    SharedString, UnderlineStyle, WrapBoundary, WrappedLineLayout,
+    SharedString, StrikethroughStyle, UnderlineStyle, WrapBoundary, WrappedLineLayout,
 };
 use derive_more::{Deref, DerefMut};
 use smallvec::SmallVec;
@@ -20,6 +20,9 @@ pub struct DecorationRun {
 
     /// The underline style for this run
     pub underline: Option<UnderlineStyle>,
+
+    /// The strikethrough style for this run
+    pub strikethrough: Option<StrikethroughStyle>,
 }
 
 /// A line of text that has been shaped and decorated.
@@ -113,6 +116,7 @@ fn paint_line(
     let mut run_end = 0;
     let mut color = black();
     let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+    let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
     let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
     let text_system = cx.text_system().clone();
     let mut glyph_origin = origin;
@@ -145,6 +149,17 @@ fn paint_line(
                     underline_origin.x = origin.x;
                     underline_origin.y += line_height;
                 }
+                if let Some((strikethrough_origin, strikethrough_style)) =
+                    current_strikethrough.as_mut()
+                {
+                    cx.paint_strikethrough(
+                        *strikethrough_origin,
+                        glyph_origin.x - strikethrough_origin.x,
+                        strikethrough_style,
+                    );
+                    strikethrough_origin.x = origin.x;
+                    strikethrough_origin.y += line_height;
+                }
 
                 glyph_origin.x = origin.x;
                 glyph_origin.y += line_height;
@@ -153,6 +168,7 @@ fn paint_line(
 
             let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
             let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+            let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
             if glyph.index >= run_end {
                 if let Some(style_run) = decoration_runs.next() {
                     if let Some((_, background_color)) = &mut current_background {
@@ -183,6 +199,24 @@ fn paint_line(
                             },
                         ));
                     }
+                    if let Some((_, strikethrough_style)) = &mut current_strikethrough {
+                        if style_run.strikethrough.as_ref() != Some(strikethrough_style) {
+                            finished_strikethrough = current_strikethrough.take();
+                        }
+                    }
+                    if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
+                        current_strikethrough.get_or_insert((
+                            point(
+                                glyph_origin.x,
+                                glyph_origin.y
+                                    + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5),
+                            ),
+                            StrikethroughStyle {
+                                color: Some(run_strikethrough.color.unwrap_or(style_run.color)),
+                                thickness: run_strikethrough.thickness,
+                            },
+                        ));
+                    }
 
                     run_end += style_run.len as usize;
                     color = style_run.color;
@@ -190,6 +224,7 @@ fn paint_line(
                     run_end = layout.len;
                     finished_background = current_background.take();
                     finished_underline = current_underline.take();
+                    finished_strikethrough = current_strikethrough.take();
                 }
             }
 
@@ -211,6 +246,14 @@ fn paint_line(
                 );
             }
 
+            if let Some((strikethrough_origin, strikethrough_style)) = finished_strikethrough {
+                cx.paint_strikethrough(
+                    strikethrough_origin,
+                    glyph_origin.x - strikethrough_origin.x,
+                    &strikethrough_style,
+                );
+            }
+
             let max_glyph_bounds = Bounds {
                 origin: glyph_origin,
                 size: max_glyph_size,
@@ -263,5 +306,13 @@ fn paint_line(
         );
     }
 
+    if let Some((strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
+        cx.paint_strikethrough(
+            strikethrough_start,
+            last_line_end_x - strikethrough_start.x,
+            &strikethrough_style,
+        );
+    }
+
     Ok(())
 }

crates/gpui/src/text_system/line_wrapper.rs πŸ”—

@@ -143,7 +143,7 @@ impl Boundary {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{font, TestAppContext, TestDispatcher, TextRun, WrapBoundary};
+    use crate::{font, TestAppContext, TestDispatcher, TextRun, WindowTextSystem, WrapBoundary};
     use rand::prelude::*;
 
     #[test]
@@ -218,13 +218,14 @@ mod tests {
     #[crate::test]
     fn test_wrap_shaped_line(cx: &mut TestAppContext) {
         cx.update(|cx| {
-            let text_system = cx.text_system().clone();
+            let text_system = WindowTextSystem::new(cx.text_system().clone());
 
             let normal = TextRun {
                 len: 0,
                 font: font("Helvetica"),
                 color: Default::default(),
                 underline: Default::default(),
+                strikethrough: None,
                 background_color: None,
             };
             let bold = TextRun {
@@ -232,6 +233,7 @@ mod tests {
                 font: font("Helvetica").bold(),
                 color: Default::default(),
                 underline: Default::default(),
+                strikethrough: None,
                 background_color: None,
             };
 

crates/gpui/src/window.rs πŸ”—

@@ -2,12 +2,12 @@ use crate::{
     px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext,
     AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId,
     DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
-    Global, GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchMode,
-    KeymatchResult, Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton,
-    MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
-    PlatformWindow, Point, PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet,
-    Subscription, TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowBounds,
-    WindowOptions,
+    Global, GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult,
+    Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton, MouseMoveEvent,
+    MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point,
+    PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet, Subscription,
+    TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowAppearance, WindowBounds,
+    WindowOptions, WindowTextSystem,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::FxHashSet;
@@ -22,7 +22,7 @@ use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
     borrow::{Borrow, BorrowMut},
-    cell::RefCell,
+    cell::{Cell, RefCell},
     collections::hash_map::Entry,
     fmt::{Debug, Display},
     future::Future,
@@ -34,7 +34,7 @@ use std::{
         atomic::{AtomicUsize, Ordering::SeqCst},
         Arc,
     },
-    time::Duration,
+    time::{Duration, Instant},
 };
 use util::{measure, ResultExt};
 
@@ -251,6 +251,7 @@ pub struct Window {
     pub(crate) platform_window: Box<dyn PlatformWindow>,
     display_id: DisplayId,
     sprite_atlas: Arc<dyn PlatformAtlas>,
+    text_system: Arc<WindowTextSystem>,
     pub(crate) rem_size: Pixels,
     pub(crate) viewport_size: Size<Pixels>,
     layout_engine: Option<TaffyLayoutEngine>,
@@ -268,17 +269,17 @@ pub struct Window {
     scale_factor: f32,
     bounds: WindowBounds,
     bounds_observers: SubscriberSet<(), AnyObserver>,
-    active: bool,
-    pub(crate) dirty: bool,
+    appearance: WindowAppearance,
+    appearance_observers: SubscriberSet<(), AnyObserver>,
+    active: Rc<Cell<bool>>,
+    pub(crate) dirty: Rc<Cell<bool>>,
+    pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
     pub(crate) refreshing: bool,
     pub(crate) drawing: bool,
     activation_observers: SubscriberSet<(), AnyObserver>,
     pub(crate) focus: Option<FocusId>,
     focus_enabled: bool,
     pending_input: Option<PendingInput>,
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub(crate) focus_invalidated: bool,
 }
 
 #[derive(Default, Debug)]
@@ -290,10 +291,6 @@ struct PendingInput {
 }
 
 impl PendingInput {
-    fn is_noop(&self) -> bool {
-        self.bindings.is_empty() && (self.keystrokes.iter().all(|k| k.ime_key.is_none()))
-    }
-
     fn input(&self) -> String {
         self.keystrokes
             .iter()
@@ -337,13 +334,35 @@ impl Window {
         let content_size = platform_window.content_size();
         let scale_factor = platform_window.scale_factor();
         let bounds = platform_window.bounds();
+        let appearance = platform_window.appearance();
+        let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone()));
+        let dirty = Rc::new(Cell::new(true));
+        let active = Rc::new(Cell::new(false));
+        let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
 
         platform_window.on_request_frame(Box::new({
             let mut cx = cx.to_async();
+            let dirty = dirty.clone();
+            let active = active.clone();
+            let last_input_timestamp = last_input_timestamp.clone();
             move || {
-                measure("frame duration", || {
-                    handle.update(&mut cx, |_, cx| cx.draw()).log_err();
-                })
+                if dirty.get() {
+                    measure("frame duration", || {
+                        handle
+                            .update(&mut cx, |_, cx| {
+                                cx.draw();
+                                cx.present();
+                            })
+                            .log_err();
+                    })
+                }
+                // Keep presenting the current scene for 1 extra second since the
+                // last input to prevent the display from underclocking the refresh rate.
+                else if active.get()
+                    && last_input_timestamp.get().elapsed() < Duration::from_secs(1)
+                {
+                    handle.update(&mut cx, |_, cx| cx.present()).log_err();
+                }
             }
         }));
         platform_window.on_resize(Box::new({
@@ -362,12 +381,20 @@ impl Window {
                     .log_err();
             }
         }));
+        platform_window.on_appearance_changed(Box::new({
+            let mut cx = cx.to_async();
+            move || {
+                handle
+                    .update(&mut cx, |_, cx| cx.appearance_changed())
+                    .log_err();
+            }
+        }));
         platform_window.on_active_status_change(Box::new({
             let mut cx = cx.to_async();
             move |active| {
                 handle
                     .update(&mut cx, |_, cx| {
-                        cx.window.active = active;
+                        cx.window.active.set(active);
                         cx.window
                             .activation_observers
                             .clone()
@@ -393,6 +420,7 @@ impl Window {
             platform_window,
             display_id,
             sprite_atlas,
+            text_system,
             rem_size: px(16.),
             viewport_size: content_size,
             layout_engine: Some(TaffyLayoutEngine::new()),
@@ -410,19 +438,25 @@ impl Window {
             scale_factor,
             bounds,
             bounds_observers: SubscriberSet::new(),
-            active: false,
-            dirty: false,
+            appearance,
+            appearance_observers: SubscriberSet::new(),
+            active,
+            dirty,
+            last_input_timestamp,
             refreshing: false,
             drawing: false,
             activation_observers: SubscriberSet::new(),
             focus: None,
             focus_enabled: true,
             pending_input: None,
-
-            #[cfg(any(test, feature = "test-support"))]
-            focus_invalidated: false,
         }
     }
+    fn new_focus_listener(
+        &mut self,
+        value: AnyWindowFocusListener,
+    ) -> (Subscription, impl FnOnce()) {
+        self.focus_listeners.insert((), value)
+    }
 }
 
 /// Indicates which region of the window is visible. Content falling outside of this mask will not be
@@ -472,7 +506,7 @@ impl<'a> WindowContext<'a> {
     pub fn refresh(&mut self) {
         if !self.window.drawing {
             self.window.refreshing = true;
-            self.window.dirty = true;
+            self.window.dirty.set(true);
         }
     }
 
@@ -505,12 +539,6 @@ impl<'a> WindowContext<'a> {
             .rendered_frame
             .dispatch_tree
             .clear_pending_keystrokes();
-
-        #[cfg(any(test, feature = "test-support"))]
-        {
-            self.window.focus_invalidated = true;
-        }
-
         self.refresh();
     }
 
@@ -530,6 +558,11 @@ impl<'a> WindowContext<'a> {
         self.window.focus_enabled = false;
     }
 
+    /// Accessor for the text system.
+    pub fn text_system(&self) -> &Arc<WindowTextSystem> {
+        &self.window.text_system
+    }
+
     /// Dispatch the given action on the currently focused element.
     pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
         let focus_handle = self.focused();
@@ -608,7 +641,7 @@ impl<'a> WindowContext<'a> {
         let entity_id = entity.entity_id();
         let entity = entity.downgrade();
         let window_handle = self.window.handle;
-        let (subscription, activate) = self.app.event_listeners.insert(
+        self.app.new_subscription(
             entity_id,
             (
                 TypeId::of::<Evt>(),
@@ -626,9 +659,7 @@ impl<'a> WindowContext<'a> {
                         .unwrap_or(false)
                 }),
             ),
-        );
-        self.app.defer(move |_| activate());
-        subscription
+        )
     }
 
     /// Creates an [`AsyncWindowContext`], which has a static lifetime and can be held across
@@ -734,6 +765,20 @@ impl<'a> WindowContext<'a> {
         self.window.bounds
     }
 
+    fn appearance_changed(&mut self) {
+        self.window.appearance = self.window.platform_window.appearance();
+
+        self.window
+            .appearance_observers
+            .clone()
+            .retain(&(), |callback| callback(self));
+    }
+
+    /// Returns the appearance of the current window.
+    pub fn appearance(&self) -> WindowAppearance {
+        self.window.appearance
+    }
+
     /// Returns the size of the drawable area within the window.
     pub fn viewport_size(&self) -> Size<Pixels> {
         self.window.viewport_size
@@ -741,7 +786,7 @@ impl<'a> WindowContext<'a> {
 
     /// Returns whether this window is focused by the operating system (receiving key events).
     pub fn is_window_active(&self) -> bool {
-        self.window.active
+        self.window.active.get()
     }
 
     /// Toggle zoom on the window.
@@ -927,16 +972,12 @@ impl<'a> WindowContext<'a> {
         &self.window.next_frame.z_index_stack
     }
 
-    /// Draw pixels to the display for this window based on the contents of its scene.
+    /// 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) {
-        self.window.dirty = false;
+        self.window.dirty.set(false);
         self.window.drawing = true;
 
-        #[cfg(any(test, feature = "test-support"))]
-        {
-            self.window.focus_invalidated = false;
-        }
-
         if let Some(requested_handler) = self.window.rendered_frame.requested_input_handler.as_mut()
         {
             let input_handler = self.window.platform_window.take_input_handler();
@@ -1001,7 +1042,7 @@ impl<'a> WindowContext<'a> {
                 self.window.focus,
             );
         self.window.next_frame.focus = self.window.focus;
-        self.window.next_frame.window_active = self.window.active;
+        self.window.next_frame.window_active = self.window.active.get();
         self.window.root_view = Some(root_view);
 
         // Set the cursor only if we're the active window.
@@ -1070,16 +1111,19 @@ impl<'a> WindowContext<'a> {
                 .clone()
                 .retain(&(), |listener| listener(&event, self));
         }
+        self.window.refreshing = false;
+        self.window.drawing = false;
+    }
 
+    fn present(&self) {
         self.window
             .platform_window
             .draw(&self.window.rendered_frame.scene);
-        self.window.refreshing = false;
-        self.window.drawing = false;
     }
 
     /// Dispatch a mouse or keyboard event on the window.
     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`.
         self.app.propagate_event = true;
         // Handlers may set this to true by calling `prevent_default`.
@@ -1243,21 +1287,12 @@ impl<'a> WindowContext<'a> {
             .dispatch_path(node_id);
 
         if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
-            let KeymatchResult {
-                bindings,
-                mut pending,
-            } = self
+            let KeymatchResult { bindings, pending } = self
                 .window
                 .rendered_frame
                 .dispatch_tree
                 .dispatch_key(&key_down_event.keystroke, &dispatch_path);
 
-            if self.window.rendered_frame.dispatch_tree.keymatch_mode == KeymatchMode::Immediate
-                && !bindings.is_empty()
-            {
-                pending = false;
-            }
-
             if pending {
                 let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
                 if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus
@@ -1272,22 +1307,17 @@ impl<'a> WindowContext<'a> {
                     currently_pending.bindings.push(binding);
                 }
 
-                // for vim compatibility, we also should check "is input handler enabled"
-                if !currently_pending.is_noop() {
-                    currently_pending.timer = Some(self.spawn(|mut cx| async move {
-                        cx.background_executor.timer(Duration::from_secs(1)).await;
-                        cx.update(move |cx| {
-                            cx.clear_pending_keystrokes();
-                            let Some(currently_pending) = cx.window.pending_input.take() else {
-                                return;
-                            };
-                            cx.replay_pending_input(currently_pending)
-                        })
-                        .log_err();
-                    }));
-                } else {
-                    currently_pending.timer = None;
-                }
+                currently_pending.timer = Some(self.spawn(|mut cx| async move {
+                    cx.background_executor.timer(Duration::from_secs(1)).await;
+                    cx.update(move |cx| {
+                        cx.clear_pending_keystrokes();
+                        let Some(currently_pending) = cx.window.pending_input.take() else {
+                            return;
+                        };
+                        cx.replay_pending_input(currently_pending)
+                    })
+                    .log_err();
+                }));
                 self.window.pending_input = Some(currently_pending);
 
                 self.propagate_event = false;
@@ -1315,8 +1345,21 @@ impl<'a> WindowContext<'a> {
             }
         }
 
+        self.dispatch_key_down_up_event(event, &dispatch_path);
+        if !self.propagate_event {
+            return;
+        }
+
+        self.dispatch_keystroke_observers(event, None);
+    }
+
+    fn dispatch_key_down_up_event(
+        &mut self,
+        event: &dyn Any,
+        dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
+    ) {
         // Capture phase
-        for node_id in &dispatch_path {
+        for node_id in dispatch_path {
             let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
 
             for key_listener in node.key_listeners.clone() {
@@ -1342,8 +1385,6 @@ impl<'a> WindowContext<'a> {
                 }
             }
         }
-
-        self.dispatch_keystroke_observers(event, None);
     }
 
     /// Determine whether a potential multi-stroke key binding is in progress on this window.
@@ -1380,6 +1421,24 @@ impl<'a> WindowContext<'a> {
             }
         }
 
+        let dispatch_path = self
+            .window
+            .rendered_frame
+            .dispatch_tree
+            .dispatch_path(node_id);
+
+        for keystroke in currently_pending.keystrokes {
+            let event = KeyDownEvent {
+                keystroke,
+                is_held: false,
+            };
+
+            self.dispatch_key_down_up_event(&event, &dispatch_path);
+            if !self.propagate_event {
+                return;
+            }
+        }
+
         if !input.is_empty() {
             if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
                 input_handler.flush_pending_input(&input, self);
@@ -1692,13 +1751,15 @@ impl VisualContext for WindowContext<'_> {
         let entity = build_view_state(&mut cx);
         cx.entities.insert(slot, entity);
 
-        cx.new_view_observers
-            .clone()
-            .retain(&TypeId::of::<V>(), |observer| {
-                let any_view = AnyView::from(view.clone());
-                (observer)(any_view, self);
+        // Non-generic part to avoid leaking SubscriberSet to invokers of `new_view`.
+        fn notify_observers(cx: &mut WindowContext, tid: TypeId, view: AnyView) {
+            cx.new_view_observers.clone().retain(&tid, |observer| {
+                let any_view = view.clone();
+                (observer)(any_view, cx);
                 true
             });
+        }
+        notify_observers(self, TypeId::of::<V>(), AnyView::from(view.clone()));
 
         view
     }
@@ -1900,7 +1961,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         let entity_id = entity.entity_id();
         let entity = entity.downgrade();
         let window_handle = self.window.handle;
-        let (subscription, activate) = self.app.observers.insert(
+        self.app.new_observer(
             entity_id,
             Box::new(move |cx| {
                 window_handle
@@ -1914,9 +1975,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                     })
                     .unwrap_or(false)
             }),
-        );
-        self.app.defer(move |_| activate());
-        subscription
+        )
     }
 
     /// Subscribe to events emitted by another model or view.
@@ -1936,7 +1995,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         let entity_id = entity.entity_id();
         let handle = entity.downgrade();
         let window_handle = self.window.handle;
-        let (subscription, activate) = self.app.event_listeners.insert(
+        self.app.new_subscription(
             entity_id,
             (
                 TypeId::of::<Evt>(),
@@ -1954,9 +2013,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                         .unwrap_or(false)
                 }),
             ),
-        );
-        self.app.defer(move |_| activate());
-        subscription
+        )
     }
 
     /// Register a callback to be invoked when the view is released.
@@ -2023,7 +2080,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         }
 
         if !self.window.drawing {
-            self.window_cx.window.dirty = true;
+            self.window_cx.window.dirty.set(true);
             self.window_cx.app.push_effect(Effect::Notify {
                 emitter: self.view.model.entity_id,
             });
@@ -2058,6 +2115,20 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         subscription
     }
 
+    /// Registers a callback to be invoked when the window appearance changes.
+    pub fn observe_window_appearance(
+        &mut self,
+        mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
+    ) -> Subscription {
+        let view = self.view.downgrade();
+        let (subscription, activate) = self.window.appearance_observers.insert(
+            (),
+            Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
+        );
+        activate();
+        subscription
+    }
+
     /// Register a listener to be called when the given focus handle receives focus.
     /// Returns a subscription and persists until the subscription is dropped.
     pub fn on_focus(
@@ -2067,9 +2138,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        let (subscription, activate) = self.window.focus_listeners.insert(
-            (),
-            Box::new(move |event, cx| {
+        let (subscription, activate) =
+            self.window.new_focus_listener(Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
                     if event.previous_focus_path.last() != Some(&focus_id)
                         && event.current_focus_path.last() == Some(&focus_id)
@@ -2078,9 +2148,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                     }
                 })
                 .is_ok()
-            }),
-        );
-        self.app.defer(move |_| activate());
+            }));
+        self.app.defer(|_| activate());
         subscription
     }
 
@@ -2093,9 +2162,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        let (subscription, activate) = self.window.focus_listeners.insert(
-            (),
-            Box::new(move |event, cx| {
+        let (subscription, activate) =
+            self.window.new_focus_listener(Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
                     if !event.previous_focus_path.contains(&focus_id)
                         && event.current_focus_path.contains(&focus_id)
@@ -2104,8 +2172,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                     }
                 })
                 .is_ok()
-            }),
-        );
+            }));
         self.app.defer(move |_| activate());
         subscription
     }
@@ -2119,9 +2186,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        let (subscription, activate) = self.window.focus_listeners.insert(
-            (),
-            Box::new(move |event, cx| {
+        let (subscription, activate) =
+            self.window.new_focus_listener(Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
                     if event.previous_focus_path.last() == Some(&focus_id)
                         && event.current_focus_path.last() != Some(&focus_id)
@@ -2130,8 +2196,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                     }
                 })
                 .is_ok()
-            }),
-        );
+            }));
         self.app.defer(move |_| activate());
         subscription
     }
@@ -2162,9 +2227,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        let (subscription, activate) = self.window.focus_listeners.insert(
-            (),
-            Box::new(move |event, cx| {
+        let (subscription, activate) =
+            self.window.new_focus_listener(Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
                     if event.previous_focus_path.contains(&focus_id)
                         && !event.current_focus_path.contains(&focus_id)
@@ -2173,8 +2237,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                     }
                 })
                 .is_ok()
-            }),
-        );
+            }));
         self.app.defer(move |_| activate());
         subscription
     }
@@ -2489,7 +2552,7 @@ impl<V: 'static + Render> WindowHandle<V> {
     pub fn is_active(&self, cx: &AppContext) -> Option<bool> {
         cx.windows
             .get(self.id)
-            .and_then(|window| window.as_ref().map(|window| window.active))
+            .and_then(|window| window.as_ref().map(|window| window.active.get()))
     }
 }
 

crates/gpui/src/window/element_cx.rs πŸ”—

@@ -32,11 +32,11 @@ use crate::{
     prelude::*, size, AnyTooltip, AppContext, AvailableSpace, Bounds, BoxShadow, ContentMask,
     Corners, CursorStyle, DevicePixels, DispatchPhase, DispatchTree, ElementId, ElementStateBox,
     EntityId, FocusHandle, FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
-    InputHandler, IsZero, KeyContext, KeyEvent, KeymatchMode, LayoutId, MonochromeSprite,
-    MouseEvent, PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad,
-    RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size,
-    StackingContext, StackingOrder, Style, TextStyleRefinement, Underline, UnderlineStyle, Window,
-    WindowContext, SUBPIXEL_VARIANTS,
+    InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, MonochromeSprite, MouseEvent, PaintQuad,
+    Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
+    RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StackingContext,
+    StackingOrder, StrikethroughStyle, Style, TextStyleRefinement, Underline, UnderlineStyle,
+    Window, WindowContext, SUBPIXEL_VARIANTS,
 };
 
 type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut ElementContext) + 'static>;
@@ -72,7 +72,7 @@ pub(crate) struct Frame {
     pub(crate) reused_views: FxHashSet<EntityId>,
 
     #[cfg(any(test, feature = "test-support"))]
-    pub(crate) debug_bounds: collections::FxHashMap<String, Bounds<Pixels>>,
+    pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
 }
 
 impl Frame {
@@ -760,6 +760,38 @@ impl<'a> ElementContext<'a> {
         );
     }
 
+    /// Paint a strikethrough into the scene for the next frame at the current z-index.
+    pub fn paint_strikethrough(
+        &mut self,
+        origin: Point<Pixels>,
+        width: Pixels,
+        style: &StrikethroughStyle,
+    ) {
+        let scale_factor = self.scale_factor();
+        let height = style.thickness;
+        let bounds = Bounds {
+            origin,
+            size: size(width, height),
+        };
+        let content_mask = self.content_mask();
+        let view_id = self.parent_view_id();
+
+        let window = &mut *self.window;
+        window.next_frame.scene.insert(
+            &window.next_frame.z_index_stack,
+            Underline {
+                view_id: view_id.into(),
+                layer_id: 0,
+                order: 0,
+                bounds: bounds.scale(scale_factor),
+                content_mask: content_mask.scale(scale_factor),
+                thickness: style.thickness.scale(scale_factor),
+                color: style.color.unwrap_or_default(),
+                wavy: false,
+            },
+        );
+    }
+
     /// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index.
     ///
     /// The y component of the origin is the baseline of the glyph.
@@ -1148,15 +1180,6 @@ impl<'a> ElementContext<'a> {
         }
     }
 
-    /// keymatch mode immediate instructs GPUI to prefer shorter action bindings.
-    /// In the case that you have a keybinding of `"cmd-k": "terminal::Clear"` and
-    /// `"cmd-k left": "workspace::MoveLeft"`, GPUI will by default wait for 1s after
-    /// you type cmd-k to see if you're going to type left.
-    /// This is problematic in the terminal
-    pub fn keymatch_mode_immediate(&mut self) {
-        self.window.next_frame.dispatch_tree.keymatch_mode = KeymatchMode::Immediate;
-    }
-
     /// Register a mouse event listener on the window for the next frame. The type of event
     /// is determined by the first parameter of the given listener. When the next frame is rendered
     /// the listener will be cleared.

crates/install_cli/Cargo.toml πŸ”—

@@ -13,7 +13,7 @@ test-support = []
 
 [dependencies]
 anyhow.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 log.workspace = true
 smol.workspace = true
-util = { path = "../util" }
+util.workspace = true

crates/journal/Cargo.toml πŸ”—

@@ -13,15 +13,15 @@ doctest = false
 anyhow.workspace = true
 chrono = "0.4"
 dirs = "4.0"
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
+editor.workspace = true
+gpui.workspace = true
 log.workspace = true
 schemars.workspace = true
 serde.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 shellexpand = "2.1.0"
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }

crates/language/Cargo.toml πŸ”—

@@ -26,50 +26,50 @@ test-support = [
 anyhow.workspace = true
 async-broadcast = "0.4"
 async-trait.workspace = true
-clock = { path = "../clock" }
-collections = { path = "../collections" }
+clock.workspace = true
+collections.workspace = true
 futures.workspace = true
-fuzzy = { path = "../fuzzy" }
-git = { path = "../git" }
+fuzzy.workspace = true
+git.workspace = true
 globset.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 lazy_static.workspace = true
 log.workspace = true
-lsp = { path = "../lsp" }
+lsp.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 rand = { workspace = true, optional = true }
 regex.workspace = true
-rpc = { path = "../rpc" }
+rpc.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 similar = "1.3"
 smallvec.workspace = true
 smol.workspace = true
-sum_tree = { path = "../sum_tree" }
-text = { path = "../text" }
-theme = { path = "../theme" }
+sum_tree.workspace = true
+text.workspace = true
+theme.workspace = true
 tree-sitter-rust = { workspace = true, optional = true }
 tree-sitter-typescript = { workspace = true, optional = true }
 pulldown-cmark.workspace = true
 tree-sitter.workspace = true
 unicase = "2.6"
-util = { path = "../util" }
+util.workspace = true
 
 [dev-dependencies]
-client = { path = "../client", features = ["test-support"] }
-collections = { path = "../collections", features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
 ctor.workspace = true
 env_logger.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 indoc.workspace = true
-lsp = { path = "../lsp", features = ["test-support"] }
+lsp = { workspace = true, features = ["test-support"] }
 rand.workspace = true
-settings = { path = "../settings", features = ["test-support"] }
-text = { path = "../text", features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
+text = { workspace = true, features = ["test-support"] }
 tree-sitter-elixir.workspace = true
 tree-sitter-embedded-template.workspace = true
 tree-sitter-heex.workspace = true
@@ -81,4 +81,4 @@ tree-sitter-ruby.workspace = true
 tree-sitter-rust.workspace = true
 tree-sitter-typescript.workspace = true
 unindent.workspace = true
-util = { path = "../util", features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/language/src/buffer.rs πŸ”—

@@ -2993,6 +2993,11 @@ impl BufferSnapshot {
         self.git_diff.hunks_intersecting_range_rev(range, self)
     }
 
+    /// Returns if the buffer contains any diagnostics.
+    pub fn has_diagnostics(&self) -> bool {
+        !self.diagnostics.is_empty()
+    }
+
     /// Returns all the diagnostics intersecting the given range.
     pub fn diagnostics_in_range<'a, T, O>(
         &'a self,

crates/language_selector/Cargo.toml πŸ”—

@@ -11,17 +11,17 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-editor = { path = "../editor" }
-fuzzy = {  path = "../fuzzy" }
-gpui = { path = "../gpui" }
-language = { path = "../language" }
-picker = { path = "../picker" }
-project = { path = "../project" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+editor.workspace = true
+fuzzy.workspace = true
+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
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }

crates/language_tools/Cargo.toml πŸ”—

@@ -11,27 +11,27 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-collections = { path = "../collections" }
-editor = { path = "../editor" }
+collections.workspace = true
+editor.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
-language = { path = "../language" }
-lsp = { path = "../lsp" }
-project = { path = "../project" }
+gpui.workspace = true
+language.workspace = true
+lsp.workspace = true
+project.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
-theme = { path = "../theme" }
+settings.workspace = true
+theme.workspace = true
 tree-sitter.workspace = true
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-client = { path = "../client", features = ["test-support"] }
-editor = { path = "../editor", features = ["test-support"] }
-release_channel = { path = "../release_channel" }
+client = { workspace = true, features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
+release_channel.workspace = true
 env_logger.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 unindent.workspace = true
-util = { path = "../util", features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/live_kit_client/Cargo.toml πŸ”—

@@ -26,13 +26,13 @@ test-support = [
 anyhow.workspace = true
 async-broadcast = "0.4"
 async-trait = { workspace = true, optional = true }
-collections = { path = "../collections", optional = true }
+collections = { workspace = true, optional = true }
 futures.workspace = true
-gpui = { path = "../gpui", optional = true }
-live_kit_server = { path = "../live_kit_server", optional = true }
+gpui = { workspace = true, optional = true }
+live_kit_server = { workspace = true, optional = true }
 log.workspace = true
-media = { path = "../media" }
-nanoid = "0.4" #TODO: optional
+media.workspace = true
+nanoid = { version ="0.4", optional = true}
 parking_lot.workspace = true
 postage.workspace = true
 
@@ -41,9 +41,9 @@ core-foundation = "0.9.3"
 
 [target.'cfg(not(target_os = "macos"))'.dependencies]
 async-trait = { workspace = true }
-collections = { path = "../collections", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-live_kit_server = { path = "../live_kit_server" }
+collections = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+live_kit_server.workspace = true
 
 [dev-dependencies]
 anyhow.workspace = true
@@ -51,13 +51,14 @@ async-trait.workspace = true
 block = "0.1"
 byteorder = "1.4"
 bytes = "1.2"
-collections = { path = "../collections", features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
+foreign-types = "0.3"
 futures.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 hmac = "0.12"
 jwt = "0.16"
-live_kit_server = { path = "../live_kit_server" }
-media = { path = "../media" }
+live_kit_server.workspace = true
+media.workspace = true
 nanoid = "0.4"
 parking_lot.workspace = true
 serde.workspace = true

crates/lsp/Cargo.toml πŸ”—

@@ -15,9 +15,9 @@ test-support = ["async-pipe"]
 [dependencies]
 anyhow.workspace = true
 async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
-collections = { path = "../collections" }
+collections.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 log.workspace = true
 lsp-types = { git = "https://github.com/zed-industries/lsp-types", branch = "updated-completion-list-item-defaults" }
 parking_lot.workspace = true
@@ -26,13 +26,13 @@ serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
 smol.workspace = true
-util = { path = "../util" }
-release_channel = { path = "../release_channel" }
+util.workspace = true
+release_channel.workspace = true
 
 [dev-dependencies]
 async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
 ctor.workspace = true
 env_logger.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 unindent.workspace = true
-util = { path = "../util", features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/lsp/src/lsp.rs πŸ”—

@@ -332,17 +332,44 @@ impl LanguageServer {
             };
 
             let header = std::str::from_utf8(&buffer)?;
-            let message_len: usize = header
+            let mut segments = header.lines();
+
+            let message_len: usize = segments
+                .next()
+                .with_context(|| {
+                    format!("unable to find the first line of the LSP message header `{header}`")
+                })?
                 .strip_prefix(CONTENT_LEN_HEADER)
-                .ok_or_else(|| anyhow!("invalid LSP message header {header:?}"))?
-                .trim_end()
-                .parse()?;
+                .with_context(|| format!("invalid LSP message header `{header}`"))?
+                .parse()
+                .with_context(|| {
+                    format!("failed to parse Content-Length of LSP message header: `{header}`")
+                })?;
+
+            if let Some(second_segment) = segments.next() {
+                match second_segment {
+                    "" => (), // Header end
+                    header_field => {
+                        if header_field.starts_with("Content-Type:") {
+                            stdout.read_until(b'\n', &mut buffer).await?;
+                        } else {
+                            anyhow::bail!(
+                                "inside `{header}`, expected a Content-Type header field or a header ending CRLF, got `{second_segment:?}`"
+                            )
+                        }
+                    }
+                }
+            } else {
+                anyhow::bail!(
+                    "unable to find the second line of the LSP message header `{header}`"
+                );
+            }
 
             buffer.resize(message_len, 0);
             stdout.read_exact(&mut buffer).await?;
 
             if let Ok(message) = str::from_utf8(&buffer) {
-                log::trace!("incoming message: {}", message);
+                log::trace!("incoming message: {message}");
                 for handler in io_handlers.lock().values_mut() {
                     handler(IoKind::StdOut, message);
                 }
@@ -533,7 +560,10 @@ impl LanguageServer {
                         completion_item: Some(CompletionItemCapability {
                             snippet_support: Some(true),
                             resolve_support: Some(CompletionItemCapabilityResolveSupport {
-                                properties: vec!["additionalTextEdits".to_string()],
+                                properties: vec![
+                                    "documentation".to_string(),
+                                    "additionalTextEdits".to_string(),
+                                ],
                             }),
                             ..Default::default()
                         }),

crates/markdown_preview/Cargo.toml πŸ”—

@@ -13,19 +13,19 @@ test-support = []
 
 [dependencies]
 anyhow.workspace = true
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
-language = { path = "../language" }
+editor.workspace = true
+gpui.workspace = true
+language.workspace = true
 lazy_static.workspace = true
 log.workspace = true
-menu = { path = "../menu" }
-project = { path = "../project" }
+menu.workspace = true
+project.workspace = true
 pulldown-cmark.workspace = true
-rich_text = { path = "../rich_text" }
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+rich_text.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }

crates/menu/Cargo.toml πŸ”—

@@ -10,5 +10,5 @@ path = "src/menu.rs"
 doctest = false
 
 [dependencies]
-gpui = { path = "../gpui" }
+gpui.workspace = true
 serde = { workspace = true }

crates/multi_buffer/Cargo.toml πŸ”—

@@ -23,54 +23,54 @@ test-support = [
 [dependencies]
 aho-corasick = "1.1"
 anyhow.workspace = true
-client = { path = "../client" }
-clock = { path = "../clock" }
-collections = { path = "../collections" }
+client.workspace = true
+clock.workspace = true
+collections.workspace = true
 convert_case = "0.6.0"
 futures.workspace = true
-git = { path = "../git" }
-gpui = { path = "../gpui" }
+git.workspace = true
+gpui.workspace = true
 indoc = "1.0.4"
 itertools = "0.10"
-language = { path = "../language" }
+language.workspace = true
 lazy_static.workspace = true
 log.workspace = true
-lsp = { path = "../lsp" }
+lsp.workspace = true
 ordered-float.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 pulldown-cmark.workspace = true
 rand.workspace = true
-rich_text = { path = "../rich_text" }
+rich_text.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smallvec.workspace = true
 smol.workspace = true
-snippet = { path = "../snippet" }
-sum_tree = { path = "../sum_tree" }
-text = { path = "../text" }
-theme = { path = "../theme" }
+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 = { path = "../util" }
+util.workspace = true
 
 [dev-dependencies]
-copilot = { path = "../copilot", features = ["test-support"] }
+copilot = { workspace = true, features = ["test-support"] }
 ctor.workspace = true
 env_logger.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
-lsp = { path = "../lsp", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
+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 = { path = "../settings", features = ["test-support"] }
-text = { path = "../text", features = ["test-support"] }
+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 = { path = "../util", features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/multi_buffer/src/multi_buffer.rs πŸ”—

@@ -3052,6 +3052,12 @@ impl MultiBufferSnapshot {
         self.has_conflict
     }
 
+    pub fn has_diagnostics(&self) -> bool {
+        self.excerpts
+            .iter()
+            .any(|excerpt| excerpt.buffer.has_diagnostics())
+    }
+
     pub fn diagnostic_group<'a, O>(
         &'a self,
         group_id: usize,

crates/node_runtime/Cargo.toml πŸ”—

@@ -21,4 +21,4 @@ serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
 smol.workspace = true
-util = { path = "../util" }
+util.workspace = true

crates/notifications/Cargo.toml πŸ”—

@@ -19,24 +19,24 @@ test-support = [
 
 [dependencies]
 anyhow.workspace = true
-channel = { path = "../channel" }
-client = { path = "../client" }
-clock = { path = "../clock" }
-collections = { path = "../collections" }
-db = { path = "../db" }
-feature_flags = { path = "../feature_flags" }
-gpui = { path = "../gpui" }
-rpc = { path = "../rpc" }
-settings = { path = "../settings" }
-sum_tree = { path = "../sum_tree" }
-text = { path = "../text" }
+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 = { path = "../util" }
+util.workspace = true
 
 [dev-dependencies]
-client = { path = "../client", features = ["test-support"] }
-collections = { path = "../collections", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-rpc = { path = "../rpc", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
-util = { path = "../util", features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+rpc = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/outline/Cargo.toml πŸ”—

@@ -10,20 +10,20 @@ path = "src/outline.rs"
 doctest = false
 
 [dependencies]
-editor = { path = "../editor" }
-fuzzy = {  path = "../fuzzy" }
-gpui = { path = "../gpui" }
-language = { path = "../language" }
+editor.workspace = true
+fuzzy.workspace = true
+gpui.workspace = true
+language.workspace = true
 ordered-float.workspace = true
-picker = { path = "../picker" }
+picker.workspace = true
 postage.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smol.workspace = true
-text = { path = "../text" }
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+text.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }

crates/outline/src/outline.rs πŸ”—

@@ -282,6 +282,7 @@ impl PickerDelegate for OutlineViewDelegate {
             line_height: relative(1.).into(),
             background_color: None,
             underline: None,
+            strikethrough: None,
             white_space: WhiteSpace::Normal,
         };
 

crates/picker/Cargo.toml πŸ”—

@@ -10,19 +10,19 @@ path = "src/picker.rs"
 doctest = false
 
 [dependencies]
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
-menu = { path = "../menu" }
+editor.workspace = true
+gpui.workspace = true
+menu.workspace = true
 parking_lot.workspace = true
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+settings.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 serde_json.workspace = true

crates/picker/src/picker.rs πŸ”—

@@ -58,13 +58,16 @@ impl<D: PickerDelegate> FocusableView for Picker<D> {
     }
 }
 
+fn create_editor(placeholder: Arc<str>, cx: &mut WindowContext<'_>) -> View<Editor> {
+    cx.new_view(|cx| {
+        let mut editor = Editor::single_line(cx);
+        editor.set_placeholder_text(placeholder, cx);
+        editor
+    })
+}
 impl<D: PickerDelegate> Picker<D> {
     pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
-        let editor = cx.new_view(|cx| {
-            let mut editor = Editor::single_line(cx);
-            editor.set_placeholder_text(delegate.placeholder_text(), cx);
-            editor
-        });
+        let editor = create_editor(delegate.placeholder_text(), cx);
         cx.subscribe(&editor, Self::on_input_editor_event).detach();
         let mut this = Self {
             delegate,

crates/plugin/Cargo.toml πŸ”—

@@ -7,6 +7,6 @@ license = "GPL-3.0-or-later"
 
 [dependencies]
 bincode = "1.3"
-plugin_macros = { path = "../plugin_macros" }
+plugin_macros.workspace = true
 serde.workspace = true
 serde_derive.workspace = true

crates/prettier/Cargo.toml πŸ”—

@@ -14,22 +14,22 @@ test-support = []
 
 [dependencies]
 anyhow.workspace = true
-client = { path = "../client" }
-collections = { path = "../collections" }
-fs = { path = "../fs" }
+client.workspace = true
+collections.workspace = true
+fs.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
-language = { path = "../language" }
+gpui.workspace = true
+language.workspace = true
 log.workspace = true
-lsp = { path = "../lsp" }
-node_runtime = { path = "../node_runtime" }
+lsp.workspace = true
+node_runtime.workspace = true
 parking_lot.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-util = { path = "../util" }
+util.workspace = true
 
 [dev-dependencies]
-fs = { path = "../fs",  features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
+fs = { workspace = true,  features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }

crates/project/Cargo.toml πŸ”—

@@ -25,62 +25,62 @@ aho-corasick = "1.1"
 anyhow.workspace = true
 async-trait.workspace = true
 backtrace = "0.3"
-client = { path = "../client" }
-clock = { path = "../clock" }
-collections = { path = "../collections" }
-copilot = { path = "../copilot" }
-db = { path = "../db" }
-fs = { path = "../fs" }
-fsevent = { path = "../fsevent" }
+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 = {  path = "../fuzzy" }
-git = { path = "../git" }
+fuzzy.workspace = true
+git.workspace = true
 globset.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 ignore = "0.4"
 itertools = "0.10"
-language = { path = "../language" }
+language.workspace = true
 lazy_static.workspace = true
 log.workspace = true
-lsp = { path = "../lsp" }
-node_runtime = { path = "../node_runtime" }
+lsp.workspace = true
+node_runtime.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
-prettier = { path = "../prettier" }
+prettier.workspace = true
 rand.workspace = true
 regex.workspace = true
-rpc = { path = "../rpc" }
+rpc.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 sha2 = "0.10"
 similar = "1.3"
 smol.workspace = true
-sum_tree = { path = "../sum_tree" }
-terminal = { path = "../terminal" }
-text = { path = "../text" }
+sum_tree.workspace = true
+terminal.workspace = true
+text.workspace = true
 thiserror.workspace = true
 toml.workspace = true
-util = { path = "../util" }
+util.workspace = true
 
 [dev-dependencies]
-client = { path = "../client", features = ["test-support"] }
-collections = { path = "../collections", features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
 ctor.workspace = true
-db = { path = "../db", features = ["test-support"] }
+db = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
-fs = { path = "../fs",  features = ["test-support"] }
+fs = { workspace = true,  features = ["test-support"] }
 git2.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
-release_channel = { path = "../release_channel" }
-lsp = { path = "../lsp", features = ["test-support"] }
-prettier = { path = "../prettier", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+release_channel.workspace = true
+lsp = { workspace = true, features = ["test-support"] }
+prettier = { workspace = true, features = ["test-support"] }
 pretty_assertions.workspace = true
-rpc = { path = "../rpc", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
+rpc = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
 tempfile.workspace = true
 unindent.workspace = true
-util = { path = "../util", features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/project/src/project.rs πŸ”—

@@ -4414,13 +4414,13 @@ impl Project {
         }
     }
 
-    pub fn definition<T: ToPointUtf16>(
+    #[inline(never)]
+    fn definition_impl(
         &self,
         buffer: &Model<Buffer>,
-        position: T,
+        position: PointUtf16,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<LocationLink>>> {
-        let position = position.to_point_utf16(buffer.read(cx));
         self.request_lsp(
             buffer.clone(),
             LanguageServerToQuery::Primary,
@@ -4428,14 +4428,22 @@ impl Project {
             cx,
         )
     }
-
-    pub fn type_definition<T: ToPointUtf16>(
+    pub fn definition<T: ToPointUtf16>(
         &self,
         buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<LocationLink>>> {
         let position = position.to_point_utf16(buffer.read(cx));
+        self.definition_impl(buffer, position, cx)
+    }
+
+    fn type_definition_impl(
+        &self,
+        buffer: &Model<Buffer>,
+        position: PointUtf16,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<LocationLink>>> {
         self.request_lsp(
             buffer.clone(),
             LanguageServerToQuery::Primary,
@@ -4443,14 +4451,23 @@ impl Project {
             cx,
         )
     }
-
-    pub fn references<T: ToPointUtf16>(
+    pub fn type_definition<T: ToPointUtf16>(
         &self,
         buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Vec<Location>>> {
+    ) -> Task<Result<Vec<LocationLink>>> {
         let position = position.to_point_utf16(buffer.read(cx));
+
+        self.type_definition_impl(buffer, position, cx)
+    }
+
+    fn references_impl(
+        &self,
+        buffer: &Model<Buffer>,
+        position: PointUtf16,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Location>>> {
         self.request_lsp(
             buffer.clone(),
             LanguageServerToQuery::Primary,
@@ -4458,14 +4475,22 @@ impl Project {
             cx,
         )
     }
-
-    pub fn document_highlights<T: ToPointUtf16>(
+    pub fn references<T: ToPointUtf16>(
         &self,
         buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Vec<DocumentHighlight>>> {
+    ) -> Task<Result<Vec<Location>>> {
         let position = position.to_point_utf16(buffer.read(cx));
+        self.references_impl(buffer, position, cx)
+    }
+
+    fn document_highlights_impl(
+        &self,
+        buffer: &Model<Buffer>,
+        position: PointUtf16,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<DocumentHighlight>>> {
         self.request_lsp(
             buffer.clone(),
             LanguageServerToQuery::Primary,
@@ -4474,6 +4499,16 @@ impl Project {
         )
     }
 
+    pub fn document_highlights<T: ToPointUtf16>(
+        &self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<DocumentHighlight>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.document_highlights_impl(buffer, position, cx)
+    }
+
     pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
         if self.is_local() {
             let mut requests = Vec::new();
@@ -4694,13 +4729,12 @@ impl Project {
         }
     }
 
-    pub fn hover<T: ToPointUtf16>(
+    fn hover_impl(
         &self,
         buffer: &Model<Buffer>,
-        position: T,
+        position: PointUtf16,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Hover>>> {
-        let position = position.to_point_utf16(buffer.read(cx));
         self.request_lsp(
             buffer.clone(),
             LanguageServerToQuery::Primary,
@@ -4708,14 +4742,23 @@ impl Project {
             cx,
         )
     }
-
-    pub fn completions<T: ToOffset + ToPointUtf16>(
+    pub fn hover<T: ToPointUtf16>(
         &self,
         buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Vec<Completion>>> {
+    ) -> Task<Result<Option<Hover>>> {
         let position = position.to_point_utf16(buffer.read(cx));
+        self.hover_impl(buffer, position, cx)
+    }
+
+    #[inline(never)]
+    fn completions_impl(
+        &self,
+        buffer: &Model<Buffer>,
+        position: PointUtf16,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>> {
         if self.is_local() {
             let snapshot = buffer.read(cx).snapshot();
             let offset = position.to_offset(&snapshot);
@@ -4762,6 +4805,15 @@ impl Project {
             Task::ready(Ok(Default::default()))
         }
     }
+    pub fn completions<T: ToOffset + ToPointUtf16>(
+        &self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.completions_impl(buffer, position, cx)
+    }
 
     pub fn resolve_completions(
         &self,
@@ -5038,14 +5090,12 @@ impl Project {
         }
     }
 
-    pub fn code_actions<T: Clone + ToOffset>(
+    fn code_actions_impl(
         &self,
         buffer_handle: &Model<Buffer>,
-        range: Range<T>,
+        range: Range<Anchor>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<CodeAction>>> {
-        let buffer = buffer_handle.read(cx);
-        let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
         self.request_lsp(
             buffer_handle.clone(),
             LanguageServerToQuery::Primary,
@@ -5054,6 +5104,17 @@ impl Project {
         )
     }
 
+    pub fn code_actions<T: Clone + ToOffset>(
+        &self,
+        buffer_handle: &Model<Buffer>,
+        range: Range<T>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<CodeAction>>> {
+        let buffer = buffer_handle.read(cx);
+        let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
+        self.code_actions_impl(buffer_handle, range, cx)
+    }
+
     pub fn apply_code_action(
         &self,
         buffer_handle: Model<Buffer>,
@@ -5420,13 +5481,12 @@ impl Project {
         Ok(project_transaction)
     }
 
-    pub fn prepare_rename<T: ToPointUtf16>(
+    fn prepare_rename_impl(
         &self,
         buffer: Model<Buffer>,
-        position: T,
+        position: PointUtf16,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Range<Anchor>>>> {
-        let position = position.to_point_utf16(buffer.read(cx));
         self.request_lsp(
             buffer,
             LanguageServerToQuery::Primary,
@@ -5434,11 +5494,20 @@ impl Project {
             cx,
         )
     }
-
-    pub fn perform_rename<T: ToPointUtf16>(
+    pub fn prepare_rename<T: ToPointUtf16>(
         &self,
         buffer: Model<Buffer>,
         position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Option<Range<Anchor>>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.prepare_rename_impl(buffer, position, cx)
+    }
+
+    fn perform_rename_impl(
+        &self,
+        buffer: Model<Buffer>,
+        position: PointUtf16,
         new_name: String,
         push_to_history: bool,
         cx: &mut ModelContext<Self>,
@@ -5455,22 +5524,28 @@ impl Project {
             cx,
         )
     }
-
-    pub fn on_type_format<T: ToPointUtf16>(
+    pub fn perform_rename<T: ToPointUtf16>(
         &self,
         buffer: Model<Buffer>,
         position: T,
+        new_name: String,
+        push_to_history: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ProjectTransaction>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.perform_rename_impl(buffer, position, new_name, push_to_history, cx)
+    }
+
+    pub fn on_type_format_impl(
+        &self,
+        buffer: Model<Buffer>,
+        position: PointUtf16,
         trigger: String,
         push_to_history: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Transaction>>> {
-        let (position, tab_size) = buffer.update(cx, |buffer, cx| {
-            let position = position.to_point_utf16(buffer);
-            (
-                position,
-                language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx)
-                    .tab_size,
-            )
+        let tab_size = buffer.update(cx, |buffer, cx| {
+            language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx).tab_size
         });
         self.request_lsp(
             buffer.clone(),
@@ -5485,6 +5560,18 @@ impl Project {
         )
     }
 
+    pub fn on_type_format<T: ToPointUtf16>(
+        &self,
+        buffer: Model<Buffer>,
+        position: T,
+        trigger: String,
+        push_to_history: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Option<Transaction>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.on_type_format_impl(buffer, position, trigger, push_to_history, cx)
+    }
+
     pub fn inlay_hints<T: ToOffset>(
         &self,
         buffer_handle: Model<Buffer>,
@@ -5493,6 +5580,15 @@ impl Project {
     ) -> Task<anyhow::Result<Vec<InlayHint>>> {
         let buffer = buffer_handle.read(cx);
         let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
+        self.inlay_hints_impl(buffer_handle, range, cx)
+    }
+    fn inlay_hints_impl(
+        &self,
+        buffer_handle: Model<Buffer>,
+        range: Range<Anchor>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<anyhow::Result<Vec<InlayHint>>> {
+        let buffer = buffer_handle.read(cx);
         let range_start = range.start;
         let range_end = range.end;
         let buffer_id = buffer.remote_id().into();

crates/project_panel/Cargo.toml πŸ”—

@@ -11,33 +11,33 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-collections = { path = "../collections" }
-db = { path = "../db" }
-editor = { path = "../editor" }
+collections.workspace = true
+db.workspace = true
+editor.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
-menu = {  path = "../menu" }
+gpui.workspace = true
+menu.workspace = true
 postage.workspace = true
 pretty_assertions.workspace = true
-project = { path = "../project" }
+project.workspace = true
 schemars.workspace = true
-search = { path = "../search" }
+search.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smallvec.workspace = true
-theme = { path = "../theme" }
-ui = { path = "../ui" }
+theme.workspace = true
+ui.workspace = true
 unicase = "2.6"
-util = { path = "../util" }
-client = { path = "../client" }
-workspace = { path = "../workspace", package = "workspace" }
+util.workspace = true
+client.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-client = { path = "../client", features = ["test-support"] }
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
 serde_json.workspace = true
-workspace = { path = "../workspace", features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/project_panel/src/project_panel.rs πŸ”—

@@ -8,6 +8,7 @@ use editor::{actions::Cancel, scroll::Autoscroll, Editor};
 use file_associations::FileAssociations;
 
 use anyhow::{anyhow, Result};
+use collections::{hash_map, HashMap};
 use gpui::{
     actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
     ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
@@ -22,14 +23,7 @@ use project::{
 };
 use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
 use serde::{Deserialize, Serialize};
-use std::{
-    cmp::Ordering,
-    collections::{hash_map, HashMap},
-    ffi::OsStr,
-    ops::Range,
-    path::Path,
-    sync::Arc,
-};
+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;
@@ -262,11 +256,14 @@ impl ProjectPanel {
                     if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
                         if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
                             let file_path = entry.path.clone();
+                            let worktree_id = worktree.read(cx).id();
+                            let entry_id = entry.id;
+
                             workspace
                                 .open_path(
                                     ProjectPath {
-                                        worktree_id: worktree.read(cx).id(),
-                                        path: entry.path.clone(),
+                                        worktree_id,
+                                        path: file_path.clone(),
                                     },
                                     None,
                                     focus_opened_item,
@@ -281,8 +278,16 @@ impl ProjectPanel {
                                         _ => None,
                                     }
                                 });
-                            if !focus_opened_item {
-                                if let Some(project_panel) = project_panel.upgrade() {
+
+                            if let Some(project_panel) = project_panel.upgrade() {
+                                // Always select the entry, regardless of whether it is opened or not.
+                                project_panel.update(cx, |project_panel, _| {
+                                    project_panel.selection = Some(Selection {
+                                        worktree_id,
+                                        entry_id
+                                    });
+                                });
+                                if !focus_opened_item {
                                     let focus_handle = project_panel.read(cx).focus_handle.clone();
                                     cx.focus(&focus_handle);
                                 }
@@ -1688,15 +1693,13 @@ impl ClipboardEntry {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use collections::HashSet;
     use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
     use pretty_assertions::assert_eq;
     use project::{project_settings::ProjectSettings, FakeFs};
     use serde_json::json;
     use settings::SettingsStore;
-    use std::{
-        collections::HashSet,
-        path::{Path, PathBuf},
-    };
+    use std::path::{Path, PathBuf};
     use workspace::AppState;
 
     #[gpui::test]
@@ -3498,7 +3501,7 @@ mod tests {
         cx: &mut VisualTestContext,
     ) -> Vec<String> {
         let mut result = Vec::new();
-        let mut project_entries = HashSet::new();
+        let mut project_entries = HashSet::default();
         let mut has_editor = false;
 
         panel.update(cx, |panel, cx| {

crates/project_symbols/Cargo.toml πŸ”—

@@ -11,29 +11,29 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-editor = { path = "../editor" }
-fuzzy = {   path = "../fuzzy" }
-gpui = { path = "../gpui" }
+editor.workspace = true
+fuzzy.workspace = true
+gpui.workspace = true
 ordered-float.workspace = true
-picker = { path = "../picker" }
+picker.workspace = true
 postage.workspace = true
-project = { path = "../project" }
+project.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smol.workspace = true
-text = { path = "../text" }
-theme = { path = "../theme" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+text.workspace = true
+theme.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
 futures.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
-lsp = { path = "../lsp", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
-release_channel = { path = "../release_channel" }
-settings = { path = "../settings", features = ["test-support"] }
-theme = { path = "../theme", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+lsp = { workspace = true, features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
+release_channel.workspace = true
+settings = { workspace = true, features = ["test-support"] }
+theme = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/quick_action_bar/Cargo.toml πŸ”—

@@ -10,14 +10,15 @@ path = "src/quick_action_bar.rs"
 doctest = false
 
 [dependencies]
-assistant = { path = "../assistant" }
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
-search = { path = "../search" }
-ui = { path = "../ui" }
-workspace = { path = "../workspace" }
+assistant.workspace = true
+editor.workspace = true
+gpui.workspace = true
+search.workspace = true
+settings.workspace = true
+ui.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/quick_action_bar/src/quick_action_bar.rs πŸ”—

@@ -1,11 +1,12 @@
 use assistant::{AssistantPanel, InlineAssist};
-use editor::Editor;
+use editor::{Editor, EditorSettings};
 
 use gpui::{
     Action, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Styled,
     Subscription, View, ViewContext, WeakView,
 };
 use search::{buffer_search, BufferSearchBar};
+use settings::{Settings, SettingsStore};
 use ui::{prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, Tooltip};
 use workspace::{
     item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -16,16 +17,26 @@ pub struct QuickActionBar {
     active_item: Option<Box<dyn ItemHandle>>,
     _inlay_hints_enabled_subscription: Option<Subscription>,
     workspace: WeakView<Workspace>,
+    show: bool,
 }
 
 impl QuickActionBar {
-    pub fn new(buffer_search_bar: View<BufferSearchBar>, workspace: &Workspace) -> Self {
-        Self {
+    pub fn new(
+        buffer_search_bar: View<BufferSearchBar>,
+        workspace: &Workspace,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let mut this = Self {
             buffer_search_bar,
             active_item: None,
             _inlay_hints_enabled_subscription: None,
             workspace: workspace.weak_handle(),
-        }
+            show: true,
+        };
+        this.apply_settings(cx);
+        cx.observe_global::<SettingsStore>(|this, cx| this.apply_settings(cx))
+            .detach();
+        this
     }
 
     fn active_editor(&self) -> Option<View<Editor>> {
@@ -33,6 +44,24 @@ impl QuickActionBar {
             .as_ref()
             .and_then(|item| item.downcast::<Editor>())
     }
+
+    fn apply_settings(&mut self, cx: &mut ViewContext<Self>) {
+        let new_show = EditorSettings::get_global(cx).toolbar.quick_actions;
+        if new_show != self.show {
+            self.show = new_show;
+            cx.emit(ToolbarItemEvent::ChangeLocation(
+                self.get_toolbar_item_location(),
+            ));
+        }
+    }
+
+    fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
+        if self.show && self.active_editor().is_some() {
+            ToolbarItemLocation::PrimaryRight
+        } else {
+            ToolbarItemLocation::Hidden
+        }
+    }
 }
 
 impl Render for QuickActionBar {
@@ -40,7 +69,6 @@ impl Render for QuickActionBar {
         let Some(editor) = self.active_editor() else {
             return div().id("empty quick action bar");
         };
-
         let inlay_hints_button = Some(QuickActionBarButton::new(
             "toggle inlay hints",
             IconName::InlayHint,
@@ -155,36 +183,28 @@ impl ToolbarItemView for QuickActionBar {
         active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut ViewContext<Self>,
     ) -> ToolbarItemLocation {
-        match active_pane_item {
-            Some(active_item) => {
-                self.active_item = Some(active_item.boxed_clone());
-                self._inlay_hints_enabled_subscription.take();
-
-                if let Some(editor) = active_item.downcast::<Editor>() {
-                    let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
-                    let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
-                    self._inlay_hints_enabled_subscription =
-                        Some(cx.observe(&editor, move |_, editor, cx| {
-                            let editor = editor.read(cx);
-                            let new_inlay_hints_enabled = editor.inlay_hints_enabled();
-                            let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
-                            let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
-                                || supports_inlay_hints != new_supports_inlay_hints;
-                            inlay_hints_enabled = new_inlay_hints_enabled;
-                            supports_inlay_hints = new_supports_inlay_hints;
-                            if should_notify {
-                                cx.notify()
-                            }
-                        }));
-                    ToolbarItemLocation::PrimaryRight
-                } else {
-                    ToolbarItemLocation::Hidden
-                }
-            }
-            None => {
-                self.active_item = None;
-                ToolbarItemLocation::Hidden
+        self.active_item = active_pane_item.map(ItemHandle::boxed_clone);
+        if let Some(active_item) = active_pane_item {
+            self._inlay_hints_enabled_subscription.take();
+
+            if let Some(editor) = active_item.downcast::<Editor>() {
+                let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
+                let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
+                self._inlay_hints_enabled_subscription =
+                    Some(cx.observe(&editor, move |_, editor, cx| {
+                        let editor = editor.read(cx);
+                        let new_inlay_hints_enabled = editor.inlay_hints_enabled();
+                        let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
+                        let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
+                            || supports_inlay_hints != new_supports_inlay_hints;
+                        inlay_hints_enabled = new_inlay_hints_enabled;
+                        supports_inlay_hints = new_supports_inlay_hints;
+                        if should_notify {
+                            cx.notify()
+                        }
+                    }));
             }
         }
+        self.get_toolbar_item_location()
     }
 }

crates/recent_projects/Cargo.toml πŸ”—

@@ -10,21 +10,21 @@ path = "src/recent_projects.rs"
 doctest = false
 
 [dependencies]
-editor = { path = "../editor" }
+editor.workspace = true
 futures.workspace = true
-fuzzy = {  path = "../fuzzy" }
-gpui = { path = "../gpui" }
-language = { path = "../language" }
+fuzzy.workspace = true
+gpui.workspace = true
+language.workspace = true
 ordered-float.workspace = true
-picker = { path = "../picker" }
+picker.workspace = true
 postage.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smol.workspace = true
-text = { path = "../text" }
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+text.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }

crates/rich_text/Cargo.toml πŸ”—

@@ -17,15 +17,15 @@ test-support = [
 
 [dependencies]
 anyhow.workspace = true
-collections = { path = "../collections" }
+collections.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
-language = { path = "../language" }
+gpui.workspace = true
+language.workspace = true
 lazy_static.workspace = true
 pulldown-cmark.workspace = true
 smallvec.workspace = true
 smol.workspace = true
-sum_tree = { path = "../sum_tree" }
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
+sum_tree.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true

crates/rich_text/src/rich_text.rs πŸ”—

@@ -13,6 +13,7 @@ use util::RangeExt;
 pub enum Highlight {
     Code,
     Id(HighlightId),
+    InlineCode(bool),
     Highlight(HighlightStyle),
     Mention,
     SelfMention,
@@ -67,6 +68,23 @@ impl RichText {
                                 background_color: Some(code_background),
                                 ..id.style(theme.syntax()).unwrap_or_default()
                             },
+                            Highlight::InlineCode(link) => {
+                                if !*link {
+                                    HighlightStyle {
+                                        background_color: Some(code_background),
+                                        ..Default::default()
+                                    }
+                                } else {
+                                    HighlightStyle {
+                                        background_color: Some(code_background),
+                                        underline: Some(UnderlineStyle {
+                                            thickness: 1.0.into(),
+                                            ..Default::default()
+                                        }),
+                                        ..Default::default()
+                                    }
+                                }
+                            }
                             Highlight::Highlight(highlight) => *highlight,
                             Highlight::Mention => HighlightStyle {
                                 font_weight: Some(FontWeight::BOLD),
@@ -184,22 +202,14 @@ pub fn render_markdown_mut(
             }
             Event::Code(t) => {
                 text.push_str(t.as_ref());
-                if link_url.is_some() {
-                    highlights.push((
-                        prev_len..text.len(),
-                        Highlight::Highlight(HighlightStyle {
-                            underline: Some(UnderlineStyle {
-                                thickness: 1.0.into(),
-                                ..Default::default()
-                            }),
-                            ..Default::default()
-                        }),
-                    ));
-                }
+                let is_link = link_url.is_some();
+
                 if let Some(link_url) = link_url.clone() {
                     link_ranges.push(prev_len..text.len());
                     link_urls.push(link_url);
                 }
+
+                highlights.push((prev_len..text.len(), Highlight::InlineCode(is_link)))
             }
             Event::Start(tag) => match tag {
                 Tag::Paragraph => new_paragraph(text, &mut list_stack),

crates/rope/Cargo.toml πŸ”—

@@ -13,10 +13,10 @@ arrayvec = "0.7.1"
 bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" }
 log.workspace = true
 smallvec.workspace = true
-sum_tree = { path = "../sum_tree" }
-util = { path = "../util" }
+sum_tree.workspace = true
+util.workspace = true
 
 [dev-dependencies]
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 rand.workspace = true
-util = { path = "../util", features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/rpc/Cargo.toml πŸ”—

@@ -18,10 +18,10 @@ anyhow.workspace = true
 async-lock = "2.4"
 async-tungstenite = "0.16"
 base64 = "0.13"
-clock = { path = "../clock" }
-collections = { path = "../collections" }
+clock.workspace = true
+collections.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui", optional = true }
+gpui = { workspace = true, optional = true }
 parking_lot.workspace = true
 prost.workspace = true
 rand.workspace = true
@@ -32,16 +32,16 @@ serde_json.workspace = true
 smol-timeout = "0.6"
 strum.workspace = true
 tracing = { version = "0.1.34", features = ["log"] }
-util = { path = "../util" }
+util.workspace = true
 zstd = "0.11"
 
 [build-dependencies]
 prost-build = "0.9"
 
 [dev-dependencies]
-collections = { path = "../collections", features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
 ctor.workspace = true
 env_logger.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 smol.workspace = true
 tempfile.workspace = true

crates/rpc/proto/zed.proto πŸ”—

@@ -1122,6 +1122,7 @@ message SendChannelMessage {
     string body = 2;
     Nonce nonce = 3;
     repeated ChatMention mentions = 4;
+    optional uint64 reply_to_message_id = 5;
 }
 
 message RemoveChannelMessage {
@@ -1173,6 +1174,7 @@ message ChannelMessage {
     uint64 sender_id = 4;
     Nonce nonce = 5;
     repeated ChatMention mentions = 6;
+    optional uint64 reply_to_message_id = 7;
 }
 
 message ChatMention {

crates/rpc/src/peer.rs πŸ”—

@@ -25,7 +25,7 @@ use std::{
 };
 use tracing::instrument;
 
-#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)]
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)]
 pub struct ConnectionId {
     pub owner_id: u32,
     pub id: u32,

crates/search/Cargo.toml πŸ”—

@@ -12,30 +12,30 @@ doctest = false
 [dependencies]
 anyhow.workspace = true
 bitflags = "1"
-collections = { path = "../collections" }
-editor = { path = "../editor" }
+collections.workspace = true
+editor.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
-language = { path = "../language" }
+gpui.workspace = true
+language.workspace = true
 log.workspace = true
-menu = { path = "../menu" }
+menu.workspace = true
 postage.workspace = true
-project = { path = "../project" }
-semantic_index = { path = "../semantic_index" }
+project.workspace = true
+semantic_index.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smallvec.workspace = true
 smol.workspace = true
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-client = { path = "../client", features = ["test-support"] }
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 unindent.workspace = true
-workspace = { path = "../workspace", features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/search/src/buffer_search.rs πŸ”—

@@ -88,6 +88,7 @@ impl BufferSearchBar {
             line_height: relative(1.3).into(),
             background_color: None,
             underline: None,
+            strikethrough: None,
             white_space: WhiteSpace::Normal,
         };
 

crates/search/src/project_search.rs πŸ”—

@@ -25,11 +25,11 @@ use project::{
 };
 use semantic_index::{SemanticIndex, SemanticIndexStatus};
 
+use collections::HashSet;
 use settings::Settings;
 use smol::stream::StreamExt;
 use std::{
     any::{Any, TypeId},
-    collections::HashSet,
     mem,
     ops::{Not, Range},
     path::PathBuf,
@@ -955,7 +955,7 @@ impl ProjectSearchView {
             semantic_state: None,
             semantic_permissioned: None,
             search_options: options,
-            panels_with_errors: HashSet::new(),
+            panels_with_errors: HashSet::default(),
             active_match_index: None,
             query_editor_was_focused: false,
             included_files_editor,
@@ -1632,6 +1632,7 @@ impl ProjectSearchBar {
             line_height: relative(1.3).into(),
             background_color: None,
             underline: None,
+            strikethrough: None,
             white_space: WhiteSpace::Normal,
         };
 

crates/semantic_index/Cargo.toml πŸ”—

@@ -10,51 +10,51 @@ path = "src/semantic_index.rs"
 doctest = false
 
 [dependencies]
-ai = { path = "../ai" }
+ai.workspace = true
 anyhow.workspace = true
 async-trait.workspace = true
-collections = { path = "../collections" }
+collections.workspace = true
 futures.workspace = true
 globset.workspace = true
-gpui = { path = "../gpui" }
-language = { path = "../language" }
+gpui.workspace = true
+language.workspace = true
 lazy_static.workspace = true
 log.workspace = true
 ndarray = { version = "0.15.0" }
 ordered-float.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
-project = { path = "../project" }
+project.workspace = true
 rand.workspace = true
-release_channel = { path = "../release_channel" }
-rpc = { path = "../rpc" }
+release_channel.workspace = true
+rpc.workspace = true
 rusqlite.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 sha1 = "0.10.5"
 smol.workspace = true
 tiktoken-rs.workspace = true
 tree-sitter.workspace = true
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-ai = { path = "../ai", features = ["test-support"] }
-client = { path = "../client" }
-collections = { path = "../collections", features = ["test-support"] }
+ai = { workspace = true, features = ["test-support"] }
+client.workspace = true
+collections = { workspace = true, features = ["test-support"] }
 ctor.workspace = true
 env_logger.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
-node_runtime = { path = "../node_runtime" }
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+node_runtime.workspace = true
 pretty_assertions.workspace = true
-project = { path = "../project", features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
 rand.workspace = true
-rpc = { path = "../rpc", features = ["test-support"] }
+rpc = { workspace = true, features = ["test-support"] }
 rust-embed.workspace = true
-settings = { path = "../settings", features = ["test-support"]}
+settings = { workspace = true, features = ["test-support"]}
 tempfile.workspace = true
 tree-sitter-cpp.workspace = true
 tree-sitter-elixir.workspace = true
@@ -66,4 +66,4 @@ tree-sitter-rust.workspace = true
 tree-sitter-toml.workspace = true
 tree-sitter-typescript.workspace = true
 unindent.workspace = true
-workspace = { path = "../workspace", features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/semantic_index/src/parsing.rs πŸ”—

@@ -3,6 +3,7 @@ use ai::{
     models::TruncationDirection,
 };
 use anyhow::{anyhow, Result};
+use collections::HashSet;
 use language::{Grammar, Language};
 use rusqlite::{
     types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef},
@@ -12,7 +13,6 @@ use sha1::{Digest, Sha1};
 use std::{
     borrow::Cow,
     cmp::{self, Reverse},
-    collections::HashSet,
     ops::Range,
     path::Path,
     sync::Arc,
@@ -267,7 +267,7 @@ impl CodeContextRetriever {
 
         let mut spans = Vec::new();
         let mut collapsed_ranges_within = Vec::new();
-        let mut parsed_name_ranges = HashSet::new();
+        let mut parsed_name_ranges = HashSet::default();
         for (i, context_match) in matches.iter().enumerate() {
             // Items which are collapsible but not embeddable have no item range
             let item_range = if let Some(item_range) = context_match.item_range.clone() {

crates/settings/Cargo.toml πŸ”—

@@ -14,14 +14,14 @@ test-support = ["gpui/test-support", "fs/test-support"]
 
 [dependencies]
 anyhow.workspace = true
-collections = { path = "../collections" }
-feature_flags = { path = "../feature_flags" }
-fs = { path = "../fs" }
+collections.workspace = true
+feature_flags.workspace = true
+fs.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 lazy_static.workspace = true
 postage.workspace = true
-release_channel = { path = "../release_channel" }
+release_channel.workspace = true
 rust-embed.workspace = true
 schemars.workspace = true
 serde.workspace = true
@@ -32,11 +32,11 @@ smallvec.workspace = true
 toml.workspace = true
 tree-sitter-json = "*"
 tree-sitter.workspace = true
-util = { path = "../util" }
+util.workspace = true
 
 [dev-dependencies]
-fs = { path = "../fs", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
+fs = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 indoc.workspace = true
 pretty_assertions.workspace = true
 unindent.workspace = true

crates/sqlez/Cargo.toml πŸ”—

@@ -7,6 +7,7 @@ license = "GPL-3.0-or-later"
 
 [dependencies]
 anyhow.workspace = true
+collections.workspace = true
 futures.workspace = true
 indoc.workspace = true
 lazy_static.workspace = true
@@ -14,5 +15,5 @@ libsqlite3-sys = { version = "0.26", features = ["bundled"] }
 parking_lot.workspace = true
 smol.workspace = true
 thread_local = "1.1.4"
-util = { path = "../util" }
+util.workspace = true
 uuid.workspace = true

crates/sqlez/src/thread_safe_connection.rs πŸ”—

@@ -1,8 +1,9 @@
 use anyhow::Context;
+use collections::HashMap;
 use futures::{channel::oneshot, Future, FutureExt};
 use lazy_static::lazy_static;
 use parking_lot::{Mutex, RwLock};
-use std::{collections::HashMap, marker::PhantomData, ops::Deref, sync::Arc, thread};
+use std::{marker::PhantomData, ops::Deref, sync::Arc, thread};
 use thread_local::ThreadLocal;
 
 use crate::{connection::Connection, domain::Migrator, util::UnboundedSyncSender};

crates/sqlez_macros/Cargo.toml πŸ”—

@@ -14,6 +14,6 @@ doctest = false
 lazy_static.workspace = true
 proc-macro2 = "1.0"
 quote = "1.0"
-sqlez = { path = "../sqlez" }
+sqlez.workspace = true
 sqlformat = "0.2"
 syn = "1.0"

crates/story/Cargo.toml πŸ”—

@@ -6,6 +6,6 @@ publish = false
 license = "GPL-3.0-or-later"
 
 [dependencies]
-gpui = { path = "../gpui" }
+gpui.workspace = true
 itertools = { package = "itertools", version = "0.10" }
 smallvec.workspace = true

crates/storybook/Cargo.toml πŸ”—

@@ -15,28 +15,28 @@ anyhow.workspace = true
 backtrace-on-stack-overflow = "0.3.0"
 chrono = "0.4"
 clap = { version = "4.4", features = ["derive", "string"] }
-collab_ui = { path = "../collab_ui", features = ["stories"] }
+collab_ui = { workspace = true, features = ["stories"] }
 ctrlc = "3.4"
 dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
-editor = { path = "../editor" }
-fuzzy = {  path = "../fuzzy" }
-gpui = { path = "../gpui" }
+editor.workspace = true
+fuzzy.workspace = true
+gpui.workspace = true
 indoc.workspace = true
 itertools = "0.11.0"
-language = { path = "../language" }
+language.workspace = true
 log.workspace = true
-menu = { path = "../menu" }
-picker = { path = "../picker" }
+menu.workspace = true
+picker.workspace = true
 rust-embed.workspace = true
 serde.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 simplelog = "0.9"
 smallvec.workspace = true
-story = { path = "../story" }
+story.workspace = true
 strum = { version = "0.25.0", features = ["derive"] }
-theme = { path = "../theme" }
-ui = { path = "../ui", features = ["stories"] }
-util = { path = "../util" }
+theme.workspace = true
+ui = { workspace = true, features = ["stories"] }
+util.workspace = true
 
 [dev-dependencies]
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }

crates/terminal/Cargo.toml πŸ”—

@@ -11,12 +11,13 @@ doctest = false
 
 
 [dependencies]
-alacritty_terminal = "0.21"
+alacritty_terminal = "0.22.0"
 anyhow.workspace = true
-db = { path = "../db" }
+collections.workspace = true
+db.workspace = true
 dirs = "4.0.0"
 futures.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 itertools = "0.10"
 lazy_static.workspace = true
 libc = "0.2"
@@ -27,13 +28,13 @@ schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 shellexpand = "2.1.0"
 smallvec.workspace = true
 smol.workspace = true
-theme = { path = "../theme" }
+theme.workspace = true
 thiserror.workspace = true
-util = { path = "../util" }
+util.workspace = true
 
 [dev-dependencies]
 rand.workspace = true

crates/terminal/src/terminal.rs πŸ”—

@@ -30,6 +30,7 @@ use mappings::mouse::{
     scroll_report,
 };
 
+use collections::{HashMap, VecDeque};
 use procinfo::LocalProcessInfo;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
@@ -39,7 +40,6 @@ use util::truncate_and_trailoff;
 
 use std::{
     cmp::{self, min},
-    collections::{HashMap, VecDeque},
     fmt::Display,
     ops::{Deref, Index, RangeInclusive},
     os::unix::prelude::AsRawFd,
@@ -86,6 +86,15 @@ pub enum Event {
     Open(MaybeNavigationTarget),
 }
 
+#[derive(Clone, Debug)]
+pub struct PathLikeTarget {
+    /// File system path, absolute or relative, existing or not.
+    /// Might have line and column number(s) attached as `file.rs:1:23`
+    pub maybe_path: String,
+    /// Current working directory of the terminal
+    pub terminal_dir: Option<PathBuf>,
+}
+
 /// A string inside terminal, potentially useful as a URI that can be opened.
 #[derive(Clone, Debug)]
 pub enum MaybeNavigationTarget {
@@ -93,7 +102,7 @@ pub enum MaybeNavigationTarget {
     Url(String),
     /// File system path, absolute or relative, existing or not.
     /// Might have line and column number(s) attached as `file.rs:1:23`
-    PathLike(String),
+    PathLike(PathLikeTarget),
 }
 
 #[derive(Clone)]
@@ -364,7 +373,7 @@ impl TerminalBuilder {
             pty,
             pty_options.hold,
             false,
-        );
+        )?;
 
         //Kick things off
         let pty_tx = event_loop.channel();
@@ -626,6 +635,12 @@ impl Terminal {
         }
     }
 
+    fn get_cwd(&self) -> Option<PathBuf> {
+        self.foreground_process_info
+            .as_ref()
+            .map(|info| info.cwd.clone())
+    }
+
     ///Takes events from Alacritty and translates them to behavior on this view
     fn process_terminal_event(
         &mut self,
@@ -800,7 +815,10 @@ impl Terminal {
                             let target = if is_url {
                                 MaybeNavigationTarget::Url(maybe_url_or_path)
                             } else {
-                                MaybeNavigationTarget::PathLike(maybe_url_or_path)
+                                MaybeNavigationTarget::PathLike(PathLikeTarget {
+                                    maybe_path: maybe_url_or_path,
+                                    terminal_dir: self.get_cwd(),
+                                })
                             };
                             cx.emit(Event::Open(target));
                         } else {
@@ -852,7 +870,10 @@ impl Terminal {
         let navigation_target = if is_url {
             MaybeNavigationTarget::Url(word)
         } else {
-            MaybeNavigationTarget::PathLike(word)
+            MaybeNavigationTarget::PathLike(PathLikeTarget {
+                maybe_path: word,
+                terminal_dir: self.get_cwd(),
+            })
         };
         cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
     }

crates/terminal/src/terminal_settings.rs πŸ”—

@@ -1,3 +1,4 @@
+use collections::HashMap;
 use gpui::{px, AbsoluteLength, AppContext, FontFeatures, Pixels};
 use schemars::{
     gen::SchemaGenerator,
@@ -7,7 +8,7 @@ use schemars::{
 use serde_derive::{Deserialize, Serialize};
 use serde_json::Value;
 use settings::SettingsJsonSchemaParams;
-use std::{collections::HashMap, path::PathBuf};
+use std::path::PathBuf;
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]

crates/terminal_view/Cargo.toml πŸ”—

@@ -11,38 +11,39 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-db = { path = "../db" }
+db.workspace = true
+collections.workspace = true
 dirs = "4.0.0"
-editor = { path = "../editor" }
+editor.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
+gpui.workspace = true
 itertools = "0.10"
-language = { path = "../language" }
+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 = { path = "../project" }
-search = { path = "../search" }
+project.workspace = true
+search.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 shellexpand = "2.1.0"
 smallvec.workspace = true
 smol.workspace = true
-terminal = { path = "../terminal" }
-theme = { path = "../theme" }
+terminal.workspace = true
+theme.workspace = true
 thiserror.workspace = true
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-client = { path = "../client", features = ["test-support"] }
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
 rand.workspace = true
-workspace = { path = "../workspace", features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/terminal_view/src/terminal_element.rs πŸ”—

@@ -4,8 +4,8 @@ use gpui::{
     ElementContext, ElementId, FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, Hsla,
     InputHandler, InteractiveBounds, InteractiveElement, InteractiveElementState, Interactivity,
     IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, MouseMoveEvent,
-    Pixels, Point, ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem,
-    UnderlineStyle, WeakView, WhiteSpace, WindowContext,
+    Pixels, Point, ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle,
+    UnderlineStyle, WeakView, WhiteSpace, WindowContext, WindowTextSystem,
 };
 use itertools::Itertools;
 use language::CursorShape;
@@ -185,7 +185,7 @@ impl TerminalElement {
         grid: &Vec<IndexedCell>,
         text_style: &TextStyle,
         // terminal_theme: &TerminalStyle,
-        text_system: &TextSystem,
+        text_system: &WindowTextSystem,
         hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
         cx: &WindowContext<'_>,
     ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
@@ -362,6 +362,7 @@ impl TerminalElement {
                 ..text_style.font()
             },
             underline,
+            strikethrough: None,
         };
 
         if let Some((style, range)) = hyperlink {
@@ -414,6 +415,7 @@ impl TerminalElement {
                 color: Some(theme.colors().link_text_hover),
                 wavy: false,
             }),
+            strikethrough: None,
             fade_out: None,
         };
 
@@ -427,6 +429,7 @@ impl TerminalElement {
             white_space: WhiteSpace::Normal,
             // These are going to be overridden per-cell
             underline: None,
+            strikethrough: None,
             color: theme.colors().text,
             font_weight: FontWeight::NORMAL,
         };
@@ -450,6 +453,13 @@ impl TerminalElement {
             let mut size = bounds.size.clone();
             size.width -= gutter;
 
+            // https://github.com/zed-industries/zed/issues/2750
+            // if the terminal is one column wide, rendering πŸ¦€
+            // causes alacritty to misbehave.
+            if size.width < cell_width * 2.0 {
+                size.width = cell_width * 2.0;
+            }
+
             TerminalSize::new(line_height, cell_width, size)
         };
 
@@ -538,6 +548,7 @@ impl TerminalElement {
                             color: theme.colors().terminal_background,
                             background_color: None,
                             underline: Default::default(),
+                            strikethrough: None,
                         }],
                     )
                     .unwrap()
@@ -776,7 +787,6 @@ impl Element for TerminalElement {
         self.interactivity
             .paint(bounds, bounds.size, state, cx, |_, _, cx| {
                 cx.handle_input(&self.focus, terminal_input_handler);
-                cx.keymatch_mode_immediate();
 
                 cx.on_key_event({
                     let this = self.terminal.clone();

crates/terminal_view/src/terminal_view.rs πŸ”—

@@ -2,7 +2,9 @@ mod persistence;
 pub mod terminal_element;
 pub mod terminal_panel;
 
+use collections::HashSet;
 use editor::{scroll::Autoscroll, Editor};
+use futures::{stream::FuturesUnordered, StreamExt};
 use gpui::{
     div, impl_actions, overlay, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
     FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, Pixels,
@@ -10,7 +12,7 @@ use gpui::{
 };
 use language::Bias;
 use persistence::TERMINAL_DB;
-use project::{search::SearchQuery, LocalWorktree, Project};
+use project::{search::SearchQuery, Fs, LocalWorktree, Metadata, Project};
 use terminal::{
     alacritty_terminal::{
         index::Point,
@@ -75,7 +77,6 @@ pub struct TerminalView {
     terminal: Model<Terminal>,
     workspace: WeakView<Workspace>,
     focus_handle: FocusHandle,
-    has_new_content: bool,
     //Currently using iTerm bell, show bell emoji in tab until input is received
     has_bell: bool,
     context_menu: Option<(View<ContextMenu>, gpui::Point<Pixels>, Subscription)>,
@@ -140,9 +141,6 @@ impl TerminalView {
         cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
         cx.subscribe(&terminal, move |this, _, event, cx| match event {
             Event::Wakeup => {
-                if !this.focus_handle.is_focused(cx) {
-                    this.has_new_content = true;
-                }
                 cx.notify();
                 cx.emit(Event::Wakeup);
                 cx.emit(ItemEvent::UpdateTab);
@@ -177,8 +175,21 @@ impl TerminalView {
             Event::NewNavigationTarget(maybe_navigation_target) => {
                 this.can_navigate_to_selected_word = match maybe_navigation_target {
                     Some(MaybeNavigationTarget::Url(_)) => true,
-                    Some(MaybeNavigationTarget::PathLike(maybe_path)) => {
-                        !possible_open_targets(&workspace, maybe_path, cx).is_empty()
+                    Some(MaybeNavigationTarget::PathLike(path_like_target)) => {
+                        if let Ok(fs) = workspace.update(cx, |workspace, cx| {
+                            workspace.project().read(cx).fs().clone()
+                        }) {
+                            let valid_files_to_open_task = possible_open_targets(
+                                fs,
+                                &workspace,
+                                &path_like_target.terminal_dir,
+                                &path_like_target.maybe_path,
+                                cx,
+                            );
+                            smol::block_on(valid_files_to_open_task).len() > 0
+                        } else {
+                            false
+                        }
                     }
                     None => false,
                 }
@@ -187,57 +198,60 @@ impl TerminalView {
             Event::Open(maybe_navigation_target) => match maybe_navigation_target {
                 MaybeNavigationTarget::Url(url) => cx.open_url(url),
 
-                MaybeNavigationTarget::PathLike(maybe_path) => {
+                MaybeNavigationTarget::PathLike(path_like_target) => {
                     if !this.can_navigate_to_selected_word {
                         return;
                     }
-                    let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx);
-                    if let Some(path) = potential_abs_paths.into_iter().next() {
-                        let task_workspace = workspace.clone();
-                        cx.spawn(|_, mut cx| async move {
-                            let fs = task_workspace.update(&mut cx, |workspace, cx| {
-                                workspace.project().read(cx).fs().clone()
-                            })?;
-                            let is_dir = fs
-                                .metadata(&path.path_like)
-                                .await?
-                                .with_context(|| {
-                                    format!("Missing metadata for file {:?}", path.path_like)
-                                })?
-                                .is_dir;
-                            let opened_items = task_workspace
-                                .update(&mut cx, |workspace, cx| {
-                                    workspace.open_paths(
-                                        vec![path.path_like],
-                                        OpenVisible::OnlyDirectories,
-                                        None,
-                                        cx,
-                                    )
-                                })
-                                .context("workspace update")?
-                                .await;
-                            anyhow::ensure!(
-                                opened_items.len() == 1,
-                                "For a single path open, expected single opened item"
-                            );
-                            let opened_item = opened_items
-                                .into_iter()
-                                .next()
-                                .unwrap()
-                                .transpose()
-                                .context("path open")?;
-                            if is_dir {
-                                task_workspace.update(&mut cx, |workspace, cx| {
-                                    workspace.project().update(cx, |_, cx| {
-                                        cx.emit(project::Event::ActivateProjectPanel);
-                                    })
-                                })?;
-                            } else {
+                    let task_workspace = workspace.clone();
+                    let Some(fs) = workspace
+                        .update(cx, |workspace, cx| {
+                            workspace.project().read(cx).fs().clone()
+                        })
+                        .ok()
+                    else {
+                        return;
+                    };
+
+                    let path_like_target = path_like_target.clone();
+                    cx.spawn(|terminal_view, mut cx| async move {
+                        let valid_files_to_open = terminal_view
+                            .update(&mut cx, |_, cx| {
+                                possible_open_targets(
+                                    fs,
+                                    &task_workspace,
+                                    &path_like_target.terminal_dir,
+                                    &path_like_target.maybe_path,
+                                    cx,
+                                )
+                            })?
+                            .await;
+                        let paths_to_open = valid_files_to_open
+                            .iter()
+                            .map(|(p, _)| p.path_like.clone())
+                            .collect();
+                        let opened_items = task_workspace
+                            .update(&mut cx, |workspace, cx| {
+                                workspace.open_paths(
+                                    paths_to_open,
+                                    OpenVisible::OnlyDirectories,
+                                    None,
+                                    cx,
+                                )
+                            })
+                            .context("workspace update")?
+                            .await;
+
+                        let mut has_dirs = false;
+                        for ((path, metadata), opened_item) in valid_files_to_open
+                            .into_iter()
+                            .zip(opened_items.into_iter())
+                        {
+                            if metadata.is_dir {
+                                has_dirs = true;
+                            } else if let Some(Ok(opened_item)) = opened_item {
                                 if let Some(row) = path.row {
                                     let col = path.column.unwrap_or(0);
-                                    if let Some(active_editor) =
-                                        opened_item.and_then(|item| item.downcast::<Editor>())
-                                    {
+                                    if let Some(active_editor) = opened_item.downcast::<Editor>() {
                                         active_editor
                                             .downgrade()
                                             .update(&mut cx, |editor, cx| {
@@ -259,10 +273,19 @@ impl TerminalView {
                                     }
                                 }
                             }
-                            anyhow::Ok(())
-                        })
-                        .detach_and_log_err(cx);
-                    }
+                        }
+
+                        if has_dirs {
+                            task_workspace.update(&mut cx, |workspace, cx| {
+                                workspace.project().update(cx, |_, cx| {
+                                    cx.emit(project::Event::ActivateProjectPanel);
+                                })
+                            })?;
+                        }
+
+                        anyhow::Ok(())
+                    })
+                    .detach_and_log_err(cx)
                 }
             },
             Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
@@ -282,9 +305,8 @@ impl TerminalView {
         Self {
             terminal,
             workspace: workspace_handle,
-            has_new_content: true,
             has_bell: false,
-            focus_handle: cx.focus_handle(),
+            focus_handle,
             context_menu: None,
             blink_state: true,
             blinking_on: false,
@@ -300,10 +322,6 @@ impl TerminalView {
         &self.terminal
     }
 
-    pub fn has_new_content(&self) -> bool {
-        self.has_new_content
-    }
-
     pub fn has_bell(&self) -> bool {
         self.has_bell
     }
@@ -451,7 +469,8 @@ impl TerminalView {
 
     ///Attempt to paste the clipboard into the terminal
     fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
-        self.terminal.update(cx, |term, _| term.copy())
+        self.terminal.update(cx, |term, _| term.copy());
+        cx.notify();
     }
 
     ///Attempt to paste the clipboard into the terminal
@@ -554,48 +573,87 @@ impl TerminalView {
     }
 }
 
+fn possible_open_paths_metadata(
+    fs: Arc<dyn Fs>,
+    row: Option<u32>,
+    column: Option<u32>,
+    potential_paths: HashSet<PathBuf>,
+    cx: &mut ViewContext<TerminalView>,
+) -> Task<Vec<(PathLikeWithPosition<PathBuf>, Metadata)>> {
+    cx.background_executor().spawn(async move {
+        let mut paths_with_metadata = Vec::with_capacity(potential_paths.len());
+
+        let mut fetch_metadata_tasks = potential_paths
+            .into_iter()
+            .map(|potential_path| async {
+                let metadata = fs.metadata(&potential_path).await.ok().flatten();
+                (
+                    PathLikeWithPosition {
+                        path_like: potential_path,
+                        row,
+                        column,
+                    },
+                    metadata,
+                )
+            })
+            .collect::<FuturesUnordered<_>>();
+
+        while let Some((path, metadata)) = fetch_metadata_tasks.next().await {
+            if let Some(metadata) = metadata {
+                paths_with_metadata.push((path, metadata));
+            }
+        }
+
+        paths_with_metadata
+    })
+}
+
 fn possible_open_targets(
+    fs: Arc<dyn Fs>,
     workspace: &WeakView<Workspace>,
+    cwd: &Option<PathBuf>,
     maybe_path: &String,
-    cx: &mut ViewContext<'_, TerminalView>,
-) -> Vec<PathLikeWithPosition<PathBuf>> {
+    cx: &mut ViewContext<TerminalView>,
+) -> Task<Vec<(PathLikeWithPosition<PathBuf>, Metadata)>> {
     let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| {
         Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf())
     })
     .expect("infallible");
+    let row = path_like.row;
+    let column = path_like.column;
     let maybe_path = path_like.path_like;
     let potential_abs_paths = if maybe_path.is_absolute() {
-        vec![maybe_path]
+        HashSet::from_iter([maybe_path])
     } else if maybe_path.starts_with("~") {
         if let Some(abs_path) = maybe_path
             .strip_prefix("~")
             .ok()
             .and_then(|maybe_path| Some(dirs::home_dir()?.join(maybe_path)))
         {
-            vec![abs_path]
+            HashSet::from_iter([abs_path])
         } else {
-            Vec::new()
+            HashSet::default()
         }
-    } else if let Some(workspace) = workspace.upgrade() {
-        workspace.update(cx, |workspace, cx| {
-            workspace
-                .worktrees(cx)
-                .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path))
-                .collect()
-        })
     } else {
-        Vec::new()
+        // First check cwd and then workspace
+        let mut potential_cwd_and_workspace_paths = HashSet::default();
+        if let Some(cwd) = cwd {
+            potential_cwd_and_workspace_paths.insert(Path::join(cwd, &maybe_path));
+        }
+        if let Some(workspace) = workspace.upgrade() {
+            workspace.update(cx, |workspace, cx| {
+                for potential_worktree_path in workspace
+                    .worktrees(cx)
+                    .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path))
+                {
+                    potential_cwd_and_workspace_paths.insert(potential_worktree_path);
+                }
+            });
+        }
+        potential_cwd_and_workspace_paths
     };
 
-    potential_abs_paths
-        .into_iter()
-        .filter(|path| path.exists())
-        .map(|path| PathLikeWithPosition {
-            path_like: path,
-            row: path_like.row,
-            column: path_like.column,
-        })
-        .collect()
+    possible_open_paths_metadata(fs, row, column, potential_abs_paths, cx)
 }
 
 pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
@@ -621,7 +679,6 @@ impl TerminalView {
     }
 
     fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
-        self.has_new_content = false;
         self.terminal.read(cx).focus_in();
         self.blink_cursors(self.blink_epoch, cx);
         cx.notify();

crates/text/Cargo.toml πŸ”—

@@ -14,8 +14,8 @@ test-support = ["rand"]
 
 [dependencies]
 anyhow.workspace = true
-clock = { path = "../clock" }
-collections = { path = "../collections" }
+clock.workspace = true
+collections.workspace = true
 digest = { version = "0.9", features = ["std"] }
 lazy_static.workspace = true
 log.workspace = true
@@ -23,15 +23,15 @@ parking_lot.workspace = true
 postage.workspace = true
 rand = { workspace = true, optional = true }
 regex.workspace = true
-rope = { path = "../rope" }
+rope.workspace = true
 smallvec.workspace = true
-sum_tree = { path = "../sum_tree" }
-util = { path = "../util" }
+sum_tree.workspace = true
+util.workspace = true
 
 [dev-dependencies]
-collections = { path = "../collections", features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
 ctor.workspace = true
 env_logger.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 rand.workspace = true
-util = { path = "../util", features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

crates/theme/Cargo.toml πŸ”—

@@ -8,11 +8,7 @@ license = "GPL-3.0-or-later"
 [features]
 default = []
 stories = ["dep:itertools", "dep:story"]
-test-support = [
-    "gpui/test-support",
-    "fs/test-support",
-    "settings/test-support"
-]
+test-support = ["gpui/test-support", "fs/test-support", "settings/test-support"]
 
 [lib]
 path = "src/theme.rs"
@@ -20,11 +16,12 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-color = { path = "../color" }
+collections.workspace = true
+color.workspace = true
 derive_more.workspace = true
-fs = { path = "../fs" }
+fs.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
+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"] }
@@ -36,13 +33,13 @@ serde_derive.workspace = true
 serde_json.workspace = true
 serde_json_lenient.workspace = true
 serde_repr.workspace = true
-settings = { path = "../settings" }
-story = { path = "../story", optional = true }
+settings.workspace = true
+story = { workspace = true, optional = true }
 toml.workspace = true
-util = { path = "../util" }
+util.workspace = true
 uuid.workspace = true
 
 [dev-dependencies]
-fs = { path = "../fs", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
+fs = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }

crates/theme/src/default_colors.rs πŸ”—

@@ -54,7 +54,7 @@ impl ThemeColors {
             tab_bar_background: neutral().light().step_2(),
             tab_inactive_background: neutral().light().step_2(),
             tab_active_background: neutral().light().step_1(),
-            search_match_background: neutral().light().step_2(),
+            search_match_background: neutral().light().step_5(),
             panel_background: neutral().light().step_2(),
             panel_focused_border: blue().light().step_5(),
             pane_focused_border: blue().light().step_5(),
@@ -148,7 +148,7 @@ impl ThemeColors {
             tab_bar_background: neutral().dark().step_2(),
             tab_inactive_background: neutral().dark().step_2(),
             tab_active_background: neutral().dark().step_1(),
-            search_match_background: neutral().dark().step_2(),
+            search_match_background: neutral().dark().step_5(),
             panel_background: neutral().dark().step_2(),
             panel_focused_border: blue().dark().step_5(),
             pane_focused_border: blue().dark().step_5(),

crates/theme/src/registry.rs πŸ”—

@@ -1,8 +1,8 @@
-use std::collections::HashMap;
 use std::path::Path;
 use std::sync::Arc;
 
 use anyhow::{anyhow, Context, Result};
+use collections::HashMap;
 use derive_more::{Deref, DerefMut};
 use fs::Fs;
 use futures::StreamExt;
@@ -64,7 +64,7 @@ impl ThemeRegistry {
     pub fn new(assets: Box<dyn AssetSource>) -> Self {
         let registry = Self {
             state: RwLock::new(ThemeRegistryState {
-                themes: HashMap::new(),
+                themes: HashMap::default(),
             }),
             assets,
         };

crates/theme/src/settings.rs πŸ”—

@@ -1,6 +1,7 @@
 use crate::one_themes::one_dark;
-use crate::{SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent};
+use crate::{Appearance, SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent};
 use anyhow::Result;
+use derive_more::{Deref, DerefMut};
 use gpui::{
     px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Global, Pixels, Subscription,
     ViewContext,
@@ -27,16 +28,104 @@ pub struct ThemeSettings {
     pub buffer_font: Font,
     pub buffer_font_size: Pixels,
     pub buffer_line_height: BufferLineHeight,
-    pub requested_theme: Option<String>,
+    pub theme_selection: Option<ThemeSelection>,
     pub active_theme: Arc<Theme>,
     pub theme_overrides: Option<ThemeStyleContent>,
 }
 
+/// The appearance of the system.
+#[derive(Debug, Clone, Copy, Deref)]
+pub struct SystemAppearance(pub Appearance);
+
+impl Default for SystemAppearance {
+    fn default() -> Self {
+        Self(Appearance::Dark)
+    }
+}
+
+#[derive(Deref, DerefMut, Default)]
+struct GlobalSystemAppearance(SystemAppearance);
+
+impl Global for GlobalSystemAppearance {}
+
+impl SystemAppearance {
+    /// Initializes the [`SystemAppearance`] for the application.
+    pub fn init(cx: &mut AppContext) {
+        *cx.default_global::<GlobalSystemAppearance>() =
+            GlobalSystemAppearance(SystemAppearance(cx.window_appearance().into()));
+    }
+
+    /// Returns the global [`SystemAppearance`].
+    ///
+    /// Inserts a default [`SystemAppearance`] if one does not yet exist.
+    pub(crate) fn default_global(cx: &mut AppContext) -> Self {
+        cx.default_global::<GlobalSystemAppearance>().0
+    }
+
+    /// Returns the global [`SystemAppearance`].
+    pub fn global(cx: &AppContext) -> Self {
+        cx.global::<GlobalSystemAppearance>().0
+    }
+
+    /// Returns a mutable reference to the global [`SystemAppearance`].
+    pub fn global_mut(cx: &mut AppContext) -> &mut Self {
+        cx.global_mut::<GlobalSystemAppearance>()
+    }
+}
+
 #[derive(Default)]
 pub(crate) struct AdjustedBufferFontSize(Pixels);
 
 impl Global for AdjustedBufferFontSize {}
 
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(untagged)]
+pub enum ThemeSelection {
+    Static(#[schemars(schema_with = "theme_name_ref")] String),
+    Dynamic {
+        #[serde(default)]
+        mode: ThemeMode,
+        #[schemars(schema_with = "theme_name_ref")]
+        light: String,
+        #[schemars(schema_with = "theme_name_ref")]
+        dark: String,
+    },
+}
+
+fn theme_name_ref(_: &mut SchemaGenerator) -> Schema {
+    Schema::new_ref("#/definitions/ThemeName".into())
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ThemeMode {
+    /// Use the specified `light` theme.
+    Light,
+
+    /// Use the specified `dark` theme.
+    Dark,
+
+    /// Use the theme based on the system's appearance.
+    #[default]
+    System,
+}
+
+impl ThemeSelection {
+    pub fn theme(&self, system_appearance: Appearance) -> &str {
+        match self {
+            Self::Static(theme) => theme,
+            Self::Dynamic { mode, light, dark } => match mode {
+                ThemeMode::Light => light,
+                ThemeMode::Dark => dark,
+                ThemeMode::System => match system_appearance {
+                    Appearance::Light => light,
+                    Appearance::Dark => dark,
+                },
+            },
+        }
+    }
+}
+
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 pub struct ThemeSettingsContent {
     #[serde(default)]
@@ -54,7 +143,7 @@ pub struct ThemeSettingsContent {
     #[serde(default)]
     pub buffer_font_features: Option<FontFeatures>,
     #[serde(default)]
-    pub theme: Option<String>,
+    pub theme: Option<ThemeSelection>,
 
     /// EXPERIMENTAL: Overrides for the current theme.
     ///
@@ -188,6 +277,7 @@ impl settings::Settings for ThemeSettings {
         cx: &mut AppContext,
     ) -> Result<Self> {
         let themes = ThemeRegistry::default_global(cx);
+        let system_appearance = SystemAppearance::default_global(cx);
 
         let mut this = Self {
             ui_font_size: defaults.ui_font_size.unwrap().into(),
@@ -205,9 +295,9 @@ impl settings::Settings for ThemeSettings {
             },
             buffer_font_size: defaults.buffer_font_size.unwrap().into(),
             buffer_line_height: defaults.buffer_line_height.unwrap(),
-            requested_theme: defaults.theme.clone(),
+            theme_selection: defaults.theme.clone(),
             active_theme: themes
-                .get(defaults.theme.as_ref().unwrap())
+                .get(defaults.theme.as_ref().unwrap().theme(*system_appearance))
                 .or(themes.get(&one_dark().name))
                 .unwrap(),
             theme_overrides: None,
@@ -229,9 +319,11 @@ impl settings::Settings for ThemeSettings {
             }
 
             if let Some(value) = &value.theme {
-                this.requested_theme = Some(value.clone());
+                this.theme_selection = Some(value.clone());
+
+                let theme_name = value.theme(*system_appearance);
 
-                if let Some(theme) = themes.get(value).log_err() {
+                if let Some(theme) = themes.get(theme_name).log_err() {
                     this.active_theme = theme;
                 }
             }
@@ -291,10 +383,6 @@ impl settings::Settings for ThemeSettings {
             .unwrap()
             .properties
             .extend([
-                (
-                    "theme".to_owned(),
-                    Schema::new_ref("#/definitions/ThemeName".into()),
-                ),
                 (
                     "buffer_font_family".to_owned(),
                     Schema::new_ref("#/definitions/FontFamilies".into()),

crates/theme/src/theme.rs πŸ”—

@@ -27,7 +27,7 @@ pub use schema::*;
 pub use settings::*;
 pub use styles::*;
 
-use gpui::{AppContext, AssetSource, Hsla, SharedString};
+use gpui::{AppContext, AssetSource, Hsla, SharedString, WindowAppearance};
 use serde::Deserialize;
 
 #[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
@@ -45,6 +45,15 @@ impl Appearance {
     }
 }
 
+impl From<WindowAppearance> for Appearance {
+    fn from(value: WindowAppearance) -> Self {
+        match value {
+            WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
+            WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
+        }
+    }
+}
+
 pub enum LoadThemes {
     /// Only load the base theme.
     ///

crates/theme_importer/Cargo.toml πŸ”—

@@ -10,10 +10,9 @@ any_ascii = "0.3.2"
 anyhow.workspace = true
 clap = { version = "4.4", features = ["derive"] }
 convert_case = "0.6.0"
-gpui = { path = "../gpui" }
+gpui.workspace = true
 indexmap = { version = "1.6.2", features = ["serde"] }
 indoc.workspace = true
-json_comments = "0.2.2"
 log.workspace = true
 palette = { version = "0.7.3", default-features = false, features = ["std"] }
 pathfinder_color = "0.5"
@@ -21,8 +20,9 @@ rust-embed.workspace = true
 schemars = { workspace = true, features = ["indexmap"] }
 serde.workspace = true
 serde_json.workspace = true
+serde_json_lenient.workspace = true
 simplelog = "0.9"
 strum = { version = "0.25.0", features = ["derive"] }
-theme = { path = "../theme" }
+theme.workspace = true
 uuid.workspace = true
 vscode_theme = "0.2.0"

crates/theme_importer/src/main.rs πŸ”—

@@ -9,7 +9,6 @@ use std::path::PathBuf;
 use anyhow::{Context, Result};
 use clap::{Parser, Subcommand};
 use indexmap::IndexMap;
-use json_comments::StripComments;
 use log::LevelFilter;
 use schemars::schema_for;
 use serde::Deserialize;
@@ -132,12 +131,11 @@ fn main() -> Result<()> {
         }
     };
 
-    let theme_without_comments = StripComments::new(theme_file);
-    let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments)
+    let vscode_theme: VsCodeTheme = serde_json_lenient::from_reader(theme_file)
         .context(format!("failed to parse theme {theme_file_path:?}"))?;
 
     let theme_metadata = ThemeMetadata {
-        name: "".to_string(),
+        name: vscode_theme.name.clone().unwrap_or("".to_string()),
         appearance: ThemeAppearanceJson::Dark,
         file_name: "".to_string(),
     };

crates/theme_selector/Cargo.toml πŸ”—

@@ -10,22 +10,22 @@ path = "src/theme_selector.rs"
 doctest = false
 
 [dependencies]
-client = { path = "../client" }
-editor = { path = "../editor" }
-feature_flags = { path = "../feature_flags" }
-fs = { path = "../fs" }
-fuzzy = {  path = "../fuzzy" }
-gpui = { path = "../gpui" }
+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 = { path = "../picker" }
+picker.workspace = true
 postage.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smol.workspace = true
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }

crates/theme_selector/src/theme_selector.rs πŸ”—

@@ -9,7 +9,9 @@ use gpui::{
 use picker::{Picker, PickerDelegate};
 use settings::{update_settings_file, SettingsStore};
 use std::sync::Arc;
-use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
+use theme::{
+    Appearance, Theme, ThemeMeta, ThemeMode, ThemeRegistry, ThemeSelection, ThemeSettings,
+};
 use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
 use util::ResultExt;
 use workspace::{ui::HighlightedLabel, ModalView, Workspace};
@@ -167,8 +169,26 @@ impl PickerDelegate for ThemeSelectorDelegate {
         self.telemetry
             .report_setting_event("theme", theme_name.to_string());
 
+        let appearance = Appearance::from(cx.appearance());
+
         update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
-            settings.theme = Some(theme_name.to_string());
+            if let Some(selection) = settings.theme.as_mut() {
+                let theme_to_update = match selection {
+                    ThemeSelection::Static(theme) => theme,
+                    ThemeSelection::Dynamic { mode, light, dark } => match mode {
+                        ThemeMode::Light => light,
+                        ThemeMode::Dark => dark,
+                        ThemeMode::System => match appearance {
+                            Appearance::Light => light,
+                            Appearance::Dark => dark,
+                        },
+                    },
+                };
+
+                *theme_to_update = theme_name.to_string();
+            } else {
+                settings.theme = Some(ThemeSelection::Static(theme_name.to_string()));
+            }
         });
 
         self.view

crates/ui/Cargo.toml πŸ”—

@@ -12,16 +12,16 @@ path = "src/ui.rs"
 [dependencies]
 anyhow.workspace = true
 chrono = "0.4"
-gpui = { path = "../gpui" }
+gpui.workspace = true
 itertools = { version = "0.11.0", optional = true }
-menu = { path = "../menu" }
+menu.workspace = true
 rand = "0.8"
 serde.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smallvec.workspace = true
-story = { path = "../story", optional = true }
+story = { workspace = true, optional = true }
 strum = { version = "0.25.0", features = ["derive"] }
-theme = { path = "../theme" }
+theme.workspace = true
 
 [features]
 default = []

crates/ui/src/components/label/highlighted_label.rs πŸ”—

@@ -79,6 +79,7 @@ impl RenderOnce for HighlightedLabel {
         let mut text_style = cx.text_style().clone();
         text_style.color = self.base.color.color(cx);
 
-        LabelLike::new().child(StyledText::new(self.label).with_highlights(&text_style, highlights))
+        self.base
+            .child(StyledText::new(self.label).with_highlights(&text_style, highlights))
     }
 }

crates/util/Cargo.toml πŸ”—

@@ -15,6 +15,7 @@ test-support = ["tempfile", "git2"]
 [dependencies]
 anyhow.workspace = true
 backtrace = "0.3"
+collections.workspace = true
 dirs = "3.0"
 futures.workspace = true
 git2 = { workspace = true, optional = true }

crates/util/src/paths.rs πŸ”—

@@ -157,7 +157,7 @@ pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
 
 /// A representation of a path-like string with optional row and column numbers.
 /// Matching values example: `te`, `test.rs:22`, `te:22:5`, etc.
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
 pub struct PathLikeWithPosition<P> {
     pub path_like: P,
     pub row: Option<u32>,

crates/util/src/test/marked_text.rs πŸ”—

@@ -1,4 +1,5 @@
-use std::{cmp::Ordering, collections::HashMap, ops::Range};
+use collections::HashMap;
+use std::{cmp::Ordering, ops::Range};
 
 /// Construct a string and a list of offsets within that string using a single
 /// string containing embedded position markers.

crates/vcs_menu/Cargo.toml πŸ”—

@@ -7,10 +7,10 @@ license = "GPL-3.0-or-later"
 
 [dependencies]
 anyhow.workspace = true
-fs = { path = "../fs" }
-fuzzy = {  path = "../fuzzy" }
-gpui = { path = "../gpui" }
-picker = { path = "../picker" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
+fs.workspace = true
+fuzzy.workspace = true
+gpui.workspace = true
+picker.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true

crates/vim/Cargo.toml πŸ”—

@@ -16,40 +16,40 @@ neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
 anyhow.workspace = true
 async-compat = { version = "0.2.1", "optional" = true }
 async-trait = { workspace = true, "optional" = true }
-collections = { path = "../collections" }
-command_palette = { path = "../command_palette" }
+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 = { path = "../copilot" }
-diagnostics = { path = "../diagnostics" }
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
+copilot.workspace = true
+diagnostics.workspace = true
+editor.workspace = true
+gpui.workspace = true
 itertools = "0.10"
-language = { path = "../language" }
+language.workspace = true
 log.workspace = true
 nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
 regex.workspace = true
-search = { path = "../search" }
+search.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
-theme = { path = "../theme" }
+settings.workspace = true
+theme.workspace = true
 tokio = { version = "1.15", "optional" = true }
-ui = { path = "../ui" }
-workspace = { path = "../workspace" }
-zed_actions = { path = "../zed_actions" }
+ui.workspace = true
+workspace.workspace = true
+zed_actions.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
 futures.workspace = true
-gpui = { path = "../gpui", features = ["test-support"] }
-release_channel = { path = "../release_channel" }
+gpui = { workspace = true, features = ["test-support"] }
+release_channel.workspace = true
 indoc.workspace = true
-language = { path = "../language", features = ["test-support"] }
-lsp = { path = "../lsp", features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+lsp = { workspace = true, features = ["test-support"] }
 parking_lot.workspace = true
-project = { path = "../project", features = ["test-support"] }
-settings = { path = "../settings" }
-theme = { path = "../theme", features = ["test-support"] }
-util = { path = "../util", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+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"] }

crates/vim/src/motion.rs πŸ”—

@@ -798,23 +798,14 @@ fn next_word_end(
             *point.row_mut() += 1;
             *point.column_mut() = 0;
         }
-        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
-            let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
-            let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 
-            left_kind != right_kind && left_kind != CharKind::Whitespace
-        });
+        point =
+            movement::find_boundary_exclusive(map, point, FindRange::MultiLine, |left, right| {
+                let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
+                let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
 
-        // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
-        // we have backtracked already
-        if !map
-            .chars_at(point)
-            .nth(1)
-            .map(|(c, _)| c == '\n')
-            .unwrap_or(true)
-        {
-            *point.column_mut() = point.column().saturating_sub(1);
-        }
+                left_kind != right_kind && left_kind != CharKind::Whitespace
+            });
         point = map.clip_point(point, Bias::Left);
     }
     point
@@ -1044,9 +1035,17 @@ fn window_top(
     map: &DisplaySnapshot,
     point: DisplayPoint,
     text_layout_details: &TextLayoutDetails,
-    times: usize,
+    mut times: usize,
 ) -> (DisplayPoint, SelectionGoal) {
-    let first_visible_line = text_layout_details.anchor.to_display_point(map);
+    let first_visible_line = text_layout_details
+        .scroll_anchor
+        .anchor
+        .to_display_point(map);
+
+    if first_visible_line.row() != 0 && text_layout_details.vertical_scroll_margin as usize > times
+    {
+        times = text_layout_details.vertical_scroll_margin.ceil() as usize;
+    }
 
     if let Some(visible_rows) = text_layout_details.visible_rows {
         let bottom_row = first_visible_line.row() + visible_rows as u32;
@@ -1070,7 +1069,10 @@ fn window_middle(
     text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
     if let Some(visible_rows) = text_layout_details.visible_rows {
-        let first_visible_line = text_layout_details.anchor.to_display_point(map);
+        let first_visible_line = text_layout_details
+            .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 new_col = point.column().min(map.line_len(new_row));
@@ -1085,11 +1087,20 @@ fn window_bottom(
     map: &DisplaySnapshot,
     point: DisplayPoint,
     text_layout_details: &TextLayoutDetails,
-    times: usize,
+    mut times: usize,
 ) -> (DisplayPoint, SelectionGoal) {
     if let Some(visible_rows) = text_layout_details.visible_rows {
-        let first_visible_line = text_layout_details.anchor.to_display_point(map);
-        let bottom_row = first_visible_line.row() + (visible_rows) as u32;
+        let first_visible_line = text_layout_details
+            .scroll_anchor
+            .anchor
+            .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()
+            && 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 new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
             first_visible_line.row()
@@ -1265,6 +1276,15 @@ mod test {
         cx.assert_shared_state("one two thˇree four").await;
     }
 
+    #[gpui::test]
+    async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        let initial_state = indoc! {r"something(Λ‡foo)"};
+        cx.set_shared_state(initial_state).await;
+        cx.simulate_shared_keystrokes(["}"]).await;
+        cx.assert_shared_state(indoc! {r"something(fooˇ)"}).await;
+    }
+
     #[gpui::test]
     async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/src/normal/scroll.rs πŸ”—

@@ -1,11 +1,10 @@
 use crate::Vim;
 use editor::{
-    display_map::ToDisplayPoint,
-    scroll::{ScrollAmount, VERTICAL_SCROLL_MARGIN},
-    DisplayPoint, Editor,
+    display_map::ToDisplayPoint, scroll::ScrollAmount, DisplayPoint, Editor, EditorSettings,
 };
 use gpui::{actions, ViewContext};
 use language::Bias;
+use settings::Settings;
 use workspace::Workspace;
 
 actions!(
@@ -77,6 +76,7 @@ fn scroll_editor(
         };
 
         let top_anchor = editor.scroll_manager.anchor().anchor;
+        let vertical_scroll_margin = EditorSettings::get_global(cx).vertical_scroll_margin;
 
         editor.change_selections(None, cx, |s| {
             s.move_with(|map, selection| {
@@ -88,8 +88,8 @@ fn scroll_editor(
                     let new_row = top.row() + selection.head().row() - old_top.row();
                     head = map.clip_point(DisplayPoint::new(new_row, head.column()), Bias::Left)
                 }
-                let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32;
-                let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1;
+                let min_row = top.row() + vertical_scroll_margin as u32;
+                let max_row = top.row() + visible_rows - vertical_scroll_margin as u32 - 1;
 
                 let new_head = if head.row() < min_row {
                     map.clip_point(DisplayPoint::new(min_row, head.column()), Bias::Left)

crates/vim/src/test/neovim_backed_test_context.rs πŸ”—

@@ -1,4 +1,4 @@
-use editor::{scroll::VERTICAL_SCROLL_MARGIN, test::editor_test_context::ContextHandle};
+use editor::test::editor_test_context::ContextHandle;
 use gpui::{px, size, Context};
 use indoc::indoc;
 use settings::SettingsStore;
@@ -155,9 +155,7 @@ impl NeovimBackedTestContext {
 
     pub async fn set_scroll_height(&mut self, rows: u32) {
         // match Zed's scrolling behavior
-        self.neovim
-            .set_option(&format!("scrolloff={}", VERTICAL_SCROLL_MARGIN))
-            .await;
+        self.neovim.set_option(&format!("scrolloff={}", 3)).await;
         // +2 to account for the vim command UI at the bottom.
         self.neovim.set_option(&format!("lines={}", rows + 2)).await;
         let (line_height, visible_line_count) = self.editor(|editor, cx| {

crates/vim/src/vim.rs πŸ”—

@@ -204,7 +204,8 @@ impl Vim {
                 let editor = editor.read(cx);
                 if editor.leader_peer_id().is_none() {
                     let newest = editor.selections.newest::<usize>(cx);
-                    local_selections_changed(newest, cx);
+                    let is_multicursor = editor.selections.count() > 1;
+                    local_selections_changed(newest, is_multicursor, cx);
                 }
             }
             EditorEvent::InputIgnored { text } => {
@@ -626,13 +627,24 @@ impl Settings for VimModeSetting {
     }
 }
 
-fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
+fn local_selections_changed(
+    newest: Selection<usize>,
+    is_multicursor: bool,
+    cx: &mut WindowContext,
+) {
     Vim::update(cx, |vim, cx| {
-        if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
-            if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
-                vim.switch_mode(Mode::VisualBlock, false, cx);
-            } else {
-                vim.switch_mode(Mode::Visual, false, cx)
+        if vim.enabled {
+            if vim.state().mode == Mode::Normal && !newest.is_empty() {
+                if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
+                    vim.switch_mode(Mode::VisualBlock, false, cx);
+                } else {
+                    vim.switch_mode(Mode::Visual, false, cx)
+                }
+            } else if newest.is_empty()
+                && !is_multicursor
+                && [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&vim.state().mode)
+            {
+                vim.switch_mode(Mode::Normal, true, cx)
             }
         }
     })

crates/welcome/Cargo.toml πŸ”—

@@ -13,25 +13,25 @@ test-support = []
 
 [dependencies]
 anyhow.workspace = true
-client = { path = "../client" }
-db = { path = "../db" }
-editor = { path = "../editor" }
-fs = { path = "../fs" }
-fuzzy = {  path = "../fuzzy" }
-gpui = { path = "../gpui" }
-install_cli = { path = "../install_cli" }
+client.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 = { path = "../picker" }
-project = { path = "../project" }
+picker.workspace = true
+project.workspace = true
 schemars.workspace = true
 serde.workspace = true
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-theme_selector = { path = "../theme_selector" }
-ui = { path = "../ui" }
-util = { path = "../util" }
-vim = { path = "../vim" }
-workspace = { path = "../workspace" }
+settings.workspace = true
+theme.workspace = true
+theme_selector.workspace = true
+ui.workspace = true
+util.workspace = true
+vim.workspace = true
+workspace.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }

crates/workspace/Cargo.toml πŸ”—

@@ -23,43 +23,43 @@ test-support = [
 anyhow.workspace = true
 async-recursion = "1.0.0"
 bincode = "1.2.1"
-call = { path = "../call" }
-client = { path = "../client" }
-collections = { path = "../collections" }
-db = { path = "../db" }
+call.workspace = true
+client.workspace = true
+collections.workspace = true
+db.workspace = true
 derive_more.workspace = true
-fs = { path = "../fs" }
+fs.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
-install_cli = { path = "../install_cli" }
+gpui.workspace = true
+install_cli.workspace = true
 itertools = "0.10"
-language = { path = "../language" }
+language.workspace = true
 lazy_static.workspace = true
 log.workspace = true
-node_runtime = { path = "../node_runtime" }
+node_runtime.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
-project = { path = "../project" }
+project.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 smallvec.workspace = true
-sqlez = { path = "../sqlez" }
-terminal = { path = "../terminal" }
-theme = { path = "../theme" }
-ui = { path = "../ui" }
-util = { path = "../util" }
+sqlez.workspace = true
+terminal.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
 uuid.workspace = true
 
 [dev-dependencies]
-call = { path = "../call", features = ["test-support"] }
-client = { path = "../client", features = ["test-support"] }
-db = { path = "../db", features = ["test-support"] }
+call = { workspace = true, features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+db = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
-fs = { path = "../fs", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
+fs = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
 indoc.workspace = true
-project = { path = "../project", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }

crates/workspace/src/pane.rs πŸ”—

@@ -72,10 +72,10 @@ pub struct CloseAllItems {
     pub save_intent: Option<SaveIntent>,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct RevealInProjectPanel {
-    pub entry_id: u64,
+    pub entry_id: Option<u64>,
 }
 
 impl_actions!(
@@ -1442,7 +1442,9 @@ impl Pane {
                         let entry_id = entry.to_proto();
                         menu = menu.separator().entry(
                             "Reveal In Project Panel",
-                            Some(Box::new(RevealInProjectPanel { entry_id })),
+                            Some(Box::new(RevealInProjectPanel {
+                                entry_id: Some(entry_id),
+                            })),
                             cx.handler_for(&pane, move |pane, cx| {
                                 pane.project.update(cx, |_, cx| {
                                     cx.emit(project::Event::RevealInProjectPanel(
@@ -1807,11 +1809,15 @@ impl Render for Pane {
             )
             .on_action(
                 cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, cx| {
-                    pane.project.update(cx, |_, cx| {
-                        cx.emit(project::Event::RevealInProjectPanel(
-                            ProjectEntryId::from_proto(action.entry_id),
-                        ))
-                    })
+                    let entry_id = action
+                        .entry_id
+                        .map(ProjectEntryId::from_proto)
+                        .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied());
+                    if let Some(entry_id) = entry_id {
+                        pane.project.update(cx, |_, cx| {
+                            cx.emit(project::Event::RevealInProjectPanel(entry_id))
+                        });
+                    }
                 }),
             )
             .when(self.active_item().is_some(), |pane| {

crates/workspace/src/toolbar.rs πŸ”—

@@ -127,7 +127,7 @@ impl Render for Toolbar {
                             h_flex()
                                 // We're using `flex_none` here to prevent some flickering that can occur when the
                                 // size of the left items container changes.
-                                .flex_none()
+                                .when_else(has_left_items, Div::flex_none, Div::flex_auto)
                                 .justify_end()
                                 .children(self.right_items().map(|item| item.to_any())),
                         )

crates/workspace/src/workspace.rs πŸ”—

@@ -12,7 +12,7 @@ mod toolbar;
 mod workspace_settings;
 
 use anyhow::{anyhow, Context as _, Result};
-use call::ActiveCall;
+use call::{call_settings::CallSettings, ActiveCall};
 use client::{
     proto::{self, ErrorCode, PeerId},
     Client, ErrorExt, Status, TypedEnvelope, UserStore,
@@ -64,7 +64,7 @@ use std::{
     sync::{atomic::AtomicUsize, Arc},
     time::Duration,
 };
-use theme::{ActiveTheme, ThemeSettings};
+use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
 pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
 use ui::Label;
@@ -682,6 +682,21 @@ impl Workspace {
                 }
                 cx.notify();
             }),
+            cx.observe_window_appearance(|_, cx| {
+                let window_appearance = cx.appearance();
+
+                *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
+
+                let mut theme_settings = ThemeSettings::get_global(cx).clone();
+
+                if let Some(theme_selection) = theme_settings.theme_selection.clone() {
+                    let theme_name = theme_selection.theme(window_appearance.into());
+
+                    if let Some(_theme) = theme_settings.switch_theme(&theme_name, cx) {
+                        ThemeSettings::override_global(theme_settings, cx);
+                    }
+                }
+            }),
             cx.observe(&left_dock, |this, _, cx| {
                 this.serialize_workspace(cx);
                 cx.notify();
@@ -1328,22 +1343,14 @@ 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 close_task = if is_remote || has_worktree || has_dirty_items {
+        let window_to_replace = if is_remote || has_worktree || has_dirty_items {
             None
         } else {
-            Some(self.prepare_to_close(false, cx))
+            window
         };
         let app_state = self.app_state.clone();
 
         cx.spawn(|_, mut cx| async move {
-            let window_to_replace = if let Some(close_task) = close_task {
-                if !close_task.await? {
-                    return Ok(());
-                }
-                window
-            } else {
-                None
-            };
             cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))?
                 .await?;
             Ok(())
@@ -2893,25 +2900,27 @@ impl Workspace {
         Ok(())
     }
 
-    fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
+    pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
         let mut is_project_item = true;
         let mut update = proto::UpdateActiveView::default();
 
-        if let Some(item) = self.active_item(cx) {
-            if item.focus_handle(cx).contains_focused(cx) {
-                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),
-                    };
+        if cx.is_window_active() {
+            if let Some(item) = self.active_item(cx) {
+                if item.focus_handle(cx).contains_focused(cx) {
+                    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),
+                        };
+                    }
                 }
             }
         }
 
-        if update.id != self.last_active_view_id {
+        if &update.id != &self.last_active_view_id {
             self.last_active_view_id = update.id.clone();
             self.update_followers(
                 is_project_item,
@@ -3974,6 +3983,8 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
     DB.last_workspace().await.log_err().flatten()
 }
 
+actions!(collab, [OpenChannelNotes]);
+
 async fn join_channel_internal(
     channel_id: u64,
     app_state: &Arc<AppState>,
@@ -4075,6 +4086,36 @@ async fn join_channel_internal(
             return Some(join_remote_project(project, host, app_state.clone(), cx));
         }
 
+        // if you are the first to join a channel, share your project
+        if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
+            if let Some(workspace) = requesting_window {
+                let project = workspace.update(cx, |workspace, cx| {
+                    if !CallSettings::get_global(cx).share_on_join {
+                        return None;
+                    }
+                    let project = workspace.project.read(cx);
+                    if project.is_local()
+                        && project.visible_worktrees(cx).any(|tree| {
+                            tree.read(cx)
+                                .root_entry()
+                                .map_or(false, |entry| entry.is_dir())
+                        })
+                    {
+                        Some(workspace.project.clone())
+                    } else {
+                        None
+                    }
+                });
+                if let Ok(Some(project)) = project {
+                    return Some(cx.spawn(|room, mut cx| async move {
+                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
+                            .await?;
+                        Ok(())
+                    }));
+                }
+            }
+        }
+
         None
     })?;
     if let Some(task) = task {
@@ -4117,6 +4158,12 @@ pub fn join_channel(
                 })?
                 .await?;
 
+            if result.is_ok() {
+                cx.update(|cx| {
+                    cx.dispatch_action(&OpenChannelNotes);
+                }).log_err();
+            }
+
             active_window = Some(window_handle);
         }
 

crates/zed/Cargo.toml πŸ”—

@@ -2,7 +2,7 @@
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.122.0"
+version = "0.123.0"
 publish = false
 license = "GPL-3.0-or-later"
 
@@ -16,98 +16,99 @@ name = "Zed"
 path = "src/main.rs"
 
 [dependencies]
-activity_indicator = { path = "../activity_indicator" }
-ai = { path = "../ai" }
+activity_indicator.workspace = true
+ai.workspace = true
 anyhow.workspace = true
-assets = { path = "../assets" }
-assistant = { path = "../assistant" }
+assets.workspace = true
+assistant.workspace = true
 async-compression.workspace = true
 async-recursion = "0.3"
 async-tar = "0.4.2"
 async-trait.workspace = true
-audio = { path = "../audio" }
-auto_update = { path = "../auto_update" }
+audio.workspace = true
+auto_update.workspace = true
 backtrace = "0.3"
-breadcrumbs = { path = "../breadcrumbs" }
-call = { path = "../call" }
-channel = { path = "../channel" }
+breadcrumbs.workspace = true
+call.workspace = true
+channel.workspace = true
 chrono = "0.4"
-cli = { path = "../cli" }
-client = { path = "../client" }
-collab_ui = { path = "../collab_ui" }
-collections = { path = "../collections" }
-command_palette = { path = "../command_palette" }
-copilot = { path = "../copilot" }
-copilot_ui = { path = "../copilot_ui" }
+cli.workspace = true
+client.workspace = true
+collab_ui.workspace = true
+collections.workspace = true
+command_palette.workspace = true
+copilot.workspace = true
+copilot_ui.workspace = true
 ctor.workspace = true
-db = { path = "../db" }
-diagnostics = { path = "../diagnostics" }
-editor = { path = "../editor" }
+db.workspace = true
+diagnostics.workspace = true
+editor.workspace = true
 env_logger.workspace = true
-feature_flags = { path = "../feature_flags" }
-feedback = { path = "../feedback" }
-file_finder = { path = "../file_finder" }
-fs = { path = "../fs" }
-fsevent = { path = "../fsevent" }
+feature_flags.workspace = true
+feedback.workspace = true
+file_finder.workspace = true
+fs.workspace = true
+fsevent.workspace = true
 futures.workspace = true
-go_to_line = { path = "../go_to_line" }
-gpui = { path = "../gpui" }
+go_to_line.workspace = true
+gpui.workspace = true
 ignore = "0.4"
 image = "0.23"
 indexmap = "1.6.2"
-install_cli = { path = "../install_cli" }
+install_cli.workspace = true
 isahc.workspace = true
 itertools = "0.11"
-journal = { path = "../journal" }
-language = { path = "../language" }
-language_selector = { path = "../language_selector" }
-language_tools = { path = "../language_tools" }
+journal.workspace = true
+language.workspace = true
+language_selector.workspace = true
+language_tools.workspace = true
 lazy_static.workspace = true
 libc = "0.2"
 log.workspace = true
-lsp = { path = "../lsp" }
-markdown_preview = { path = "../markdown_preview" }
-menu = { path = "../menu" }
+lsp.workspace = true
+markdown_preview.workspace = true
+menu.workspace = true
 mimalloc = "0.1"
-node_runtime = { path = "../node_runtime" }
-notifications = { path = "../notifications" }
+node_runtime.workspace = true
+notifications.workspace = true
 num_cpus = "1.13.0"
-outline = { path = "../outline" }
+outline.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
-project = { path = "../project" }
-project_panel = { path = "../project_panel" }
-project_symbols = { path = "../project_symbols" }
-quick_action_bar = { path = "../quick_action_bar" }
+project.workspace = true
+project_panel.workspace = true
+project_symbols.workspace = true
+quick_action_bar.workspace = true
 rand.workspace = true
-recent_projects = { path = "../recent_projects" }
+recent_projects.workspace = true
 regex.workspace = true
-release_channel = { path = "../release_channel" }
-rope = { path = "../rope" }
-rpc = { path = "../rpc" }
+release_channel.workspace = true
+rope.workspace = true
+rpc.workspace = true
 rsa = "0.4"
 rust-embed.workspace = true
 schemars.workspace = true
-search = { path = "../search" }
-semantic_index = { path = "../semantic_index" }
+search.workspace = true
+semantic_index.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-settings = { path = "../settings" }
+settings.workspace = true
 shellexpand = "2.1.0"
 simplelog = "0.9"
 smallvec.workspace = true
 smol.workspace = true
-sum_tree = { path = "../sum_tree" }
+sum_tree.workspace = true
 tempfile.workspace = true
-terminal_view = { path = "../terminal_view" }
-text = { path = "../text" }
-theme = { path = "../theme" }
-theme_selector = { path = "../theme_selector" }
+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-bash.workspace = true
+tree-sitter-beancount.workspace = true
 tree-sitter-c-sharp.workspace = true
 tree-sitter-c.workspace = true
 tree-sitter-cpp.workspace = true
@@ -123,6 +124,7 @@ 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
@@ -130,6 +132,7 @@ 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-proto.workspace = true
 tree-sitter-purescript.workspace = true
@@ -148,22 +151,22 @@ tree-sitter-zig.workspace = true
 tree-sitter.workspace = true
 url.workspace = true
 urlencoding = "2.1.2"
-util = { path = "../util" }
+util.workspace = true
 uuid.workspace = true
-vim = { path = "../vim" }
-welcome = { path = "../welcome" }
-workspace = { path = "../workspace" }
-zed_actions = { path = "../zed_actions" }
+vim.workspace = true
+welcome.workspace = true
+workspace.workspace = true
+zed_actions.workspace = true
 
 [dev-dependencies]
-call = { path = "../call", features = ["test-support"] }
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
-text = { path = "../text", features = ["test-support"] }
+call = { workspace = true, features = ["test-support"] }
+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
-workspace = { path = "../workspace", features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }
 
 [package.metadata.bundle-dev]
 icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]

crates/zed/src/languages.rs πŸ”—

@@ -25,6 +25,7 @@ mod json;
 mod language_plugin;
 mod lua;
 mod nu;
+mod ocaml;
 mod php;
 mod purescript;
 mod python;
@@ -66,6 +67,7 @@ pub fn init(
     };
 
     language("bash", tree_sitter_bash::language(), vec![]);
+    language("beancount", tree_sitter_beancount::language(), vec![]);
     language(
         "c",
         tree_sitter_c::language(),
@@ -302,6 +304,16 @@ pub fn init(
         tree_sitter_nu::language(),
         vec![Arc::new(nu::NuLanguageServer {})],
     );
+    language(
+        "ocaml",
+        tree_sitter_ocaml::language_ocaml(),
+        vec![Arc::new(ocaml::OCamlLspAdapter)],
+    );
+    language(
+        "ocaml-interface",
+        tree_sitter_ocaml::language_ocaml_interface(),
+        vec![Arc::new(ocaml::OCamlLspAdapter)],
+    );
     language(
         "vue",
         tree_sitter_vue::language(),
@@ -313,6 +325,8 @@ pub fn init(
         vec![Arc::new(uiua::UiuaLanguageServer {})],
     );
     language("proto", tree_sitter_proto::language(), vec![]);
+    language("terraform", tree_sitter_hcl::language(), vec![]);
+    language("hcl", tree_sitter_hcl::language(), vec![]);
 
     if let Ok(children) = std::fs::read_dir(&*PLUGINS_DIR) {
         for child in children {

crates/zed/src/languages/beancount/highlights.scm πŸ”—

@@ -0,0 +1,21 @@
+(comment) @comment
+(headline) @comment
+[
+    (payee)
+    (narration)
+    (string)
+] @string
+
+(number) @number
+(date) @function
+(currency) @constant
+(account) @identifier
+
+[
+    (option)
+    (include)
+    (open)
+    (balance)
+    (pad)
+    (close)
+] @keyword

crates/zed/src/languages/elm.rs πŸ”—

@@ -1,9 +1,13 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
+use gpui::AppContext;
 use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
+use project::project_settings::ProjectSettings;
+use serde_json::Value;
+use settings::Settings;
 use smol::fs;
 use std::{
     any::Any,
@@ -13,6 +17,7 @@ use std::{
 };
 use util::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";
 
 fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
@@ -32,7 +37,7 @@ impl ElmLspAdapter {
 #[async_trait]
 impl LspAdapter for ElmLspAdapter {
     fn name(&self) -> LanguageServerName {
-        LanguageServerName("elm-language-server".into())
+        LanguageServerName(SERVER_NAME.into())
     }
 
     fn short_name(&self) -> &'static str {
@@ -88,6 +93,27 @@ impl LspAdapter for ElmLspAdapter {
     ) -> Option<LanguageServerBinary> {
         get_cached_server_binary(container_dir, &*self.node).await
     }
+
+    fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {
+        // elm-language-server expects workspace didChangeConfiguration notification
+        // params to be the same as lsp initialization_options
+        let override_options = ProjectSettings::get_global(cx)
+            .lsp
+            .get(SERVER_NAME)
+            .and_then(|s| s.initialization_options.clone())
+            .unwrap_or_default();
+
+        match override_options.clone().as_object_mut() {
+            Some(op) => {
+                // elm-language-server requests workspace configuration
+                // for the `elmLS` section, so we have to nest
+                // another copy of initialization_options there
+                op.insert("elmLS".into(), override_options);
+                serde_json::to_value(op).unwrap_or_default()
+            }
+            None => override_options,
+        }
+    }
 }
 
 async fn get_cached_server_binary(

crates/zed/src/languages/go.rs πŸ”—

@@ -398,7 +398,6 @@ mod tests {
         let highlight_type = grammar.highlight_id_for_name("type").unwrap();
         let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
         let highlight_number = grammar.highlight_id_for_name("number").unwrap();
-        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
 
         assert_eq!(
             language
@@ -454,7 +453,7 @@ mod tests {
             Some(CodeLabel {
                 text: "two.Three a.Bcd".to_string(),
                 filter_range: 0..9,
-                runs: vec![(4..9, highlight_field), (12..15, highlight_type)],
+                runs: vec![(12..15, highlight_type)],
             })
         );
     }

crates/zed/src/languages/go/highlights.scm πŸ”—

@@ -1,6 +1,10 @@
-(identifier) @variable
 (type_identifier) @type
-(field_identifier) @property
+(field_identifier) @variable.member
+
+(keyed_element
+  .
+  (literal_element
+    (identifier) @variable.member))
 
 (call_expression
   function: (identifier) @function)
@@ -15,6 +19,15 @@
 (method_declaration
   name: (field_identifier) @function.method)
 
+[
+  "("
+  ")"
+  "{"
+  "}"
+  "["
+  "]"
+] @punctuation.bracket
+
 [
   "--"
   "-"

crates/zed/src/languages/go/outline.scm πŸ”—

@@ -7,20 +7,21 @@
     "func" @context
     name: (identifier) @name
     parameters: (parameter_list
-      "(" @context
-      ")" @context)) @item
+      "("
+      ")")) @item
 
 (method_declaration
     "func" @context
     receiver: (parameter_list
         "(" @context
         (parameter_declaration
+            name: (_) @name
             type: (_) @context)
         ")" @context)
     name: (field_identifier) @name
     parameters: (parameter_list
-      "(" @context
-      ")" @context)) @item
+      "("
+      ")")) @item
 
 (const_declaration
     "const" @context
@@ -40,4 +41,4 @@
       ")" @context)) @item
 
 (field_declaration
-    name: (_) @name) @item
+    name: (_) @name) @item

crates/zed/src/languages/haskell/outline.scm πŸ”—

@@ -0,0 +1,26 @@
+(adt
+  "data" @context
+  name: (type) @name) @item
+
+(type_alias
+  "type" @context
+  name: (type) @name) @item
+
+(newtype
+  "newtype" @context
+  name: (type) @name) @item
+
+(signature
+  name: (variable) @name) @item
+
+(class
+  "class" @context
+  (class_head) @name) @item
+
+(instance
+  "instance" @context
+  (instance_head) @name) @item
+
+(foreign_import
+  "foreign" @context
+  (impent) @name) @item

crates/zed/src/languages/hcl/config.toml πŸ”—

@@ -0,0 +1,13 @@
+name = "HCL"
+path_suffixes = ["hcl"]
+line_comments = ["# ", "// "]
+block_comment = ["/*", "*/"]
+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 = ["comment", "string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
+]

crates/zed/src/languages/hcl/highlights.scm πŸ”—

@@ -0,0 +1,117 @@
+; https://github.com/nvim-treesitter/nvim-treesitter/blob/cb79d2446196d25607eb1d982c96939abdf67b8e/queries/hcl/highlights.scm
+; highlights.scm
+[
+  "!"
+  "\*"
+  "/"
+  "%"
+  "\+"
+  "-"
+  ">"
+  ">="
+  "<"
+  "<="
+  "=="
+  "!="
+  "&&"
+  "||"
+] @operator
+
+[
+  "{"
+  "}"
+  "["
+  "]"
+  "("
+  ")"
+] @punctuation.bracket
+
+[
+  "."
+  ".*"
+  ","
+  "[*]"
+] @punctuation.delimiter
+
+[
+  (ellipsis)
+  "\?"
+  "=>"
+] @punctuation.special
+
+[
+  ":"
+  "="
+] @punctuation
+
+[
+  "for"
+  "endfor"
+  "in"
+  "if"
+  "else"
+  "endif"
+] @keyword
+
+[
+  (quoted_template_start) ; "
+  (quoted_template_end) ; "
+  (template_literal) ; non-interpolation/directive content
+] @string
+
+[
+  (heredoc_identifier) ; END
+  (heredoc_start) ; << or <<-
+] @punctuation.delimiter
+
+[
+  (template_interpolation_start) ; ${
+  (template_interpolation_end) ; }
+  (template_directive_start) ; %{
+  (template_directive_end) ; }
+  (strip_marker) ; ~
+] @punctuation.special
+
+(numeric_lit) @number
+
+(bool_lit) @boolean
+
+(null_lit) @constant
+
+(comment) @comment
+
+(identifier) @variable
+
+(body
+  (block
+    (identifier) @keyword))
+
+(body
+  (block
+    (body
+      (block
+        (identifier) @type))))
+
+(function_call
+  (identifier) @function)
+
+(attribute
+  (identifier) @variable)
+
+; { key: val }
+;
+; highlight identifier keys as though they were block attributes
+(object_elem
+  key:
+    (expression
+      (variable_expr
+        (identifier) @variable)))
+
+; var.foo, data.bar
+;
+; first element in get_attr is a variable.builtin or a reference to a variable.builtin
+(expression
+  (variable_expr
+    (identifier) @variable)
+  (get_attr
+    (identifier) @variable))

crates/zed/src/languages/hcl/indents.scm πŸ”—

@@ -0,0 +1,11 @@
+; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/hcl/indents.scm
+[
+  (block)
+  (object)
+  (tuple)
+  (function_call)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "(" ")" @end) @indent
+(_ "{" "}" @end) @indent

crates/zed/src/languages/hcl/injections.scm πŸ”—

@@ -0,0 +1,6 @@
+; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/hcl/injections.scm
+
+(heredoc_template
+  (template_literal) @content
+  (heredoc_identifier) @language
+  (#downcase! @language))

crates/zed/src/languages/ocaml-interface/config.toml πŸ”—

@@ -0,0 +1,13 @@
+name = "OCaml Interface"
+path_suffixes = ["mli"]
+block_comment = ["(* ", "*)"]
+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 = 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 },
+]

crates/zed/src/languages/ocaml-interface/indents.scm πŸ”—

@@ -0,0 +1,21 @@
+[
+  (type_binding)
+
+  (value_specification)
+  (method_specification)
+
+  (external)
+  (field_declaration)
+] @indent
+
+(_ "<" ">" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent
+
+(_ "object" @start "end" @end) @indent
+
+(signature
+  "sig" @start
+  "end" @end) @indent
+
+";;" @outdent

crates/zed/src/languages/ocaml-interface/outline.scm πŸ”—

@@ -0,0 +1,48 @@
+(module_type_definition
+  "module" @context
+  "type" @context
+  name: (_) @name) @item
+
+(module_definition
+    "module" @context
+    (module_binding name: (_) @name)) @item
+
+(type_definition
+    "type" @context
+    (type_binding name: (_) @name)) @item
+
+(class_definition
+    "class" @context
+    (class_binding
+      "virtual"? @context
+      name: (_) @name)) @item
+
+(class_type_definition
+  "class" @context
+  "type" @context
+  (class_type_binding
+    "virtual"? @context
+    name: (_) @name)) @item
+
+(instance_variable_definition
+  "val" @context
+  "method"? @context
+  name: (_) @name) @item
+
+(method_specification
+  "method" @context
+  "virtual"? @context
+   (method_name) @name) @item
+
+(value_specification
+    "val" @context
+    (value_name) @name) @item
+
+(external
+  "external" @context
+  (value_name) @name) @item
+
+(exception_definition
+    "exception" @context
+    (constructor_declaration
+      (constructor_name) @name)) @item

crates/zed/src/languages/ocaml.rs πŸ”—

@@ -0,0 +1,317 @@
+use std::{any::Any, ops::Range, path::PathBuf, sync::Arc};
+
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{CodeLabel, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
+use rope::Rope;
+
+const OPERATOR_CHAR: [char; 17] = [
+    '~', '!', '?', '%', '<', ':', '.', '$', '&', '*', '+', '-', '/', '=', '>', '@', '^',
+];
+
+pub struct OCamlLspAdapter;
+
+#[async_trait]
+impl LspAdapter for OCamlLspAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName("ocamllsp".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "ocaml"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _: Box<dyn 'static + Send + Any>,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!(
+            "ocamllsp (ocaml-language-server) must be installed manually."
+        ))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "ocamllsp".into(),
+            arguments: vec![],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp::CompletionItem,
+        language: &Arc<language::Language>,
+    ) -> Option<CodeLabel> {
+        let name = &completion.label;
+        let detail = completion.detail.as_ref().map(|s| s.replace("\n", " "));
+
+        match completion.kind.zip(detail) {
+            // Error of 'b : ('a, 'b) result
+            // Stack_overflow : exn
+            Some((CompletionItemKind::CONSTRUCTOR | CompletionItemKind::ENUM_MEMBER, detail)) => {
+                let (argument, return_t) = detail
+                    .split_once("->")
+                    .map_or((None, detail.as_str()), |(arg, typ)| {
+                        (Some(arg.trim()), typ.trim())
+                    });
+
+                let constr_decl = argument.map_or(name.to_string(), |argument| {
+                    format!("{} of {}", name, argument)
+                });
+
+                let constr_host = if return_t.ends_with("exn") {
+                    "exception "
+                } else {
+                    "type t = "
+                };
+
+                let source_host = Rope::from([constr_host, &constr_decl].join(" "));
+                let mut source_highlight = {
+                    let constr_host_len = constr_host.len() + 1;
+
+                    language.highlight_text(
+                        &source_host,
+                        Range {
+                            start: constr_host_len,
+                            end: constr_host_len + constr_decl.len(),
+                        },
+                    )
+                };
+
+                let signature_host: Rope = Rope::from(format!("let _ : {} = ()", return_t));
+
+                // We include the ': ' in the range as we use it later
+                let mut signature_highlight =
+                    language.highlight_text(&signature_host, 6..8 + return_t.len());
+
+                if let Some(last) = source_highlight.last() {
+                    let offset = last.0.end + 1;
+
+                    signature_highlight.iter_mut().for_each(|(r, _)| {
+                        r.start += offset;
+                        r.end += offset;
+                    });
+                };
+
+                Some(CodeLabel {
+                    text: format!("{} : {}", constr_decl, return_t),
+                    runs: {
+                        source_highlight.append(&mut signature_highlight);
+                        source_highlight
+                    },
+                    filter_range: 0..name.len(),
+                })
+            }
+            // 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("?") =>
+            {
+                let label = name.trim_start_matches(&['~', '?']);
+                let text = format!("{} : {}", label, detail);
+
+                let signature_host = Rope::from(format!("let _ : {} = ()", detail));
+                let signature_highlight =
+                    &mut language.highlight_text(&signature_host, 6..8 + detail.len());
+
+                let offset = label.len() + 1;
+                for (r, _) in signature_highlight.iter_mut() {
+                    r.start += offset;
+                    r.end += offset;
+                }
+
+                let mut label_highlight = vec![(
+                    0..0 + label.len(),
+                    language.grammar()?.highlight_id_for_name("property")?,
+                )];
+
+                Some(CodeLabel {
+                    text,
+                    runs: {
+                        label_highlight.append(signature_highlight);
+                        label_highlight
+                    },
+                    filter_range: 0..label.len(),
+                })
+            }
+            // version: string;
+            Some((CompletionItemKind::FIELD, detail)) => {
+                let (_record_t, field_t) = detail.split_once("->")?;
+
+                let text = format!("{}: {};", name, field_t);
+                let source_host: Rope = Rope::from(format!("type t = {{ {} }}", text));
+
+                let runs: Vec<(Range<usize>, language::HighlightId)> =
+                    language.highlight_text(&source_host, 11..11 + text.len());
+
+                Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..name.len(),
+                })
+            }
+            // let* : 'a t -> ('a -> 'b t) -> 'b t
+            Some((CompletionItemKind::VALUE, detail))
+                if name.contains(OPERATOR_CHAR)
+                    || (name.starts_with("let") && name.contains(OPERATOR_CHAR)) =>
+            {
+                let text = format!("{} : {}", name, detail);
+
+                let source_host = Rope::from(format!("let ({}) : {} = ()", name, detail));
+                let mut runs = language.highlight_text(&source_host, 5..6 + text.len());
+
+                if runs.len() > 1 {
+                    // ')'
+                    runs.remove(1);
+
+                    for run in &mut runs[1..] {
+                        run.0.start -= 1;
+                        run.0.end -= 1;
+                    }
+                }
+
+                Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..name.len(),
+                })
+            }
+            // version : Version.t list -> Version.t option Lwt.t
+            Some((CompletionItemKind::VALUE, detail)) => {
+                let text = format!("{} : {}", name, detail);
+
+                let source_host = Rope::from(format!("let {} = ()", text));
+                let runs = language.highlight_text(&source_host, 4..4 + text.len());
+
+                Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..name.len(),
+                })
+            }
+            // status : string
+            Some((CompletionItemKind::METHOD, detail)) => {
+                let text = format!("{} : {}", name, detail);
+
+                let method_host = Rope::from(format!("class c : object method {} end", text));
+                let runs = language.highlight_text(&method_host, 24..24 + text.len());
+
+                Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..name.len(),
+                })
+            }
+            Some((kind, _)) => {
+                let highlight_name = match kind {
+                    CompletionItemKind::MODULE | CompletionItemKind::INTERFACE => "title",
+                    CompletionItemKind::KEYWORD => "keyword",
+                    CompletionItemKind::TYPE_PARAMETER => "type",
+                    _ => return None,
+                };
+
+                Some(CodeLabel {
+                    text: name.clone(),
+                    runs: vec![(
+                        0..name.len(),
+                        language.grammar()?.highlight_id_for_name(highlight_name)?,
+                    )],
+                    filter_range: 0..name.len(),
+                })
+            }
+            _ => None,
+        }
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: SymbolKind,
+        language: &Arc<language::Language>,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            SymbolKind::PROPERTY => {
+                let text = format!("type t = {{ {}: (); }}", name);
+                let filter_range: Range<usize> = 0..name.len();
+                let display_range = 11..11 + name.len();
+                (text, filter_range, display_range)
+            }
+            SymbolKind::FUNCTION
+                if name.contains(OPERATOR_CHAR)
+                    || (name.starts_with("let") && name.contains(OPERATOR_CHAR)) =>
+            {
+                let text = format!("let ({}) () = ()", name);
+
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end + 1;
+                (text, filter_range, display_range)
+            }
+            SymbolKind::FUNCTION => {
+                let text = format!("let {} () = ()", name);
+
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            SymbolKind::CONSTRUCTOR => {
+                let text = format!("type t = {}", name);
+                let filter_range = 0..name.len();
+                let display_range = 9..9 + name.len();
+                (text, filter_range, display_range)
+            }
+            SymbolKind::MODULE => {
+                let text = format!("module {} = struct end", name);
+                let filter_range = 7..7 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            SymbolKind::CLASS => {
+                let text = format!("class {} = object end", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            SymbolKind::METHOD => {
+                let text = format!("class c = object method {} = () end", name);
+                let filter_range = 0..name.len();
+                let display_range = 17..24 + name.len();
+                (text, filter_range, display_range)
+            }
+            SymbolKind::STRING => {
+                let text = format!("type {} = T", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}

crates/zed/src/languages/ocaml/brackets.scm πŸ”—

@@ -0,0 +1,12 @@
+("(" @open ")" @close)
+("[" @open "]" @close)
+("[|" @open "|]" @close)
+("{" @open "}" @close)
+("<" @open ">" @close)
+("\"" @open "\"" @close)
+
+("begin" @open "end" @close)
+("struct" @open "end" @close)
+("sig" @open "end" @close)
+("object" @open "end" @close)
+("do" @open "done" @close)

crates/zed/src/languages/ocaml/config.toml πŸ”—

@@ -0,0 +1,18 @@
+name = "OCaml"
+path_suffixes = ["ml"]
+block_comment = ["(* ", "*)"]
+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 = 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 }
+]

crates/zed/src/languages/ocaml/highlights.scm πŸ”—

@@ -0,0 +1,142 @@
+; Modules
+;--------
+
+[(module_name) (module_type_name)] @title
+
+; Types
+;------
+
+[(class_name) (class_type_name) (type_constructor)] @type
+
+[(constructor_name) (tag)] @constructor
+
+; Functions
+;----------
+
+(let_binding
+  pattern: (value_name) @function
+  (parameter))
+
+(let_binding
+  pattern: (value_name) @function
+  body: [(fun_expression) (function_expression)])
+
+(value_specification (value_name) @function)
+
+(external (value_name) @function)
+
+(method_name) @function
+
+(infix_expression
+  left: (value_path (value_name) @function)
+  operator: (concat_operator) @operator
+  (#eq? @operator "@@"))
+
+(infix_expression
+  operator: (rel_operator) @operator
+  right: (value_path (value_name) @function)
+  (#eq? @operator "|>"))
+
+(application_expression
+  function: (value_path (value_name) @function))
+
+; Variables
+;----------
+
+[(type_variable) (value_pattern)] @variable
+
+; Properties
+;-----------
+
+[(label_name) (field_name) (instance_variable_name)] @property
+
+; Constants
+;----------
+
+(boolean) @boolean
+
+[(number) (signed_number)] @number
+
+[(string) (character)] @string
+
+(quoted_string "{" @string "}" @string) @string
+(quoted_string_content) @string
+
+
+(escape_sequence) @string.escape
+
+[
+  (conversion_specification)
+  (pretty_printing_indication)
+] @punctuation.special
+
+; Operators
+;----------
+
+(match_expression (match_operator) @keyword)
+
+(value_definition [(let_operator) (let_and_operator)] @keyword)
+
+[
+  (prefix_operator)
+  (sign_operator)
+  (pow_operator)
+  (mult_operator)
+  (add_operator)
+  (concat_operator)
+  (rel_operator)
+  (and_operator)
+  (or_operator)
+  (assign_operator)
+  (hash_operator)
+  (indexing_operator)
+  (let_operator)
+  (let_and_operator)
+  (match_operator)
+] @operator
+
+["*" "#" "::" "<-"] @operator
+
+; Keywords
+;---------
+
+[
+  "and" "as" "assert" "begin" "class" "constraint" "do" "done" "downto" "else"
+  "end" "exception" "external" "for" "fun" "function" "functor" "if" "in"
+  "include" "inherit" "initializer" "lazy" "let" "match" "method" "module"
+  "mutable" "new" "nonrec" "object" "of" "open" "private" "rec" "sig" "struct"
+  "then" "to" "try" "type" "val" "virtual" "when" "while" "with"
+] @keyword
+
+; Punctuation
+;------------
+
+["(" ")" "[" "]" "{" "}" "[|" "|]" "[<" "[>"] @punctuation.bracket
+
+(object_type ["<" ">"] @punctuation.bracket)
+
+[
+  "," "." ";" ":" "=" "|" "~" "?" "+" "-" "!" ">" "&"
+  "->" ";;" ":>" "+=" ":=" ".."
+] @punctuation.delimiter
+
+; Attributes
+;-----------
+
+[
+  (attribute)
+  (item_attribute)
+  (floating_attribute)
+  (extension)
+  (item_extension)
+  (quoted_extension)
+  (quoted_item_extension) 
+  "%"
+] @attribute
+
+(attribute_id) @tag
+
+; Comments
+;---------
+
+[(comment) (line_number_directive) (directive) (shebang)] @comment

crates/zed/src/languages/ocaml/indents.scm πŸ”—

@@ -0,0 +1,43 @@
+[
+  (let_binding)
+  (type_binding)
+
+  (method_definition)
+  
+  (external)
+  (value_specification)
+  (method_specification)
+
+  (match_case)
+
+  (function_expression)
+
+  (field_declaration)
+  (field_expression)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "[|" "|]" @end) @indent
+(_ "<" ">" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent
+
+(_ "object" @start "end" @end) @indent
+
+(structure
+  "struct" @start
+  "end" @end) @indent
+
+(signature
+  "sig" @start
+  "end" @end) @indent
+
+(parenthesized_expression
+  "begin" @start
+  "end") @indent
+
+(do_clause
+  "do" @start
+  "done" @end) @indent
+
+";;" @outdent

crates/zed/src/languages/ocaml/outline.scm πŸ”—

@@ -0,0 +1,59 @@
+(_structure_item/value_definition
+    "let" @context
+    (let_binding
+      pattern: (_) @name)) @item
+
+(_structure_item/exception_definition
+  "exception" @context
+  (constructor_declaration
+    (constructor_name) @name)) @item
+
+(_structure_item/module_definition
+  "module" @context
+  (module_binding
+    name: (module_name) @name)) @item
+
+(module_type_definition
+  "module" @context
+  "type" @context
+  name: (_) @name) @item
+ 
+(type_definition
+  "type" @context
+  (type_binding name: (_) @name)) @item
+
+(value_specification
+  "val" @context
+  (value_name) @name) @item
+  
+(class_definition
+  "class" @context
+  (class_binding
+    "virtual"? @context
+    name: (_) @name)) @item
+
+(class_type_definition
+  "class" @context
+  "type" @context
+  (class_type_binding
+    "virtual"? @context
+    name: (_) @name)) @item
+
+(instance_variable_definition
+  "val" @context
+  "method"? @context
+  name: (_) @name) @item
+
+(method_specification
+  "method" @context
+  "virtual"? @context
+   (method_name) @name) @item
+
+(method_definition
+  "method" @context
+  "virtual"? @context
+  name: (_) @name) @item
+
+(external
+  "external" @context
+  (value_name) @name) @item

crates/zed/src/languages/terraform/config.toml πŸ”—

@@ -0,0 +1,13 @@
+name = "Terraform"
+path_suffixes = ["tf", "tfvars"]
+line_comments = ["# ", "// "]
+block_comment = ["/*", "*/"]
+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 = ["comment", "string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
+]

crates/zed/src/languages/terraform/highlights.scm πŸ”—

@@ -0,0 +1,159 @@
+; https://github.com/nvim-treesitter/nvim-treesitter/blob/cb79d2446196d25607eb1d982c96939abdf67b8e/queries/hcl/highlights.scm
+; highlights.scm
+[
+  "!"
+  "\*"
+  "/"
+  "%"
+  "\+"
+  "-"
+  ">"
+  ">="
+  "<"
+  "<="
+  "=="
+  "!="
+  "&&"
+  "||"
+] @operator
+
+[
+  "{"
+  "}"
+  "["
+  "]"
+  "("
+  ")"
+] @punctuation.bracket
+
+[
+  "."
+  ".*"
+  ","
+  "[*]"
+] @punctuation.delimiter
+
+[
+  (ellipsis)
+  "\?"
+  "=>"
+] @punctuation.special
+
+[
+  ":"
+  "="
+] @punctuation
+
+[
+  "for"
+  "endfor"
+  "in"
+  "if"
+  "else"
+  "endif"
+] @keyword
+
+[
+  (quoted_template_start) ; "
+  (quoted_template_end) ; "
+  (template_literal) ; non-interpolation/directive content
+] @string
+
+[
+  (heredoc_identifier) ; END
+  (heredoc_start) ; << or <<-
+] @punctuation.delimiter
+
+[
+  (template_interpolation_start) ; ${
+  (template_interpolation_end) ; }
+  (template_directive_start) ; %{
+  (template_directive_end) ; }
+  (strip_marker) ; ~
+] @punctuation.special
+
+(numeric_lit) @number
+
+(bool_lit) @boolean
+
+(null_lit) @constant
+
+(comment) @comment
+
+(identifier) @variable
+
+(body
+  (block
+    (identifier) @keyword))
+
+(body
+  (block
+    (body
+      (block
+        (identifier) @type))))
+
+(function_call
+  (identifier) @function)
+
+(attribute
+  (identifier) @variable)
+
+; { key: val }
+;
+; highlight identifier keys as though they were block attributes
+(object_elem
+  key:
+    (expression
+      (variable_expr
+        (identifier) @variable)))
+
+; var.foo, data.bar
+;
+; first element in get_attr is a variable.builtin or a reference to a variable.builtin
+(expression
+  (variable_expr
+    (identifier) @variable)
+  (get_attr
+    (identifier) @variable))
+
+; https://github.com/nvim-treesitter/nvim-treesitter/blob/cb79d2446196d25607eb1d982c96939abdf67b8e/queries/terraform/highlights.scm
+; Terraform specific references
+;
+;
+; local/module/data/var/output
+(expression
+  (variable_expr
+    (identifier) @variable
+    (#any-of? @variable "data" "var" "local" "module" "output"))
+  (get_attr
+    (identifier) @variable))
+
+; path.root/cwd/module
+(expression
+  (variable_expr
+    (identifier) @type
+    (#eq? @type "path"))
+  (get_attr
+    (identifier) @variable
+    (#any-of? @variable "root" "cwd" "module")))
+
+; terraform.workspace
+(expression
+  (variable_expr
+    (identifier) @type
+    (#eq? @type "terraform"))
+  (get_attr
+    (identifier) @variable
+    (#any-of? @variable "workspace")))
+
+; Terraform specific keywords
+; FIXME: ideally only for identifiers under a `variable` block to minimize false positives
+((identifier) @type
+  (#any-of? @type "bool" "string" "number" "object" "tuple" "list" "map" "set" "any"))
+
+(object_elem
+  val:
+    (expression
+      (variable_expr
+        (identifier) @type
+        (#any-of? @type "bool" "string" "number" "object" "tuple" "list" "map" "set" "any"))))

crates/zed/src/languages/terraform/indents.scm πŸ”—

@@ -0,0 +1,14 @@
+; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/hcl/indents.scm
+[
+  (block)
+  (object)
+  (tuple)
+  (function_call)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "(" ")" @end) @indent
+(_ "{" "}" @end) @indent
+
+; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/terraform/indents.scm
+; inherits: hcl

crates/zed/src/languages/terraform/injections.scm πŸ”—

@@ -0,0 +1,9 @@
+; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/hcl/injections.scm
+
+(heredoc_template
+  (template_literal) @content
+  (heredoc_identifier) @language
+  (#downcase! @language))
+
+; https://github.com/nvim-treesitter/nvim-treesitter/blob/ce4adf11cfe36fc5b0e5bcdce0c7c6e8fbc9798a/queries/terraform/injections.scm
+; inherits: hcl

crates/zed/src/main.rs πŸ”—

@@ -44,7 +44,7 @@ use std::{
     thread,
     time::Duration,
 };
-use theme::{ActiveTheme, ThemeRegistry, ThemeSettings};
+use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
 use util::{
     async_maybe,
     http::{self, HttpClient, ZedHttpClient},
@@ -127,6 +127,7 @@ fn main() {
             AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx);
         }
 
+        SystemAppearance::init(cx);
         OpenListener::set_global(listener.clone(), cx);
 
         load_embedded_fonts(cx);
@@ -475,6 +476,7 @@ fn init_paths() {
 fn init_logger() {
     if stdout_is_a_pty() {
         Builder::new()
+            .parse_default_env()
             .format(|buf, record| {
                 use env_logger::fmt::Color;
 
@@ -814,7 +816,7 @@ async fn load_login_shell_environment() -> Result<()> {
         "SHELL environment variable is not assigned so we can't source login environment variables",
     )?;
     let output = Command::new(&shell)
-        .args(["-lic", &format!("echo {marker} && /usr/bin/env -0")])
+        .args(["-l", "-i", "-c", &format!("echo {marker}; /usr/bin/env -0")])
         .output()
         .await
         .context("failed to spawn login shell to source login environment variables")?;
@@ -896,27 +898,39 @@ fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
             if let Some(theme_registry) =
                 cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
             {
-                if let Some(()) = theme_registry
-                    .load_user_themes(&paths::THEMES_DIR.clone(), fs)
+                let themes_dir = paths::THEMES_DIR.as_ref();
+                match fs
+                    .metadata(themes_dir)
                     .await
-                    .log_err()
+                    .ok()
+                    .flatten()
+                    .map(|m| m.is_dir)
                 {
-                    cx.update(|cx| {
-                        let mut theme_settings = ThemeSettings::get_global(cx).clone();
-
-                        if let Some(requested_theme) = theme_settings.requested_theme.clone() {
-                            if let Some(_theme) = theme_settings.switch_theme(&requested_theme, cx)
-                            {
-                                ThemeSettings::override_global(theme_settings, cx);
-                            }
-                        }
-                    })
-                    .log_err();
+                    Some(is_dir) => {
+                        anyhow::ensure!(is_dir, "Themes dir path {themes_dir:?} is not a directory")
+                    }
+                    None => {
+                        fs.create_dir(themes_dir).await.with_context(|| {
+                            format!("Failed to create themes dir at path {themes_dir:?}")
+                        })?;
+                    }
                 }
+                theme_registry.load_user_themes(themes_dir, fs).await?;
+                cx.update(|cx| {
+                    let mut theme_settings = ThemeSettings::get_global(cx).clone();
+                    if let Some(theme_selection) = theme_settings.theme_selection.clone() {
+                        let theme_name = theme_selection.theme(*SystemAppearance::global(cx));
+
+                        if let Some(_theme) = theme_settings.switch_theme(&theme_name, cx) {
+                            ThemeSettings::override_global(theme_settings, cx);
+                        }
+                    }
+                })?;
             }
+            anyhow::Ok(())
         }
     })
-    .detach();
+    .detach_and_log_err(cx);
 }
 
 //todo!(linux): Port fsevents to linux
@@ -945,11 +959,14 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
                             cx.update(|cx| {
                                 let mut theme_settings = ThemeSettings::get_global(cx).clone();
 
-                                if let Some(requested_theme) =
-                                    theme_settings.requested_theme.clone()
+                                if let Some(theme_selection) =
+                                    theme_settings.theme_selection.clone()
                                 {
+                                    let theme_name =
+                                        theme_selection.theme(*SystemAppearance::global(cx));
+
                                     if let Some(_theme) =
-                                        theme_settings.switch_theme(&requested_theme, cx)
+                                        theme_settings.switch_theme(&theme_name, cx)
                                     {
                                         ThemeSettings::override_global(theme_settings, cx);
                                     }

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 collections::HashMap;
 use editor::scroll::Autoscroll;
 use editor::Editor;
 use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
@@ -10,7 +11,6 @@ use gpui::{AppContext, AsyncAppContext, Global};
 use itertools::Itertools;
 use language::{Bias, Point};
 use release_channel::parse_zed_link;
-use std::collections::HashMap;
 use std::ffi::OsStr;
 use std::os::unix::prelude::OsStrExt;
 use std::path::Path;
@@ -176,7 +176,7 @@ pub async fn handle_cli_connection(
     if let Some(request) = requests.next().await {
         match request {
             CliRequest::Open { paths, wait } => {
-                let mut caret_positions = HashMap::new();
+                let mut caret_positions = HashMap::default();
 
                 let paths = if paths.is_empty() {
                     workspace::last_opened_workspace_paths()

crates/zed/src/zed.rs πŸ”—

@@ -353,7 +353,7 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewCo
             toolbar.add_item(buffer_search_bar.clone(), cx);
 
             let quick_action_bar =
-                cx.new_view(|_| QuickActionBar::new(buffer_search_bar, workspace));
+                cx.new_view(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
             toolbar.add_item(quick_action_bar, cx);
             let diagnostic_editor_controls = cx.new_view(|_| diagnostics::ToolbarControls::new());
             toolbar.add_item(diagnostic_editor_controls, cx);
@@ -733,6 +733,7 @@ fn open_settings_file(
 mod tests {
     use super::*;
     use assets::Assets;
+    use collections::HashSet;
     use editor::{scroll::Autoscroll, DisplayPoint, Editor};
     use gpui::{
         actions, Action, AnyWindowHandle, AppContext, AssetSource, Entity, TestAppContext,
@@ -742,10 +743,7 @@ mod tests {
     use project::{project_settings::ProjectSettings, Project, ProjectPath};
     use serde_json::json;
     use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
-    use std::{
-        collections::HashSet,
-        path::{Path, PathBuf},
-    };
+    use std::path::{Path, PathBuf};
     use theme::{ThemeRegistry, ThemeSettings};
     use workspace::{
         item::{Item, ItemHandle},

crates/zed_actions/Cargo.toml πŸ”—

@@ -6,5 +6,5 @@ publish = false
 license = "GPL-3.0-or-later"
 
 [dependencies]
-gpui = { path = "../gpui" }
+gpui.workspace = true
 serde.workspace = true

docs/src/configuring_zed.md πŸ”—

@@ -190,6 +190,23 @@ List of `string` values
 2. Position the dock to the right of the workspace like a side panel: `right`
 3. Position the dock full screen over the entire workspace: `expanded`
 
+## Editor Toolbar
+
+- Description: Whether or not to show various elements in the editor toolbar.
+- Setting: `toolbar`
+- Default:
+
+```json
+"toolbar": {
+  "breadcrumbs": true,
+  "quick_actions": true
+},
+```
+
+**Options**
+
+Each option controls displaying of a particular toolbar element. If all elements are hidden, the editor toolbar is not displayed.
+
 ## Enable Language Server
 
 - Description: Whether or not to use language servers to provide code intelligence.

docs/src/configuring_zed__key_bindings.md πŸ”—

@@ -33,6 +33,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,
+        }
+    }
+]
+```
+
+
 ## All key bindings
 
 ### Global

docs/src/developing_zed__building_zed.md πŸ”—

@@ -11,7 +11,9 @@ 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
+- 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/)
 
@@ -74,7 +76,7 @@ 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 `xcode-select --switch /Applications/Xcode.app/Contents/Developer`
+Try `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer`
 
 ### Cargo errors claiming that a dependency is using unstable features
 

docs/src/languages/elm.md πŸ”—

@@ -2,3 +2,24 @@
 
 - Tree Sitter: [tree-sitter-elm](https://github.com/elm-tooling/tree-sitter-elm)
 - Language Server: [elm-language-server](https://github.com/elm-tooling/elm-language-server)
+
+### Setting up `elm-language-server`
+
+Elm language server can be configured in your `settings.json`, e.g.:
+
+```json
+{
+  "lsp": {
+    "elm-language-server": {
+      "initialization_options": {
+        "disableElmLSDiagnostics": true,
+        "onlyUpdateDiagnosticsOnSave": false,
+        "elmReviewDiagnostics": "warning"
+      }
+    }
+  }
+}
+```
+
+`elm-format`, `elm-review` and `elm` need to be installed and made available in the environment
+or configured in the settings. See the [full list of server settings here](https://github.com/elm-tooling/elm-language-server?tab=readme-ov-file#server-settings).

docs/src/languages/ocaml.md πŸ”—

@@ -0,0 +1,31 @@
+# OCaml
+
+- Tree Sitter: [tree-sitter-ocaml](https://github.com/tree-sitter/tree-sitter-ocaml)
+- 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://opam.ocaml.org/doc/Install.html). 
+
+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
+$ ocamllsp --help
+```
+
+in your terminal. If you get a help message, you're good to go. If not, please revisit the installation instructions for `ocamllsp` and ensure it's properly installed.
+
+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 
+
+```sh
+$ zed .
+```
+
+Voila! You should have Zed running with OCaml support, no additional setup required.

script/zed-local πŸ”—

@@ -142,6 +142,7 @@ setTimeout(() => {
         ZED_RPC_URL: "http://localhost:8080/rpc",
         ZED_ADMIN_API_TOKEN: "secret",
         ZED_WINDOW_SIZE: size,
+        ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed",
         PATH: process.env.PATH,
         RUST_LOG: process.env.RUST_LOG || "info",
       },