Compare commits

..

53 Commits

Author SHA1 Message Date
Conrad Irwin
df81a91bf9 WIPWIPWIPWIPW 2024-12-17 15:33:38 -07:00
Conrad Irwin
7adcf903cc fix merge conflicts 2024-12-17 14:55:25 -07:00
Conrad Irwin
062b4a2dd8 Merge branch 'main' into new-diff-map 2024-12-17 14:46:59 -07:00
Conrad Irwin
2cca833d7b Add cmp 2024-12-17 12:50:36 -07:00
Conrad Irwin
0b3715ae44 Add DisplayAnchor 2024-12-17 12:25:44 -07:00
Conrad Irwin
2b68817ed8 Fix test_{addition,deletion,modification}_reverts 2024-12-16 23:11:16 -07:00
Conrad Irwin
d1239f6cdb fix test_edits_around_expanded_deletion_hunks 2024-12-16 22:35:09 -07:00
Conrad Irwin
d600c58d3c fix test toggle diff hunks (again) 2024-12-16 22:06:41 -07:00
Conrad Irwin
c160ce6a24 handle diff chunks with no trailing newline 2024-12-16 21:31:41 -07:00
Max Brunsfeld
9450de73b3 Remove more of the old DiffMap 2024-12-16 16:48:03 -08:00
Max Brunsfeld
5cc3bd1585 Fix collapsing of hunks when base text changes
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-16 16:34:33 -08:00
Max Brunsfeld
87d50ee54c Fix handling of modified empty lines in assert_state_with_diff helper 2024-12-16 16:32:27 -08:00
Max Brunsfeld
2f5ea92fe1 Fix bugs in DiffMap::sync
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-16 15:30:45 -08:00
Max Brunsfeld
2625719ca2 Put DiffMap before InlayMap in DisplayMap
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-16 14:58:43 -08:00
Conrad Irwin
88b4628c00 rewrite diff map to not depend on inlay map 2024-12-16 14:38:39 -07:00
Max Brunsfeld
241c7a6645 Extract logic around custom text highlights out of InlayChunks iterator
Co-Authored-By: Conrad <conrad@zed.dev>
Co-Authored-By: Agus <agus@zed.dev>
2024-12-16 11:56:25 -08:00
Max Brunsfeld
f6aa7df14f Make EditorTestContext::assert_state_with_diff use new DiffMap
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
2024-12-16 10:34:45 -08:00
Max Brunsfeld
472fee6b62 When buffer is edited, compute DiffMap edits using deltas 2024-12-15 20:30:18 -08:00
Max Brunsfeld
093c3d3652 Add failing unit test for editing inside of an insertion hunk 2024-12-13 17:49:15 -08:00
Max Brunsfeld
1f28437c82 Sync display map immediately when updating diff state 2024-12-13 17:32:39 -08:00
Max Brunsfeld
2266b3cb96 Perform the same range expansion in has_expanded_diff_hunks_in_ranges 2024-12-13 17:26:23 -08:00
Max Brunsfeld
4369890646 Move logic for defining edits outside of recompute_transforms 2024-12-13 17:17:57 -08:00
Max Brunsfeld
82fbe27c08 Fix some problems with expanding and collapsing diff hunks
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-13 16:42:07 -08:00
Conrad Irwin
41c5e45ca2 Fix bugs in DiffMap's handling of edits and diff updates 2024-12-13 12:54:08 -08:00
Max Brunsfeld
243d7c6d5a WIP - Failing diff map test for hunks staying expanded on diff updates 2024-12-12 17:58:10 -08:00
Max Brunsfeld
9025fb226a Update more editor rendering to use the new DiffMap 2024-12-12 17:33:04 -08:00
Max Brunsfeld
07eb573a2c Highlight background of expanded hunks
Co-authored-by: Michael <michael@zed.dev>
2024-12-12 16:51:50 -08:00
Max Brunsfeld
b552e32b18 Replace buffer rows iterators with row info iterators that also give git status
Co-authored-by: Michael <michael@zed.dev>
2024-12-12 16:40:56 -08:00
Max Brunsfeld
9b526a4d42 Start work on providing git statuses for each line of diff map
Co-authored-by: Michael <michael@zed.dev>
2024-12-12 15:50:27 -08:00
Max Brunsfeld
cc4ecacc04 Fix dropping of ChangeSet in DiffMap
Co-authored-by: Michael <michael@zed.dev>
2024-12-12 14:51:19 -08:00
Max Brunsfeld
dc814cb3af Start integrating the new DiffMap into the editor
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-12 14:26:58 -08:00
Max Brunsfeld
ad87dfe908 Get more editor tests passing
Correctly handle multiple edits to DiffMap

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-12 12:52:29 -08:00
Max Brunsfeld
99edc9583c Fix DiffMap::buffer_rows to handle last empty line of buffer
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-12 11:55:37 -08:00
Max Brunsfeld
c015553b1f Get assertions about DiffMap::chunks passing
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-12 10:41:40 -08:00
Max Brunsfeld
e53d8bb8d2 wip 2024-12-12 09:59:37 -08:00
Conrad Irwin
6e724c1168 seek on DiffMapBufferRows 2024-12-11 23:43:29 -07:00
Conrad Irwin
a8a9177878 implement text_summary_for_range 2024-12-11 23:12:58 -07:00
Conrad Irwin
9dc5ebab85 better naming? 2024-12-11 22:13:28 -07:00
Conrad Irwin
2630dfdd23 implement clip_point 2024-12-11 22:04:12 -07:00
Conrad Irwin
20ec116932 Implement to_offset/to_point 2024-12-11 21:33:07 -07:00
Max Brunsfeld
dac0a07770 Merge branch 'main' into new-diff-map 2024-12-11 17:10:46 -08:00
Max Brunsfeld
61e42b986a Integrate DiffMap into the DisplayMap 2024-12-11 17:10:34 -08:00
Max Brunsfeld
a20715d663 Start implementing DiffMap::buffer_rows 2024-12-11 11:36:44 -08:00
Max Brunsfeld
33833533bd Get DiffMap::sync working for a basic test
Co-authored-by: Cole <cole@zed.dev>
2024-12-10 17:19:29 -08:00
Max Brunsfeld
023f9239b4 Restructure DiffMap::sync to prepare for handling buffer edits
Co-authored-by: Cole <cole@zed.dev>
2024-12-10 16:30:47 -08:00
Max Brunsfeld
946ae93f68 Compute diff map edits in a single pass through the old and new trees
Co-authored-by: Cole <cole@zed.dev>
2024-12-09 16:26:16 -08:00
Max Brunsfeld
011880c48c Start work on expanding and collapsing hunks by range
Co-authored-by: Cole <cole@zed.dev>
2024-12-09 11:55:13 -08:00
Max Brunsfeld
440b632b09 Start work on expanding and collapsing hunks 2024-12-05 17:58:53 -08:00
Max Brunsfeld
d0a8b0f8b1 Start work on returning edits from DiffMap::sync
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-05 15:38:55 -08:00
Max Brunsfeld
09082d103e Implement DiffMap::set_all_hunks_expanded
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-12-05 14:37:16 -08:00
Max Brunsfeld
90c9ad4fd6 Get DiffMapChunks working to some degree
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-12-05 14:30:59 -08:00
Max Brunsfeld
67ffe999e5 Start work on DiffMapSnapshot::chunks
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
2024-12-05 12:39:04 -08:00
Max Brunsfeld
159c2239cc Stub in the structure of a DiffMap to go in the display map stack
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
2024-12-04 16:49:53 -08:00
292 changed files with 14971 additions and 13840 deletions

View File

@@ -18,11 +18,8 @@ body:
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: Zed version, release channel, architecture (x86_64 or aarch64), OS (macOS version / Linux distro and version) and RAM amount.
placeholder: |
<!-- In Zed run `copy system specs into clipboard` from the Zed command palette and paste here. -->
<!-- Alternatively spawn `request feature` and this field will be autopopulated -->
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. If you are unable to run the command, please include your Zed version and release channel, operating system and version, RAM amount, and architecture.
validations:
required: true
- type: textarea

View File

@@ -20,13 +20,8 @@ body:
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: Zed version, release channel, architecture (x86_64 or aarch64), OS (macOS version / Linux distro and version) and RAM amount.
placeholder: |
<!-- In Zed run `copy system specs into clipboard` from the Zed command palette and paste here. -->
<!-- Alternatively spawn `file bug report` and this field will be autopopulated -->
<!-- If Zed won't launch, include the equivalent with other relevant details (e.g. video card driver version for display bugs, etc) -->
<!-- Zed Version: 0.xxx.x; Channel: Stable, OS: macOS xx.xx, RAM: XXGB, Architecture: x86_64"
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. If you are unable to run the command, please include your Zed version and release channel, operating system and version, RAM amount, and architecture.
validations:
required: true
- type: textarea

View File

@@ -7,10 +7,14 @@ on:
- "v[0-9]+.[0-9]+.x"
tags:
- "v*"
paths-ignore:
- "docs/**"
pull_request:
branches:
- "**"
merge_group:
paths-ignore:
- "docs/**/*"
- ".github/workflows/community_*"
concurrency:
# Allow only one workflow per any non-`main` branch.
@@ -24,23 +28,6 @@ env:
RUSTFLAGS: "-D warnings"
jobs:
check_docs_only:
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.check_changes.outputs.docs_only }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Check for non-docs changes
id: check_changes
run: |
if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -qvE '^docs/'; then
echo "docs_only=false" >> $GITHUB_OUTPUT
else
echo "docs_only=true" >> $GITHUB_OUTPUT
fi
migration_checks:
name: Check Postgres and Protobuf migrations, mergability
if: github.repository_owner == 'zed-industries'
@@ -94,10 +81,6 @@ jobs:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# To support writing comments that they will certainly be revisited.
- name: Check for todo! and FIXME comments
run: script/check-todos
- name: Run style checks
uses: ./.github/actions/check_style
@@ -113,7 +96,6 @@ jobs:
runs-on:
- self-hosted
- test
needs: check_docs_only
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -121,35 +103,29 @@ jobs:
clean: false
- name: cargo clippy
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/clippy
- name: Check unused dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: bnjbvr/cargo-machete@main
- name: Check licenses
if: needs.check_docs_only.outputs.docs_only == 'false'
run: |
script/check-licenses
script/generate-licenses /tmp/zed_licenses_output
- name: Check for new vulnerable dependencies
if: github.event_name == 'pull_request' && needs.check_docs_only.outputs.docs_only == 'false'
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
with:
license-check: false
- name: Run tests
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: ./.github/actions/run_tests
- name: Build collab
if: needs.check_docs_only.outputs.docs_only == 'false'
run: cargo build -p collab
- name: Build other binaries and features
if: needs.check_docs_only.outputs.docs_only == 'false'
run: |
cargo build --workspace --bins --all-features
cargo check -p gpui --features "macos-blade"
@@ -163,7 +139,6 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
needs: check_docs_only
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
@@ -174,26 +149,21 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
- name: Install Linux dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/linux
- name: cargo clippy
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/clippy
- name: Run tests
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: ./.github/actions/run_tests
- name: Build other binaries and features
if: needs.check_docs_only.outputs.docs_only == 'false'
run: |
cargo build -p zed
cargo check -p workspace
@@ -204,7 +174,6 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
needs: check_docs_only
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
@@ -215,18 +184,15 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
- name: Install Clang & Mold
if: needs.check_docs_only.outputs.docs_only == 'false'
run: ./script/remote-server && ./script/install-mold 2.34.0
- name: Build Remote Server
if: needs.check_docs_only.outputs.docs_only == 'false'
run: cargo build -p remote_server
# todo(windows): Actually run the tests
@@ -235,7 +201,6 @@ jobs:
name: (Windows) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
runs-on: hosted-windows-1
needs: check_docs_only
steps:
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
@@ -246,19 +211,16 @@ jobs:
clean: false
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github"
- name: cargo clippy
if: needs.check_docs_only.outputs.docs_only == 'false'
# Windows can't run shell scripts, so we need to use `cargo xtask`.
run: cargo xtask clippy
- name: Build Zed
if: needs.check_docs_only.outputs.docs_only == 'false'
run: cargo build
bundle-mac:
@@ -327,14 +289,14 @@ jobs:
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
@@ -364,8 +326,6 @@ jobs:
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -385,7 +345,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
@@ -412,8 +372,6 @@ jobs:
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -433,7 +391,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz

View File

@@ -18,7 +18,7 @@ jobs:
fi
echo "::set-output name=URL::$URL"
- name: Get content
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
uses: 2428392/gh-truncate-string-action@e6b5885fb83c81ca9a700a91b079baec2133be3e # v1.4.0
id: get-content
with:
stringToTruncate: |

View File

@@ -7,7 +7,6 @@ on:
push:
branches:
- main
merge_group:
jobs:
check_formatting:

552
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -337,7 +337,7 @@ zeta = { path = "crates/zeta" }
#
aho-corasick = "1.1"
alacritty_terminal = "0.24"
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91d034ff8b53867143c005acfaa14609147c9a2c" }
any_vec = "0.14"
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }
@@ -355,13 +355,13 @@ async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
blade-util = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.21"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
cocoa = "0.26"
@@ -385,7 +385,7 @@ futures-lite = "1.13"
git2 = { version = "0.19", default-features = false }
globset = "0.4"
handlebars = "4.3"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
hyper = "0.14"
@@ -401,13 +401,14 @@ jupyter-websocket-client = { version = "0.8.0" }
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev="060964da10574cd9bf06463a53bf6e0769c5c45e", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
livekit = { git = "https://github.com/zed-industries/rust-sdks", rev="799f10133d93ba2a88642cd480d01ec4da53408c", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nbformat = { version = "0.9.0" }
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.19.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
@@ -471,7 +472,7 @@ sys-locale = "0.3.1"
sysinfo = "0.31.0"
tempfile = "3.9.0"
thiserror = "1.0.29"
tiktoken-rs = "0.6.0"
tiktoken-rs = "0.5.9"
time = { version = "0.3", features = [
"macros",
"parsing",
@@ -497,7 +498,7 @@ tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex",
tree-sitter-diff = "0.1.0"
tree-sitter-html = "0.20"
tree-sitter-jsdoc = "0.23"
tree-sitter-json = "0.24"
tree-sitter-json = "0.23"
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
tree-sitter-python = "0.23"
tree-sitter-regex = "0.23"
@@ -524,7 +525,6 @@ wasmtime-wasi = "24"
which = "6.0.0"
wit-component = "0.201"
zstd = "0.11"
metal = "0.30"
[workspace.dependencies.async-stripe]
git = "https://github.com/zed-industries/async-stripe"

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.58 2H2.5V12.08C2.5 12.5892 2.70229 13.0776 3.06235 13.4376C3.42242 13.7977 3.91078 14 4.42 14H12.58C13.0892 14 13.5776 13.7977 13.9376 13.4376C14.2977 13.0776 14.5 12.5892 14.5 12.08V3.92C14.5 3.41078 14.2977 2.92242 13.9376 2.56235C13.5776 2.20229 13.0892 2 12.58 2ZM3.358 11.6285C3.34615 12.6668 3.96437 13.2311 4.96636 13.232H4.96621C6.06429 13.2456 6.70951 12.4798 6.63088 11.3867H5.48085C5.4899 11.601 5.47243 11.8974 5.36026 12.0313C5.27992 12.1441 5.16183 12.2005 5.00645 12.2005C4.67402 12.1952 4.50788 11.9534 4.50788 11.4753V9.19488C4.50788 8.94247 4.54407 8.75168 4.61645 8.62283C4.73423 8.38524 5.17961 8.3584 5.34825 8.58663C5.47804 8.71252 5.48974 9.04683 5.48101 9.26757H6.63104C6.66099 8.70582 6.53494 8.10381 6.20079 7.80913C5.65853 7.23521 4.37403 7.26765 3.82039 7.80102C3.51213 8.07495 3.358 8.47525 3.358 9.00159V11.6285ZM7.04116 11.3867C7.01043 12.4573 7.50713 13.2473 8.61739 13.232L8.61723 13.2317C10.1571 13.2967 10.5874 11.592 9.96023 10.4759C9.74995 10.1097 9.16994 9.80702 8.71379 9.62981C8.36155 9.46772 8.21038 9.3086 8.20711 8.92079C8.20711 8.55559 8.35983 8.37291 8.66543 8.37291C8.83688 8.37291 8.95357 8.42939 9.01519 8.54217C9.10317 8.6754 9.12454 9.0409 9.11565 9.26742H10.1612C10.1866 8.71627 10.0554 8.11739 9.75509 7.81303C9.26822 7.22257 7.99791 7.24909 7.5115 7.82504C7.0109 8.29179 6.97783 9.4437 7.3346 9.96848C7.49278 10.205 7.75143 10.409 8.1107 10.5809C8.15897 10.6051 8.21552 10.6314 8.27633 10.6598C8.53247 10.7792 8.86416 10.9338 8.97119 11.1046C9.16073 11.3241 9.13593 11.8913 9.00333 12.0877C8.9336 12.1952 8.81285 12.2489 8.64141 12.2489C8.25703 12.2785 8.09666 11.8534 8.12677 11.3867H7.04116ZM10.5474 11.3867C10.5167 12.4573 11.0134 13.2473 12.1236 13.232L12.1235 13.2317C13.6634 13.2967 14.0936 11.592 13.4665 10.4759C13.2562 10.1097 12.6762 9.80702 12.2201 9.62981C11.8678 9.46772 11.7166 9.3086 11.7134 8.92079C11.7134 8.55559 11.8661 8.37291 12.1717 8.37291C12.3431 8.37291 12.4598 8.42939 12.5214 8.54217C12.6094 8.6754 12.6308 9.0409 12.6219 9.26742H13.6674C13.6928 8.71627 13.5617 8.11739 13.2614 7.81303C12.7745 7.22257 11.5042 7.24909 11.0178 7.82504C10.5172 8.29179 10.4841 9.4437 10.8409 9.96848C10.999 10.205 11.2577 10.409 11.617 10.5809C11.6652 10.6051 11.7218 10.6314 11.7826 10.6598C12.0387 10.7792 12.3704 10.9338 12.4775 11.1046C12.667 11.3241 12.6422 11.8913 12.5096 12.0877C12.4399 12.1952 12.3191 12.2489 12.1477 12.2489C11.7633 12.2785 11.6029 11.8534 11.633 11.3867H10.5474Z" fill="black"/>
<path d="M11.7633 4.2078H4.23674L4.3551 5.5189H10.1429L9.99592 6.87645H6.20408L6.33877 8.16255H9.86939L9.66122 9.92379L8 10.3275L6.3102 9.92021L6.20408 8.86633H4.7102L4.87755 10.7955L8 11.6457L11.0694 10.8812L11.7633 4.2078ZM2 2H14L12.9061 12.7818L7.98775 14L3.09388 12.7818L2 2Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 403 B

View File

@@ -1,5 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.27772 1.38585L4.39187 4.07909C4.34653 4.21692 4.26946 4.34219 4.16685 4.44479C4.06425 4.5474 3.93898 4.62447 3.80115 4.66981L1.10791 5.55566L3.80115 6.44151C3.93898 6.48685 4.06425 6.56392 4.16685 6.66653C4.26946 6.76913 4.34653 6.8944 4.39187 7.03223L5.27772 9.72547L6.16357 7.03223C6.20891 6.8944 6.28598 6.76913 6.38859 6.66653C6.49119 6.56392 6.61646 6.48685 6.7543 6.44151L9.44753 5.55566L6.7543 4.66981C6.61646 4.62447 6.49119 4.5474 6.38859 4.44479C6.28598 4.34219 6.20891 4.21692 6.16357 4.07909L5.27772 1.38585Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.35938 12.3555C8.35938 12.0664 8.52734 11.8086 9.00781 11.3594L10.2031 10.2344C10.6094 9.85156 10.7891 9.60156 10.7891 9.34375C10.7891 9.05469 10.5781 8.85938 10.2734 8.85938C10.0391 8.85938 9.87109 8.95312 9.66406 9.21094C9.42578 9.50781 9.25391 9.60938 8.99219 9.60938C8.61719 9.60938 8.35156 9.35938 8.35156 9.01172C8.35156 8.25 9.26953 7.57812 10.3594 7.57812C11.4961 7.57812 12.3438 8.26172 12.3438 9.17969C12.3438 9.75391 12.0391 10.3008 11.418 10.8516L10.4961 11.6719V11.7344H11.8047C12.2578 11.7344 12.5391 11.9766 12.5391 12.3711C12.5391 12.7656 12.2656 13 11.8047 13H9.08203C8.65234 13 8.35938 12.7383 8.35938 12.3555Z" fill="black"/>
<path d="M11.0834 1.38585V3.71918M9.91675 2.55248H12.2501" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -260,8 +260,6 @@
"ctrl-f4": "pane::CloseActiveItem",
"alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
"alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes",
"ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
"ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
"ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
"ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }],
"ctrl-shift-f": "project_search::ToggleFocus",

View File

@@ -327,8 +327,6 @@
"cmd-w": "pane::CloseActiveItem",
"alt-cmd-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
"cmd-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
"cmd-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
"cmd-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
"cmd-k cmd-w": ["pane::CloseAllItems", { "close_pinned": false }],
"cmd-f": "project_search::ToggleFocus",
@@ -450,6 +448,7 @@
"cmd-s": "workspace::Save",
"cmd-k s": "workspace::SaveWithoutFormat",
"cmd-shift-s": "workspace::SaveAs",
"cmd-n": "workspace::NewFile",
"cmd-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"cmd-1": ["workspace::ActivatePane", 0],
@@ -496,7 +495,6 @@
"context": "Workspace && !Terminal",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "workspace::NewFile",
"cmd-shift-r": "task::Spawn",
"cmd-alt-r": "task::Rerun",
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
@@ -763,7 +761,6 @@
"cmd-v": "terminal::Paste",
"cmd-a": "editor::SelectAll",
"cmd-k": "terminal::Clear",
"cmd-n": "workspace::NewTerminal",
"ctrl-enter": "assistant::InlineAssist",
// Some nice conveniences
"cmd-backspace": ["terminal::SendText", "\u0015"],

View File

@@ -230,8 +230,8 @@
"ctrl-pageup": "pane::ActivatePrevItem",
"insert": "vim::InsertBefore",
// tree-sitter related commands
"[ x": "vim::SelectLargerSyntaxNode",
"] x": "vim::SelectSmallerSyntaxNode",
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPrevDiagnostic",
"] c": "editor::GoToHunk",

View File

@@ -160,9 +160,6 @@
/// Whether to show the signature help after completion or a bracket pair inserted.
/// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
"show_signature_help_after_edits": false,
/// Whether to show the inline completions next to the completions provided by a language server.
/// Only has an effect if inline completion provider supports it.
"show_inline_completions_in_menu": true,
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -481,10 +478,8 @@
"default_width": 240
},
"chat_panel": {
// When to show the chat panel button in the status bar.
// Can be 'never', 'always', or 'when_in_call',
// or a boolean (interpreted as 'never'/'always').
"button": "when_in_call",
// Whether to show the chat panel button in the status bar.
"button": true,
// Where to the chat panel. Can be 'left' or 'right'.
"dock": "right",
// Default width of the chat panel.
@@ -1103,9 +1098,6 @@
"prettier": {
"allowed": true
}
},
"Zig": {
"language_servers": ["zls", "..."]
}
},
// Different settings for specific language models.

View File

@@ -1458,10 +1458,6 @@ impl Panel for AssistantPanel {
fn toggle_action(&self) -> Box<dyn Action> {
Box::new(ToggleFocus)
}
fn activation_priority(&self) -> u32 {
4
}
}
impl EventEmitter<PanelEvent> for AssistantPanel {}
@@ -1560,7 +1556,6 @@ impl ContextEditor {
let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_scrollbars(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_runnables(false, cx);
@@ -4970,8 +4965,8 @@ fn fold_toggle(
) -> impl Fn(
MultiBufferRow,
bool,
Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>,
&mut WindowContext,
Arc<dyn Fn(bool, &mut WindowContext<'_>) + Send + Sync>,
&mut WindowContext<'_>,
) -> AnyElement {
move |row, is_folded, fold, _cx| {
Disclosure::new((name, row.0 as u64), !is_folded)

View File

@@ -22,7 +22,6 @@ use paths::contexts_dir;
use project::Project;
use regex::Regex;
use rpc::AnyProtoClient;
use std::sync::LazyLock;
use std::{
cmp::Reverse,
ffi::OsStr,
@@ -754,8 +753,8 @@ impl ContextStore {
continue;
}
static ASSISTANT_CONTEXT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r" - \d+.zed.json$").unwrap());
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
@@ -764,15 +763,11 @@ impl ContextStore {
.zip(metadata)
{
// This is used to filter out contexts saved by the new assistant.
if !ASSISTANT_CONTEXT_REGEX.is_match(file_name) {
if !re.is_match(file_name) {
continue;
}
if let Some(title) = ASSISTANT_CONTEXT_REGEX
.replace(file_name, "")
.lines()
.next()
{
if let Some(title) = re.replace(file_name, "").lines().next() {
contexts.push(SavedContextMetadata {
title: title.to_string(),
path,

View File

@@ -149,7 +149,6 @@ impl SlashCommandCompletionProvider {
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
confirm,
resolved: true,
})
})
.collect()
@@ -243,7 +242,6 @@ impl SlashCommandCompletionProvider {
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
confirm,
resolved: true,
}
})
.collect())
@@ -332,6 +330,16 @@ impl CompletionProvider for SlashCommandCompletionProvider {
Task::ready(Ok(true))
}
fn apply_additional_edits_for_completion(
&self,
_: Model<Buffer>,
_: project::Completion,
_: bool,
_: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,

View File

@@ -5,7 +5,7 @@ use assistant_slash_command::{
};
use feature_flags::FeatureFlag;
use futures::StreamExt;
use gpui::{AppContext, AsyncAppContext, AsyncWindowContext, Task, WeakView, WindowContext};
use gpui::{AppContext, AsyncAppContext, Task, WeakView};
use language::{CodeLabel, LspAdapterDelegate};
use language_model::{
LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
@@ -14,7 +14,7 @@ use language_model::{
use semantic_index::{FileSummary, SemanticDb};
use smol::channel;
use std::sync::{atomic::AtomicBool, Arc};
use ui::{prelude::*, BorrowAppContext};
use ui::{prelude::*, BorrowAppContext, WindowContext};
use util::ResultExt;
use workspace::Workspace;
@@ -115,7 +115,7 @@ impl SlashCommand for AutoCommand {
return Task::ready(Err(anyhow!("no project indexer")));
};
let task = cx.spawn(|cx: AsyncWindowContext| async move {
let task = cx.spawn(|cx: gpui::AsyncWindowContext| async move {
let summaries = project_index
.read_with(&cx, |project_index, cx| project_index.all_summaries(cx))?
.await?;

View File

@@ -281,7 +281,7 @@ fn tab_items_for_queries(
fn active_item_buffer(
workspace: &mut Workspace,
cx: &mut ViewContext<Workspace>,
cx: &mut ui::ViewContext<Workspace>,
) -> anyhow::Result<BufferSnapshot> {
let active_editor = workspace
.active_item(cx)

View File

@@ -27,8 +27,8 @@ enum SlashCommandEntry {
Info(SlashCommandInfo),
Advert {
name: SharedString,
renderer: fn(&mut WindowContext) -> AnyElement,
on_confirm: fn(&mut WindowContext),
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
on_confirm: fn(&mut WindowContext<'_>),
},
}

View File

@@ -50,7 +50,6 @@ parking_lot.workspace = true
picker.workspace = true
project.workspace = true
proto.workspace = true
release_channel.workspace = true
rope.workspace = true
schemars.workspace = true
serde.workspace = true

View File

@@ -3,9 +3,8 @@ use std::sync::Arc;
use assistant_tool::ToolWorkingSet;
use collections::HashMap;
use gpui::{
list, AbsoluteLength, AnyElement, AppContext, DefiniteLength, EdgesRefinement, Empty, Length,
ListAlignment, ListOffset, ListState, Model, StyleRefinement, Subscription,
TextStyleRefinement, View, WeakView,
list, AnyElement, AppContext, Empty, ListAlignment, ListState, Model, StyleRefinement,
Subscription, TextStyleRefinement, View, WeakView,
};
use language::LanguageRegistry;
use language_model::Role;
@@ -90,11 +89,10 @@ impl ActiveThread {
self.list_state.splice(old_len..old_len, 1);
let theme_settings = ThemeSettings::get_global(cx);
let colors = cx.theme().colors();
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = TextSize::Small.rems(cx);
let mut text_style = cx.text_style();
let buffer_font_size = theme_settings.buffer_font_size;
let mut text_style = cx.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_size: Some(ui_font_size.into()),
@@ -107,26 +105,6 @@ impl ActiveThread {
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
code_block: StyleRefinement {
margin: EdgesRefinement {
top: Some(Length::Definite(rems(1.0).into())),
left: Some(Length::Definite(rems(0.).into())),
right: Some(Length::Definite(rems(0.).into())),
bottom: Some(Length::Definite(rems(1.).into())),
},
padding: EdgesRefinement {
top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
},
background: Some(colors.editor_foreground.opacity(0.01).into()),
border_color: Some(colors.border_variant.opacity(0.3)),
border_widths: EdgesRefinement {
top: Some(AbsoluteLength::Pixels(Pixels(1.0))),
left: Some(AbsoluteLength::Pixels(Pixels(1.))),
right: Some(AbsoluteLength::Pixels(Pixels(1.))),
bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
},
text: Some(TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(buffer_font_size.into()),
@@ -136,8 +114,8 @@ impl ActiveThread {
},
inline_code: TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(buffer_font_size.into()),
background_color: Some(colors.editor_foreground.opacity(0.01)),
font_size: Some(ui_font_size.into()),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
..Default::default()
@@ -153,10 +131,6 @@ impl ActiveThread {
)
});
self.rendered_messages_by_id.insert(*id, markdown);
self.list_state.scroll_to(ListOffset {
item_ix: old_len,
offset_in_item: Pixels(0.0),
});
}
fn handle_thread_event(
@@ -230,12 +204,11 @@ impl ActiveThread {
};
let context = self.thread.read(cx).context_for_message(message_id);
let colors = cx.theme().colors();
let (role_icon, role_name, role_color) = match message.role {
Role::User => (IconName::Person, "You", Color::Muted),
Role::Assistant => (IconName::ZedAssistant, "Assistant", Color::Accent),
Role::System => (IconName::Settings, "System", Color::Default),
let (role_icon, role_name) = match message.role {
Role::User => (IconName::Person, "You"),
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
Role::System => (IconName::Settings, "System"),
};
div()
@@ -245,45 +218,36 @@ impl ActiveThread {
.child(
v_flex()
.border_1()
.border_color(colors.border_variant)
.bg(colors.editor_background)
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(
h_flex()
.py_1p5()
.px_2p5()
.border_b_1()
.border_color(colors.border_variant)
.justify_between()
.py_1()
.px_2()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(role_icon)
.size(IconSize::XSmall)
.color(role_color),
.color(Color::Muted),
)
.child(
Label::new(role_name)
.size(LabelSize::XSmall)
.color(role_color),
),
.child(Label::new(role_name).size(LabelSize::XSmall)),
),
)
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
.child(v_flex().px_2().py_1().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
if !context.is_empty() {
parent.child(
h_flex()
.flex_wrap()
.gap_1()
.px_1p5()
.pb_1p5()
.children(context.iter().map(|c| ContextPill::new(c.clone()))),
)
} else {
parent
}
parent.child(
h_flex().flex_wrap().gap_2().p_1p5().children(
context
.iter()
.map(|context| ContextPill::new(context.clone())),
),
)
}),
)
.into_any()

View File

@@ -1,18 +1,14 @@
mod active_thread;
mod assistant_model_selector;
mod assistant_panel;
mod assistant_settings;
mod buffer_codegen;
mod context;
mod context_picker;
mod context_store;
mod context_strip;
mod inline_assistant;
mod inline_prompt_editor;
mod message_editor;
mod prompts;
mod streaming_diff;
mod terminal_codegen;
mod terminal_inline_assistant;
mod thread;
mod thread_history;

View File

@@ -1,85 +0,0 @@
use fs::Fs;
use gpui::View;
use language_model::LanguageModelRegistry;
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::update_settings_file;
use std::sync::Arc;
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
use crate::{assistant_settings::AssistantSettings, ToggleModelSelector};
pub struct AssistantModelSelector {
selector: View<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
}
impl AssistantModelSelector {
pub(crate) fn new(
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
cx: &mut WindowContext,
) -> Self {
Self {
selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _cx| settings.set_model(model.clone()),
);
},
cx,
)
}),
menu_handle,
}
}
}
impl Render for AssistantModelSelector {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.selector.focus_handle(cx).clone();
LanguageModelSelectorPopoverMenu::new(
self.selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match active_model {
Some(model) => h_flex()
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
}),
)
.with_handle(self.menu_handle.clone())
}
}

View File

@@ -27,22 +27,9 @@ use crate::{NewThread, OpenHistory, ToggleFocus};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace
.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
})
.register_action(|workspace, _: &NewThread, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
panel.update(cx, |panel, cx| panel.new_thread(cx));
workspace.focus_panel::<AssistantPanel>(cx);
}
})
.register_action(|workspace, _: &OpenHistory, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(cx);
panel.update(cx, |panel, cx| panel.open_history(cx));
}
});
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
});
},
)
.detach();
@@ -170,12 +157,6 @@ impl AssistantPanel {
self.message_editor.focus_handle(cx).focus(cx);
}
fn open_history(&mut self, cx: &mut ViewContext<Self>) {
self.active_view = ActiveView::History;
self.history.focus_handle(cx).focus(cx);
cx.notify();
}
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
let Some(thread) = self
.thread_store
@@ -276,7 +257,7 @@ impl Panel for AssistantPanel {
}
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
Some(IconName::ZedAssistant2)
Some(IconName::ZedAssistant)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@@ -286,10 +267,6 @@ impl Panel for AssistantPanel {
fn toggle_action(&self) -> Box<dyn Action> {
Box::new(ToggleFocus)
}
fn activation_priority(&self) -> u32 {
3
}
}
impl AssistantPanel {
@@ -595,7 +572,9 @@ impl Render for AssistantPanel {
this.new_thread(cx);
}))
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
this.open_history(cx);
this.active_view = ActiveView::History;
this.history.focus_handle(cx).focus(cx);
cx.notify();
}))
.child(self.render_toolbar(cx))
.map(|parent| match self.active_view {

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ pub struct Context {
pub text: SharedString,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
Directory,

View File

@@ -10,7 +10,6 @@ use gpui::{
WeakModel, WeakView,
};
use picker::{Picker, PickerDelegate};
use release_channel::ReleaseChannel;
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::Workspace;
@@ -23,12 +22,6 @@ use crate::context_picker::thread_context_picker::ThreadContextPicker;
use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore;
#[derive(Debug, Clone, Copy)]
pub enum ConfirmBehavior {
KeepOpen,
Close,
}
#[derive(Debug, Clone)]
enum ContextPickerMode {
Default,
@@ -48,30 +41,25 @@ impl ContextPicker {
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
) -> Self {
let mut entries = Vec::new();
entries.push(ContextPickerEntry {
name: "File".into(),
kind: ContextKind::File,
icon: IconName::File,
});
let release_channel = ReleaseChannel::global(cx);
// The directory context picker isn't fully implemented yet, so limit it
// to development builds.
if release_channel == ReleaseChannel::Dev {
entries.push(ContextPickerEntry {
let mut entries = vec![
ContextPickerEntry {
name: "File".into(),
kind: ContextKind::File,
icon: IconName::File,
},
ContextPickerEntry {
name: "Folder".into(),
kind: ContextKind::Directory,
icon: IconName::Folder,
});
}
entries.push(ContextPickerEntry {
name: "Fetch".into(),
kind: ContextKind::FetchedUrl,
icon: IconName::Globe,
});
},
ContextPickerEntry {
name: "Fetch".into(),
kind: ContextKind::FetchedUrl,
icon: IconName::Globe,
},
];
if thread_store.is_some() {
entries.push(ContextPickerEntry {
@@ -86,7 +74,6 @@ impl ContextPicker {
workspace,
thread_store,
context_store,
confirm_behavior,
entries,
selected_ix: 0,
};
@@ -149,7 +136,6 @@ pub(crate) struct ContextPickerDelegate {
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
entries: Vec<ContextPickerEntry>,
selected_ix: usize,
}
@@ -189,7 +175,6 @@ impl PickerDelegate for ContextPickerDelegate {
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
cx,
)
}));
@@ -200,7 +185,6 @@ impl PickerDelegate for ContextPickerDelegate {
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
cx,
)
}));
@@ -211,7 +195,6 @@ impl PickerDelegate for ContextPickerDelegate {
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.confirm_behavior,
cx,
)
}));
@@ -223,7 +206,6 @@ impl PickerDelegate for ContextPickerDelegate {
thread_store.clone(),
self.context_picker.clone(),
self.context_store.clone(),
self.confirm_behavior,
cx,
)
}));

View File

@@ -1,8 +1,6 @@
// TODO: Remove this when we finish the implementation.
// TODO: Remove this once we've implemented the functionality.
#![allow(unused)]
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use fuzzy::PathMatch;
@@ -13,8 +11,7 @@ use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
pub struct DirectoryContextPicker {
@@ -26,15 +23,10 @@ impl DirectoryContextPicker {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = DirectoryContextPickerDelegate::new(
context_picker,
workspace,
context_store,
confirm_behavior,
);
let delegate =
DirectoryContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
@@ -57,7 +49,6 @@ pub struct DirectoryContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<PathMatch>,
selected_index: usize,
}
@@ -67,76 +58,15 @@ impl DirectoryContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
}
fn search(
&mut self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
cx: &mut ViewContext<Picker<Self>>,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let directory_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.directories(false, 0).map(move |entry| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: true,
})
});
Task::ready(directory_matches.collect())
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Directories,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
}
impl PickerDelegate for DirectoryContextPickerDelegate {
@@ -150,7 +80,7 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
@@ -158,67 +88,13 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
"Search folders…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn(|this, mut cx| async move {
let mut paths = search_task.await;
let empty_path = Path::new("");
paths.retain(|path_match| path_match.path.as_ref() != empty_path);
this.update(&mut cx, |this, _cx| {
this.delegate.matches = paths;
})
.log_err();
})
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
// TODO: Implement this once we fix the issues with the file context picker.
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return;
};
let path = mat.path.clone();
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
this.update(&mut cx, |this, cx| {
let mut text = String::new();
// TODO: Add the files from the selected directory.
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.insert_context(
ContextKind::Directory,
path.to_string_lossy().to_string(),
text,
);
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
}
anyhow::Ok(())
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx)
fn confirm(&mut self, _secondary: bool, _cx: &mut ViewContext<Picker<Self>>) {
// TODO: Implement this once we fix the issues with the file context picker.
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
@@ -232,18 +108,10 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
fn render_match(
&self,
ix: usize,
selected: bool,
_ix: usize,
_selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let path_match = &self.matches[ix];
let directory_name = path_match.path.to_string_lossy().to_string();
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(h_flex().gap_2().child(Label::new(directory_name))),
)
None
}
}

View File

@@ -12,7 +12,7 @@ use ui::{prelude::*, ListItem, ViewContext};
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
pub struct FetchContextPicker {
@@ -24,15 +24,9 @@ impl FetchContextPicker {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(
context_picker,
workspace,
context_store,
confirm_behavior,
);
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
@@ -62,7 +56,6 @@ pub struct FetchContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
url: String,
}
@@ -71,13 +64,11 @@ impl FetchContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
context_store,
confirm_behavior,
url: String::new(),
}
}
@@ -176,7 +167,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
"Enter a URL…".into()
}
@@ -193,7 +184,6 @@ impl PickerDelegate for FetchContextPickerDelegate {
let http_client = workspace.read(cx).client().http_client().clone();
let url = self.url.clone();
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
let text = Self::build_message(http_client, &url).await?;
@@ -202,14 +192,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
.context_store
.update(cx, |context_store, _cx| {
context_store.insert_context(ContextKind::FetchedUrl, url, text);
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
}
anyhow::Ok(())
})
})??;
anyhow::Ok(())

View File

@@ -1,6 +1,6 @@
use std::fmt::Write as _;
use std::ops::RangeInclusive;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
@@ -13,7 +13,7 @@ use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
pub struct FileContextPicker {
@@ -25,15 +25,9 @@ impl FileContextPicker {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FileContextPickerDelegate::new(
context_picker,
workspace,
context_store,
confirm_behavior,
);
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
@@ -56,7 +50,6 @@ pub struct FileContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<PathMatch>,
selected_index: usize,
}
@@ -66,13 +59,11 @@ impl FileContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
@@ -88,37 +79,44 @@ impl FileContextPickerDelegate {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let recent_matches = workspace
.recent_navigation_history(Some(10), cx)
let entries = workspace.recent_navigation_history(Some(10), cx);
let entries = entries
.into_iter()
.filter_map(|(project_path, _)| {
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix: worktree.read(cx).root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: false,
.map(|entries| entries.0)
.chain(project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let id = worktree.id();
worktree
.child_entries(Path::new(""))
.filter(|entry| entry.kind.is_file())
.map(move |entry| project::ProjectPath {
worktree_id: id,
path: entry.path.clone(),
})
}))
.collect::<Vec<_>>();
let path_prefix: Arc<str> = Arc::default();
Task::ready(
entries
.into_iter()
.filter_map(|entry| {
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
full_path.push(&entry.path);
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
path: full_path.into(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
})
});
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.files(true, 0).map(move |entry| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
});
Task::ready(recent_matches.chain(file_matches).collect())
.collect(),
)
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
@@ -192,9 +190,7 @@ impl PickerDelegate for FileContextPickerDelegate {
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
let mat = &self.matches[self.selected_index];
let workspace = self.workspace.clone();
let Some(project) = workspace
@@ -205,7 +201,6 @@ impl PickerDelegate for FileContextPickerDelegate {
};
let path = mat.path.clone();
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
let Some(open_buffer_task) = project
.update(&mut cx, |project, cx| {
@@ -219,31 +214,22 @@ impl PickerDelegate for FileContextPickerDelegate {
let buffer = open_buffer_task.await?;
this.update(&mut cx, |this, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| {
let mut text = String::new();
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
this.delegate.context_store.update(cx, |context_store, cx| {
let mut text = String::new();
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str("```\n");
text.push_str("```\n");
context_store.insert_context(
ContextKind::File,
path.to_string_lossy().to_string(),
text,
);
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
}
anyhow::Ok(())
context_store.insert_context(
ContextKind::File,
path.to_string_lossy().to_string(),
text,
);
})
})??;
anyhow::Ok(())
@@ -267,30 +253,16 @@ impl PickerDelegate for FileContextPickerDelegate {
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let path_match = &self.matches[ix];
let (file_name, directory) = if path_match.path.as_ref() == Path::new("") {
(SharedString::from(path_match.path_prefix.clone()), None)
} else {
let file_name = path_match
.path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
.into();
let mut directory = format!("{}/", path_match.path_prefix);
if let Some(parent) = path_match
.path
.parent()
.filter(|parent| parent != &Path::new(""))
{
directory.push_str(&parent.to_string_lossy());
directory.push('/');
}
(file_name, Some(directory))
};
let file_name = path_match
.path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let directory = path_match
.path
.parent()
.map(|directory| format!("{}/", directory.to_string_lossy()));
Some(
ListItem::new(ix).inset(true).toggle_state(selected).child(

View File

@@ -6,7 +6,7 @@ use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem};
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_picker::ContextPicker;
use crate::context_store;
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
@@ -20,15 +20,10 @@ impl ThreadContextPicker {
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = ThreadContextPickerDelegate::new(
thread_store,
context_picker,
context_store,
confirm_behavior,
);
let delegate =
ThreadContextPickerDelegate::new(thread_store, context_picker, context_store);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
ThreadContextPicker { picker }
@@ -57,7 +52,6 @@ pub struct ThreadContextPickerDelegate {
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<ThreadContextEntry>,
selected_index: usize,
}
@@ -67,13 +61,11 @@ impl ThreadContextPickerDelegate {
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
ThreadContextPickerDelegate {
thread_store,
context_picker,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
@@ -154,9 +146,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let Some(entry) = self.matches.get(self.selected_index) else {
return;
};
let entry = &self.matches[self.selected_index];
let Some(thread_store) = self.thread_store.upgrade() else {
return;
@@ -190,11 +180,6 @@ impl PickerDelegate for ThreadContextPickerDelegate {
context_store.insert_context(ContextKind::Thread, entry.summary.clone(), text);
})
.ok();
match self.confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => self.dismissed(cx),
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
@@ -218,7 +203,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(Label::new(thread.summary.clone())),
.child(thread.summary.clone()),
)
}
}

View File

@@ -4,12 +4,11 @@ use gpui::{FocusHandle, Model, View, WeakModel, WeakView};
use ui::{prelude::*, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::Workspace;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore;
use crate::ui::ContextPill;
use crate::ToggleContextPicker;
use settings::Settings;
pub struct ContextStrip {
context_store: Model<ContextStore>,
@@ -34,7 +33,6 @@ impl ContextStrip {
workspace.clone(),
thread_store.clone(),
context_store.downgrade(),
ConfirmBehavior::KeepOpen,
cx,
)
}),
@@ -46,7 +44,7 @@ impl ContextStrip {
impl Render for ContextStrip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let context = self.context_store.read(cx).context().clone();
let context = self.context_store.read(cx).context();
let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone();
@@ -77,29 +75,6 @@ impl Render for ContextStrip {
})
.with_handle(self.context_picker_menu_handle.clone()),
)
.when(context.is_empty(), {
|parent| {
parent.child(
h_flex()
.id("no-content-info")
.ml_1p5()
.gap_2()
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::Small.rems(cx))
.text_color(cx.theme().colors().text_muted)
.child("Add Context")
.children(
ui::KeyBinding::for_action_in(
&ToggleContextPicker,
&self.focus_handle,
cx,
)
.map(|binding| binding.into_any_element()),
)
.opacity(0.5),
)
}
})
.children(context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
@@ -112,21 +87,19 @@ impl Render for ContextStrip {
}))
})
}))
.when(!context.is_empty(), {
move |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.icon_size(IconSize::Small)
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click({
let context_store = self.context_store.clone();
cx.listener(move |_this, _event, cx| {
context_store.update(cx, |this, _cx| this.clear());
cx.notify();
})
}),
)
}
.when(!context.is_empty(), |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.icon_size(IconSize::Small)
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click({
let context_store = self.context_store.clone();
cx.listener(move |_this, _event, cx| {
context_store.update(cx, |this, _cx| this.clear());
cx.notify();
})
}),
)
})
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,20 @@
use std::sync::Arc;
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
use editor::{Editor, EditorElement, EditorStyle};
use fs::Fs;
use gpui::{
AppContext, DismissEvent, FocusableView, Model, Subscription, TextStyle, View, WeakModel,
WeakView,
};
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::LanguageModelSelector;
use rope::Point;
use settings::Settings;
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::{update_settings_file, Settings};
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
SwitchWithLabel,
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenuHandle,
Tooltip,
};
use workspace::Workspace;
use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::assistant_settings::AssistantSettings;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
use crate::thread::{RequestKind, Thread};
@@ -31,12 +27,9 @@ pub struct MessageEditor {
context_store: Model<ContextStore>,
context_strip: View<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
inline_context_picker: View<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: View<AssistantModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
language_model_selector: View<LanguageModelSelector>,
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
use_tools: bool,
_subscriptions: Vec<Subscription>,
}
impl MessageEditor {
@@ -49,32 +42,14 @@ impl MessageEditor {
) -> Self {
let context_store = cx.new_model(|_cx| ContextStore::new());
let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let editor = cx.new_view(|cx| {
let mut editor = Editor::auto_height(10, cx);
editor.set_placeholder_text("Ask anything", cx);
let mut editor = Editor::auto_height(80, cx);
editor.set_placeholder_text("Ask anything, @ to add context", cx);
editor.set_show_indent_guides(false, cx);
editor
});
let inline_context_picker = cx.new_view(|cx| {
ContextPicker::new(
workspace.clone(),
Some(thread_store.clone()),
context_store.downgrade(),
ConfirmBehavior::Close,
cx,
)
});
let subscriptions = vec![
cx.subscribe(&editor, Self::handle_editor_event),
cx.subscribe(
&inline_context_picker,
Self::handle_inline_context_picker_event,
),
];
Self {
thread,
@@ -91,19 +66,26 @@ impl MessageEditor {
)
}),
context_picker_menu_handle,
inline_context_picker,
inline_context_picker_menu_handle,
model_selector: cx.new_view(|cx| {
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _cx| settings.set_model(model.clone()),
);
},
cx,
)
}),
model_selector_menu_handle,
language_model_selector_menu_handle: PopoverMenuHandle::default(),
use_tools: false,
_subscriptions: subscriptions,
}
}
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
self.model_selector_menu_handle.toggle(cx)
self.language_model_selector_menu_handle.toggle(cx);
}
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
@@ -161,38 +143,48 @@ impl MessageEditor {
None
}
fn handle_editor_event(
&mut self,
editor: View<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
) {
match event {
EditorEvent::SelectionsChanged { .. } => {
editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let newest_cursor = editor.selections.newest::<Point>(cx).head();
if newest_cursor.column > 0 {
let behind_cursor = Point::new(newest_cursor.row, newest_cursor.column - 1);
let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
if char_behind_cursor == Some('@') {
self.inline_context_picker_menu_handle.show(cx);
}
}
});
}
_ => {}
}
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.language_model_selector.focus_handle(cx).clone();
fn handle_inline_context_picker_event(
&mut self,
_inline_context_picker: View<ContextPicker>,
_event: &DismissEvent,
cx: &mut ViewContext<Self>,
) {
let editor_focus_handle = self.editor.focus_handle(cx);
cx.focus(&editor_focus_handle);
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match active_model {
Some(model) => h_flex()
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
}),
)
.with_handle(self.language_model_selector_menu_handle.clone())
}
}
@@ -207,7 +199,6 @@ impl Render for MessageEditor {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.5;
let focus_handle = self.editor.focus_handle(cx);
let inline_context_picker = self.inline_context_picker.clone();
let bg_color = cx.theme().colors().editor_background;
v_flex()
@@ -220,72 +211,58 @@ impl Render for MessageEditor {
.p_2()
.bg(bg_color)
.child(self.context_strip.clone())
.child(
v_flex()
.gap_4()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,
line_height: line_height.into(),
..Default::default()
};
.child(div().id("thread_editor").overflow_y_scroll().h_12().child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,
line_height: line_height.into(),
..Default::default()
};
EditorElement::new(
&self.editor,
EditorStyle {
background: bg_color,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.child(
PopoverMenu::new("inline-context-picker")
.menu(move |_cx| Some(inline_context_picker.clone()))
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.with_handle(self.inline_context_picker_menu_handle.clone()),
)
EditorElement::new(
&self.editor,
EditorStyle {
background: bg_color,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}))
.child(
h_flex()
.justify_between()
.child(CheckboxWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
};
}),
))
.child(
h_flex()
.justify_between()
.child(SwitchWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => {
false
}
};
}),
))
.gap_1()
.child(self.render_language_model_selector(cx))
.child(
h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, cx);
}),
),
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, cx);
}),
),
),
)

View File

@@ -1,192 +0,0 @@
use crate::inline_prompt_editor::CodegenStatus;
use client::telemetry::Telemetry;
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{AppContext, EventEmitter, Model, ModelContext, Task};
use language_model::{LanguageModelRegistry, LanguageModelRequest};
use language_models::report_assistant_event;
use std::{sync::Arc, time::Instant};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
pub struct TerminalCodegen {
pub status: CodegenStatus,
pub telemetry: Option<Arc<Telemetry>>,
terminal: Model<Terminal>,
generation: Task<()>,
pub message_id: Option<String>,
transaction: Option<TerminalTransaction>,
}
impl EventEmitter<CodegenEvent> for TerminalCodegen {}
impl TerminalCodegen {
pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
Self {
terminal,
telemetry,
status: CodegenStatus::Idle,
generation: Task::ready(()),
message_id: None,
transaction: None,
}
}
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
let model_api_key = model.api_key(cx);
let http_client = cx.http_client();
let telemetry = self.telemetry.clone();
self.status = CodegenStatus::Pending;
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
self.generation = cx.spawn(|this, mut cx| async move {
let model_telemetry_id = model.telemetry_id();
let model_provider_id = model.provider_id();
let response = model.stream_completion_text(prompt, &cx).await;
let generate = async {
let message_id = response
.as_ref()
.ok()
.and_then(|response| response.message_id.clone());
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
let task = cx.background_executor().spawn({
let message_id = message_id.clone();
let executor = cx.background_executor().clone();
async move {
let mut response_latency = None;
let request_start = Instant::now();
let task = async {
let mut chunks = response?.stream;
while let Some(chunk) = chunks.next().await {
if response_latency.is_none() {
response_latency = Some(request_start.elapsed());
}
let chunk = chunk?;
hunks_tx.send(chunk).await?;
}
anyhow::Ok(())
};
let result = task.await;
let error_message = result.as_ref().err().map(|error| error.to_string());
report_assistant_event(
AssistantEvent {
conversation_id: None,
kind: AssistantKind::InlineTerminal,
message_id,
phase: AssistantPhase::Response,
model: model_telemetry_id,
model_provider: model_provider_id.to_string(),
response_latency,
error_message,
language_name: None,
},
telemetry,
http_client,
model_api_key,
&executor,
);
result?;
anyhow::Ok(())
}
});
this.update(&mut cx, |this, _| {
this.message_id = message_id;
})?;
while let Some(hunk) = hunks_rx.next().await {
this.update(&mut cx, |this, cx| {
if let Some(transaction) = &mut this.transaction {
transaction.push(hunk, cx);
cx.notify();
}
})?;
}
task.await?;
anyhow::Ok(())
};
let result = generate.await;
this.update(&mut cx, |this, cx| {
if let Err(error) = result {
this.status = CodegenStatus::Error(error);
} else {
this.status = CodegenStatus::Done;
}
cx.emit(CodegenEvent::Finished);
cx.notify();
})
.ok();
});
cx.notify();
}
pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
self.status = CodegenStatus::Done;
self.generation = Task::ready(());
cx.emit(CodegenEvent::Finished);
cx.notify();
}
pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.complete(cx);
}
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.undo(cx);
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum CodegenEvent {
Finished,
}
pub const CLEAR_INPUT: &str = "\x15";
const CARRIAGE_RETURN: &str = "\x0d";
struct TerminalTransaction {
terminal: Model<Terminal>,
}
impl TerminalTransaction {
pub fn start(terminal: Model<Terminal>) -> Self {
Self { terminal }
}
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
let input = Self::sanitize_input(hunk);
self.terminal
.update(cx, |terminal, _| terminal.input(input));
}
pub fn undo(&self, cx: &mut AppContext) {
self.terminal
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
}
pub fn complete(&self, cx: &mut AppContext) {
self.terminal.update(cx, |terminal, _| {
terminal.input(CARRIAGE_RETURN.to_string())
});
}
fn sanitize_input(input: String) -> String {
input.replace(['\r', '\n'], "")
}
}

View File

@@ -1,29 +1,37 @@
use crate::assistant_settings::AssistantSettings;
use crate::context::attach_context_to_message;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::inline_prompt_editor::{
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
};
use crate::context_strip::ContextStrip;
use crate::prompts::PromptBuilder;
use crate::terminal_codegen::{CodegenEvent, TerminalCodegen, CLEAR_INPUT};
use crate::thread_store::ThreadStore;
use crate::ToggleContextPicker;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque};
use editor::{actions::SelectAll, MultiBuffer};
use editor::{
actions::{MoveDown, MoveUp, SelectAll},
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
};
use fs::Fs;
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{
AppContext, Context, FocusableView, Global, Model, Subscription, UpdateGlobal, View, WeakModel,
WeakView,
AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext,
Subscription, Task, TextStyle, UpdateGlobal, View, WeakModel, WeakView,
};
use language::Buffer;
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event;
use std::sync::Arc;
use settings::{update_settings_file, Settings};
use std::{cmp, sync::Arc, time::Instant};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
use terminal_view::TerminalView;
use ui::prelude::*;
use theme::ThemeSettings;
use ui::{prelude::*, text_for_action, IconButtonShape, PopoverMenuHandle, Tooltip};
use util::ResultExt;
use workspace::{notifications::NotificationId, Toast, Workspace};
@@ -39,6 +47,17 @@ pub fn init(
const DEFAULT_CONTEXT_LINES: usize = 50;
const PROMPT_HISTORY_MAX_LEN: usize = 20;
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
struct TerminalInlineAssistId(usize);
impl TerminalInlineAssistId {
fn post_inc(&mut self) -> TerminalInlineAssistId {
let id = *self;
self.0 += 1;
id
}
}
pub struct TerminalInlineAssistant {
next_assist_id: TerminalInlineAssistId,
assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
@@ -79,10 +98,10 @@ impl TerminalInlineAssistant {
MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx)
});
let context_store = cx.new_model(|_cx| ContextStore::new());
let codegen = cx.new_model(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new_view(|cx| {
PromptEditor::new_terminal(
PromptEditor::new(
assist_id,
self.prompt_history.clone(),
prompt_buffer.clone(),
@@ -131,11 +150,11 @@ impl TerminalInlineAssistant {
fn handle_prompt_editor_event(
&mut self,
prompt_editor: View<PromptEditor<TerminalCodegen>>,
prompt_editor: View<PromptEditor>,
event: &PromptEditorEvent,
cx: &mut WindowContext,
) {
let assist_id = prompt_editor.read(cx).id();
let assist_id = prompt_editor.read(cx).id;
match event {
PromptEditorEvent::StartRequested => {
self.start_assist(assist_id, cx);
@@ -361,8 +380,8 @@ impl TerminalInlineAssistant {
struct TerminalInlineAssist {
terminal: WeakView<TerminalView>,
prompt_editor: Option<View<PromptEditor<TerminalCodegen>>>,
codegen: Model<TerminalCodegen>,
prompt_editor: Option<View<PromptEditor>>,
codegen: Model<Codegen>,
workspace: WeakView<Workspace>,
context_store: Model<ContextStore>,
_subscriptions: Vec<Subscription>,
@@ -372,12 +391,12 @@ impl TerminalInlineAssist {
pub fn new(
assist_id: TerminalInlineAssistId,
terminal: &View<TerminalView>,
prompt_editor: View<PromptEditor<TerminalCodegen>>,
prompt_editor: View<PromptEditor>,
workspace: WeakView<Workspace>,
context_store: Model<ContextStore>,
cx: &mut WindowContext,
) -> Self {
let codegen = prompt_editor.read(cx).codegen().clone();
let codegen = prompt_editor.read(cx).codegen.clone();
Self {
terminal: terminal.downgrade(),
prompt_editor: Some(prompt_editor.clone()),
@@ -428,3 +447,660 @@ impl TerminalInlineAssist {
}
}
}
enum PromptEditorEvent {
StartRequested,
StopRequested,
ConfirmRequested { execute: bool },
CancelRequested,
DismissRequested,
Resized { height_in_lines: u8 },
}
struct PromptEditor {
id: TerminalInlineAssistId,
height_in_lines: u8,
editor: View<Editor>,
context_strip: View<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
pending_prompt: String,
codegen: Model<Codegen>,
_codegen_subscription: Subscription,
editor_subscriptions: Vec<Subscription>,
}
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let status = &self.codegen.read(cx).status;
let mut buttons = Vec::new();
buttons.extend(match status {
CodegenStatus::Idle => vec![
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
.into_any_element(),
IconButton::new("start", IconName::SparkleAlt)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
.into_any_element(),
],
CodegenStatus::Pending => vec![
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Cancel Assist", cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
.into_any_element(),
IconButton::new("stop", IconName::Stop)
.icon_color(Color::Error)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::with_meta(
"Interrupt Generation",
Some(&menu::Cancel),
"Changes won't be discarded",
cx,
)
})
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
.into_any_element(),
],
CodegenStatus::Error(_) | CodegenStatus::Done => {
let cancel = IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
.into_any_element();
let has_error = matches!(status, CodegenStatus::Error(_));
if has_error || self.edited_since_done {
vec![
cancel,
IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::with_meta(
"Restart Generation",
Some(&menu::Confirm),
"Changes will be discarded",
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::StartRequested);
}))
.into_any_element(),
]
} else {
vec![
cancel,
IconButton::new("accept", IconName::Check)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
}))
.into_any_element(),
IconButton::new("confirm", IconName::Play)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::for_action(
"Execute Generated Command",
&menu::SecondaryConfirm,
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
}))
.into_any_element(),
]
}
}
});
v_flex()
.border_y_1()
.border_color(cx.theme().status().info_border)
.py_2()
.size_full()
.child(
h_flex()
.key_context("PromptEditor")
.bg(cx.theme().colors().editor_background)
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::secondary_confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.child(
h_flex()
.w_12()
.justify_center()
.gap_2()
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
let error_message = SharedString::from(error.to_string());
Some(
div()
.id("error")
.tooltip(move |cx| {
Tooltip::text(error_message.clone(), cx)
})
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
} else {
None
},
),
)
.child(div().flex_1().child(self.render_prompt_editor(cx)))
.child(h_flex().gap_1().pr_4().children(buttons)),
)
.child(h_flex().child(self.context_strip.clone()))
}
}
impl FocusableView for PromptEditor {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.editor.focus_handle(cx)
}
}
impl PromptEditor {
const MAX_LINES: u8 = 8;
#[allow(clippy::too_many_arguments)]
fn new(
id: TerminalInlineAssistId,
prompt_history: VecDeque<String>,
prompt_buffer: Model<MultiBuffer>,
codegen: Model<Codegen>,
fs: Arc<dyn Fs>,
context_store: Model<ContextStore>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_editor = cx.new_view(|cx| {
let mut editor = Editor::new(
EditorMode::AutoHeight {
max_lines: Self::MAX_LINES as usize,
},
prompt_buffer,
None,
false,
cx,
);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text(Self::placeholder_text(cx), cx);
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let mut this = Self {
id,
height_in_lines: 1,
editor: prompt_editor.clone(),
context_strip: cx.new_view(|cx| {
ContextStrip::new(
context_store,
workspace.clone(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
cx,
)
}),
context_picker_menu_handle,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
edited_since_done: false,
prompt_history,
prompt_history_ix: None,
pending_prompt: String::new(),
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(),
codegen,
};
this.count_lines(cx);
this.subscribe_to_editor(cx);
this
}
fn placeholder_text(cx: &WindowContext) -> String {
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
.map(|keybinding| format!("{keybinding} for context"))
.unwrap_or_default();
format!("Generate…{context_keybinding} ↓↑ for history")
}
fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
self.editor_subscriptions.clear();
self.editor_subscriptions
.push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
self.editor_subscriptions
.push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
}
fn prompt(&self, cx: &AppContext) -> String {
self.editor.read(cx).text(cx)
}
fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
let height_in_lines = cmp::max(
2, // Make the editor at least two lines tall, to account for padding and buttons.
cmp::min(
self.editor
.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
Self::MAX_LINES as u32,
),
) as u8;
if height_in_lines != self.height_in_lines {
self.height_in_lines = height_in_lines;
cx.emit(PromptEditorEvent::Resized { height_in_lines });
}
}
fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
self.count_lines(cx);
}
fn handle_prompt_editor_events(
&mut self,
_: View<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
) {
match event {
EditorEvent::Edited { .. } => {
let prompt = self.editor.read(cx).text(cx);
if self
.prompt_history_ix
.map_or(true, |ix| self.prompt_history[ix] != prompt)
{
self.prompt_history_ix.take();
self.pending_prompt = prompt;
}
self.edited_since_done = true;
cx.notify();
}
_ => {}
}
}
fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle => {
self.editor
.update(cx, |editor, _| editor.set_read_only(false));
}
CodegenStatus::Pending => {
self.editor
.update(cx, |editor, _| editor.set_read_only(true));
}
CodegenStatus::Done | CodegenStatus::Error(_) => {
self.edited_since_done = false;
self.editor
.update(cx, |editor, _| editor.set_read_only(false));
}
}
}
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
self.context_picker_menu_handle.toggle(cx);
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
cx.emit(PromptEditorEvent::CancelRequested);
}
CodegenStatus::Pending => {
cx.emit(PromptEditorEvent::StopRequested);
}
}
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle => {
if !self.editor.read(cx).text(cx).trim().is_empty() {
cx.emit(PromptEditorEvent::StartRequested);
}
}
CodegenStatus::Pending => {
cx.emit(PromptEditorEvent::DismissRequested);
}
CodegenStatus::Done => {
if self.edited_since_done {
cx.emit(PromptEditorEvent::StartRequested);
} else {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
}
}
CodegenStatus::Error(_) => {
cx.emit(PromptEditorEvent::StartRequested);
}
}
}
fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
}
}
fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.prompt_history_ix {
if ix > 0 {
self.prompt_history_ix = Some(ix - 1);
let prompt = self.prompt_history[ix - 1].as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_beginning(&Default::default(), cx);
});
}
} else if !self.prompt_history.is_empty() {
self.prompt_history_ix = Some(self.prompt_history.len() - 1);
let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_beginning(&Default::default(), cx);
});
}
}
fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.prompt_history_ix {
if ix < self.prompt_history.len() - 1 {
self.prompt_history_ix = Some(ix + 1);
let prompt = self.prompt_history[ix + 1].as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_end(&Default::default(), cx)
});
} else {
self.prompt_history_ix = None;
let prompt = self.pending_prompt.as_str();
self.editor.update(cx, |editor, cx| {
editor.set_text(prompt, cx);
editor.move_to_end(&Default::default(), cx)
});
}
}
}
fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if self.editor.read(cx).read_only(cx) {
cx.theme().colors().text_disabled
} else {
cx.theme().colors().text
},
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_size: settings.buffer_font_size.into(),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),
..Default::default()
};
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
}
#[derive(Debug)]
pub enum CodegenEvent {
Finished,
}
impl EventEmitter<CodegenEvent> for Codegen {}
const CLEAR_INPUT: &str = "\x15";
const CARRIAGE_RETURN: &str = "\x0d";
struct TerminalTransaction {
terminal: Model<Terminal>,
}
impl TerminalTransaction {
pub fn start(terminal: Model<Terminal>) -> Self {
Self { terminal }
}
pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
let input = Self::sanitize_input(hunk);
self.terminal
.update(cx, |terminal, _| terminal.input(input));
}
pub fn undo(&self, cx: &mut AppContext) {
self.terminal
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
}
pub fn complete(&self, cx: &mut AppContext) {
self.terminal.update(cx, |terminal, _| {
terminal.input(CARRIAGE_RETURN.to_string())
});
}
fn sanitize_input(input: String) -> String {
input.replace(['\r', '\n'], "")
}
}
pub struct Codegen {
status: CodegenStatus,
telemetry: Option<Arc<Telemetry>>,
terminal: Model<Terminal>,
generation: Task<()>,
message_id: Option<String>,
transaction: Option<TerminalTransaction>,
}
impl Codegen {
pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
Self {
terminal,
telemetry,
status: CodegenStatus::Idle,
generation: Task::ready(()),
message_id: None,
transaction: None,
}
}
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
let model_api_key = model.api_key(cx);
let http_client = cx.http_client();
let telemetry = self.telemetry.clone();
self.status = CodegenStatus::Pending;
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
self.generation = cx.spawn(|this, mut cx| async move {
let model_telemetry_id = model.telemetry_id();
let model_provider_id = model.provider_id();
let response = model.stream_completion_text(prompt, &cx).await;
let generate = async {
let message_id = response
.as_ref()
.ok()
.and_then(|response| response.message_id.clone());
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
let task = cx.background_executor().spawn({
let message_id = message_id.clone();
let executor = cx.background_executor().clone();
async move {
let mut response_latency = None;
let request_start = Instant::now();
let task = async {
let mut chunks = response?.stream;
while let Some(chunk) = chunks.next().await {
if response_latency.is_none() {
response_latency = Some(request_start.elapsed());
}
let chunk = chunk?;
hunks_tx.send(chunk).await?;
}
anyhow::Ok(())
};
let result = task.await;
let error_message = result.as_ref().err().map(|error| error.to_string());
report_assistant_event(
AssistantEvent {
conversation_id: None,
kind: AssistantKind::InlineTerminal,
message_id,
phase: AssistantPhase::Response,
model: model_telemetry_id,
model_provider: model_provider_id.to_string(),
response_latency,
error_message,
language_name: None,
},
telemetry,
http_client,
model_api_key,
&executor,
);
result?;
anyhow::Ok(())
}
});
this.update(&mut cx, |this, _| {
this.message_id = message_id;
})?;
while let Some(hunk) = hunks_rx.next().await {
this.update(&mut cx, |this, cx| {
if let Some(transaction) = &mut this.transaction {
transaction.push(hunk, cx);
cx.notify();
}
})?;
}
task.await?;
anyhow::Ok(())
};
let result = generate.await;
this.update(&mut cx, |this, cx| {
if let Err(error) = result {
this.status = CodegenStatus::Error(error);
} else {
this.status = CodegenStatus::Done;
}
cx.emit(CodegenEvent::Finished);
cx.notify();
})
.ok();
});
cx.notify();
}
pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
self.status = CodegenStatus::Done;
self.generation = Task::ready(());
cx.emit(CodegenEvent::Finished);
cx.notify();
}
pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.complete(cx);
}
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
if let Some(transaction) = self.transaction.take() {
transaction.undo(cx);
}
}
}
enum CodegenStatus {
Idle,
Pending,
Done,
Error(anyhow::Error),
}

View File

@@ -3,7 +3,7 @@ use std::rc::Rc;
use gpui::ClickEvent;
use ui::{prelude::*, IconButtonShape};
use crate::context::{Context, ContextKind};
use crate::context::Context;
#[derive(IntoElement)]
pub struct ContextPill {
@@ -27,28 +27,15 @@ impl ContextPill {
impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let padding_right = if self.on_remove.is_some() {
px(2.)
} else {
px(4.)
};
let icon = match self.context.kind {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageCircle,
};
h_flex()
.gap_1()
.pl_1()
.pr(padding_right)
.pl_1p5()
.pr_0p5()
.pb(px(1.))
.border_1()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(cx.theme().colors().element_background)
.rounded_md()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
.when_some(self.on_remove, |parent, on_remove| {
parent.child(

View File

@@ -18,7 +18,7 @@ pub struct UpdateNotification {
impl EventEmitter<DismissEvent> for UpdateNotification {}
impl Render for UpdateNotification {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
let app_name = ReleaseChannel::global(cx).display_name();
v_flex()

View File

@@ -16,10 +16,10 @@ doctest = false
editor.workspace = true
gpui.workspace = true
itertools.workspace = true
outline.workspace = true
theme.workspace = true
ui.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -39,17 +39,10 @@ impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
impl Render for Breadcrumbs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
const MAX_SEGMENTS: usize = 12;
let element = h_flex()
.id("breadcrumb-container")
.flex_grow()
.overflow_x_scroll()
.text_ui(cx);
let element = h_flex().text_ui(cx);
let Some(active_item) = self.active_item.as_ref() else {
return element;
};
let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
return element;
};
@@ -59,7 +52,6 @@ impl Render for Breadcrumbs {
prefix_end_ix,
segments.len().saturating_sub(MAX_SEGMENTS / 2),
);
if suffix_start_ix > prefix_end_ix {
segments.splice(
prefix_end_ix..suffix_start_ix,
@@ -90,7 +82,6 @@ impl Render for Breadcrumbs {
});
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
match active_item
.downcast::<Editor>()
.map(|editor| editor.downgrade())
@@ -102,11 +93,8 @@ impl Render for Breadcrumbs {
.on_click({
let editor = editor.clone();
move |_, cx| {
if let Some((editor, callback)) = editor
.upgrade()
.zip(zed_actions::outline::TOGGLE_OUTLINE.get())
{
callback(editor.to_any(), cx);
if let Some(editor) = editor.upgrade() {
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
}
}
})
@@ -114,15 +102,15 @@ impl Render for Breadcrumbs {
if let Some(editor) = editor.upgrade() {
let focus_handle = editor.read(cx).focus_handle(cx);
Tooltip::for_action_in(
"Show Symbol Outline",
&zed_actions::outline::ToggleOutline,
"Show symbol outline",
&editor::actions::ToggleOutline,
&focus_handle,
cx,
)
} else {
Tooltip::for_action(
"Show Symbol Outline",
&zed_actions::outline::ToggleOutline,
"Show symbol outline",
&editor::actions::ToggleOutline,
cx,
)
}

View File

@@ -40,7 +40,6 @@ schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
settings.workspace = true
telemetry.workspace = true
util.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]

View File

@@ -250,9 +250,7 @@ impl ActiveCall {
cx.spawn(move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| {
this.report_call_event("Participant Invited", cx)
})?;
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
@@ -320,7 +318,7 @@ impl ActiveCall {
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("Incoming Call Accepted", cx)
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
@@ -333,7 +331,7 @@ impl ActiveCall {
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
@@ -368,7 +366,7 @@ impl ActiveCall {
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("Channel Joined", cx)
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
@@ -376,7 +374,7 @@ impl ActiveCall {
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("Call Ended", cx);
self.report_call_event("hang up", cx);
Audio::end_call(cx);
@@ -395,7 +393,7 @@ impl ActiveCall {
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("Project Shared", cx);
self.report_call_event("share project", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
@@ -408,7 +406,7 @@ impl ActiveCall {
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("Project Unshared", cx);
self.report_call_event("unshare project", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
@@ -488,15 +486,35 @@ impl ActiveCall {
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
telemetry::event!(
operation,
room_id = room.id(),
channel_id = room.channel_id()
)
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
}
}
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: u64,
channel_id: Option<ChannelId>,
client: &Arc<Client>,
) {
let telemetry = client.telemetry();
telemetry.report_call_event(operation, Some(room_id), channel_id)
}
pub fn report_call_event_for_channel(
operation: &'static str,
channel_id: ChannelId,
client: &Arc<Client>,
cx: &AppContext,
) {
let room = ActiveCall::global(cx).read(cx).room();
let telemetry = client.telemetry();
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
}
#[cfg(test)]
mod test {
use gpui::TestAppContext;

View File

@@ -243,9 +243,7 @@ impl ActiveCall {
cx.spawn(move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| {
this.report_call_event("Participant Invited", cx)
})?;
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
@@ -313,7 +311,7 @@ impl ActiveCall {
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("Incoming Call Accepted", cx)
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
@@ -326,7 +324,7 @@ impl ActiveCall {
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
@@ -361,7 +359,7 @@ impl ActiveCall {
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("Channel Joined", cx)
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
@@ -369,7 +367,7 @@ impl ActiveCall {
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("Call Ended", cx);
self.report_call_event("hang up", cx);
Audio::end_call(cx);
@@ -388,7 +386,7 @@ impl ActiveCall {
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("Project Shared", cx);
self.report_call_event("share project", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
@@ -401,7 +399,7 @@ impl ActiveCall {
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("Project Unshared", cx);
self.report_call_event("unshare project", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
@@ -481,15 +479,35 @@ impl ActiveCall {
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
telemetry::event!(
operation,
room_id = room.id(),
channel_id = room.channel_id()
);
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
}
}
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: u64,
channel_id: Option<ChannelId>,
client: &Arc<Client>,
) {
let telemetry = client.telemetry();
telemetry.report_call_event(operation, Some(room_id), channel_id)
}
pub fn report_call_event_for_channel(
operation: &'static str,
channel_id: ChannelId,
client: &Arc<Client>,
cx: &AppContext,
) {
let room = ActiveCall::global(cx).read(cx).room();
let telemetry = client.telemetry();
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
}
#[cfg(test)]
mod test {
use gpui::TestAppContext;

View File

@@ -25,6 +25,7 @@ anyhow.workspace = true
clap.workspace = true
collections.workspace = true
ipc-channel = "0.19"
once_cell.workspace = true
parking_lot.workspace = true
paths.workspace = true
release_channel.workspace = true

View File

@@ -18,12 +18,6 @@ use std::{
use tempfile::NamedTempFile;
use util::paths::PathWithPosition;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use {
std::io::IsTerminal,
util::{load_login_shell_environment, load_shell_from_passwd, ResultExt},
};
struct Detect;
trait InstalledApp {
@@ -167,16 +161,7 @@ fn main() -> Result<()> {
None
};
// On Linux, desktop entry uses `cli` to spawn `zed`, so we need to load env vars from the shell
// since it doesn't inherit env vars from the terminal.
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if !std::io::stdout().is_terminal() {
load_shell_from_passwd().log_err();
load_login_shell_environment().log_err();
}
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
let exit_status = Arc::new(Mutex::new(None));
let mut paths = vec![];
let mut urls = vec![];
@@ -257,7 +242,6 @@ fn main() -> Result<()> {
if args.foreground {
app.run_foreground(url)?;
} else {
eprintln!("Logs are written to {:?}", paths::log_file());
app.launch(url)?;
sender.join().unwrap()?;
pipe_handle.join().unwrap()?;
@@ -278,7 +262,6 @@ mod linux {
os::unix::net::{SocketAddr, UnixDatagram},
path::{Path, PathBuf},
process::{self, ExitStatus},
sync::LazyLock,
thread,
time::Duration,
};
@@ -286,11 +269,12 @@ mod linux {
use anyhow::anyhow;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
use once_cell::sync::Lazy;
use crate::{Detect, InstalledApp};
static RELEASE_CHANNEL: LazyLock<String> =
LazyLock::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
static RELEASE_CHANNEL: Lazy<String> =
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
struct App(PathBuf);

View File

@@ -27,6 +27,7 @@ futures.workspace = true
gpui.workspace = true
http_client.workspace = true
log.workspace = true
once_cell.workspace = true
paths.workspace = true
parking_lot.workspace = true
postage.workspace = true

View File

@@ -8,6 +8,7 @@ use futures::channel::mpsc;
use futures::{Future, StreamExt};
use gpui::{AppContext, BackgroundExecutor, Task};
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use release_channel::ReleaseChannel;
use settings::{Settings, SettingsStore};
@@ -15,15 +16,10 @@ use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::Write;
use std::time::Instant;
use std::{
env, mem,
path::PathBuf,
sync::{Arc, LazyLock},
time::Duration,
};
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use telemetry_events::{
AppEvent, AssistantEvent, CallEvent, EditEvent, Event, EventRequestBody, EventWrapper,
InlineCompletionEvent,
InlineCompletionEvent, InlineCompletionRating, InlineCompletionRatingEvent, SettingEvent,
};
use util::{ResultExt, TryFutureExt};
use worktree::{UpdatedEntriesSet, WorktreeId};
@@ -88,7 +84,7 @@ 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: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
option_env!("ZED_CLIENT_CHECKSUM_SEED")
.map(|s| s.as_bytes().into())
.or_else(|| {
@@ -353,6 +349,24 @@ impl Telemetry {
self.report_event(event)
}
pub fn report_inline_completion_rating_event(
self: &Arc<Self>,
rating: InlineCompletionRating,
input_events: Arc<str>,
input_excerpt: Arc<str>,
output_excerpt: Arc<str>,
feedback: String,
) {
let event = Event::InlineCompletionRating(InlineCompletionRatingEvent {
rating,
input_events,
input_excerpt,
output_excerpt,
feedback,
});
self.report_event(event);
}
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
self.report_event(Event::Assistant(event));
}
@@ -380,6 +394,15 @@ impl Telemetry {
event
}
pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
let event = Event::Setting(SettingEvent {
setting: setting.to_string(),
value,
});
self.report_event(event)
}
pub fn log_edit_event(self: &Arc<Self>, environment: &'static str, is_via_ssh: bool) {
let mut state = self.state.lock();
let period_data = state.event_coalescer.log_event(environment);

View File

@@ -16,9 +16,7 @@ use util::TryFutureExt as _;
pub type UserId = u64;
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct ChannelId(pub u64);
impl std::fmt::Display for ChannelId {

View File

@@ -8,12 +8,7 @@ It contains our back-end logic for collaboration, to which we connect from the Z
## Database setup
Before you can run the collab server locally, you'll need to set up a zed Postgres database. Follow the steps sequentially:
1. Ensure you have postgres installed. If not, install with `brew install postgresql@15`.
2. Follow the steps on Brew's formula and verify your `$PATH` contains `/opt/homebrew/opt/postgresql@15/bin`.
3. If you hadn't done it before, create the `postgres` user with `createuser -s postgres`.
4. You are now ready to run the `bootstrap` script:
Before you can run the collab server locally, you'll need to set up a zed Postgres database.
```sh
script/bootstrap

View File

@@ -252,7 +252,7 @@ spec:
value: "${AUTO_JOIN_CHANNEL_ID}"
securityContext:
capabilities:
# TODO - Switch to the more restrictive `PERFMON` capability.
# FIXME - Switch to the more restrictive `PERFMON` capability.
# This capability isn't yet available in a stable version of Debian.
add: ["SYS_ADMIN"]
terminationGracePeriodSeconds: 10

View File

@@ -279,7 +279,6 @@ pub async fn post_panic(
let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
.map_err(|_| Error::http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
let incident_id = uuid::Uuid::new_v4().to_string();
let panic = report.panic;
if panic.os_name == "Linux" && panic.os_version == Some("1.0.0".to_string()) {
@@ -289,37 +288,11 @@ pub async fn post_panic(
))?;
}
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
let response = blob_store_client
.head_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".json")
.send()
.await;
if response.is_ok() {
log::info!("We've already uploaded this crash");
return Ok(());
}
blob_store_client
.put_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".json")
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
.body(ByteStream::from(body.to_vec()))
.send()
.await
.map_err(|e| log::error!("Failed to upload crash: {}", e))
.ok();
}
tracing::error!(
service = "client",
version = %panic.app_version,
os_name = %panic.os_name,
os_version = %panic.os_version.clone().unwrap_or_default(),
incident_id = %incident_id,
installation_id = %panic.installation_id.clone().unwrap_or_default(),
description = %panic.payload,
backtrace = %panic.backtrace.join("\n"),
@@ -358,19 +331,10 @@ pub async fn post_panic(
panic.app_version
)))
.add_field({
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
hostname.strip_prefix("http://").unwrap_or_default()
});
slack::Text::markdown(format!(
"*{} {}:*\n<https://{}.{}/{}.json|{}…>",
"*OS:*\n{} {}",
panic.os_name,
panic.os_version.unwrap_or_default(),
CRASH_REPORTS_BUCKET,
hostname,
incident_id,
incident_id.chars().take(8).collect::<String>(),
panic.os_version.unwrap_or_default()
))
})
})
@@ -397,12 +361,6 @@ pub async fn post_panic(
}
fn report_to_slack(panic: &Panic) -> bool {
// Panics on macOS should make their way to Slack as a crash report,
// so we don't need to send them a second time via this channel.
if panic.os_name == "macOS" {
return false;
}
if panic.payload.contains("ERROR_SURFACE_LOST_KHR") {
return false;
}

View File

@@ -9,6 +9,7 @@ use editor::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
ToggleCodeActions, Undo,
},
display_map::RowInfo,
test::editor_test_context::{AssertionContextManager, EditorTestContext},
Editor,
};
@@ -20,7 +21,6 @@ use language::{
language_settings::{AllLanguageSettings, InlayHintSettings},
FakeLspAdapter,
};
use multi_buffer::MultiBufferRow;
use project::{
project_settings::{InlineBlameSettings, ProjectSettings},
SERVER_PROGRESS_THROTTLE_TIMEOUT,
@@ -2075,7 +2075,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(0..4)
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
cx,
)
.collect::<Vec<_>>()
});
@@ -2114,7 +2122,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(0..4)
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
cx,
)
.collect::<Vec<_>>()
});
@@ -2141,7 +2157,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(0..4)
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
cx,
)
.collect::<Vec<_>>()
});

View File

@@ -56,7 +56,6 @@ serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
story = { workspace = true, optional = true }
telemetry.workspace = true
theme.workspace = true
time.workspace = true
time_format.workspace = true

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use call::ActiveCall;
use call::report_call_event_for_channel;
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelStore};
use client::{
proto::{self, PeerId},
@@ -66,13 +66,11 @@ impl ChannelView {
cx.spawn(|mut cx| async move {
let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
telemetry::event!(
"Channel Notes Opened",
report_call_event_for_channel(
"open channel notes",
channel_id,
room_id = ActiveCall::global(cx)
.read(cx)
.room()
.map(|r| r.read(cx).id())
&workspace.read(cx).app_state().client,
cx,
);
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
})?;

View File

@@ -1,4 +1,4 @@
use crate::{collab_panel, ChatPanelButton, ChatPanelSettings};
use crate::{collab_panel, ChatPanelSettings};
use anyhow::Result;
use call::{room, ActiveCall};
use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore};
@@ -1096,7 +1096,7 @@ impl FocusableView for ChatPanel {
}
impl Panel for ChatPanel {
fn position(&self, cx: &WindowContext) -> DockPosition {
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
ChatPanelSettings::get_global(cx).dock
}
@@ -1112,7 +1112,7 @@ impl Panel for ChatPanel {
);
}
fn size(&self, cx: &WindowContext) -> Pixels {
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
self.width
.unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
}
@@ -1135,15 +1135,7 @@ impl Panel for ChatPanel {
}
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
match ChatPanelSettings::get_global(cx).button {
ChatPanelButton::Never => None,
ChatPanelButton::Always => Some(ui::IconName::MessageBubbles),
ChatPanelButton::WhenInCall => ActiveCall::global(cx)
.read(cx)
.room()
.filter(|room| room.read(cx).contains_guests())
.map(|_| ui::IconName::MessageBubbles),
}
Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@@ -1160,10 +1152,6 @@ impl Panel for ChatPanel {
.room()
.is_some_and(|room| room.read(cx).contains_guests())
}
fn activation_priority(&self) -> u32 {
7
}
}
impl EventEmitter<PanelEvent> for ChatPanel {}

View File

@@ -79,6 +79,16 @@ impl CompletionProvider for MessageEditorCompletionProvider {
Task::ready(Ok(false))
}
fn apply_additional_edits_for_completion(
&self,
_buffer: Model<Buffer>,
_completion: Completion,
_push_to_history: bool,
_cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
_buffer: &Model<Buffer>,
@@ -309,7 +319,6 @@ impl MessageEditor {
server_id: LanguageServerId(0), // TODO: Make this optional or something?
lsp_completion: Default::default(), // TODO: Make this optional or something?
confirm: None,
resolved: true,
}
})
.collect()

View File

@@ -2719,7 +2719,7 @@ impl Render for CollabPanel {
impl EventEmitter<PanelEvent> for CollabPanel {}
impl Panel for CollabPanel {
fn position(&self, cx: &WindowContext) -> DockPosition {
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
CollaborationPanelSettings::get_global(cx).dock
}
@@ -2735,7 +2735,7 @@ impl Panel for CollabPanel {
);
}
fn size(&self, cx: &WindowContext) -> Pixels {
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
self.width
.unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
}
@@ -2746,7 +2746,7 @@ impl Panel for CollabPanel {
cx.notify();
}
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::IconName> {
CollaborationPanelSettings::get_global(cx)
.button
.then_some(ui::IconName::UserGroup)
@@ -2763,10 +2763,6 @@ impl Panel for CollabPanel {
fn persistent_name() -> &'static str {
"CollabPanel"
}
fn activation_priority(&self) -> u32 {
6
}
}
impl FocusableView for CollabPanel {

View File

@@ -14,7 +14,7 @@ use gpui::{
};
use panel_settings::MessageEditorSettings;
pub use panel_settings::{
ChatPanelButton, ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
};
use release_channel::ReleaseChannel;
use settings::Settings;

View File

@@ -662,7 +662,7 @@ impl Panel for NotificationPanel {
"NotificationPanel"
}
fn position(&self, cx: &WindowContext) -> DockPosition {
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
NotificationPanelSettings::get_global(cx).dock
}
@@ -678,7 +678,7 @@ impl Panel for NotificationPanel {
);
}
fn size(&self, cx: &WindowContext) -> Pixels {
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
self.width
.unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
}
@@ -702,7 +702,7 @@ impl Panel for NotificationPanel {
}
}
fn icon(&self, cx: &WindowContext) -> Option<IconName> {
fn icon(&self, cx: &gpui::WindowContext) -> Option<IconName> {
let show_button = NotificationPanelSettings::get_global(cx).button;
if !show_button {
return None;
@@ -731,10 +731,6 @@ impl Panel for NotificationPanel {
fn toggle_action(&self) -> Box<dyn gpui::Action> {
Box::new(ToggleFocus)
}
fn activation_priority(&self) -> u32 {
8
}
}
pub struct NotificationToast {

View File

@@ -1,6 +1,6 @@
use gpui::Pixels;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use workspace::dock::DockPosition;
@@ -11,82 +11,13 @@ pub struct CollaborationPanelSettings {
pub default_width: Pixels,
}
#[derive(Clone, Copy, Default, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ChatPanelButton {
Never,
Always,
#[default]
WhenInCall,
}
impl<'de> Deserialize<'de> for ChatPanelButton {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = ChatPanelButton;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
r#"a boolean or one of "never", "always", "when_in_call""#
)
}
fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match b {
false => Ok(ChatPanelButton::Never),
true => Ok(ChatPanelButton::Always),
}
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match s {
"never" => Ok(ChatPanelButton::Never),
"always" => Ok(ChatPanelButton::Always),
"when_in_call" => Ok(ChatPanelButton::WhenInCall),
_ => Err(E::unknown_variant(s, &["never", "always", "when_in_call"])),
}
}
}
deserializer.deserialize_any(Visitor)
}
}
#[derive(Deserialize, Debug)]
pub struct ChatPanelSettings {
pub button: ChatPanelButton,
pub button: bool,
pub dock: DockPosition,
pub default_width: Pixels,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct ChatPanelSettingsContent {
/// When to show the panel button in the status bar.
///
/// Default: only when in a call
pub button: Option<ChatPanelButton>,
/// Where to dock the panel.
///
/// Default: right
pub dock: Option<DockPosition>,
/// Default width of the panel in pixels.
///
/// Default: 240
pub default_width: Option<f32>,
}
#[derive(Deserialize, Debug)]
pub struct NotificationPanelSettings {
pub button: bool,
@@ -135,7 +66,7 @@ impl Settings for CollaborationPanelSettings {
impl Settings for ChatPanelSettings {
const KEY: Option<&'static str> = Some("chat_panel");
type FileContent = ChatPanelSettingsContent;
type FileContent = PanelSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,

View File

@@ -36,26 +36,21 @@ pub struct CommandPalette {
picker: View<Picker<CommandPaletteDelegate>>,
}
/// Removes subsequent whitespace characters and double colons from the query.
///
/// This improves the likelihood of a match by either humanized name or keymap-style name.
fn normalize_query(input: &str) -> String {
fn trim_consecutive_whitespaces(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut last_char = None;
let mut last_char_was_whitespace = false;
for char in input.trim().chars() {
match (last_char, char) {
(Some(':'), ':') => continue,
(Some(last_char), char) if last_char.is_whitespace() && char.is_whitespace() => {
continue
}
_ => {
last_char = Some(char);
if char.is_whitespace() {
if !last_char_was_whitespace {
result.push(char);
}
last_char_was_whitespace = true;
} else {
result.push(char);
last_char_was_whitespace = false;
}
result.push(char);
}
result
}
@@ -263,7 +258,7 @@ impl PickerDelegate for CommandPaletteDelegate {
let mut commands = self.all_commands.clone();
let hit_counts = cx.global::<HitCounts>().clone();
let executor = cx.background_executor().clone();
let query = normalize_query(query.as_str());
let query = trim_consecutive_whitespaces(query.as_str());
async move {
commands.sort_by_key(|action| {
(
@@ -468,25 +463,6 @@ mod tests {
);
}
#[test]
fn test_normalize_query() {
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
assert_eq!(
normalize_query("editor::GoToDefinition"),
"editor:GoToDefinition"
);
assert_eq!(
normalize_query("editor::::GoToDefinition"),
"editor:GoToDefinition"
);
assert_eq!(
normalize_query("editor: :GoToDefinition"),
"editor: :GoToDefinition"
);
}
#[gpui::test]
async fn test_command_palette(cx: &mut TestAppContext) {
let app_state = init_test(cx);
@@ -557,40 +533,6 @@ mod tests {
assert!(palette.delegate.matches.is_empty())
});
}
#[gpui::test]
async fn test_normalized_matches(cx: &mut TestAppContext) {
let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let editor = cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_text("abc", cx);
editor
});
workspace.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
editor.update(cx, |editor, cx| editor.focus(cx))
});
// Test normalize (trimming whitespace and double colons)
cx.simulate_keystrokes("cmd-shift-p");
let palette = workspace.update(cx, |workspace, cx| {
workspace
.active_modal::<CommandPalette>(cx)
.unwrap()
.read(cx)
.picker
.clone()
});
cx.simulate_input("Editor:: Backspace");
palette.update(cx, |palette, _| {
assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
});
}
#[gpui::test]
async fn test_go_to_line(cx: &mut TestAppContext) {

View File

@@ -28,6 +28,7 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
ui.workspace = true
url = { workspace = true, features = ["serde"] }
util.workspace = true
workspace.workspace = true

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use anyhow::{anyhow, bail};
use assistant_tool::Tool;
use gpui::{Model, Task, WindowContext};
use gpui::{Model, Task};
use crate::manager::ContextServerManager;
use crate::types;
@@ -52,7 +52,7 @@ impl Tool for ContextServerTool {
self: std::sync::Arc<Self>,
input: serde_json::Value,
_workspace: gpui::WeakView<workspace::Workspace>,
cx: &mut WindowContext,
cx: &mut ui::WindowContext,
) -> gpui::Task<gpui::Result<String>> {
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
cx.foreground_executor().spawn({

View File

@@ -34,9 +34,9 @@ pub enum Model {
Gpt4,
#[serde(alias = "gpt-3.5-turbo", rename = "gpt-3.5-turbo")]
Gpt3_5Turbo,
#[serde(alias = "o1-preview", rename = "o1")]
#[serde(alias = "o1-preview", rename = "o1-preview-2024-09-12")]
O1Preview,
#[serde(alias = "o1-mini", rename = "o1-mini")]
#[serde(alias = "o1-mini", rename = "o1-mini-2024-09-12")]
O1Mini,
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
Claude3_5Sonnet,

View File

@@ -59,10 +59,6 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
"Copilot"
}
fn show_completions_in_menu() -> bool {
false
}
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -330,15 +326,17 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
// We want to show both: the inline completion and the completion menu
assert!(editor.context_menu_visible());
assert!(!editor.context_menu_contains_inline_completion());
assert!(!editor.has_active_inline_completion());
assert!(editor.context_menu_contains_inline_completion());
assert!(editor.has_active_inline_completion());
// Since we have both, the copilot suggestion is not shown inline
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
// Confirming a non-copilot completion inserts it and hides the context menu, without showing
// the copilot suggestion afterwards.
editor.context_menu_next(&Default::default(), cx);
editor
.confirm_completion(&Default::default(), cx)
.unwrap()
@@ -386,12 +384,23 @@ mod tests {
// Ensure existing inline completion is interpolated when inserting again.
cx.simulate_keystroke("c");
drop(handle_completion_request(
&mut cx,
indoc! {"
one.c|<>
two
three
"},
vec!["completion_a", "completion_b"],
));
executor.run_until_parked();
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(!editor.context_menu_contains_inline_completion());
// Since we have an LSP completion too, the inline completion is
// shown in the menu now
assert!(editor.context_menu_visible());
assert!(editor.context_menu_contains_inline_completion());
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
});
@@ -407,9 +416,16 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
assert!(editor.context_menu_contains_inline_completion());
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
// Canceling should first hide the menu and make Copilot suggestion visible.
editor.cancel(&Default::default(), cx);
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
assert!(!editor.context_menu_contains_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
@@ -912,8 +928,8 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.context_menu_visible());
assert!(!editor.context_menu_contains_inline_completion());
assert!(!editor.has_active_inline_completion(),);
assert!(editor.context_menu_contains_inline_completion());
assert!(editor.has_active_inline_completion(),);
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
});
}

View File

@@ -18,7 +18,6 @@ collections.workspace = true
ctor.workspace = true
editor.workspace = true
env_logger.workspace = true
feature_flags.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true

View File

@@ -14,7 +14,6 @@ use editor::{
scroll::Autoscroll,
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
use feature_flags::FeatureFlagAppExt;
use gpui::{
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement,
@@ -22,8 +21,7 @@ use gpui::{
WeakView, WindowContext,
};
use language::{
Bias, Buffer, BufferRow, BufferSnapshot, Diagnostic, DiagnosticEntry, DiagnosticSeverity,
Point, Selection, SelectionGoal, ToTreeSitterPoint,
Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
};
use lsp::LanguageServerId;
use project::{DiagnosticSummary, Project, ProjectPath};
@@ -31,10 +29,9 @@ use project_diagnostics_settings::ProjectDiagnosticsSettings;
use settings::Settings;
use std::{
any::{Any, TypeId},
cmp,
cmp::Ordering,
mem,
ops::{Range, RangeInclusive},
ops::Range,
sync::Arc,
time::Duration,
};
@@ -44,7 +41,6 @@ use ui::{h_flex, prelude::*, Icon, IconName, Label};
use util::ResultExt;
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
searchable::SearchableItemHandle,
ItemNavHistory, ToolbarItemLocation, Workspace,
};
@@ -166,7 +162,7 @@ impl ProjectDiagnosticsEditor {
let excerpts = cx.new_model(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
let editor = cx.new_view(|cx| {
let mut editor =
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), true, cx);
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx);
editor.set_vertical_scroll_margin(5, cx);
editor
});
@@ -425,28 +421,31 @@ impl ProjectDiagnosticsEditor {
blocks: Default::default(),
block_count: 0,
};
let mut pending_range: Option<(Range<Point>, Range<Point>, usize)> = None;
let mut pending_range: Option<(Range<Point>, usize)> = None;
let mut is_first_excerpt_for_group = true;
for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
let expanded_range = resolved_entry.as_ref().map(|entry| {
context_range_for_entry(entry, self.context, &snapshot, cx)
});
if let Some((range, context_range, start_ix)) = &mut pending_range {
if let Some(expanded_range) = expanded_range.clone() {
// If the entries are overlapping or next to each-other, merge them into one excerpt.
if context_range.end.row + 1 >= expanded_range.start.row {
context_range.end = context_range.end.max(expanded_range.end);
if let Some((range, start_ix)) = &mut pending_range {
if let Some(entry) = resolved_entry.as_ref() {
if entry.range.start.row <= range.end.row + 1 + self.context * 2 {
range.end = range.end.max(entry.range.end);
continue;
}
}
let excerpt_start =
Point::new(range.start.row.saturating_sub(self.context), 0);
let excerpt_end = snapshot.clip_point(
Point::new(range.end.row + self.context, u32::MAX),
Bias::Left,
);
let excerpt_id = excerpts
.insert_excerpts_after(
prev_excerpt_id,
buffer.clone(),
[ExcerptRange {
context: context_range.clone(),
context: excerpt_start..excerpt_end,
primary: Some(range.clone()),
}],
cx,
@@ -503,9 +502,8 @@ impl ProjectDiagnosticsEditor {
pending_range.take();
}
if let Some(entry) = resolved_entry.as_ref() {
let range = entry.range.clone();
pending_range = Some((range, expanded_range.unwrap(), ix));
if let Some(entry) = resolved_entry {
pending_range = Some((entry.range.clone(), ix));
}
}
@@ -812,10 +810,6 @@ impl Item for ProjectDiagnosticsEditor {
}
}
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
}
@@ -836,76 +830,65 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None);
let message: SharedString = message;
Arc::new(move |cx| {
let color = cx.theme().colors();
let highlight_style: HighlightStyle = color.text_accent.into();
let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
h_flex()
.id(DIAGNOSTIC_HEADER)
.block_mouse_down()
.h(2. * cx.line_height())
.pl_10()
.pr_5()
.w_full()
.relative()
.justify_between()
.gap_2()
.child(
div()
.top(px(0.))
.absolute()
.w_full()
.h_px()
.bg(color.border_variant),
h_flex()
.gap_3()
.map(|stack| {
stack.child(
svg()
.size(cx.text_style().font_size)
.flex_none()
.map(|icon| {
if diagnostic.severity == DiagnosticSeverity::ERROR {
icon.path(IconName::XCircle.path())
.text_color(Color::Error.color(cx))
} else {
icon.path(IconName::Warning.path())
.text_color(Color::Warning.color(cx))
}
}),
)
})
.child(
h_flex()
.gap_1()
.child(
StyledText::new(message.clone()).with_highlights(
&cx.text_style(),
code_ranges
.iter()
.map(|range| (range.clone(), highlight_style)),
),
)
.when_some(diagnostic.code.as_ref(), |stack, code| {
stack.child(
div()
.child(SharedString::from(format!("({code})")))
.text_color(cx.theme().colors().text_muted),
)
}),
),
)
.child(
h_flex()
.block_mouse_down()
.h(2. * cx.line_height())
.pl_10()
.pr_5()
.w_full()
.justify_between()
.gap_2()
.child(
h_flex()
.gap_3()
.map(|stack| {
stack.child(svg().size(cx.text_style().font_size).flex_none().map(
|icon| {
if diagnostic.severity == DiagnosticSeverity::ERROR {
icon.path(IconName::XCircle.path())
.text_color(Color::Error.color(cx))
} else {
icon.path(IconName::Warning.path())
.text_color(Color::Warning.color(cx))
}
},
))
})
.child(
h_flex()
.gap_1()
.child(
StyledText::new(message.clone()).with_highlights(
&cx.text_style(),
code_ranges
.iter()
.map(|range| (range.clone(), highlight_style)),
),
)
.when_some(diagnostic.code.as_ref(), |stack, code| {
stack.child(
div()
.child(SharedString::from(format!("({code})")))
.text_color(cx.theme().colors().text_muted),
)
}),
),
)
.child(h_flex().gap_1().when_some(
diagnostic.source.as_ref(),
|stack, source| {
stack.child(
div()
.child(SharedString::from(source.clone()))
.text_color(cx.theme().colors().text_muted),
)
},
)),
.gap_1()
.when_some(diagnostic.source.as_ref(), |stack, source| {
stack.child(
div()
.child(SharedString::from(source.clone()))
.text_color(cx.theme().colors().text_muted),
)
}),
)
.into_any_element()
})
@@ -935,169 +918,3 @@ fn compare_diagnostics(
})
.then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
}
const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
fn context_range_for_entry(
entry: &DiagnosticEntry<Point>,
context: u32,
snapshot: &BufferSnapshot,
cx: &AppContext,
) -> Range<Point> {
if cx.is_staff() {
if let Some(rows) = heuristic_syntactic_expand(
entry.range.clone(),
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
snapshot,
cx,
) {
return Range {
start: Point::new(*rows.start(), 0),
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
};
}
}
Range {
start: Point::new(entry.range.start.row.saturating_sub(context), 0),
end: snapshot.clip_point(
Point::new(entry.range.end.row + context, u32::MAX),
Bias::Left,
),
}
}
/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
/// to the specified `max_row_count`.
///
/// If there is a containing outline item that is less than `max_row_count`, it will be returned.
/// Otherwise fairly arbitrary heuristics are applied to attempt to return a logical block of code.
fn heuristic_syntactic_expand<'a>(
input_range: Range<Point>,
max_row_count: u32,
snapshot: &'a BufferSnapshot,
cx: &'a AppContext,
) -> Option<RangeInclusive<BufferRow>> {
let input_row_count = input_range.end.row - input_range.start.row;
if input_row_count > max_row_count {
return None;
}
// If the outline node contains the diagnostic and is small enough, just use that.
let outline_range = snapshot.outline_range_containing(input_range.clone());
if let Some(outline_range) = outline_range.clone() {
// Remove blank lines from start and end
if let Some(start_row) = (outline_range.start.row..outline_range.end.row)
.find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
{
if let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1)
.rev()
.find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
{
let row_count = end_row.saturating_sub(start_row);
if row_count <= max_row_count {
return Some(RangeInclusive::new(
outline_range.start.row,
outline_range.end.row,
));
}
}
}
}
let mut node = snapshot.syntax_ancestor(input_range.clone())?;
loop {
let node_start = Point::from_ts_point(node.start_position());
let node_end = Point::from_ts_point(node.end_position());
let node_range = node_start..node_end;
let row_count = node_end.row - node_start.row + 1;
// Stop if we've exceeded the row count or reached an outline node. Then, find the interval
// of node children which contains the query range. For example, this allows just returning
// the header of a declaration rather than the entire declaration.
if row_count > max_row_count || outline_range == Some(node_range.clone()) {
let mut cursor = node.walk();
let mut included_child_start = None;
let mut included_child_end = None;
let mut previous_end = node_start;
if cursor.goto_first_child() {
loop {
let child_node = cursor.node();
let child_range = previous_end..Point::from_ts_point(child_node.end_position());
if included_child_start.is_none() && child_range.contains(&input_range.start) {
included_child_start = Some(child_range.start);
}
if child_range.contains(&input_range.end) {
included_child_end = Some(child_range.end);
}
previous_end = child_range.end;
if !cursor.goto_next_sibling() {
break;
}
}
}
let end = included_child_end.unwrap_or(node_range.end);
if let Some(start) = included_child_start {
let row_count = end.row - start.row;
if row_count < max_row_count {
return Some(RangeInclusive::new(start.row, end.row));
}
}
log::info!(
"Expanding to ancestor started on {} node exceeding row limit of {max_row_count}.",
node.grammar_name()
);
return None;
}
let node_name = node.grammar_name();
let node_row_range = RangeInclusive::new(node_range.start.row, node_range.end.row);
if node_name.ends_with("block") {
return Some(node_row_range);
} else if node_name.ends_with("statement") || node_name.ends_with("declaration") {
// Expand to the nearest dedent or blank line for statements and declarations.
let tab_size = snapshot.settings_at(node_range.start, cx).tab_size.get();
let indent_level = snapshot
.line_indent_for_row(node_range.start.row)
.len(tab_size);
let rows_remaining = max_row_count.saturating_sub(row_count);
let Some(start_row) = (node_range.start.row.saturating_sub(rows_remaining)
..node_range.start.row)
.rev()
.find(|row| is_line_blank_or_indented_less(indent_level, *row, tab_size, snapshot))
else {
return Some(node_row_range);
};
let rows_remaining = max_row_count.saturating_sub(node_range.end.row - start_row);
let Some(end_row) = (node_range.end.row + 1
..cmp::min(
node_range.end.row + rows_remaining + 1,
snapshot.row_count(),
))
.find(|row| is_line_blank_or_indented_less(indent_level, *row, tab_size, snapshot))
else {
return Some(node_row_range);
};
return Some(RangeInclusive::new(start_row, end_row));
}
// TODO: doing this instead of walking a cursor as that doesn't work - why?
let Some(parent) = node.parent() else {
log::info!(
"Expanding to ancestor reached the top node, so using default context line count.",
);
return None;
};
node = parent;
}
}
fn is_line_blank_or_indented_less(
indent_level: u32,
row: u32,
tab_size: u32,
snapshot: &BufferSnapshot,
) -> bool {
let line_indent = snapshot.line_indent_for_row(row);
line_indent.is_line_blank() || line_indent.len(tab_size) < indent_level
}

View File

@@ -167,10 +167,10 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(16), EXCERPT_HEADER.into()),
(DisplayRow(18), DIAGNOSTIC_HEADER.into()),
(DisplayRow(27), EXCERPT_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(15), EXCERPT_HEADER.into()),
(DisplayRow(16), DIAGNOSTIC_HEADER.into()),
(DisplayRow(25), EXCERPT_HEADER.into()),
]
);
assert_eq!(
@@ -184,7 +184,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
" let x = vec![];\n",
" let y = vec![];\n",
"\n", // supporting diagnostic
@@ -196,7 +195,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" c(y);\n",
"\n", // supporting diagnostic
" d(x);\n",
"\n", // expand
"\n", // context ellipsis
// diagnostic group 2
"\n", // primary message
@@ -208,13 +206,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" a(x);\n",
"\n", // supporting diagnostic
" b(y);\n",
"\n", // expand
"\n", // context ellipsis
" c(y);\n",
" d(x);\n",
"\n", // supporting diagnostic
"}",
"\n", // expand
"}"
)
);
@@ -222,7 +218,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor.update(cx, |editor, cx| {
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(13), 6)..DisplayPoint::new(DisplayRow(13), 6)]
[DisplayPoint::new(DisplayRow(12), 6)..DisplayPoint::new(DisplayRow(12), 6)]
);
});
@@ -257,12 +253,12 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), FILE_HEADER.into()),
(DisplayRow(12), DIAGNOSTIC_HEADER.into()),
(DisplayRow(25), EXCERPT_HEADER.into()),
(DisplayRow(27), DIAGNOSTIC_HEADER.into()),
(DisplayRow(36), EXCERPT_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), FILE_HEADER.into()),
(DisplayRow(9), DIAGNOSTIC_HEADER.into()),
(DisplayRow(22), EXCERPT_HEADER.into()),
(DisplayRow(23), DIAGNOSTIC_HEADER.into()),
(DisplayRow(32), EXCERPT_HEADER.into()),
]
);
@@ -277,7 +273,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"const a: i32 = 'a';\n",
"\n", // supporting diagnostic
"const b: i32 = c;\n",
@@ -289,8 +284,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"\n", // expand
" let x = vec![];\n",
" let y = vec![];\n",
"\n", // supporting diagnostic
@@ -306,7 +299,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 2
"\n", // primary message
"\n", // filename
"\n", // expand
"fn main() {\n",
" let x = vec![];\n",
"\n", // supporting diagnostic
@@ -314,13 +306,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" a(x);\n",
"\n", // supporting diagnostic
" b(y);\n",
"\n", // expand
"\n", // context ellipsis
" c(y);\n",
" d(x);\n",
"\n", // supporting diagnostic
"}",
"\n", // expand
"}"
)
);
@@ -328,7 +318,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor.update(cx, |editor, cx| {
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(22), 6)..DisplayPoint::new(DisplayRow(22), 6)]
[DisplayPoint::new(DisplayRow(19), 6)..DisplayPoint::new(DisplayRow(19), 6)]
);
});
@@ -376,14 +366,14 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), EXCERPT_HEADER.into()),
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
(DisplayRow(15), FILE_HEADER.into()),
(DisplayRow(19), DIAGNOSTIC_HEADER.into()),
(DisplayRow(32), EXCERPT_HEADER.into()),
(DisplayRow(34), DIAGNOSTIC_HEADER.into()),
(DisplayRow(43), EXCERPT_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
(DisplayRow(13), FILE_HEADER.into()),
(DisplayRow(15), DIAGNOSTIC_HEADER.into()),
(DisplayRow(28), EXCERPT_HEADER.into()),
(DisplayRow(29), DIAGNOSTIC_HEADER.into()),
(DisplayRow(38), EXCERPT_HEADER.into()),
]
);
@@ -398,7 +388,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"const a: i32 = 'a';\n",
"\n", // supporting diagnostic
"const b: i32 = c;\n",
@@ -406,7 +395,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 2
"\n", // primary message
"\n", // padding
"\n", // expand
"const a: i32 = 'a';\n",
"const b: i32 = c;\n",
"\n", // supporting diagnostic
@@ -418,8 +406,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"\n", // expand
" let x = vec![];\n",
" let y = vec![];\n",
"\n", // supporting diagnostic
@@ -435,7 +421,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 2
"\n", // primary message
"\n", // filename
"\n", // expand
"fn main() {\n",
" let x = vec![];\n",
"\n", // supporting diagnostic
@@ -443,13 +428,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" a(x);\n",
"\n", // supporting diagnostic
" b(y);\n",
"\n", // expand
"\n", // context ellipsis
" c(y);\n",
" d(x);\n",
"\n", // supporting diagnostic
"}",
"\n", // expand
"}"
)
);
}
@@ -530,7 +513,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -541,9 +524,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"a();\n", //
"b();", "\n", // expand
"b();",
)
);
@@ -579,9 +561,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(9), DIAGNOSTIC_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(6), EXCERPT_HEADER.into()),
(DisplayRow(7), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -592,10 +574,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"a();\n", // location
"b();\n", //
"\n", // expand
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
@@ -603,7 +583,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
"a();\n", // context
"b();\n", //
"c();", // context
"\n", // expand
)
);
@@ -650,9 +629,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), EXCERPT_HEADER.into()),
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -663,11 +642,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"a();\n", // location
"b();\n", //
"c();\n", // context
"\n", // expand
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
@@ -675,7 +652,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
"b();\n", // context
"c();\n", //
"d();", // context
"\n", // expand
)
);
@@ -711,9 +687,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), EXCERPT_HEADER.into()),
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -724,11 +700,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"b();\n", // location
"c();\n", //
"d();\n", // context
"\n", // expand
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
@@ -736,7 +710,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
"c();\n", // context
"d();\n", //
"e();", // context
"\n", // expand
)
);
}

View File

@@ -388,4 +388,6 @@ gpui::actions!(
]
);
action_as!(outline, ToggleOutline as Toggle);
action_as!(go_to_line, ToggleGoToLine as Toggle);

View File

@@ -16,7 +16,7 @@ fn is_c_language(language: &Language) -> bool {
pub fn switch_source_header(
editor: &mut Editor,
_: &SwitchSourceHeader,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) {
let Some(project) = &editor.project else {
return;

View File

@@ -1,8 +1,11 @@
use std::cell::RefCell;
use std::{cell::Cell, cmp::Reverse, ops::Range, rc::Rc};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
Model, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
UniformListScrollHandle, ViewContext, WeakView,
Model, ScrollStrategy, SharedString, StrikethroughStyle, StyledText, UniformListScrollHandle,
ViewContext, WeakView,
};
use language::Buffer;
use language::{CodeLabel, Documentation};
@@ -10,16 +13,9 @@ use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use project::{CodeAction, Completion, TaskSourceKind};
use std::{
cell::RefCell,
cmp::{min, Reverse},
iter,
ops::Range,
rc::Rc,
};
use task::ResolvedTask;
use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, Styled};
use util::ResultExt;
use ui::{prelude::*, Color, IntoElement, ListItem, Popover, Styled};
use util::ResultExt as _;
use workspace::Workspace;
use crate::{
@@ -30,11 +26,6 @@ use crate::{
};
use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
pub const MENU_GAP: Pixels = px(4.);
pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
pub enum CodeContextMenu {
Completions(CompletionsMenu),
CodeActions(CodeActionsMenu),
@@ -118,31 +109,20 @@ impl CodeContextMenu {
CodeContextMenu::CodeActions(menu) => menu.origin(cursor_position),
}
}
pub fn render(
&self,
style: &EditorStyle,
max_height_in_lines: u32,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement {
match self {
CodeContextMenu::Completions(menu) => menu.render(style, max_height_in_lines, cx),
CodeContextMenu::Completions(menu) => {
menu.render(style, max_height_in_lines, workspace, cx)
}
CodeContextMenu::CodeActions(menu) => menu.render(style, max_height_in_lines, cx),
}
}
pub fn render_aside(
&self,
style: &EditorStyle,
max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<AnyElement> {
match self {
CodeContextMenu::Completions(menu) => menu.render_aside(style, max_size, workspace, cx),
CodeContextMenu::CodeActions(_) => None,
}
}
}
pub enum ContextMenuOrigin {
@@ -162,8 +142,8 @@ pub struct CompletionsMenu {
pub selected_item: usize,
scroll_handle: UniformListScrollHandle,
resolve_completions: bool,
pub aside_was_displayed: Cell<bool>,
show_completion_documentation: bool,
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
}
#[derive(Clone, Debug)]
@@ -180,6 +160,7 @@ impl CompletionsMenu {
initial_position: Anchor,
buffer: Model<Buffer>,
completions: Box<[Completion]>,
aside_was_displayed: bool,
) -> Self {
let match_candidates = completions
.iter()
@@ -199,7 +180,7 @@ impl CompletionsMenu {
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
last_rendered_range: RefCell::new(None).into(),
aside_was_displayed: Cell::new(aside_was_displayed),
}
}
@@ -224,7 +205,6 @@ impl CompletionsMenu {
documentation: None,
lsp_completion: Default::default(),
confirm: None,
resolved: true,
})
.collect();
@@ -256,8 +236,8 @@ impl CompletionsMenu {
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: false,
aside_was_displayed: Cell::new(false),
show_completion_documentation: false,
last_rendered_range: RefCell::new(None).into(),
}
}
@@ -266,7 +246,11 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(0, provider, cx);
self.selected_item = 0;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
}
fn select_prev(
@@ -274,7 +258,15 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(self.prev_match_index(), provider, cx);
if self.selected_item > 0 {
self.selected_item -= 1;
} else {
self.selected_item = self.entries.len() - 1;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
}
fn select_next(
@@ -282,7 +274,15 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(self.next_match_index(), provider, cx);
if self.selected_item + 1 < self.entries.len() {
self.selected_item += 1;
} else {
self.selected_item = 0;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
}
fn select_last(
@@ -290,38 +290,11 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(self.entries.len() - 1, provider, cx);
}
fn update_selection_index(
&mut self,
match_index: usize,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item != match_index {
self.selected_item = match_index;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_visible_completions(provider, cx);
cx.notify();
}
}
fn prev_match_index(&self) -> usize {
if self.selected_item > 0 {
self.selected_item - 1
} else {
self.entries.len() - 1
}
}
fn next_match_index(&self) -> usize {
if self.selected_item + 1 < self.entries.len() {
self.selected_item + 1
} else {
0
}
self.selected_item = self.entries.len() - 1;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
}
pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
@@ -341,12 +314,10 @@ impl CompletionsMenu {
}
}
.into();
if self.selected_item != 0 && self.selected_item + 1 < self.entries.len() {
self.selected_item += 1;
}
self.selected_item = 0;
}
pub fn resolve_visible_completions(
pub fn resolve_selected_completion(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
@@ -358,76 +329,24 @@ impl CompletionsMenu {
return;
};
// Attempt to resolve completions for every item that will be displayed. This matters
// because single line documentation may be displayed inline with the completion.
//
// When navigating to the very beginning or end of completions, `last_rendered_range` may
// have no overlap with the completions that will be displayed, so instead use a range based
// on the last rendered count.
const APPROXIMATE_VISIBLE_COUNT: usize = 12;
let last_rendered_range = self.last_rendered_range.borrow().clone();
let visible_count = last_rendered_range
.clone()
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
let entry_range = if self.selected_item == 0 {
0..min(visible_count, self.entries.len())
} else if self.selected_item == self.entries.len() - 1 {
self.entries.len().saturating_sub(visible_count)..self.entries.len()
} else {
last_rendered_range.map_or(0..0, |range| {
min(range.start, self.entries.len())..min(range.end, self.entries.len())
})
};
match &self.entries[self.selected_item] {
CompletionEntry::Match(entry) => {
let completion_index = entry.candidate_id;
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
vec![completion_index],
self.completions.clone(),
cx,
);
// Expand the range to resolve more completions than are predicted to be visible, to reduce
// jank on navigation.
const EXTRA_TO_RESOLVE: usize = 4;
let entry_indices = util::iterate_expanded_and_wrapped_usize_range(
entry_range.clone(),
EXTRA_TO_RESOLVE,
EXTRA_TO_RESOLVE,
self.entries.len(),
);
// Avoid work by sometimes filtering out completions that already have documentation.
// This filtering doesn't happen if the completions are currently being updated.
let completions = self.completions.borrow();
let candidate_ids = entry_indices
.flat_map(|i| Self::entry_candidate_id(&self.entries[i]))
.filter(|i| completions[*i].documentation.is_none());
// Current selection is always resolved even if it already has documentation, to handle
// out-of-spec language servers that return more results later.
let candidate_ids = match Self::entry_candidate_id(&self.entries[self.selected_item]) {
None => candidate_ids.collect::<Vec<usize>>(),
Some(selected_candidate_id) => iter::once(selected_candidate_id)
.chain(candidate_ids.filter(|id| *id != selected_candidate_id))
.collect::<Vec<usize>>(),
};
if candidate_ids.is_empty() {
return;
}
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
candidate_ids,
self.completions.clone(),
cx,
);
cx.spawn(move |editor, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
editor.update(&mut cx, |_, cx| cx.notify()).ok();
cx.spawn(move |editor, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
editor.update(&mut cx, |_, cx| cx.notify()).ok();
}
})
.detach();
}
})
.detach();
}
fn entry_candidate_id(entry: &CompletionEntry) -> Option<usize> {
match entry {
CompletionEntry::Match(entry) => Some(entry.candidate_id),
CompletionEntry::InlineCompletionHint { .. } => None,
CompletionEntry::InlineCompletionHint { .. } => {}
}
}
@@ -443,8 +362,11 @@ impl CompletionsMenu {
&self,
style: &EditorStyle,
max_height_in_lines: u32,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement {
let max_height = max_height_in_lines as f32 * cx.line_height();
let completions = self.completions.borrow_mut();
let show_completion_documentation = self.show_completion_documentation;
let widest_completion_ix = self
@@ -471,19 +393,76 @@ impl CompletionsMenu {
}) => provider_name.len(),
})
.map(|(ix, _)| ix);
drop(completions);
let selected_item = self.selected_item;
let style = style.clone();
let multiline_docs = match &self.entries[selected_item] {
CompletionEntry::Match(mat) if show_completion_documentation => {
match &completions[mat.candidate_id].documentation {
Some(Documentation::MultiLinePlainText(text)) => {
Some(div().child(SharedString::from(text.clone())))
}
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
Some(div().child(render_parsed_markdown(
"completions_markdown",
parsed,
&style,
workspace,
cx,
)))
}
Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
Some(div().child("No documentation"))
}
_ => None,
}
}
CompletionEntry::InlineCompletionHint(hint) => Some(match &hint.text {
InlineCompletionText::Edit { text, highlights } => div()
.my_1()
.rounded(px(6.))
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
gpui::StyledText::new(text.clone())
.with_highlights(&style.text, highlights.clone()),
),
InlineCompletionText::Move(text) => div().child(text.clone()),
}),
_ => None,
};
let aside_contents = if let Some(multiline_docs) = multiline_docs {
Some(multiline_docs)
} else if show_completion_documentation && self.aside_was_displayed.get() {
Some(div().child("Fetching documentation..."))
} else {
None
};
self.aside_was_displayed.set(aside_contents.is_some());
let aside_contents = aside_contents.map(|div| {
div.id("multiline_docs")
.max_h(max_height)
.flex_1()
.px_1p5()
.py_1()
.max_w(px(640.))
.w(px(450.))
.overflow_y_scroll()
.occlude()
});
drop(completions);
let completions = self.completions.clone();
let matches = self.entries.clone();
let last_rendered_range = self.last_rendered_range.clone();
let style = style.clone();
let list = uniform_list(
cx.view().clone(),
"completions",
matches.len(),
move |_editor, range, cx| {
last_rendered_range.borrow_mut().replace(range.clone());
let start_ix = range.start;
let completions_guard = completions.borrow_mut();
@@ -609,70 +588,12 @@ impl CompletionsMenu {
.with_width_from_item(widest_completion_ix)
.with_sizing_behavior(ListSizingBehavior::Infer);
Popover::new().child(list).into_any_element()
}
fn render_aside(
&self,
style: &EditorStyle,
max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<AnyElement> {
if !self.show_completion_documentation {
return None;
}
let multiline_docs = match &self.entries[self.selected_item] {
CompletionEntry::Match(mat) => {
match self.completions.borrow_mut()[mat.candidate_id]
.documentation
.as_ref()?
{
Documentation::MultiLinePlainText(text) => {
div().child(SharedString::from(text.clone()))
}
Documentation::MultiLineMarkdown(parsed) if !parsed.text.is_empty() => div()
.child(render_parsed_markdown(
"completions_markdown",
parsed,
&style,
workspace,
cx,
)),
Documentation::MultiLineMarkdown(_) => return None,
Documentation::SingleLine(_) => return None,
Documentation::Undocumented => return None,
}
}
CompletionEntry::InlineCompletionHint(hint) => match &hint.text {
InlineCompletionText::Edit { text, highlights } => div()
.mx_1()
.rounded(px(6.))
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
gpui::StyledText::new(text.clone())
.with_highlights(&style.text, highlights.clone()),
),
InlineCompletionText::Move(text) => div().child(text.clone()),
},
};
Some(
Popover::new()
.child(
multiline_docs
.id("multiline_docs")
.px(MENU_ASIDE_X_PADDING / 2.)
.max_w(max_size.width)
.max_h(max_size.height)
.overflow_y_scroll()
.occlude(),
)
.into_any_element(),
)
Popover::new()
.child(list)
.when_some(aside_contents, |popover, aside_contents| {
popover.aside(aside_contents)
})
.into_any_element()
}
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {

View File

@@ -20,6 +20,7 @@
mod block_map;
mod crease_map;
mod custom_highlights;
mod diff_map;
mod fold_map;
mod inlay_map;
pub(crate) mod invisibles;
@@ -30,17 +31,19 @@ use crate::{
hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt,
};
pub use block_map::{
Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap,
BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
StickyHeaderExcerpt,
Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement,
BlockPoint, BlockProperties, BlockRows, BlockStyle, CustomBlockId, RenderBlock,
};
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
use diff_map::{DiffMap, DiffMapSnapshot, DiffOffset, DiffPoint};
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
use fold_map::{FoldMap, FoldSnapshot};
use git::diff::DiffHunkStatus;
use gpui::{
AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
AnyElement, AppContext, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels,
UnderlineStyle,
};
pub(crate) use inlay_map::Inlay;
use inlay_map::{InlayMap, InlaySnapshot};
@@ -52,9 +55,10 @@ use language::{
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
ToOffset, ToPoint,
Anchor, AnchorRangeExt, MultiBuffer, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
};
use project::buffer_store::BufferChangeSet;
use serde::Deserialize;
use std::{
any::TypeId,
@@ -67,7 +71,7 @@ use std::{
};
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
use text::{BufferId, LineIndent};
use ui::{px, SharedString, WindowContext};
use unicode_segmentation::UnicodeSegmentation;
use wrap_map::{WrapMap, WrapSnapshot};
@@ -97,6 +101,8 @@ pub struct DisplayMap {
buffer_subscription: BufferSubscription,
/// Decides where the [`Inlay`]s should be displayed.
inlay_map: InlayMap,
/// Decides where diff hunks should be.
diff_map: Model<DiffMap>,
/// Decides where the fold indicators should be and tracks parts of a source file that are currently folded.
fold_map: FoldMap,
/// Keeps track of hard tabs in a buffer.
@@ -135,7 +141,8 @@ impl DisplayMap {
let tab_size = Self::tab_size(&buffer, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let crease_map = CreaseMap::new(&buffer_snapshot);
let (inlay_map, snapshot) = InlayMap::new(buffer_snapshot);
let (diff_map, snapshot) = DiffMap::new(buffer.clone(), cx);
let (inlay_map, snapshot) = InlayMap::new(snapshot);
let (fold_map, snapshot) = FoldMap::new(snapshot);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
@@ -154,6 +161,7 @@ impl DisplayMap {
buffer_subscription,
fold_map,
inlay_map,
diff_map,
tab_map,
wrap_map,
block_map,
@@ -169,7 +177,10 @@ impl DisplayMap {
pub fn snapshot(&mut self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (diff_snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), edits, cx)
});
let (inlay_snapshot, edits) = self.inlay_map.sync(diff_snapshot, edits);
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
@@ -218,7 +229,10 @@ impl DisplayMap {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot.clone(), edits);
let (snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), edits, cx)
});
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
@@ -291,6 +305,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -320,6 +337,9 @@ impl DisplayMap {
.collect::<Vec<_>>();
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -342,6 +362,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -356,6 +379,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -388,6 +414,71 @@ impl DisplayMap {
self.crease_map.remove(crease_ids, &snapshot)
}
pub fn add_change_set(
&mut self,
change_set: Model<BufferChangeSet>,
cx: &mut ModelContext<Self>,
) {
self.diff_map.update(cx, |diff_map, cx| {
diff_map.add_change_set(change_set, cx);
});
}
pub fn has_multiple_hunks(&self, cx: &AppContext) -> bool {
self.diff_map.read(cx).has_multiple_hunks()
}
pub fn has_expanded_diff_hunks_in_ranges(
&mut self,
ranges: &[Range<multi_buffer::Anchor>],
cx: &mut ModelContext<Self>,
) -> bool {
self.diff_map
.read(cx)
.has_expanded_diff_hunks_in_ranges(ranges)
}
pub fn set_all_hunks_expanded(&mut self, cx: &mut ModelContext<Self>) {
self.update_diff_map(cx, |diff_map, cx| diff_map.set_all_hunks_expanded(cx))
}
pub fn expand_diff_hunks(&mut self, ranges: Vec<Range<Anchor>>, cx: &mut ModelContext<Self>) {
self.update_diff_map(cx, |diff_map, cx| diff_map.expand_diff_hunks(ranges, cx))
}
pub fn collapse_diff_hunks(&mut self, ranges: Vec<Range<Anchor>>, cx: &mut ModelContext<Self>) {
self.update_diff_map(cx, |diff_map, cx| diff_map.collapse_diff_hunks(ranges, cx))
}
pub fn diff_base_for<'a>(
&'a self,
buffer_id: BufferId,
cx: &'a AppContext,
) -> Option<&'a Model<BufferChangeSet>> {
self.diff_map.read(cx).diff_base_for(buffer_id)
}
fn update_diff_map(
&mut self,
cx: &mut ModelContext<Self>,
f: impl FnOnce(&mut DiffMap, &mut ModelContext<DiffMap>),
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| {
f(diff_map, cx);
diff_map.sync(snapshot, edits, cx)
});
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.write(snapshot, edits);
}
pub fn insert_blocks(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
@@ -396,6 +487,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -414,6 +508,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -432,6 +529,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -450,6 +550,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -525,7 +628,10 @@ impl DisplayMap {
}
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), edits, cx)
});
let (snapshot, edits) = self.inlay_map.sync(snapshot.clone(), edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -711,17 +817,35 @@ impl DisplaySnapshot {
self.fold_snapshot.fold_count()
}
pub fn diff_hunks<'a>(&'a self) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
self.diff_snapshot()
.diff_hunks_in_range(0..self.buffer_snapshot.len())
}
pub fn diff_hunks_in_range<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
self.diff_snapshot().diff_hunks_in_range(range)
}
pub fn diff_hunks_in_range_rev<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
self.diff_snapshot().diff_hunks_in_range_rev(range)
}
pub fn has_diff_hunks(&self) -> bool {
self.diff_snapshot().has_diff_hunks()
}
pub fn is_empty(&self) -> bool {
self.buffer_snapshot.len() == 0
}
pub fn buffer_rows(
&self,
start_row: DisplayRow,
) -> impl Iterator<Item = Option<MultiBufferRow>> + '_ {
self.block_snapshot
.buffer_rows(BlockRow(start_row.0))
.map(|row| row.map(MultiBufferRow))
pub fn row_infos(&self, start_row: DisplayRow) -> impl Iterator<Item = RowInfo> + '_ {
self.block_snapshot.row_infos(BlockRow(start_row.0))
}
pub fn widest_line_number(&self) -> u32 {
@@ -730,7 +854,7 @@ impl DisplaySnapshot {
pub fn prev_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
loop {
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
let mut inlay_point = self.inlay_snapshot.make_inlay_point(point);
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left);
fold_point.0.column = 0;
inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
@@ -752,7 +876,7 @@ impl DisplaySnapshot {
) -> (MultiBufferPoint, DisplayPoint) {
let original_point = point;
loop {
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
let mut inlay_point = self.inlay_snapshot.make_inlay_point(point);
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
fold_point.0.column = self.fold_snapshot.line_len(fold_point.row());
inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
@@ -796,7 +920,7 @@ impl DisplaySnapshot {
}
pub fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
let inlay_point = self.inlay_snapshot.to_inlay_point(point);
let inlay_point = self.inlay_snapshot.make_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
@@ -814,9 +938,15 @@ impl DisplaySnapshot {
.to_offset(self.display_point_to_inlay_point(point, bias))
}
pub fn display_point_to_diff_offset(&self, point: DisplayPoint, bias: Bias) -> DiffOffset {
self.diff_snapshot()
.point_to_offset(self.display_point_to_diff_point(point, bias))
}
pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset {
self.inlay_snapshot
.to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
let multibuffer_offset = anchor.to_offset(&self.buffer_snapshot);
let diff_offset = self.diff_snapshot().to_diff_offset(multibuffer_offset);
self.inlay_snapshot.to_inlay_offset(diff_offset)
}
pub fn display_point_to_anchor(&self, point: DisplayPoint, bias: Bias) -> Anchor {
@@ -832,6 +962,17 @@ impl DisplaySnapshot {
fold_point.to_inlay_point(&self.fold_snapshot)
}
fn diff_point_to_display_point(&self, diff_point: DiffPoint, bias: Bias) -> DisplayPoint {
let inlay_point = self.inlay_snapshot.to_inlay_point(diff_point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
self.fold_point_to_display_point(fold_point)
}
fn display_point_to_diff_point(&self, point: DisplayPoint, bias: Bias) -> DiffPoint {
self.inlay_snapshot
.to_diff_point(self.display_point_to_inlay_point(point, bias))
}
pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
@@ -1069,6 +1210,22 @@ impl DisplaySnapshot {
})
}
pub fn anchor_before(&self, point: DisplayPoint) -> DisplayAnchor {
let diff_point = self.display_point_to_diff_point(point, Bias::Left);
self.diff_snapshot().point_to_anchor(diff_point, Bias::Left)
}
pub fn anchor_after(&self, point: DisplayPoint) -> DisplayAnchor {
let diff_point = self.display_point_to_diff_point(point, Bias::Left);
self.diff_snapshot()
.point_to_anchor(diff_point, Bias::Right)
}
pub fn anchor_to_point(&self, anchor: DisplayAnchor) -> DisplayPoint {
let diff_point = self.diff_snapshot().anchor_to_point(anchor);
self.diff_point_to_display_point(diff_point, Bias::Left)
}
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
let mut clipped = self.block_snapshot.clip_point(point.0, bias);
if self.clip_at_line_ends {
@@ -1106,10 +1263,6 @@ impl DisplaySnapshot {
.map(|(row, block)| (DisplayRow(row), block))
}
pub fn sticky_header_excerpt(&self, row: DisplayRow) -> Option<StickyHeaderExcerpt<'_>> {
self.block_snapshot.sticky_header_excerpt(row.0)
}
pub fn block_for_id(&self, id: BlockId) -> Option<Block> {
self.block_snapshot.block_for_id(id)
}
@@ -1312,6 +1465,51 @@ impl DisplaySnapshot {
pub fn excerpt_header_height(&self) -> u32 {
self.block_snapshot.excerpt_header_height
}
pub(crate) fn diff_snapshot(&self) -> &DiffMapSnapshot {
&self.inlay_snapshot.diff_map_snapshot
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
pub struct DisplayAnchor {
pub anchor: multi_buffer::Anchor,
pub diff_base_anchor: Option<text::Anchor>,
}
impl DisplayAnchor {
pub fn min() -> Self {
Self {
anchor: multi_buffer::Anchor::min(),
diff_base_anchor: None,
}
}
pub fn max() -> Self {
Self {
anchor: multi_buffer::Anchor::max(),
diff_base_anchor: None,
}
}
pub fn cmp(&self, other: &DisplayAnchor, map: &DisplaySnapshot) -> std::cmp::Ordering {
map.diff_snapshot().compare_anchors(self, other)
}
}
pub trait DisplayCoordinate {
fn to_display_anchor(self, map: &DisplaySnapshot) -> DisplayAnchor;
}
impl DisplayCoordinate for DisplayAnchor {
fn to_display_anchor(self, _: &DisplaySnapshot) -> DisplayAnchor {
self
}
}
impl DisplayCoordinate for DisplayPoint {
fn to_display_anchor(self, map: &DisplaySnapshot) -> DisplayAnchor {
map.anchor_before(self)
}
}
#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]

View File

@@ -2,7 +2,7 @@ use super::{
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
Highlights,
};
use crate::{EditorStyle, GutterDimensions};
use crate::{EditorStyle, GutterDimensions, RowInfo};
use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, AppContext, EntityId, Pixels, WindowContext};
use language::{Chunk, Patch, Point};
@@ -399,9 +399,9 @@ pub struct BlockChunks<'a> {
}
#[derive(Clone)]
pub struct BlockBufferRows<'a> {
pub struct BlockRows<'a> {
transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
input_buffer_rows: wrap_map::WrapBufferRows<'a>,
input_rows: wrap_map::WrapRows<'a>,
output_row: BlockRow,
started: bool,
}
@@ -1360,7 +1360,7 @@ impl BlockSnapshot {
}
}
pub(super) fn buffer_rows(&self, start_row: BlockRow) -> BlockBufferRows {
pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&start_row, Bias::Right, &());
let (output_start, input_start) = cursor.start();
@@ -1373,9 +1373,9 @@ impl BlockSnapshot {
0
};
let input_start_row = input_start.0 + overshoot;
BlockBufferRows {
BlockRows {
transforms: cursor,
input_buffer_rows: self.wrap_snapshot.buffer_rows(input_start_row),
input_rows: self.wrap_snapshot.row_infos(input_start_row),
output_row: start_row,
started: false,
}
@@ -1411,66 +1411,6 @@ impl BlockSnapshot {
})
}
pub fn sticky_header_excerpt(&self, top_row: u32) -> Option<StickyHeaderExcerpt<'_>> {
let mut cursor = self.transforms.cursor::<BlockRow>(&());
cursor.seek(&BlockRow(top_row), Bias::Left, &());
while let Some(transform) = cursor.item() {
let start = cursor.start().0;
let end = cursor.end(&()).0;
match &transform.block {
Some(Block::ExcerptBoundary {
prev_excerpt,
next_excerpt,
starts_new_buffer,
show_excerpt_controls,
..
}) => {
let matches_start = if *show_excerpt_controls && prev_excerpt.is_some() {
start < top_row
} else {
start <= top_row
};
if matches_start && top_row <= end {
return next_excerpt.as_ref().map(|excerpt| StickyHeaderExcerpt {
next_buffer_row: None,
next_excerpt_controls_present: *show_excerpt_controls,
excerpt,
});
}
let next_buffer_row = if *starts_new_buffer { Some(end) } else { None };
return prev_excerpt.as_ref().map(|excerpt| StickyHeaderExcerpt {
excerpt,
next_buffer_row,
next_excerpt_controls_present: *show_excerpt_controls,
});
}
Some(Block::FoldedBuffer {
prev_excerpt: Some(excerpt),
..
}) if top_row <= start => {
return Some(StickyHeaderExcerpt {
next_buffer_row: Some(end),
next_excerpt_controls_present: false,
excerpt,
});
}
Some(Block::FoldedBuffer { .. }) | Some(Block::Custom(_)) | None => {}
}
// This is needed to iterate past None / FoldedBuffer / Custom blocks. For FoldedBuffer,
// if scrolled slightly past the header of a folded block, the next block is needed for
// the sticky header.
cursor.next(&());
}
None
}
pub fn block_for_id(&self, block_id: BlockId) -> Option<Block> {
let buffer = self.wrap_snapshot.buffer_snapshot();
let wrap_point = match block_id {
@@ -1754,13 +1694,6 @@ impl<'a> BlockChunks<'a> {
}
}
pub struct StickyHeaderExcerpt<'a> {
pub excerpt: &'a ExcerptInfo,
pub next_excerpt_controls_present: bool,
// TODO az remove option
pub next_buffer_row: Option<u32>,
}
impl<'a> Iterator for BlockChunks<'a> {
type Item = Chunk<'a>;
@@ -1833,8 +1766,8 @@ impl<'a> Iterator for BlockChunks<'a> {
}
}
impl<'a> Iterator for BlockBufferRows<'a> {
type Item = Option<u32>;
impl<'a> Iterator for BlockRows<'a> {
type Item = RowInfo;
fn next(&mut self) -> Option<Self::Item> {
if self.started {
@@ -1863,7 +1796,7 @@ impl<'a> Iterator for BlockBufferRows<'a> {
.as_ref()
.map_or(true, |block| block.is_replacement())
{
self.input_buffer_rows.seek(self.transforms.start().1 .0);
self.input_rows.seek(self.transforms.start().1 .0);
}
}
@@ -1871,15 +1804,15 @@ impl<'a> Iterator for BlockBufferRows<'a> {
if let Some(block) = transform.block.as_ref() {
if block.is_replacement() && self.transforms.start().0 == self.output_row {
if matches!(block, Block::FoldedBuffer { .. }) {
Some(None)
Some(RowInfo::default())
} else {
Some(self.input_buffer_rows.next().unwrap())
Some(self.input_rows.next().unwrap())
}
} else {
Some(None)
Some(RowInfo::default())
}
} else {
Some(self.input_buffer_rows.next().unwrap())
Some(self.input_rows.next().unwrap())
}
}
}
@@ -1995,7 +1928,8 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
mod tests {
use super::*;
use crate::display_map::{
fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap,
diff_map::DiffMap, fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap,
wrap_map::WrapMap,
};
use gpui::{div, font, px, AppContext, Context as _, Element};
use itertools::Itertools;
@@ -2029,7 +1963,8 @@ mod tests {
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx));
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (wrap_map, wraps_snapshot) =
@@ -2154,7 +2089,10 @@ mod tests {
);
assert_eq!(
snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
snapshot
.row_infos(BlockRow(0))
.map(|row_info| row_info.buffer_row)
.collect::<Vec<_>>(),
&[
Some(0),
None,
@@ -2175,8 +2113,10 @@ mod tests {
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot, subscription.consume().into_inner(), cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
@@ -2238,8 +2178,8 @@ mod tests {
.width;
}
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone());
let (_, diff_snapshot) = DiffMap::new(multi_buffer.clone(), cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot);
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx);
@@ -2277,7 +2217,8 @@ mod tests {
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let _subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_diff_map, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx));
let (_inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (_wrap_map, wraps_snapshot) =
@@ -2379,7 +2320,8 @@ mod tests {
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx));
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) = cx.update(|cx| {
@@ -2423,7 +2365,8 @@ mod tests {
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let buffer_subscription = buffer.update(cx, |buffer, _cx| buffer.subscribe());
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx));
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let tab_size = 1.try_into().unwrap();
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, tab_size);
@@ -2450,10 +2393,14 @@ mod tests {
buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(
buffer_snapshot.clone(),
buffer_subscription.consume().into_inner(),
);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(
buffer_snapshot,
buffer_subscription.consume().into_inner(),
cx,
)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot.clone(), diff_edits);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
@@ -2473,10 +2420,14 @@ mod tests {
);
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(
buffer_snapshot.clone(),
buffer_subscription.consume().into_inner(),
);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(
buffer_snapshot.clone(),
buffer_subscription.consume().into_inner(),
cx,
)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
@@ -2591,7 +2542,8 @@ mod tests {
let buffer_id_2 = buffer_ids[1];
let buffer_id_3 = buffer_ids[2];
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx));
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wrap_snapshot) =
@@ -2604,7 +2556,10 @@ mod tests {
"\n\n\n111\n\n\n\n\n222\n\n\n333\n\n\n444\n\n\n\n\n555\n\n\n666\n"
);
assert_eq!(
blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
blocks_snapshot
.row_infos(BlockRow(0))
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
@@ -2680,7 +2635,10 @@ mod tests {
"\n\n\n111\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
);
assert_eq!(
blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
blocks_snapshot
.row_infos(BlockRow(0))
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
@@ -2755,7 +2713,10 @@ mod tests {
"\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
);
assert_eq!(
blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
blocks_snapshot
.row_infos(BlockRow(0))
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
@@ -2820,7 +2781,10 @@ mod tests {
);
assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n\n555\n\n\n666\n\n");
assert_eq!(
blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
blocks_snapshot
.row_infos(BlockRow(0))
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
@@ -2874,7 +2838,10 @@ mod tests {
"Should have extra newline for 111 buffer, due to a new block added when it was folded"
);
assert_eq!(
blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
blocks_snapshot
.row_infos(BlockRow(0))
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
@@ -2928,7 +2895,10 @@ mod tests {
"Should have a single, first buffer left after folding"
);
assert_eq!(
blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
blocks_snapshot
.row_infos(BlockRow(0))
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
@@ -2962,7 +2932,8 @@ mod tests {
assert_eq!(buffer_ids.len(), 1);
let buffer_id = buffer_ids[0];
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx));
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wrap_snapshot) =
@@ -2998,7 +2969,10 @@ mod tests {
);
assert_eq!(blocks_snapshot.text(), "\n");
assert_eq!(
blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
blocks_snapshot
.row_infos(BlockRow(0))
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![None, None],
"When fully folded, should be no buffer rows"
);
@@ -3044,7 +3018,8 @@ mod tests {
};
let mut buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx));
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (wrap_map, wraps_snapshot) = cx
@@ -3102,8 +3077,10 @@ mod tests {
})
.collect::<Vec<_>>();
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), vec![]);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), vec![], cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, tab_size);
@@ -3140,8 +3117,10 @@ mod tests {
.map(|block| block.id)
.collect::<HashSet<_>>();
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), vec![]);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), vec![], cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, tab_size);
@@ -3161,8 +3140,11 @@ mod tests {
log::info!("Noop fold/unfold operation on a singleton buffer");
continue;
}
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), vec![], cx)
});
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), vec![]);
inlay_map.sync(diff_snapshot.clone(), diff_edits);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, tab_size);
@@ -3247,8 +3229,10 @@ mod tests {
}
}
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), buffer_edits, cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
@@ -3451,7 +3435,8 @@ mod tests {
);
assert_eq!(
blocks_snapshot
.buffer_rows(BlockRow(start_row as u32))
.row_infos(BlockRow(start_row as u32))
.map(|row_info| row_info.buffer_row)
.collect::<Vec<_>>(),
&expected_buffer_rows[start_row..],
"incorrect buffer_rows starting at row {:?}",

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
use crate::RowInfo;
use super::{
inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
Highlights,
@@ -139,7 +141,7 @@ impl<'a> FoldMapWriter<'a> {
let mut folds = Vec::new();
let snapshot = self.0.snapshot.inlay_snapshot.clone();
for (range, fold_text) in ranges.into_iter() {
let buffer = &snapshot.buffer;
let buffer = snapshot.buffer();
let range = range.start.to_offset(buffer)..range.end.to_offset(buffer);
// Ignore any empty ranges.
@@ -161,14 +163,14 @@ impl<'a> FoldMapWriter<'a> {
});
let inlay_range =
snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
snapshot.make_inlay_offset(range.start)..snapshot.make_inlay_offset(range.end);
edits.push(InlayEdit {
old: inlay_range.clone(),
new: inlay_range,
});
}
let buffer = &snapshot.buffer;
let buffer = snapshot.buffer();
folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(&a.range, &b.range, buffer));
self.0.snapshot.folds = {
@@ -220,7 +222,7 @@ impl<'a> FoldMapWriter<'a> {
let mut edits = Vec::new();
let mut fold_ixs_to_delete = Vec::new();
let snapshot = self.0.snapshot.inlay_snapshot.clone();
let buffer = &snapshot.buffer;
let buffer = snapshot.buffer();
for range in ranges.into_iter() {
let range = range.start.to_offset(buffer)..range.end.to_offset(buffer);
let mut folds_cursor =
@@ -230,8 +232,8 @@ impl<'a> FoldMapWriter<'a> {
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer);
if should_unfold(fold) {
if offset_range.end > offset_range.start {
let inlay_range = snapshot.to_inlay_offset(offset_range.start)
..snapshot.to_inlay_offset(offset_range.end);
let inlay_range = snapshot.make_inlay_offset(offset_range.start)
..snapshot.make_inlay_offset(offset_range.end);
edits.push(InlayEdit {
old: inlay_range.clone(),
new: inlay_range,
@@ -275,7 +277,7 @@ impl FoldMap {
pub(crate) fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
let this = Self {
snapshot: FoldSnapshot {
folds: SumTree::new(&inlay_snapshot.buffer),
folds: SumTree::new(inlay_snapshot.buffer()),
transforms: SumTree::from_item(
Transform {
summary: TransformSummary {
@@ -336,9 +338,7 @@ impl FoldMap {
let mut folds = self.snapshot.folds.iter().peekable();
while let Some(fold) = folds.next() {
if let Some(next_fold) = folds.peek() {
let comparison = fold
.range
.cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer);
let comparison = fold.range.cmp(&next_fold.range, self.snapshot.buffer());
assert!(comparison.is_le());
}
}
@@ -410,31 +410,31 @@ impl FoldMap {
InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize);
let anchor = inlay_snapshot
.buffer
.buffer()
.anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
let mut folds_cursor = self
.snapshot
.folds
.cursor::<FoldRange>(&inlay_snapshot.buffer);
.cursor::<FoldRange>(&inlay_snapshot.buffer());
folds_cursor.seek(
&FoldRange(anchor..Anchor::max()),
Bias::Left,
&inlay_snapshot.buffer,
&inlay_snapshot.buffer(),
);
let mut folds = iter::from_fn({
let inlay_snapshot = &inlay_snapshot;
move || {
let item = folds_cursor.item().map(|fold| {
let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer);
let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer());
let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer());
(
fold.clone(),
inlay_snapshot.to_inlay_offset(buffer_start)
..inlay_snapshot.to_inlay_offset(buffer_end),
inlay_snapshot.make_inlay_offset(buffer_start)
..inlay_snapshot.make_inlay_offset(buffer_end),
)
});
folds_cursor.next(&inlay_snapshot.buffer);
folds_cursor.next(&inlay_snapshot.buffer());
item
}
})
@@ -578,6 +578,10 @@ pub struct FoldSnapshot {
}
impl FoldSnapshot {
pub fn buffer(&self) -> &MultiBufferSnapshot {
self.inlay_snapshot.buffer()
}
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(FoldOffset(0)..self.len(), false, Highlights::default())
@@ -587,7 +591,7 @@ impl FoldSnapshot {
#[cfg(test)]
pub fn fold_count(&self) -> usize {
self.folds.items(&self.inlay_snapshot.buffer).len()
self.folds.items(self.inlay_snapshot.buffer()).len()
}
pub fn text_summary_for_range(&self, range: Range<FoldPoint>) -> TextSummary {
@@ -641,6 +645,32 @@ impl FoldSnapshot {
summary
}
pub fn make_fold_point(&self, point: Point, bias: Bias) -> FoldPoint {
self.to_fold_point(self.inlay_snapshot.make_inlay_point(point), bias)
}
pub fn make_fold_offset(&self, buffer_offset: usize, bias: Bias) -> FoldOffset {
self.to_fold_offset(self.inlay_snapshot.make_inlay_offset(buffer_offset), bias)
}
pub fn to_fold_offset(&self, inlay_offset: InlayOffset, bias: Bias) -> FoldOffset {
let mut cursor = self.transforms.cursor::<(InlayOffset, FoldOffset)>(&());
cursor.seek(&inlay_offset, Bias::Right, &());
if cursor.item().map_or(false, |t| t.is_fold()) {
if bias == Bias::Left || inlay_offset == cursor.start().0 {
cursor.start().1
} else {
cursor.end(&()).1
}
} else {
let overshoot = inlay_offset.0 - cursor.start().0 .0;
FoldOffset(cmp::min(
cursor.start().1 .0 + overshoot,
cursor.end(&()).1 .0,
))
}
}
pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint {
let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>(&());
cursor.seek(&point, Bias::Right, &());
@@ -673,7 +703,7 @@ impl FoldSnapshot {
(line_end - line_start) as u32
}
pub fn buffer_rows(&self, start_row: u32) -> FoldBufferRows {
pub fn row_infos(&self, start_row: u32) -> FoldRows {
if start_row > self.transforms.summary().output.lines.row {
panic!("invalid display row {}", start_row);
}
@@ -684,11 +714,11 @@ impl FoldSnapshot {
let overshoot = fold_point.0 - cursor.start().0 .0;
let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot);
let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row());
let input_rows = self.inlay_snapshot.row_infos(inlay_point.row());
FoldBufferRows {
FoldRows {
fold_point,
input_buffer_rows,
input_rows,
cursor,
}
}
@@ -706,12 +736,12 @@ impl FoldSnapshot {
where
T: ToOffset,
{
let buffer = &self.inlay_snapshot.buffer;
let buffer = self.inlay_snapshot.buffer();
let range = range.start.to_offset(buffer)..range.end.to_offset(buffer);
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
iter::from_fn(move || {
let item = folds.item();
folds.next(&self.inlay_snapshot.buffer);
folds.next(buffer);
item
})
}
@@ -720,8 +750,8 @@ impl FoldSnapshot {
where
T: ToOffset,
{
let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer);
let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
let buffer_offset = offset.to_offset(self.inlay_snapshot.buffer());
let inlay_offset = self.inlay_snapshot.make_inlay_offset(buffer_offset);
let mut cursor = self.transforms.cursor::<InlayOffset>(&());
cursor.seek(&inlay_offset, Bias::Right, &());
cursor.item().map_or(false, |t| t.placeholder.is_some())
@@ -730,7 +760,7 @@ impl FoldSnapshot {
pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
let mut inlay_point = self
.inlay_snapshot
.to_inlay_point(Point::new(buffer_row.0, 0));
.make_inlay_point(Point::new(buffer_row.0, 0));
let mut cursor = self.transforms.cursor::<InlayPoint>(&());
cursor.seek(&inlay_point, Bias::Right, &());
loop {
@@ -870,7 +900,7 @@ fn intersecting_folds<'a>(
range: Range<usize>,
inclusive: bool,
) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize> {
let buffer = &inlay_snapshot.buffer;
let buffer = inlay_snapshot.buffer();
let start = buffer.anchor_before(range.start.to_offset(buffer));
let end = buffer.anchor_after(range.end.to_offset(buffer));
let mut cursor = folds.filter::<_, usize>(buffer, move |summary| {
@@ -1134,25 +1164,25 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
}
#[derive(Clone)]
pub struct FoldBufferRows<'a> {
pub struct FoldRows<'a> {
cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>,
input_buffer_rows: InlayBufferRows<'a>,
input_rows: InlayBufferRows<'a>,
fold_point: FoldPoint,
}
impl<'a> FoldBufferRows<'a> {
impl<'a> FoldRows<'a> {
pub(crate) fn seek(&mut self, row: u32) {
let fold_point = FoldPoint::new(row, 0);
self.cursor.seek(&fold_point, Bias::Left, &());
let overshoot = fold_point.0 - self.cursor.start().0 .0;
let inlay_point = InlayPoint(self.cursor.start().1 .0 + overshoot);
self.input_buffer_rows.seek(inlay_point.row());
self.input_rows.seek(inlay_point.row());
self.fold_point = fold_point;
}
}
impl<'a> Iterator for FoldBufferRows<'a> {
type Item = Option<u32>;
impl<'a> Iterator for FoldRows<'a> {
type Item = RowInfo;
fn next(&mut self) -> Option<Self::Item> {
let mut traversed_fold = false;
@@ -1166,11 +1196,11 @@ impl<'a> Iterator for FoldBufferRows<'a> {
if self.cursor.item().is_some() {
if traversed_fold {
self.input_buffer_rows.seek(self.cursor.start().1.row());
self.input_buffer_rows.next();
self.input_rows.seek(self.cursor.start().1 .0.row);
self.input_rows.next();
}
*self.fold_point.row_mut() += 1;
self.input_buffer_rows.next()
self.input_rows.next()
} else {
None
}
@@ -1380,7 +1410,10 @@ pub type FoldEdit = Edit<FoldOffset>;
#[cfg(test)]
mod tests {
use super::*;
use crate::{display_map::inlay_map::InlayMap, MultiBuffer, ToPoint};
use crate::{
display_map::{diff_map::DiffMap, inlay_map::InlayMap},
MultiBuffer, ToPoint,
};
use collections::HashSet;
use rand::prelude::*;
use settings::SettingsStore;
@@ -1395,8 +1428,8 @@ mod tests {
init_test(cx);
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot, vec![]);
@@ -1431,8 +1464,14 @@ mod tests {
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(
buffer_snapshot.clone(),
subscription.consume().into_inner(),
cx,
)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (snapshot3, edits) = map.read(inlay_snapshot, inlay_edits);
assert_eq!(snapshot3.text(), "123a⋯c123c⋯eeeee");
assert_eq!(
@@ -1453,8 +1492,11 @@ mod tests {
buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], None, cx);
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot, subscription.consume().into_inner(), cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (snapshot4, _) = map.read(inlay_snapshot.clone(), inlay_edits);
assert_eq!(snapshot4.text(), "123a⋯c123456eee");
@@ -1474,8 +1516,8 @@ mod tests {
init_test(cx);
let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
{
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
@@ -1521,8 +1563,10 @@ mod tests {
buffer.edit([(0..1, "12345")], None, cx);
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot, subscription.consume().into_inner(), cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (snapshot, _) = map.read(inlay_snapshot, inlay_edits);
assert_eq!(snapshot.text(), "12345⋯fghijkl");
}
@@ -1531,8 +1575,8 @@ mod tests {
#[gpui::test]
fn test_overlapping_folds(cx: &mut gpui::AppContext) {
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot);
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
@@ -1550,8 +1594,8 @@ mod tests {
init_test(cx);
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
@@ -1566,8 +1610,10 @@ mod tests {
buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx);
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot, subscription.consume().into_inner(), cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (snapshot, _) = map.read(inlay_snapshot, inlay_edits);
assert_eq!(snapshot.text(), "aa⋯eeeee");
}
@@ -1576,7 +1622,8 @@ mod tests {
fn test_folds_in_range(cx: &mut gpui::AppContext) {
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
@@ -1618,7 +1665,8 @@ mod tests {
MultiBuffer::build_random(&mut rng, cx)
};
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
@@ -1648,8 +1696,10 @@ mod tests {
}),
};
let (inlay_snapshot, new_inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), buffer_edits, cx)
});
let (inlay_snapshot, new_inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
log::info!("inlay text {:?}", inlay_snapshot.text());
let inlay_edits = Patch::new(inlay_edits)
@@ -1660,8 +1710,8 @@ mod tests {
let mut expected_text: String = inlay_snapshot.text().to_string();
for fold_range in map.merged_folds().into_iter().rev() {
let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start);
let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end);
let fold_inlay_start = inlay_snapshot.make_inlay_offset(fold_range.start);
let fold_inlay_end = inlay_snapshot.make_inlay_offset(fold_range.end);
expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "");
}
@@ -1676,19 +1726,19 @@ mod tests {
let mut expected_buffer_rows = Vec::new();
for fold_range in map.merged_folds() {
let fold_start = inlay_snapshot
.to_point(inlay_snapshot.to_inlay_offset(fold_range.start))
.to_point(inlay_snapshot.make_inlay_offset(fold_range.start))
.row();
let fold_end = inlay_snapshot
.to_point(inlay_snapshot.to_inlay_offset(fold_range.end))
.to_point(inlay_snapshot.make_inlay_offset(fold_range.end))
.row();
expected_buffer_rows.extend(
inlay_snapshot
.buffer_rows(prev_row)
.row_infos(prev_row)
.take((1 + fold_start - prev_row) as usize),
);
prev_row = 1 + fold_end;
}
expected_buffer_rows.extend(inlay_snapshot.buffer_rows(prev_row));
expected_buffer_rows.extend(inlay_snapshot.row_infos(prev_row));
assert_eq!(
expected_buffer_rows.len(),
@@ -1777,7 +1827,7 @@ mod tests {
let mut fold_row = 0;
while fold_row < expected_buffer_rows.len() as u32 {
assert_eq!(
snapshot.buffer_rows(fold_row).collect::<Vec<_>>(),
snapshot.row_infos(fold_row).collect::<Vec<_>>(),
expected_buffer_rows[(fold_row as usize)..],
"wrong buffer rows starting at fold row {}",
fold_row,
@@ -1879,8 +1929,8 @@ mod tests {
let text = sample_text(6, 6, 'a') + "\n";
let buffer = MultiBuffer::build_simple(&text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot);
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
@@ -1892,10 +1942,19 @@ mod tests {
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee\nffffff\n");
assert_eq!(
snapshot.buffer_rows(0).collect::<Vec<_>>(),
snapshot
.row_infos(0)
.map(|info| info.buffer_row)
.collect::<Vec<_>>(),
[Some(0), Some(3), Some(5), Some(6)]
);
assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [Some(6)]);
assert_eq!(
snapshot
.row_infos(3)
.map(|info| info.buffer_row)
.collect::<Vec<_>>(),
[Some(6)]
);
}
fn init_test(cx: &mut gpui::AppContext) {
@@ -1906,7 +1965,7 @@ mod tests {
impl FoldMap {
fn merged_folds(&self) -> Vec<Range<usize>> {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let buffer = inlay_snapshot.buffer();
let mut folds = self.snapshot.folds.items(buffer);
// Ensure sorting doesn't change how folds get merged and displayed.
folds.sort_by(|a, b| a.range.cmp(&b.range, buffer));
@@ -1942,7 +2001,7 @@ mod tests {
match rng.gen_range(0..=100) {
0..=39 if !self.snapshot.folds.is_empty() => {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let buffer = inlay_snapshot.buffer();
let mut to_unfold = Vec::new();
for _ in 0..rng.gen_range(1..=3) {
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
@@ -1958,7 +2017,7 @@ mod tests {
}
_ => {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let buffer = inlay_snapshot.buffer();
let mut to_fold = Vec::new();
for _ in 0..rng.gen_range(1..=2) {
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);

View File

@@ -1,7 +1,7 @@
use crate::{HighlightStyles, InlayId};
use crate::{HighlightStyles, InlayId, RowInfo};
use collections::BTreeSet;
use language::{Chunk, Edit, Point, TextSummary};
use multi_buffer::{Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, ToOffset};
use multi_buffer::{Anchor, MultiBufferSnapshot, ToOffset};
use std::{
cmp,
ops::{Add, AddAssign, Range, Sub, SubAssign},
@@ -9,7 +9,10 @@ use std::{
use sum_tree::{Bias, Cursor, SumTree};
use text::{Patch, Rope};
use super::{custom_highlights::CustomHighlightsChunks, Highlights};
use super::{
diff_map::{DiffEdit, DiffMapChunks, DiffMapRows, DiffMapSnapshot, DiffOffset, DiffPoint},
Highlights,
};
/// Decides where the [`Inlay`]s should be displayed.
///
@@ -21,7 +24,7 @@ pub struct InlayMap {
#[derive(Clone)]
pub struct InlaySnapshot {
pub buffer: MultiBufferSnapshot,
pub diff_map_snapshot: DiffMapSnapshot,
transforms: SumTree<Transform>,
pub version: usize,
}
@@ -172,37 +175,37 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint {
}
}
impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize {
impl<'a> sum_tree::Dimension<'a, TransformSummary> for DiffOffset {
fn zero(_cx: &()) -> Self {
Default::default()
DiffOffset(0)
}
fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
*self += &summary.input.len;
self.0 += &summary.input.len;
}
}
impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point {
impl<'a> sum_tree::Dimension<'a, TransformSummary> for DiffPoint {
fn zero(_cx: &()) -> Self {
Default::default()
}
fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
*self += &summary.input.lines;
self.0 += &summary.input.lines;
}
}
#[derive(Clone)]
pub struct InlayBufferRows<'a> {
transforms: Cursor<'a, Transform, (InlayPoint, Point)>,
buffer_rows: MultiBufferRows<'a>,
transforms: Cursor<'a, Transform, (InlayPoint, DiffPoint)>,
diff_rows: DiffMapRows<'a>,
inlay_row: u32,
max_buffer_row: MultiBufferRow,
max_point: DiffPoint,
}
pub struct InlayChunks<'a> {
transforms: Cursor<'a, Transform, (InlayOffset, usize)>,
buffer_chunks: CustomHighlightsChunks<'a>,
transforms: Cursor<'a, Transform, (InlayOffset, DiffOffset)>,
diff_map_chunks: DiffMapChunks<'a>,
buffer_chunk: Option<Chunk<'a>>,
inlay_chunks: Option<text::Chunks<'a>>,
inlay_chunk: Option<&'a str>,
@@ -214,21 +217,21 @@ pub struct InlayChunks<'a> {
}
impl<'a> InlayChunks<'a> {
pub fn offset(&self) -> InlayOffset {
self.output_offset
}
pub fn seek(&mut self, new_range: Range<InlayOffset>) {
self.transforms.seek(&new_range.start, Bias::Right, &());
let buffer_range = self.snapshot.to_buffer_offset(new_range.start)
..self.snapshot.to_buffer_offset(new_range.end);
self.buffer_chunks.seek(buffer_range);
let diff_range = self.snapshot.to_diff_offset(new_range.start)
..self.snapshot.to_diff_offset(new_range.end);
self.diff_map_chunks.seek(diff_range);
self.inlay_chunks = None;
self.buffer_chunk = None;
self.output_offset = new_range.start;
self.max_output_offset = new_range.end;
}
pub fn offset(&self) -> InlayOffset {
self.output_offset
}
}
impl<'a> Iterator for InlayChunks<'a> {
@@ -243,9 +246,9 @@ impl<'a> Iterator for InlayChunks<'a> {
Transform::Isomorphic(_) => {
let chunk = self
.buffer_chunk
.get_or_insert_with(|| self.buffer_chunks.next().unwrap());
.get_or_insert_with(|| self.diff_map_chunks.next().unwrap());
if chunk.text.is_empty() {
*chunk = self.buffer_chunks.next().unwrap();
*chunk = self.diff_map_chunks.next().unwrap();
}
let (prefix, suffix) = chunk.text.split_at(
@@ -344,33 +347,33 @@ impl<'a> InlayBufferRows<'a> {
let inlay_point = InlayPoint::new(row, 0);
self.transforms.seek(&inlay_point, Bias::Left, &());
let mut buffer_point = self.transforms.start().1;
let buffer_row = MultiBufferRow(if row == 0 {
let mut diff_point = self.transforms.start().1;
let row = if row == 0 {
0
} else {
match self.transforms.item() {
Some(Transform::Isomorphic(_)) => {
buffer_point += inlay_point.0 - self.transforms.start().0 .0;
buffer_point.row
diff_point.0 += inlay_point.0 - self.transforms.start().0 .0;
diff_point.row()
}
_ => cmp::min(buffer_point.row + 1, self.max_buffer_row.0),
_ => cmp::min(diff_point.row() + 1, self.max_point.row()),
}
});
};
self.inlay_row = inlay_point.row();
self.buffer_rows.seek(buffer_row);
self.diff_rows.seek(row);
}
}
impl<'a> Iterator for InlayBufferRows<'a> {
type Item = Option<u32>;
type Item = RowInfo;
fn next(&mut self) -> Option<Self::Item> {
let buffer_row = if self.inlay_row == 0 {
self.buffer_rows.next().unwrap()
let row_info = if self.inlay_row == 0 {
self.diff_rows.next().unwrap()
} else {
match self.transforms.item()? {
Transform::Inlay(_) => None,
Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(),
Transform::Inlay(_) => Default::default(),
Transform::Isomorphic(_) => self.diff_rows.next().unwrap(),
}
};
@@ -378,7 +381,7 @@ impl<'a> Iterator for InlayBufferRows<'a> {
self.transforms
.seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &());
Some(buffer_row)
Some(row_info)
}
}
@@ -393,11 +396,14 @@ impl InlayPoint {
}
impl InlayMap {
pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) {
pub fn new(diff_map_snapshot: DiffMapSnapshot) -> (Self, InlaySnapshot) {
let version = 0;
let snapshot = InlaySnapshot {
buffer: buffer.clone(),
transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), &()),
transforms: SumTree::from_iter(
Some(Transform::Isomorphic(diff_map_snapshot.text_summary())),
&(),
),
diff_map_snapshot,
version,
};
@@ -412,133 +418,121 @@ impl InlayMap {
pub fn sync(
&mut self,
buffer_snapshot: MultiBufferSnapshot,
mut buffer_edits: Vec<text::Edit<usize>>,
diff_map_snapshot: DiffMapSnapshot,
mut diff_edits: Vec<DiffEdit>,
) -> (InlaySnapshot, Vec<InlayEdit>) {
let snapshot = &mut self.snapshot;
if buffer_edits.is_empty()
&& snapshot.buffer.trailing_excerpt_update_count()
!= buffer_snapshot.trailing_excerpt_update_count()
if diff_edits.is_empty()
&& snapshot.buffer().trailing_excerpt_update_count()
!= diff_map_snapshot.buffer().trailing_excerpt_update_count()
{
buffer_edits.push(Edit {
old: snapshot.buffer.len()..snapshot.buffer.len(),
new: buffer_snapshot.len()..buffer_snapshot.len(),
diff_edits.push(Edit {
old: snapshot.diff_map_snapshot.len()..snapshot.diff_map_snapshot.len(),
new: diff_map_snapshot.len()..diff_map_snapshot.len(),
});
}
if buffer_edits.is_empty() {
if snapshot.buffer.edit_count() != buffer_snapshot.edit_count()
|| snapshot.buffer.non_text_state_update_count()
!= buffer_snapshot.non_text_state_update_count()
|| snapshot.buffer.trailing_excerpt_update_count()
!= buffer_snapshot.trailing_excerpt_update_count()
{
snapshot.version += 1;
}
snapshot.buffer = buffer_snapshot;
(snapshot.clone(), Vec::new())
} else {
let mut inlay_edits = Patch::default();
let mut new_transforms = SumTree::default();
let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>(&());
let mut buffer_edits_iter = buffer_edits.iter().peekable();
while let Some(buffer_edit) = buffer_edits_iter.next() {
new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &());
if let Some(Transform::Isomorphic(transform)) = cursor.item() {
if cursor.end(&()).0 == buffer_edit.old.start {
push_isomorphic(&mut new_transforms, transform.clone());
cursor.next(&());
}
}
// Remove all the inlays and transforms contained by the edit.
let old_start =
cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0);
cursor.seek(&buffer_edit.old.end, Bias::Right, &());
let old_end =
cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0);
// Push the unchanged prefix.
let prefix_start = new_transforms.summary().input.len;
let prefix_end = buffer_edit.new.start;
push_isomorphic(
&mut new_transforms,
buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
);
let new_start = InlayOffset(new_transforms.summary().output.len);
let start_ix = match self.inlays.binary_search_by(|probe| {
probe
.position
.to_offset(&buffer_snapshot)
.cmp(&buffer_edit.new.start)
.then(std::cmp::Ordering::Greater)
}) {
Ok(ix) | Err(ix) => ix,
};
for inlay in &self.inlays[start_ix..] {
let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
if buffer_offset > buffer_edit.new.end {
break;
}
let prefix_start = new_transforms.summary().input.len;
let prefix_end = buffer_offset;
push_isomorphic(
&mut new_transforms,
buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
);
if inlay.position.is_valid(&buffer_snapshot) {
new_transforms.push(Transform::Inlay(inlay.clone()), &());
}
}
// Apply the rest of the edit.
let transform_start = new_transforms.summary().input.len;
push_isomorphic(
&mut new_transforms,
buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end),
);
let new_end = InlayOffset(new_transforms.summary().output.len);
inlay_edits.push(Edit {
old: old_start..old_end,
new: new_start..new_end,
});
// If the next edit doesn't intersect the current isomorphic transform, then
// we can push its remainder.
if buffer_edits_iter
.peek()
.map_or(true, |edit| edit.old.start >= cursor.end(&()).0)
{
let transform_start = new_transforms.summary().input.len;
let transform_end =
buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end);
push_isomorphic(
&mut new_transforms,
buffer_snapshot.text_summary_for_range(transform_start..transform_end),
);
let mut inlay_edits = Patch::default();
let mut new_transforms = SumTree::default();
let mut cursor = snapshot.transforms.cursor::<(DiffOffset, InlayOffset)>(&());
let mut diff_edits_iter = diff_edits.iter().peekable();
while let Some(diff_edit) = diff_edits_iter.next() {
new_transforms.append(cursor.slice(&diff_edit.old.start, Bias::Left, &()), &());
if let Some(Transform::Isomorphic(transform)) = cursor.item() {
if cursor.end(&()).0 == diff_edit.old.start {
push_isomorphic(&mut new_transforms, transform.clone());
cursor.next(&());
}
}
new_transforms.append(cursor.suffix(&()), &());
if new_transforms.is_empty() {
new_transforms.push(Transform::Isomorphic(Default::default()), &());
// Remove all the inlays and transforms contained by the edit.
let old_start =
cursor.start().1 + InlayOffset((diff_edit.old.start - cursor.start().0).0);
cursor.seek(&diff_edit.old.end, Bias::Right, &());
let old_end = cursor.start().1 + InlayOffset((diff_edit.old.end - cursor.start().0).0);
// Push the unchanged prefix.
let prefix_start = DiffOffset(new_transforms.summary().input.len);
let prefix_end = diff_edit.new.start;
push_isomorphic(
&mut new_transforms,
diff_map_snapshot.text_summary_for_range(prefix_start..prefix_end),
);
let new_start = InlayOffset(new_transforms.summary().output.len);
let edit_buffer_range = diff_map_snapshot.to_multibuffer_offset(diff_edit.new.start)
..diff_map_snapshot.to_multibuffer_offset(diff_edit.new.end);
let start_ix = match self.inlays.binary_search_by(|probe| {
probe
.position
.to_offset(diff_map_snapshot.buffer())
.cmp(&edit_buffer_range.start)
.then(std::cmp::Ordering::Greater)
}) {
Ok(ix) | Err(ix) => ix,
};
for inlay in &self.inlays[start_ix..] {
let buffer_offset = inlay.position.to_offset(&diff_map_snapshot.buffer);
let diff_offset = diff_map_snapshot.to_diff_offset(buffer_offset);
if buffer_offset > edit_buffer_range.end {
break;
}
let prefix_start = DiffOffset(new_transforms.summary().input.len);
let prefix_end = diff_offset;
push_isomorphic(
&mut new_transforms,
diff_map_snapshot.text_summary_for_range(prefix_start..prefix_end),
);
if inlay.position.is_valid(&diff_map_snapshot.buffer) {
new_transforms.push(Transform::Inlay(inlay.clone()), &());
}
}
drop(cursor);
snapshot.transforms = new_transforms;
snapshot.version += 1;
snapshot.buffer = buffer_snapshot;
snapshot.check_invariants();
// Apply the rest of the edit.
let transform_start = DiffOffset(new_transforms.summary().input.len);
push_isomorphic(
&mut new_transforms,
diff_map_snapshot.text_summary_for_range(transform_start..diff_edit.new.end),
);
let new_end = InlayOffset(new_transforms.summary().output.len);
inlay_edits.push(Edit {
old: old_start..old_end,
new: new_start..new_end,
});
(snapshot.clone(), inlay_edits.into_inner())
// If the next edit doesn't intersect the current isomorphic transform, then
// we can push its remainder.
if diff_edits_iter
.peek()
.map_or(true, |edit| edit.old.start >= cursor.end(&()).0)
{
let transform_start = DiffOffset(new_transforms.summary().input.len);
let transform_end = diff_edit.new.end + (cursor.end(&()).0 - diff_edit.old.end);
push_isomorphic(
&mut new_transforms,
diff_map_snapshot.text_summary_for_range(transform_start..transform_end),
);
cursor.next(&());
}
}
new_transforms.append(cursor.suffix(&()), &());
if new_transforms.is_empty() {
new_transforms.push(Transform::Isomorphic(Default::default()), &());
}
drop(cursor);
snapshot.transforms = new_transforms;
snapshot.version += 1;
snapshot.diff_map_snapshot = diff_map_snapshot;
snapshot.check_invariants();
(snapshot.clone(), inlay_edits.into_inner())
}
pub fn splice(
@@ -552,7 +546,9 @@ impl InlayMap {
self.inlays.retain(|inlay| {
let retain = !to_remove.contains(&inlay.id);
if !retain {
let offset = inlay.position.to_offset(&snapshot.buffer);
let offset = snapshot
.diff_map_snapshot
.to_diff_offset(inlay.position.to_offset(&snapshot.diff_map_snapshot.buffer));
edits.insert(offset);
}
retain
@@ -564,11 +560,16 @@ impl InlayMap {
continue;
}
let offset = inlay_to_insert.position.to_offset(&snapshot.buffer);
match self.inlays.binary_search_by(|probe| {
probe
let offset = snapshot.diff_map_snapshot.to_diff_offset(
inlay_to_insert
.position
.cmp(&inlay_to_insert.position, &snapshot.buffer)
.to_offset(&snapshot.diff_map_snapshot.buffer),
);
match self.inlays.binary_search_by(|probe| {
probe.position.cmp(
&inlay_to_insert.position,
&snapshot.diff_map_snapshot.buffer,
)
}) {
Ok(ix) | Err(ix) => {
self.inlays.insert(ix, inlay_to_insert);
@@ -585,7 +586,7 @@ impl InlayMap {
new: offset..offset,
})
.collect();
let buffer_snapshot = snapshot.buffer.clone();
let buffer_snapshot = snapshot.diff_map_snapshot.clone();
let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
(snapshot, edits)
}
@@ -608,7 +609,8 @@ impl InlayMap {
let snapshot = &mut self.snapshot;
for i in 0..rng.gen_range(1..=5) {
if self.inlays.is_empty() || rng.gen() {
let position = snapshot.buffer.random_byte_range(0, rng).start;
let position = snapshot.buffer().random_byte_range(0, rng).start;
let anchor = snapshot.buffer().anchor_at(position, Bias::Left);
let bias = if rng.gen() { Bias::Left } else { Bias::Right };
let len = if rng.gen_bool(0.01) {
0
@@ -626,7 +628,7 @@ impl InlayMap {
InlayId::InlineCompletion(post_inc(next_inlay_id))
};
log::info!(
"creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}",
"creating inlay {:?} at buffer offset {:?} with bias {:?} and text {:?}",
inlay_id,
position,
bias,
@@ -635,7 +637,7 @@ impl InlayMap {
to_insert.push(Inlay {
id: inlay_id,
position: snapshot.buffer.anchor_at(position, bias),
position: anchor,
text: text.into(),
});
} else {
@@ -659,16 +661,16 @@ impl InlaySnapshot {
pub fn to_point(&self, offset: InlayOffset) -> InlayPoint {
let mut cursor = self
.transforms
.cursor::<(InlayOffset, (InlayPoint, usize))>(&());
.cursor::<(InlayOffset, (InlayPoint, DiffOffset))>(&());
cursor.seek(&offset, Bias::Right, &());
let overshoot = offset.0 - cursor.start().0 .0;
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
let buffer_offset_start = cursor.start().1 .1;
let buffer_offset_end = buffer_offset_start + overshoot;
let buffer_start = self.buffer.offset_to_point(buffer_offset_start);
let buffer_end = self.buffer.offset_to_point(buffer_offset_end);
InlayPoint(cursor.start().1 .0 .0 + (buffer_end - buffer_start))
let buffer_offset_end = buffer_offset_start + DiffOffset(overshoot);
let buffer_start = self.diff_map_snapshot.offset_to_point(buffer_offset_start);
let buffer_end = self.diff_map_snapshot.offset_to_point(buffer_offset_end);
cursor.start().1 .0 + InlayPoint(buffer_end.0 - buffer_start.0)
}
Some(Transform::Inlay(inlay)) => {
let overshoot = inlay.text.offset_to_point(overshoot);
@@ -689,51 +691,68 @@ impl InlaySnapshot {
pub fn to_offset(&self, point: InlayPoint) -> InlayOffset {
let mut cursor = self
.transforms
.cursor::<(InlayPoint, (InlayOffset, Point))>(&());
.cursor::<(InlayPoint, (InlayOffset, DiffPoint))>(&());
cursor.seek(&point, Bias::Right, &());
let overshoot = point.0 - cursor.start().0 .0;
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
let buffer_point_start = cursor.start().1 .1;
let buffer_point_end = buffer_point_start + overshoot;
let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start);
let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end);
InlayOffset(cursor.start().1 .0 .0 + (buffer_offset_end - buffer_offset_start))
let buffer_point_end = buffer_point_start + DiffPoint(overshoot);
let buffer_offset_start =
self.diff_map_snapshot.point_to_offset(buffer_point_start);
let buffer_offset_end = self.diff_map_snapshot.point_to_offset(buffer_point_end);
cursor.start().1 .0 + InlayOffset((buffer_offset_end - buffer_offset_start).0)
}
Some(Transform::Inlay(inlay)) => {
let overshoot = inlay.text.point_to_offset(overshoot);
InlayOffset(cursor.start().1 .0 .0 + overshoot)
cursor.start().1 .0 + InlayOffset(overshoot)
}
None => self.len(),
}
}
pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&());
pub fn to_diff_point(&self, point: InlayPoint) -> DiffPoint {
let mut cursor = self.transforms.cursor::<(InlayPoint, DiffPoint)>(&());
cursor.seek(&point, Bias::Right, &());
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
let overshoot = point.0 - cursor.start().0 .0;
cursor.start().1 + overshoot
cursor.start().1 + DiffPoint(overshoot)
}
Some(Transform::Inlay(_)) => cursor.start().1,
None => self.buffer.max_point(),
None => self.diff_map_snapshot.max_point(),
}
}
pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize {
let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&());
pub fn to_diff_offset(&self, offset: InlayOffset) -> DiffOffset {
let mut cursor = self.transforms.cursor::<(InlayOffset, DiffOffset)>(&());
cursor.seek(&offset, Bias::Right, &());
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
let overshoot = offset - cursor.start().0;
cursor.start().1 + overshoot.0
cursor.start().1 + DiffOffset(overshoot.0)
}
Some(Transform::Inlay(_)) => cursor.start().1,
None => self.buffer.len(),
None => self.diff_map_snapshot.len(),
}
}
pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset {
let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>(&());
pub fn buffer(&self) -> &MultiBufferSnapshot {
&self.diff_map_snapshot.buffer
}
pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
self.diff_map_snapshot
.to_multibuffer_point(self.to_diff_point(point))
}
pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize {
self.diff_map_snapshot
.to_multibuffer_offset(self.to_diff_offset(offset))
}
pub fn to_inlay_offset(&self, offset: DiffOffset) -> InlayOffset {
let mut cursor = self.transforms.cursor::<(DiffOffset, InlayOffset)>(&());
cursor.seek(&offset, Bias::Left, &());
loop {
match cursor.item() {
@@ -748,7 +767,7 @@ impl InlaySnapshot {
}
return cursor.end(&()).1;
} else {
let overshoot = offset - cursor.start().0;
let overshoot = offset.0 - cursor.start().0 .0;
return InlayOffset(cursor.start().1 .0 + overshoot);
}
}
@@ -765,8 +784,17 @@ impl InlaySnapshot {
}
}
}
pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>(&());
pub fn make_inlay_point(&self, multibuffer_point: Point) -> InlayPoint {
self.to_inlay_point(self.diff_map_snapshot.to_diff_point(multibuffer_point))
}
pub fn make_inlay_offset(&self, multibuffer_point: usize) -> InlayOffset {
self.to_inlay_offset(self.diff_map_snapshot.to_diff_offset(multibuffer_point))
}
pub fn to_inlay_point(&self, point: DiffPoint) -> InlayPoint {
let mut cursor = self.transforms.cursor::<(DiffPoint, InlayPoint)>(&());
cursor.seek(&point, Bias::Left, &());
loop {
match cursor.item() {
@@ -782,7 +810,7 @@ impl InlaySnapshot {
return cursor.end(&()).1;
} else {
let overshoot = point - cursor.start().0;
return InlayPoint(cursor.start().1 .0 + overshoot);
return InlayPoint(cursor.start().1 .0 + overshoot.0);
}
}
Some(Transform::Inlay(inlay)) => {
@@ -800,7 +828,7 @@ impl InlaySnapshot {
}
pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint {
let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&());
let mut cursor = self.transforms.cursor::<(InlayPoint, DiffPoint)>(&());
cursor.seek(&point, Bias::Left, &());
loop {
match cursor.item() {
@@ -836,10 +864,11 @@ impl InlaySnapshot {
}
} else {
let overshoot = point.0 - cursor.start().0 .0;
let buffer_point = cursor.start().1 + overshoot;
let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias);
let clipped_overshoot = clipped_buffer_point - cursor.start().1;
let clipped_point = InlayPoint(cursor.start().0 .0 + clipped_overshoot);
let diff_point = cursor.start().1 + DiffPoint(overshoot);
let clipped_diff_point =
self.diff_map_snapshot.clip_point(diff_point, bias);
let clipped_overshoot = clipped_diff_point - cursor.start().1;
let clipped_point = cursor.start().0 + InlayPoint(clipped_overshoot.0);
if clipped_point == point {
return clipped_point;
} else {
@@ -897,17 +926,19 @@ impl InlaySnapshot {
pub fn text_summary_for_range(&self, range: Range<InlayOffset>) -> TextSummary {
let mut summary = TextSummary::default();
let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&());
let mut cursor = self.transforms.cursor::<(InlayOffset, DiffOffset)>(&());
cursor.seek(&range.start, Bias::Right, &());
let overshoot = range.start.0 - cursor.start().0 .0;
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
let buffer_start = cursor.start().1;
let suffix_start = buffer_start + overshoot;
let suffix_end =
buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0);
summary = self.buffer.text_summary_for_range(suffix_start..suffix_end);
let suffix_start = buffer_start + DiffOffset(overshoot);
let suffix_end = buffer_start
+ DiffOffset(cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0);
summary = self
.diff_map_snapshot
.text_summary_for_range(suffix_start..suffix_end);
cursor.next(&());
}
Some(Transform::Inlay(inlay)) => {
@@ -928,10 +959,10 @@ impl InlaySnapshot {
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
let prefix_start = cursor.start().1;
let prefix_end = prefix_start + overshoot;
let prefix_end = prefix_start + DiffOffset(overshoot);
summary += self
.buffer
.text_summary_for_range::<TextSummary, _>(prefix_start..prefix_end);
.diff_map_snapshot
.text_summary_for_range(prefix_start..prefix_end);
}
Some(Transform::Inlay(inlay)) => {
let prefix_end = overshoot;
@@ -944,30 +975,30 @@ impl InlaySnapshot {
summary
}
pub fn buffer_rows(&self, row: u32) -> InlayBufferRows<'_> {
let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&());
pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> {
let mut cursor = self.transforms.cursor::<(InlayPoint, DiffPoint)>(&());
let inlay_point = InlayPoint::new(row, 0);
cursor.seek(&inlay_point, Bias::Left, &());
let max_buffer_row = self.buffer.max_row();
let mut buffer_point = cursor.start().1;
let buffer_row = if row == 0 {
MultiBufferRow(0)
let max_point = self.diff_map_snapshot.max_point();
let mut diff_point = cursor.start().1;
let diff_row = if row == 0 {
0
} else {
match cursor.item() {
Some(Transform::Isomorphic(_)) => {
buffer_point += inlay_point.0 - cursor.start().0 .0;
MultiBufferRow(buffer_point.row)
diff_point += DiffPoint(inlay_point.0 - cursor.start().0 .0);
diff_point.row()
}
_ => cmp::min(MultiBufferRow(buffer_point.row + 1), max_buffer_row),
_ => cmp::min(diff_point.row() + 1, max_point.row()),
}
};
InlayBufferRows {
transforms: cursor,
inlay_row: inlay_point.row(),
buffer_rows: self.buffer.buffer_rows(buffer_row),
max_buffer_row,
diff_rows: self.diff_map_snapshot.row_infos(diff_row),
max_point,
}
}
@@ -987,20 +1018,17 @@ impl InlaySnapshot {
language_aware: bool,
highlights: Highlights<'a>,
) -> InlayChunks<'a> {
let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&());
let mut cursor = self.transforms.cursor::<(InlayOffset, DiffOffset)>(&());
cursor.seek(&range.start, Bias::Right, &());
let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
let buffer_chunks = CustomHighlightsChunks::new(
buffer_range,
language_aware,
highlights.text_highlights,
&self.buffer,
);
let diff_range = self.to_diff_offset(range.start)..self.to_diff_offset(range.end);
let diff_map_chunks =
self.diff_map_snapshot
.chunks(diff_range, language_aware, highlights.text_highlights);
InlayChunks {
transforms: cursor,
buffer_chunks,
diff_map_chunks,
inlay_chunks: None,
inlay_chunk: None,
buffer_chunk: None,
@@ -1022,7 +1050,10 @@ impl InlaySnapshot {
fn check_invariants(&self) {
#[cfg(any(debug_assertions, feature = "test-support"))]
{
assert_eq!(self.transforms.summary().input, self.buffer.text_summary());
assert_eq!(
self.transforms.summary().input,
self.diff_map_snapshot.text_summary()
);
let mut transforms = self.transforms.iter().peekable();
while let Some(transform) = transforms.next() {
let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_));
@@ -1063,7 +1094,7 @@ fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
mod tests {
use super::*;
use crate::{
display_map::{InlayHighlights, TextHighlights},
display_map::{DiffMap, InlayHighlights, TextHighlights},
hover_links::InlayHighlight,
InlayId, MultiBuffer,
};
@@ -1163,7 +1194,8 @@ mod tests {
fn test_basic_inlays(cx: &mut AppContext) {
let buffer = MultiBuffer::build_simple("abcdefghi", cx);
let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
let (diff_map, diff_map_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_map_snapshot.clone());
assert_eq!(inlay_snapshot.text(), "abcdefghi");
let mut next_inlay_id = 0;
@@ -1177,27 +1209,27 @@ mod tests {
);
assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
assert_eq!(
inlay_snapshot.to_inlay_point(Point::new(0, 0)),
inlay_snapshot.make_inlay_point(Point::new(0, 0)),
InlayPoint::new(0, 0)
);
assert_eq!(
inlay_snapshot.to_inlay_point(Point::new(0, 1)),
inlay_snapshot.make_inlay_point(Point::new(0, 1)),
InlayPoint::new(0, 1)
);
assert_eq!(
inlay_snapshot.to_inlay_point(Point::new(0, 2)),
inlay_snapshot.make_inlay_point(Point::new(0, 2)),
InlayPoint::new(0, 2)
);
assert_eq!(
inlay_snapshot.to_inlay_point(Point::new(0, 3)),
inlay_snapshot.make_inlay_point(Point::new(0, 3)),
InlayPoint::new(0, 3)
);
assert_eq!(
inlay_snapshot.to_inlay_point(Point::new(0, 4)),
inlay_snapshot.make_inlay_point(Point::new(0, 4)),
InlayPoint::new(0, 9)
);
assert_eq!(
inlay_snapshot.to_inlay_point(Point::new(0, 5)),
inlay_snapshot.make_inlay_point(Point::new(0, 5)),
InlayPoint::new(0, 10)
);
assert_eq!(
@@ -1229,18 +1261,26 @@ mod tests {
buffer.update(cx, |buffer, cx| {
buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx)
});
let (inlay_snapshot, _) = inlay_map.sync(
buffer.read(cx).snapshot(cx),
buffer_edits.consume().into_inner(),
);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(
buffer.read(cx).snapshot(cx),
buffer_edits.consume().into_inner(),
cx,
)
});
let (inlay_snapshot, _) = inlay_map.sync(diff_snapshot, diff_edits);
assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi");
// An edit surrounding the inlay should invalidate it.
buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx));
let (inlay_snapshot, _) = inlay_map.sync(
buffer.read(cx).snapshot(cx),
buffer_edits.consume().into_inner(),
);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(
buffer.read(cx).snapshot(cx),
buffer_edits.consume().into_inner(),
cx,
)
});
let (inlay_snapshot, _) = inlay_map.sync(diff_snapshot, diff_edits);
assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
let (inlay_snapshot, _) = inlay_map.splice(
@@ -1262,10 +1302,16 @@ mod tests {
// Edits ending where the inlay starts should not move it if it has a left bias.
buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx));
let (inlay_snapshot, _) = inlay_map.sync(
buffer.read(cx).snapshot(cx),
buffer_edits.consume().into_inner(),
);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(
buffer.read(cx).snapshot(cx),
buffer_edits.consume().into_inner(),
cx,
)
});
let (inlay_snapshot, _) = inlay_map.sync(diff_snapshot, diff_edits);
assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi");
assert_eq!(
@@ -1450,7 +1496,8 @@ mod tests {
#[gpui::test]
fn test_inlay_buffer_rows(cx: &mut AppContext) {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
let (_diff_map, diff_map_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_map_snapshot);
assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi");
let mut next_inlay_id = 0;
@@ -1476,7 +1523,10 @@ mod tests {
);
assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
assert_eq!(
inlay_snapshot.buffer_rows(0).collect::<Vec<_>>(),
inlay_snapshot
.row_infos(0)
.map(|r| r.buffer_row)
.collect::<Vec<_>>(),
vec![Some(0), None, Some(1), None, None, Some(2)]
);
}
@@ -1501,7 +1551,8 @@ mod tests {
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
let mut next_inlay_id = 0;
log::info!("buffer text: {:?}", buffer_snapshot.text());
let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_map_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(diff_map_snapshot.clone());
for _ in 0..operations {
let mut inlay_edits = Patch::default();
@@ -1524,8 +1575,12 @@ mod tests {
}),
};
let (new_diff_snapshot, new_diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer.read(cx).snapshot(cx), buffer_edits, cx)
});
let (new_inlay_snapshot, new_inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
inlay_map.sync(new_diff_snapshot, new_diff_edits);
inlay_snapshot = new_inlay_snapshot;
inlay_edits = inlay_edits.compose(new_inlay_edits);
@@ -1547,7 +1602,7 @@ mod tests {
}
assert_eq!(inlay_snapshot.text(), expected_text.to_string());
let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::<Vec<_>>();
let expected_buffer_rows = inlay_snapshot.row_infos(0).collect::<Vec<_>>();
assert_eq!(
expected_buffer_rows.len() as u32,
expected_text.max_point().row + 1
@@ -1555,7 +1610,7 @@ mod tests {
for row_start in 0..expected_buffer_rows.len() {
assert_eq!(
inlay_snapshot
.buffer_rows(row_start as u32)
.row_infos(row_start as u32)
.collect::<Vec<_>>(),
&expected_buffer_rows[row_start..],
"incorrect buffer rows starting at {}",
@@ -1673,14 +1728,14 @@ mod tests {
assert_eq!(expected_text.len(), inlay_snapshot.len().0);
let mut buffer_point = Point::default();
let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
let mut inlay_point = inlay_snapshot.make_inlay_point(buffer_point);
let mut buffer_chars = buffer_snapshot.chars_at(0);
loop {
// Ensure conversion from buffer coordinates to inlay coordinates
// is consistent.
let buffer_offset = buffer_snapshot.point_to_offset(buffer_point);
assert_eq!(
inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)),
inlay_snapshot.to_point(inlay_snapshot.make_inlay_offset(buffer_offset)),
inlay_point
);
@@ -1707,7 +1762,7 @@ mod tests {
}
// Ensure that moving forward in the buffer always moves the inlay point forward as well.
let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
let new_inlay_point = inlay_snapshot.make_inlay_point(buffer_point);
assert!(new_inlay_point > inlay_point);
inlay_point = new_inlay_point;
} else {
@@ -1767,7 +1822,7 @@ mod tests {
// Ensure the clipped points are at valid buffer locations.
assert_eq!(
inlay_snapshot
.to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)),
.to_inlay_point(inlay_snapshot.to_diff_point(clipped_left_point)),
clipped_left_point,
"to_buffer_point({:?}) = {:?}",
clipped_left_point,
@@ -1775,7 +1830,7 @@ mod tests {
);
assert_eq!(
inlay_snapshot
.to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)),
.to_inlay_point(inlay_snapshot.to_diff_point(clipped_right_point)),
clipped_right_point,
"to_buffer_point({:?}) = {:?}",
clipped_right_point,

View File

@@ -164,7 +164,7 @@ pub struct TabSnapshot {
impl TabSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
&self.fold_snapshot.inlay_snapshot.buffer
self.fold_snapshot.inlay_snapshot.buffer()
}
pub fn line_len(&self, row: u32) -> u32 {
@@ -272,8 +272,8 @@ impl TabSnapshot {
}
}
pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
self.fold_snapshot.buffer_rows(row)
pub fn rows(&self, row: u32) -> fold_map::FoldRows<'_> {
self.fold_snapshot.row_infos(row)
}
#[cfg(test)]
@@ -317,8 +317,7 @@ impl TabSnapshot {
}
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
let fold_point = self.fold_snapshot.make_fold_point(point, bias);
self.to_tab_point(fold_point)
}
@@ -602,7 +601,7 @@ impl<'a> Iterator for TabChunks<'a> {
mod tests {
use super::*;
use crate::{
display_map::{fold_map::FoldMap, inlay_map::InlayMap},
display_map::{diff_map::DiffMap, fold_map::FoldMap, inlay_map::InlayMap},
MultiBuffer,
};
use rand::{prelude::StdRng, Rng};
@@ -610,8 +609,8 @@ mod tests {
#[gpui::test]
fn test_expand_tabs(cx: &mut gpui::AppContext) {
let buffer = MultiBuffer::build_simple("", cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = DiffMap::new(buffer, cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
@@ -627,8 +626,8 @@ mod tests {
let output = "A BC DEF G HI J K L M";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = DiffMap::new(buffer, cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
@@ -674,8 +673,8 @@ mod tests {
let input = "abcdefg⋯hij";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
@@ -688,8 +687,8 @@ mod tests {
let input = "\t \thello";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot);
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
@@ -749,8 +748,11 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
log::info!("Buffer text: {:?}", buffer_snapshot.text());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
log::info!("DiffMap text: {:?}", diff_snapshot.text());
let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
fold_map.randomly_mutate(&mut rng);
let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);

View File

@@ -1,5 +1,7 @@
use crate::RowInfo;
use super::{
fold_map::FoldBufferRows,
fold_map::FoldRows,
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
Highlights,
};
@@ -60,16 +62,16 @@ pub struct WrapChunks<'a> {
}
#[derive(Clone)]
pub struct WrapBufferRows<'a> {
input_buffer_rows: FoldBufferRows<'a>,
input_buffer_row: Option<u32>,
pub struct WrapRows<'a> {
input_buffer_rows: FoldRows<'a>,
input_buffer_row: RowInfo,
output_row: u32,
soft_wrapped: bool,
max_output_row: u32,
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
}
impl<'a> WrapBufferRows<'a> {
impl<'a> WrapRows<'a> {
pub(crate) fn seek(&mut self, start_row: u32) {
self.transforms
.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
@@ -717,7 +719,7 @@ impl WrapSnapshot {
self.transforms.summary().output.longest_row
}
pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows {
pub fn row_infos(&self, start_row: u32) -> WrapRows {
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
let mut input_row = transforms.start().1.row();
@@ -725,9 +727,9 @@ impl WrapSnapshot {
input_row += start_row - transforms.start().0.row();
}
let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic());
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row);
let mut input_buffer_rows = self.tab_snapshot.rows(input_row);
let input_buffer_row = input_buffer_rows.next().unwrap();
WrapBufferRows {
WrapRows {
transforms,
input_buffer_row,
input_buffer_rows,
@@ -847,7 +849,7 @@ impl WrapSnapshot {
}
let text = language::Rope::from(self.text().as_str());
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
let mut input_buffer_rows = self.tab_snapshot.rows(0);
let mut expected_buffer_rows = Vec::new();
let mut prev_tab_row = 0;
for display_row in 0..=self.max_point().row() {
@@ -855,7 +857,7 @@ impl WrapSnapshot {
if tab_point.row() == prev_tab_row && display_row != 0 {
expected_buffer_rows.push(None);
} else {
expected_buffer_rows.push(input_buffer_rows.next().unwrap());
expected_buffer_rows.push(input_buffer_rows.next().unwrap().buffer_row);
}
prev_tab_row = tab_point.row();
@@ -864,7 +866,8 @@ impl WrapSnapshot {
for start_display_row in 0..expected_buffer_rows.len() {
assert_eq!(
self.buffer_rows(start_display_row as u32)
self.row_infos(start_display_row as u32)
.map(|row_info| row_info.buffer_row)
.collect::<Vec<_>>(),
&expected_buffer_rows[start_display_row..],
"invalid buffer_rows({}..)",
@@ -958,8 +961,8 @@ impl<'a> Iterator for WrapChunks<'a> {
}
}
impl<'a> Iterator for WrapBufferRows<'a> {
type Item = Option<u32>;
impl<'a> Iterator for WrapRows<'a> {
type Item = RowInfo;
fn next(&mut self) -> Option<Self::Item> {
if self.output_row > self.max_output_row {
@@ -979,7 +982,11 @@ impl<'a> Iterator for WrapBufferRows<'a> {
self.soft_wrapped = true;
}
Some(if soft_wrapped { None } else { buffer_row })
Some(if soft_wrapped {
RowInfo::default()
} else {
buffer_row
})
}
}
@@ -1161,7 +1168,7 @@ fn consolidate_wrap_edits(edits: Vec<WrapEdit>) -> Vec<WrapEdit> {
mod tests {
use super::*;
use crate::{
display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
display_map::{diff_map::DiffMap, fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
MultiBuffer,
};
use gpui::{font, px, test::observe};
@@ -1209,9 +1216,11 @@ mod tests {
});
let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
log::info!("Buffer text: {:?}", buffer_snapshot.text());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx));
log::info!("DiffMap text: {:?}", diff_snapshot.text());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot);
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
@@ -1293,8 +1302,10 @@ mod tests {
}
log::info!("Buffer text: {:?}", buffer_snapshot.text());
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), buffer_edits, cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
log::info!("FoldMap text: {:?}", fold_snapshot.text());

View File

@@ -89,7 +89,7 @@ use gpui::{
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
pub(crate) use hunk_diff::HoveredHunk;
use hunk_diff::{diff_hunk_to_display, DiffMap, DiffMapSnapshot};
use hunk_diff::{diff_hunk_to_display, DiffMap};
use indent_guides::ActiveIndentGuidesState;
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use inline_completion::Direction;
@@ -128,7 +128,6 @@ use multi_buffer::{
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
};
use project::{
buffer_store::BufferChangeSet,
lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle},
project_settings::{GitGutterSetting, ProjectSettings},
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
@@ -606,7 +605,6 @@ pub struct Editor {
mode: EditorMode,
show_breadcrumbs: bool,
show_gutter: bool,
show_scrollbars: bool,
show_line_numbers: Option<bool>,
use_relative_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
@@ -727,7 +725,6 @@ pub struct EditorSnapshot {
git_blame_gutter_max_author_length: Option<usize>,
pub display_snapshot: DisplaySnapshot,
pub placeholder_text: Option<Arc<str>>,
diff_map: DiffMapSnapshot,
is_focused: bool,
scroll_anchor: ScrollAnchor,
ongoing_scroll: OngoingScroll,
@@ -992,14 +989,12 @@ pub(crate) struct FocusedBlock {
}
#[derive(Clone)]
enum JumpData {
MultiBufferRow(MultiBufferRow),
MultiBufferPoint {
excerpt_id: ExcerptId,
position: Point,
anchor: text::Anchor,
line_offset_from_top: u32,
},
struct JumpData {
excerpt_id: ExcerptId,
position: Point,
anchor: text::Anchor,
path: Option<project::ProjectPath>,
line_offset_from_top: u32,
}
impl Editor {
@@ -1209,7 +1204,12 @@ impl Editor {
let mut code_action_providers = Vec::new();
if let Some(project) = project.clone() {
get_unstaged_changes_for_buffers(&project, buffer.read(cx).all_buffers(), cx);
get_unstaged_changes_for_buffers(
&project,
buffer.read(cx).all_buffers(),
display_map.clone(),
cx,
);
code_action_providers.push(Rc::new(project) as Rc<_>);
}
@@ -1238,7 +1238,6 @@ impl Editor {
project,
blink_manager: blink_manager.clone(),
show_local_selections: true,
show_scrollbars: true,
mode,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode == EditorMode::Full,
@@ -1573,7 +1572,6 @@ impl Editor {
scroll_anchor: self.scroll_manager.anchor(),
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
placeholder_text: self.placeholder_text.clone(),
diff_map: self.diff_map.snapshot(),
is_focused: self.focus_handle.is_focused(cx),
current_line_highlight: self
.current_line_highlight
@@ -2478,7 +2476,7 @@ impl Editor {
}
if self.hide_context_menu(cx).is_some() {
if self.show_inline_completions_in_menu(cx) && self.has_active_inline_completion() {
if self.has_active_inline_completion() {
self.update_visible_inline_completion(cx);
}
return true;
@@ -2861,7 +2859,6 @@ impl Editor {
);
}
let had_active_inline_completion = this.has_active_inline_completion();
this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| {
s.select(new_selections)
});
@@ -2882,9 +2879,7 @@ impl Editor {
this.show_signature_help(&ShowSignatureHelp, cx);
}
let trigger_in_words =
this.show_inline_completions_in_menu(cx) || !had_active_inline_completion;
this.trigger_completion_on_input(&text, trigger_in_words, cx);
this.trigger_completion_on_input(&text, true, cx);
linked_editing_ranges::refresh_linked_ranges(this, cx);
this.refresh_inline_completion(true, false, cx);
});
@@ -3515,7 +3510,7 @@ impl Editor {
}
}
fn visible_inlay_hints(&self, cx: &ViewContext<Editor>) -> Vec<Inlay> {
fn visible_inlay_hints(&self, cx: &ViewContext<'_, Editor>) -> Vec<Inlay> {
self.display_map
.read(cx)
.current_inlays()
@@ -3690,6 +3685,10 @@ impl Editor {
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position);
let aside_was_displayed = match self.context_menu.borrow().deref() {
Some(CodeContextMenu::Completions(menu)) => menu.aside_was_displayed.get(),
_ => false,
};
let trigger_kind = match &options.trigger {
Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
CompletionTriggerKind::TRIGGER_CHARACTER
@@ -3724,6 +3723,7 @@ impl Editor {
position,
buffer.clone(),
completions.into(),
aside_was_displayed,
);
menu.filter(query.as_deref(), cx.background_executor().clone())
@@ -3747,14 +3747,11 @@ impl Editor {
if editor.focus_handle.is_focused(cx) && menu.is_some() {
let mut menu = menu.unwrap();
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
if editor.show_inline_completions_in_menu(cx) {
if let Some(hint) = editor.inline_completion_menu_hint(cx) {
menu.show_inline_completion_hint(hint);
}
} else {
editor.discard_inline_completion(false, cx);
if let Some(hint) = editor.inline_completion_menu_hint(cx) {
editor.hide_active_inline_completion(cx);
menu.show_inline_completion_hint(hint);
}
*editor.context_menu.borrow_mut() =
@@ -3763,14 +3760,9 @@ impl Editor {
cx.notify();
} else if editor.completion_tasks.len() <= 1 {
// If there are no more completion tasks and the last menu was
// empty, we should hide it.
let was_hidden = editor.hide_context_menu(cx).is_none();
// If it was already hidden and we don't show inline
// completions in the menu, we should also show the
// inline-completion when available.
if was_hidden && editor.show_inline_completions_in_menu(cx) {
editor.update_visible_inline_completion(cx);
}
// empty, we should hide it. If it was already hidden, we should
// also show the copilot completion when available.
editor.hide_context_menu(cx);
}
})?;
@@ -3824,19 +3816,14 @@ impl Editor {
return Some(Task::ready(Ok(())));
}
CompletionEntry::Match(mat) => {
if self.show_inline_completions_in_menu(cx) {
self.discard_inline_completion(true, cx);
}
self.discard_inline_completion(true, cx);
mat
}
};
let buffer_handle = completions_menu.buffer;
let completion = completions_menu
.completions
.borrow()
.get(mat.candidate_id)?
.clone();
let completions = completions_menu.completions.borrow_mut();
let completion = completions.get(mat.candidate_id)?;
cx.stop_propagation();
let snippet;
@@ -3980,11 +3967,9 @@ impl Editor {
}
let provider = self.completion_provider.as_ref()?;
drop(completion);
let apply_edits = provider.apply_additional_edits_for_completion(
buffer_handle,
completions_menu.completions.clone(),
mat.candidate_id,
completion.clone(),
true,
cx,
);
@@ -4569,9 +4554,7 @@ impl Editor {
_: &AcceptInlineCompletion,
cx: &mut ViewContext<Self>,
) {
if self.show_inline_completions_in_menu(cx) {
self.hide_context_menu(cx);
}
self.hide_context_menu(cx);
let Some(active_inline_completion) = self.active_inline_completion.as_ref() else {
return;
@@ -4720,6 +4703,17 @@ impl Editor {
Some(active_inline_completion.completion)
}
fn hide_active_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_inline_completion) = self.active_inline_completion.as_ref() {
self.splice_inlays(
active_inline_completion.inlay_ids.clone(),
Default::default(),
cx,
);
self.clear_highlights::<InlineCompletionHighlight>(cx);
}
}
fn update_visible_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
let selection = self.selections.newest_anchor();
let cursor = selection.head();
@@ -4727,11 +4721,7 @@ impl Editor {
let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
let excerpt_id = cursor.excerpt_id;
let completions_menu_has_precedence = !self.show_inline_completions_in_menu(cx)
&& (self.context_menu.borrow().is_some()
|| (!self.completion_tasks.is_empty() && !self.has_active_inline_completion()));
if completions_menu_has_precedence
|| !offset_selection.is_empty()
if !offset_selection.is_empty()
|| self
.active_inline_completion
.as_ref()
@@ -4755,10 +4745,16 @@ impl Editor {
let edits = completion
.edits
.into_iter()
.flat_map(|(range, new_text)| {
let start = multibuffer.anchor_in_excerpt(excerpt_id, range.start)?;
let end = multibuffer.anchor_in_excerpt(excerpt_id, range.end)?;
Some((start..end, new_text))
.map(|(range, new_text)| {
(
multibuffer
.anchor_in_excerpt(excerpt_id, range.start)
.unwrap()
..multibuffer
.anchor_in_excerpt(excerpt_id, range.end)
.unwrap(),
new_text,
)
})
.collect::<Vec<_>>();
if edits.is_empty() {
@@ -4789,32 +4785,34 @@ impl Editor {
invalidation_row_range = edit_start_row..cursor_row;
completion = InlineCompletion::Move(first_edit_start);
} else {
if edits
.iter()
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
{
let mut inlays = Vec::new();
for (range, new_text) in &edits {
let inlay = Inlay::inline_completion(
post_inc(&mut self.next_inlay_id),
range.start,
new_text.as_str(),
);
inlay_ids.push(inlay.id);
inlays.push(inlay);
}
if !self.has_active_completions_menu() {
if edits
.iter()
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
{
let mut inlays = Vec::new();
for (range, new_text) in &edits {
let inlay = Inlay::inline_completion(
post_inc(&mut self.next_inlay_id),
range.start,
new_text.as_str(),
);
inlay_ids.push(inlay.id);
inlays.push(inlay);
}
self.splice_inlays(vec![], inlays, cx);
} else {
let background_color = cx.theme().status().deleted_background;
self.highlight_text::<InlineCompletionHighlight>(
edits.iter().map(|(range, _)| range.clone()).collect(),
HighlightStyle {
background_color: Some(background_color),
..Default::default()
},
cx,
);
self.splice_inlays(vec![], inlays, cx);
} else {
let background_color = cx.theme().status().deleted_background;
self.highlight_text::<InlineCompletionHighlight>(
edits.iter().map(|(range, _)| range.clone()).collect(),
HighlightStyle {
background_color: Some(background_color),
..Default::default()
},
cx,
);
}
}
invalidation_row_range = edit_start_row..edit_end_row;
@@ -4834,7 +4832,7 @@ impl Editor {
invalidation_range,
});
if self.show_inline_completions_in_menu(cx) && self.has_active_completions_menu() {
if self.has_active_completions_menu() {
if let Some(hint) = self.inline_completion_menu_hint(cx) {
match self.context_menu.borrow_mut().as_mut() {
Some(CodeContextMenu::Completions(menu)) => {
@@ -4860,7 +4858,7 @@ impl Editor {
let text = match &self.active_inline_completion.as_ref()?.completion {
InlineCompletion::Edit(edits) => {
inline_completion_edit_text(&editor_snapshot, edits, true, cx)
inline_completion_edit_text(&editor_snapshot, edits, cx)
}
InlineCompletion::Move(target) => {
let target_point =
@@ -4885,13 +4883,6 @@ impl Editor {
Some(self.inline_completion_provider.as_ref()?.provider.clone())
}
fn show_inline_completions_in_menu(&self, cx: &AppContext) -> bool {
EditorSettings::get_global(cx).show_inline_completions_in_menu
&& self
.inline_completion_provider()
.map_or(false, |provider| provider.show_completions_in_menu())
}
fn render_code_actions_indicator(
&self,
_style: &EditorStyle,
@@ -5094,7 +5085,7 @@ impl Editor {
}))
}
#[cfg(any(feature = "test-support", test))]
#[cfg(any(test, feature = "test-support"))]
pub fn context_menu_visible(&self) -> bool {
self.context_menu
.borrow()
@@ -5130,27 +5121,12 @@ impl Editor {
) -> Option<AnyElement> {
self.context_menu.borrow().as_ref().and_then(|menu| {
if menu.visible() {
Some(menu.render(style, max_height_in_lines, cx))
} else {
None
}
})
}
fn render_context_menu_aside(
&self,
style: &EditorStyle,
max_size: Size<Pixels>,
cx: &mut ViewContext<Editor>,
) -> Option<AnyElement> {
self.context_menu.borrow().as_ref().and_then(|menu| {
if menu.visible() {
menu.render_aside(
Some(menu.render(
style,
max_size,
max_height_in_lines,
self.workspace.as_ref().map(|(w, _)| w.clone()),
cx,
)
))
} else {
None
}
@@ -5160,11 +5136,7 @@ impl Editor {
fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<CodeContextMenu> {
cx.notify();
self.completion_tasks.clear();
let context_menu = self.context_menu.borrow_mut().take();
if context_menu.is_some() && !self.show_inline_completions_in_menu(cx) {
self.update_visible_inline_completion(cx);
}
context_menu
self.context_menu.borrow_mut().take()
}
fn show_snippet_choices(
@@ -5453,8 +5425,7 @@ impl Editor {
if end_point == start_point {
let offset = text::ToOffset::to_offset(&range.start, &snapshot)
.saturating_sub(1);
start_point =
snapshot.clip_point(TP::to_point(&offset, &snapshot), Bias::Left);
start_point = TP::to_point(&offset, &snapshot);
};
(start_point..end_point, empty_str.clone())
@@ -5950,10 +5921,9 @@ impl Editor {
pub fn revert_file(&mut self, _: &RevertFile, cx: &mut ViewContext<Self>) {
let mut revert_changes = HashMap::default();
let snapshot = self.snapshot(cx);
for hunk in hunks_for_ranges(
Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter(),
&snapshot,
) {
for hunk in snapshot
.hunks_for_ranges(Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter())
{
self.prepare_revert_change(&mut revert_changes, &hunk, cx);
}
if !revert_changes.is_empty() {
@@ -5971,7 +5941,12 @@ impl Editor {
}
pub fn revert_selected_hunks(&mut self, _: &RevertSelectedHunks, cx: &mut ViewContext<Self>) {
let revert_changes = self.gather_revert_changes(&self.selections.all(cx), cx);
let selections = self.selections.all(cx).into_iter().map(|s| s.range());
let mut revert_changes = HashMap::default();
let snapshot = self.snapshot(cx);
for hunk in &snapshot.hunks_for_ranges(selections) {
self.prepare_revert_change(&mut revert_changes, &hunk, cx);
}
if !revert_changes.is_empty() {
self.transact(cx, |editor, cx| {
editor.revert(revert_changes, cx);
@@ -6008,28 +5983,18 @@ impl Editor {
}
}
fn gather_revert_changes(
&mut self,
selections: &[Selection<Point>],
cx: &mut ViewContext<Editor>,
) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>> {
let mut revert_changes = HashMap::default();
let snapshot = self.snapshot(cx);
for hunk in hunks_for_selections(&snapshot, selections) {
self.prepare_revert_change(&mut revert_changes, &hunk, cx);
}
revert_changes
}
pub fn prepare_revert_change(
&mut self,
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
hunk: &MultiBufferDiffHunk,
cx: &AppContext,
cx: &mut WindowContext,
) -> Option<()> {
let change_set = self
.display_map
.read(cx)
.diff_base_for(hunk.buffer_id, cx)?;
let buffer = self.buffer.read(cx).buffer(hunk.buffer_id)?;
let buffer = buffer.read(cx);
let change_set = &self.diff_map.diff_bases.get(&hunk.buffer_id)?.change_set;
let original_text = change_set
.read(cx)
.base_text
@@ -8784,10 +8749,9 @@ impl Editor {
.map(|selection| {
let old_range = selection.start..selection.end;
let mut new_range = old_range.clone();
let mut new_node = None;
while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone())
while let Some(containing_range) =
buffer.range_for_syntax_ancestor(new_range.clone())
{
new_node = Some(node);
new_range = containing_range;
if !display_map.intersects_fold(new_range.start)
&& !display_map.intersects_fold(new_range.end)
@@ -8796,17 +8760,6 @@ impl Editor {
}
}
if let Some(node) = new_node {
// Log the ancestor, to support using this action as a way to explore TreeSitter
// nodes. Parent and grandparent are also logged because this operation will not
// visit nodes that have the same range as their parent.
log::info!("Node: {node:?}");
let parent = node.parent();
log::info!("Parent: {parent:?}");
let grandparent = parent.and_then(|x| x.parent());
log::info!("Grandparent: {grandparent:?}");
}
selected_larger_node |= new_range != old_range;
Selection {
id: selection.id,
@@ -8938,7 +8891,7 @@ impl Editor {
fn templates_with_tags(
project: &Model<Project>,
runnable: &mut Runnable,
cx: &WindowContext,
cx: &WindowContext<'_>,
) -> Vec<(TaskSourceKind, TaskTemplate)> {
let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
let (worktree_id, file) = project
@@ -9250,16 +9203,15 @@ impl Editor {
&mut self,
snapshot: &EditorSnapshot,
position: Point,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) -> Option<MultiBufferDiffHunk> {
for (ix, position) in [position, Point::zero()].into_iter().enumerate() {
if let Some(hunk) = self.go_to_next_hunk_in_direction(
snapshot,
position,
ix > 0,
snapshot.diff_map.diff_hunks_in_range(
snapshot.diff_hunks_in_range(
position + Point::new(1, 0)..snapshot.buffer_snapshot.max_point(),
&snapshot.buffer_snapshot,
),
cx,
) {
@@ -9279,7 +9231,7 @@ impl Editor {
&mut self,
snapshot: &EditorSnapshot,
position: Point,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) -> Option<MultiBufferDiffHunk> {
for (ix, position) in [position, snapshot.buffer_snapshot.max_point()]
.into_iter()
@@ -9289,9 +9241,7 @@ impl Editor {
snapshot,
position,
ix > 0,
snapshot
.diff_map
.diff_hunks_in_range_rev(Point::zero()..position, &snapshot.buffer_snapshot),
snapshot.diff_hunks_in_range_rev(Point::zero()..position),
cx,
) {
return Some(hunk);
@@ -10421,7 +10371,7 @@ impl Editor {
self.end_transaction_at(Instant::now(), cx)
}
pub fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext<Self>) {
fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext<Self>) {
self.end_selection(cx);
if let Some(tx_id) = self
.buffer
@@ -10435,7 +10385,7 @@ impl Editor {
}
}
pub fn end_transaction_at(
fn end_transaction_at(
&mut self,
now: Instant,
cx: &mut ViewContext<Self>,
@@ -10974,6 +10924,29 @@ impl Editor {
self.display_map.read(cx).fold_placeholder.clone()
}
pub fn set_expand_all_diff_hunks(&mut self, cx: &mut AppContext) {
self.display_map.update(cx, |display_map, cx| {
display_map.set_all_hunks_expanded(cx);
});
}
pub fn expand_all_diff_hunks(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext<Self>) {
self.display_map.update(cx, |display_map, cx| {
display_map.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx)
});
}
pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext<Self>) {
let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect();
self.display_map.update(cx, |display_map, cx| {
if display_map.has_expanded_diff_hunks_in_ranges(&ranges, cx) {
display_map.collapse_diff_hunks(ranges, cx)
} else {
display_map.expand_diff_hunks(ranges, cx)
}
})
}
pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut ViewContext<Self>) {
if hovered != self.gutter_hovered {
self.gutter_hovered = hovered;
@@ -11269,11 +11242,6 @@ impl Editor {
cx.notify();
}
pub fn set_show_scrollbars(&mut self, show_scrollbars: bool, cx: &mut ViewContext<Self>) {
self.show_scrollbars = show_scrollbars;
cx.notify();
}
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut ViewContext<Self>) {
self.show_line_numbers = Some(show_line_numbers);
cx.notify();
@@ -12282,7 +12250,12 @@ impl Editor {
let buffer_id = buffer.read(cx).remote_id();
if !self.diff_map.diff_bases.contains_key(&buffer_id) {
if let Some(project) = &self.project {
get_unstaged_changes_for_buffers(project, [buffer.clone()], cx);
get_unstaged_changes_for_buffers(
project,
[buffer.clone()],
self.display_map.clone(),
cx,
);
}
}
cx.emit(EditorEvent::ExcerptsAdded {
@@ -12460,46 +12433,28 @@ impl Editor {
let mut new_selections_by_buffer = HashMap::default();
match &jump_data {
Some(JumpData::MultiBufferPoint {
excerpt_id,
position,
anchor,
line_offset_from_top,
}) => {
Some(jump_data) => {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
if let Some(buffer) = multi_buffer_snapshot
.buffer_id_for_excerpt(*excerpt_id)
.buffer_id_for_excerpt(jump_data.excerpt_id)
.and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id))
{
let buffer_snapshot = buffer.read(cx).snapshot();
let jump_to_point = if buffer_snapshot.can_resolve(anchor) {
language::ToPoint::to_point(anchor, &buffer_snapshot)
let jump_to_point = if buffer_snapshot.can_resolve(&jump_data.anchor) {
language::ToPoint::to_point(&jump_data.anchor, &buffer_snapshot)
} else {
buffer_snapshot.clip_point(*position, Bias::Left)
buffer_snapshot.clip_point(jump_data.position, Bias::Left)
};
let jump_to_offset = buffer_snapshot.point_to_offset(jump_to_point);
new_selections_by_buffer.insert(
buffer,
(
vec![jump_to_offset..jump_to_offset],
Some(*line_offset_from_top),
Some(jump_data.line_offset_from_top),
),
);
}
}
Some(JumpData::MultiBufferRow(row)) => {
let point = MultiBufferPoint::new(row.0, 0);
if let Some((buffer, buffer_point, _)) =
self.buffer.read(cx).point_to_buffer_point(point, cx)
{
let buffer_offset = buffer.read(cx).point_to_offset(buffer_point);
new_selections_by_buffer
.entry(buffer)
.or_insert((Vec::new(), None))
.0
.push(buffer_offset..buffer_offset)
}
}
None => {
let selections = self.selections.all::<usize>(cx);
let buffer = self.buffer.read(cx);
@@ -12983,14 +12938,6 @@ impl Editor {
.and_then(|item| item.to_any().downcast_ref::<T>())
}
pub fn add_change_set(
&mut self,
change_set: Model<BufferChangeSet>,
cx: &mut ViewContext<Self>,
) {
self.diff_map.add_change_set(change_set, cx);
}
fn character_size(&self, cx: &mut ViewContext<Self>) -> gpui::Point<Pixels> {
let text_layout_details = self.text_layout_details(cx);
let style = &text_layout_details.editor_style;
@@ -13012,7 +12959,8 @@ impl Editor {
fn get_unstaged_changes_for_buffers(
project: &Model<Project>,
buffers: impl IntoIterator<Item = Model<Buffer>>,
cx: &mut ViewContext<Editor>,
display_map: Model<DisplayMap>,
cx: &mut AppContext,
) {
let mut tasks = Vec::new();
project.update(cx, |project, cx| {
@@ -13020,16 +12968,17 @@ fn get_unstaged_changes_for_buffers(
tasks.push(project.open_unstaged_changes(buffer.clone(), cx))
}
});
cx.spawn(|this, mut cx| async move {
cx.spawn(|mut cx| async move {
let change_sets = futures::future::join_all(tasks).await;
this.update(&mut cx, |this, cx| {
for change_set in change_sets {
if let Some(change_set) = change_set.log_err() {
this.diff_map.add_change_set(change_set, cx);
display_map
.update(&mut cx, |display_map, cx| {
for change_set in change_sets {
if let Some(change_set) = change_set.log_err() {
display_map.add_change_set(change_set, cx);
}
}
}
})
.ok();
})
.ok();
})
.detach();
}
@@ -13317,56 +13266,6 @@ fn test_wrap_with_prefix() {
);
}
fn hunks_for_selections(
snapshot: &EditorSnapshot,
selections: &[Selection<Point>],
) -> Vec<MultiBufferDiffHunk> {
hunks_for_ranges(
selections.iter().map(|selection| selection.range()),
snapshot,
)
}
pub fn hunks_for_ranges(
ranges: impl Iterator<Item = Range<Point>>,
snapshot: &EditorSnapshot,
) -> Vec<MultiBufferDiffHunk> {
let mut hunks = Vec::new();
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
HashMap::default();
for query_range in ranges {
let query_rows =
MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1);
for hunk in snapshot.diff_map.diff_hunks_in_range(
Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0),
&snapshot.buffer_snapshot,
) {
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it
// when the caret is just above or just below the deleted hunk.
let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed;
let related_to_selection = if allow_adjacent {
hunk.row_range.overlaps(&query_rows)
|| hunk.row_range.start == query_rows.end
|| hunk.row_range.end == query_rows.start
} else {
hunk.row_range.overlaps(&query_rows)
};
if related_to_selection {
if !processed_buffer_rows
.entry(hunk.buffer_id)
.or_default()
.insert(hunk.buffer_range.start..hunk.buffer_range.end)
{
continue;
}
hunks.push(hunk);
}
}
}
hunks
}
pub trait CollaborationHub {
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator>;
fn user_participant_indices<'a>(
@@ -13472,14 +13371,11 @@ pub trait CompletionProvider {
fn apply_additional_edits_for_completion(
&self,
_buffer: Model<Buffer>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_completion_index: usize,
_push_to_history: bool,
_cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
buffer: Model<Buffer>,
completion: Completion,
push_to_history: bool,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>>;
fn is_completion_trigger(
&self,
@@ -13638,7 +13534,6 @@ fn snippet_completions(
Some(Completion {
old_range: range,
new_text: snippet.body.clone(),
resolved: false,
label: CodeLabel {
text: matching_prefix.clone(),
runs: vec![],
@@ -13704,30 +13599,19 @@ impl CompletionProvider for Model<Project> {
cx: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
self.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.resolve_completions(buffer, completion_indices, completions, cx)
})
project.resolve_completions(buffer, completion_indices, completions, cx)
})
}
fn apply_additional_edits_for_completion(
&self,
buffer: Model<Buffer>,
completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
completion: Completion,
push_to_history: bool,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
self.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.apply_additional_edits_for_completion(
buffer,
completions,
completion_index,
push_to_history,
cx,
)
})
project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
})
}
@@ -13862,7 +13746,7 @@ impl SemanticsProvider for Model<Project> {
fn inlay_hint_settings(
location: Anchor,
snapshot: &MultiBufferSnapshot,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) -> InlayHintSettings {
let file = snapshot.file_at(location);
let language = snapshot.language_at(location).map(|l| l.name());
@@ -13930,6 +13814,45 @@ impl EditorSnapshot {
})
}
pub fn hunks_for_ranges(
&self,
ranges: impl Iterator<Item = Range<Point>>,
) -> Vec<MultiBufferDiffHunk> {
let mut hunks = Vec::new();
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
HashMap::default();
for query_range in ranges {
let query_rows =
MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1);
for hunk in self.diff_snapshot().diff_hunks_in_range(
Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0),
) {
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it
// when the caret is just above or just below the deleted hunk.
let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed;
let related_to_selection = if allow_adjacent {
hunk.row_range.overlaps(&query_rows)
|| hunk.row_range.start == query_rows.end
|| hunk.row_range.end == query_rows.start
} else {
hunk.row_range.overlaps(&query_rows)
};
if related_to_selection {
if !processed_buffer_rows
.entry(hunk.buffer_id)
.or_default()
.insert(hunk.buffer_range.start..hunk.buffer_range.end)
{
continue;
}
hunks.push(hunk);
}
}
}
hunks
}
pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
self.display_snapshot.buffer_snapshot.language_at(position)
}
@@ -14684,7 +14607,6 @@ pub fn diagnostic_block_renderer(
fn inline_completion_edit_text(
editor_snapshot: &EditorSnapshot,
edits: &Vec<(Range<Anchor>, String)>,
include_deletions: bool,
cx: &WindowContext,
) -> InlineCompletionText {
let edit_start = edits
@@ -14708,24 +14630,12 @@ fn inline_completion_edit_text(
offset = old_offset_range.end;
let start = text.len();
let color = if include_deletions && new_text.is_empty() {
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(old_offset_range.start..offset, false)
.map(|chunk| chunk.text),
);
cx.theme().status().deleted_background
} else {
text.push_str(new_text);
cx.theme().status().created_background
};
text.push_str(new_text);
let end = text.len();
highlights.push((
start..end,
HighlightStyle {
background_color: Some(color),
background_color: Some(cx.theme().status().created_background),
..Default::default()
},
));

View File

@@ -35,7 +35,6 @@ pub struct EditorSettings {
pub auto_signature_help: bool,
pub show_signature_help_after_edits: bool,
pub jupyter: Jupyter,
pub show_inline_completions_in_menu: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -301,12 +300,6 @@ pub struct EditorSettingsContent {
/// Default: false
pub show_signature_help_after_edits: Option<bool>,
/// Whether to show the inline completions next to the completions provided by a language server.
/// Only has an effect if inline completion provider supports it.
///
/// Default: true
pub show_inline_completions_in_menu: Option<bool>,
/// Jupyter REPL settings.
pub jupyter: Option<JupyterContent>,
}

View File

@@ -25,18 +25,14 @@ use language::{
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use multi_buffer::MultiBufferIndentGuide;
use parking_lot::Mutex;
use pretty_assertions::{assert_eq, assert_ne};
use project::{buffer_store::BufferChangeSet, FakeFs};
use project::{
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
project_settings::{LspSettings, ProjectSettings},
};
use serde_json::{self, json};
use std::sync::atomic::{self, AtomicBool, AtomicUsize};
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use std::{
iter,
sync::atomic::{self, AtomicUsize},
};
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
use unindent::Unindent;
use util::{
@@ -3363,8 +3359,7 @@ async fn test_custom_newlines_cause_no_false_positive_diffs(
let snapshot = editor.snapshot(cx);
assert_eq!(
snapshot
.diff_map
.diff_hunks_in_range(0..snapshot.buffer_snapshot.len(), &snapshot.buffer_snapshot)
.diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
.collect::<Vec<_>>(),
Vec::new(),
"Should not have any diffs for files with custom newlines"
@@ -8402,6 +8397,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
additional edit
"});
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
update_test_language_settings(&mut cx, |settings| {
@@ -10697,14 +10693,10 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
..lsp::CompletionItem::default()
};
let item1 = item1.clone();
cx.handle_request::<lsp::request::Completion, _, _>({
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let item1 = item1.clone();
move |_, _, _| {
let item1 = item1.clone();
let item2 = item2.clone();
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
}
let item2 = item2.clone();
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
})
.next()
.await;
@@ -10731,41 +10723,43 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
}
});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
let item1 = item1.clone();
move |_, item_to_resolve, _| {
let item1 = item1.clone();
async move {
if item1 == item_to_resolve {
Ok(lsp::CompletionItem {
label: "method id()".to_string(),
filter_text: Some("id".to_string()),
detail: Some("Now resolved!".to_string()),
documentation: Some(lsp::Documentation::String("Docs".to_string())),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(
lsp::Position::new(0, 22),
lsp::Position::new(0, 22),
),
new_text: ".id".to_string(),
})),
..lsp::CompletionItem::default()
})
} else {
Ok(item_to_resolve)
}
}
}
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| async move {
Ok(lsp::CompletionItem {
label: "method id()".to_string(),
filter_text: Some("id".to_string()),
detail: Some("Now resolved!".to_string()),
documentation: Some(lsp::Documentation::String("Docs".to_string())),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(),
})),
..lsp::CompletionItem::default()
})
})
.next()
.await
.unwrap();
.await;
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.context_menu_next(&Default::default(), cx);
});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| async move {
Ok(lsp::CompletionItem {
label: "invalid changed label".to_string(),
detail: Some("Now resolved!".to_string()),
documentation: Some(lsp::Documentation::String("Docs".to_string())),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(),
})),
..lsp::CompletionItem::default()
})
})
.next()
.await;
cx.run_until_parked();
cx.update_editor(|editor, _| {
let context_menu = editor.context_menu.borrow_mut();
let context_menu = context_menu
@@ -10789,7 +10783,7 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
}
#[gpui::test]
async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
@@ -10808,181 +10802,8 @@ async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke(".");
let unresolved_item_1 = lsp::CompletionItem {
label: "id".to_string(),
filter_text: Some("id".to_string()),
detail: None,
documentation: None,
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(),
})),
..lsp::CompletionItem::default()
};
let resolved_item_1 = lsp::CompletionItem {
additional_text_edits: Some(vec![lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
new_text: "!!".to_string(),
}]),
..unresolved_item_1.clone()
};
let unresolved_item_2 = lsp::CompletionItem {
label: "other".to_string(),
filter_text: Some("other".to_string()),
detail: None,
documentation: None,
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".other".to_string(),
})),
..lsp::CompletionItem::default()
};
let resolved_item_2 = lsp::CompletionItem {
additional_text_edits: Some(vec![lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
new_text: "??".to_string(),
}]),
..unresolved_item_2.clone()
};
let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let unresolved_item_1 = unresolved_item_1.clone();
let resolved_item_1 = resolved_item_1.clone();
let unresolved_item_2 = unresolved_item_2.clone();
let resolved_item_2 = resolved_item_2.clone();
let resolve_requests_1 = resolve_requests_1.clone();
let resolve_requests_2 = resolve_requests_2.clone();
move |unresolved_request, _| {
let unresolved_item_1 = unresolved_item_1.clone();
let resolved_item_1 = resolved_item_1.clone();
let unresolved_item_2 = unresolved_item_2.clone();
let resolved_item_2 = resolved_item_2.clone();
let resolve_requests_1 = resolve_requests_1.clone();
let resolve_requests_2 = resolve_requests_2.clone();
async move {
if unresolved_request == unresolved_item_1 {
resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
Ok(resolved_item_1.clone())
} else if unresolved_request == unresolved_item_2 {
resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
Ok(resolved_item_2.clone())
} else {
panic!("Unexpected completion item {unresolved_request:?}")
}
}
}
})
.detach();
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let unresolved_item_1 = unresolved_item_1.clone();
let unresolved_item_2 = unresolved_item_2.clone();
async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
unresolved_item_1,
unresolved_item_2,
])))
}
})
.next()
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.update_editor(|editor, _| {
let context_menu = editor.context_menu.borrow_mut();
let context_menu = context_menu
.as_ref()
.expect("Should have the context menu deployed");
match context_menu {
CodeContextMenu::Completions(completions_menu) => {
let completions = completions_menu.completions.borrow_mut();
assert_eq!(
completions
.iter()
.map(|completion| &completion.label.text)
.collect::<Vec<_>>(),
vec!["id", "other"]
)
}
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
}
});
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.context_menu_next(&ContextMenuNext, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.context_menu_prev(&ContextMenuPrev, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.context_menu_next(&ContextMenuNext, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor
.compose_completion(&ComposeCompletion::default(), cx)
.expect("No task returned")
})
.await
.expect("Completion failed");
cx.run_until_parked();
cx.update_editor(|editor, cx| {
assert_eq!(
resolve_requests_1.load(atomic::Ordering::Acquire),
1,
"Should always resolve once despite multiple selections"
);
assert_eq!(
resolve_requests_2.load(atomic::Ordering::Acquire),
1,
"Should always resolve once after multiple selections and applying the completion"
);
assert_eq!(
editor.text(cx),
"fn main() { let a = ??.other; }",
"Should use resolved data when applying the completion"
);
});
}
#[gpui::test]
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let item_0 = lsp::CompletionItem {
label: "abs".into(),
insert_text: Some("abs".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "abs".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
};
let items = iter::once(item_0.clone())
.chain((11..51).map(|i| lsp::CompletionItem {
label: format!("item_{}", i),
insert_text: Some(format!("item_{}", i)),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
}))
.collect::<Vec<_>>();
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "default": "data"});
let default_data = json!({ "very": "special"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
@@ -10996,49 +10817,123 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
},
};
let item_0_out = lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
insert_text_format: Some(default_insert_text_format),
..item_0
};
let items_out = iter::once(item_0_out)
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
data: Some(default_data.clone()),
insert_text_mode: Some(default_insert_text_mode),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: item.label.clone(),
})),
..item.clone()
}))
.collect::<Vec<lsp::CompletionItem>>();
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
let expect_first_item = Arc::new(AtomicBool::new(true));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_default_data = default_data.clone();
let closure_resolve_requests_number = resolve_requests_number.clone();
let closure_expect_first_item = expect_first_item.clone();
let closure_default_commit_characters = default_commit_characters.clone();
move |item_to_resolve, _| {
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
let default_data = closure_default_data.clone();
let default_commit_characters = closure_default_commit_characters.clone();
let expect_first_item = closure_expect_first_item.clone();
async move {
if expect_first_item.load(atomic::Ordering::Acquire) {
assert_eq!(
item_to_resolve.label, "Some(2)",
"Should have selected the first item"
);
assert_eq!(
item_to_resolve.data,
Some(json!({ "very": "special"})),
"First item should bring its own data for resolving"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"First item had no own commit characters and should inherit the default ones"
);
assert!(
matches!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
),
"First item should bring its own edit range for resolving"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(default_insert_text_format),
"First item had no own insert text format and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
"First item should bring its own insert text mode for resolving"
);
Ok(item_to_resolve)
} else {
assert_eq!(
item_to_resolve.label, "vec![2]",
"Should have selected the last item"
);
assert_eq!(
item_to_resolve.data,
Some(default_data),
"Last item has no own resolve data and should inherit the default one"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"Last item had no own commit characters and should inherit the default ones"
);
assert_eq!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: "vec![2]".to_string()
})),
"Last item had no own edit range and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(lsp::InsertTextFormat::PLAIN_TEXT),
"Last item should bring its own insert text format for resolving"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(default_insert_text_mode),
"Last item had no own insert text mode and should inherit the default one"
);
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
resolve_provider: Some(true),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke(".");
Ok(item_to_resolve)
}
}
}
}).detach();
let completion_data = default_data.clone();
let completion_characters = default_commit_characters.clone();
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let default_data = completion_data.clone();
let default_commit_characters = completion_characters.clone();
let items = items.clone();
async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
items,
items: vec![
lsp::CompletionItem {
label: "Some(2)".into(),
insert_text: Some("Some(2)".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "Some(2)".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
},
lsp::CompletionItem {
label: "vec![2]".into(),
insert_text: Some("vec![2]".into()),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
},
],
item_defaults: Some(lsp::CompletionListItemDefaults {
data: Some(default_data.clone()),
commit_characters: Some(default_commit_characters.clone()),
@@ -11055,21 +10950,6 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
.next()
.await;
let resolved_items = Arc::new(Mutex::new(Vec::new()));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_resolved_items = resolved_items.clone();
move |item_to_resolve, _| {
let closure_resolved_items = closure_resolved_items.clone();
async move {
closure_resolved_items.lock().push(item_to_resolve.clone());
Ok(item_to_resolve)
}
}
})
.detach();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.run_until_parked();
@@ -11078,51 +10958,39 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
match menu.as_ref().expect("should have the completions menu") {
CodeContextMenu::Completions(completions_menu) => {
assert_eq!(
completions_menu
.entries
.iter()
.flat_map(|c| match c {
CompletionEntry::Match(mat) => Some(mat.string.clone()),
_ => None,
})
.collect::<Vec<String>>(),
items_out
.iter()
.map(|completion| completion.label.clone())
.collect::<Vec<String>>()
completion_menu_entries(&completions_menu.entries),
vec!["Some(2)", "vec![2]"]
);
}
CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
}
});
// Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
// with 4 from the end.
assert_eq!(
*resolved_items.lock(),
[
&items_out[0..16],
&items_out[items_out.len() - 4..items_out.len()]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
resolve_requests_number.load(atomic::Ordering::Acquire),
1,
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
);
resolved_items.lock().clear();
cx.update_editor(|editor, cx| {
editor.context_menu_prev(&ContextMenuPrev, cx);
editor.context_menu_first(&ContextMenuFirst, cx);
});
cx.run_until_parked();
// Completions that have already been resolved are skipped.
assert_eq!(
*resolved_items.lock(),
items_out[items_out.len() - 16..items_out.len() - 4]
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
resolve_requests_number.load(atomic::Ordering::Acquire),
2,
"After re-selecting the first item, another resolve request should have been sent"
);
expect_first_item.store(false, atomic::Ordering::Release);
cx.update_editor(|editor, cx| {
editor.context_menu_last(&ContextMenuLast, cx);
});
cx.run_until_parked();
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
3,
"After selecting the other item, another resolve request should have been sent"
);
resolved_items.lock().clear();
}
#[gpui::test]
@@ -11260,7 +11128,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
},
..Default::default()
},
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
Some(tree_sitter_rust::LANGUAGE.into()),
)));
update_test_language_settings(cx, |settings| {
settings.defaults.prettier = Some(PrettierSettings {
@@ -11786,7 +11654,9 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
cx,
)
});
editor.diff_map.add_change_set(change_set, cx)
editor.display_map.update(cx, |display_map, cx| {
display_map.add_change_set(change_set, cx)
});
}
});
cx.executor().run_until_parked();
@@ -12204,12 +12074,34 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
);
cx.update_editor(|editor, cx| {
for _ in 0..3 {
for _ in 0..2 {
editor.go_to_next_hunk(&GoToHunk, cx);
editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
}
});
executor.run_until_parked();
cx.assert_state_with_diff(
r#"
- use some::mod;
+ ˇuse some::modified;
fn main() {
- println!("hello");
+ println!("hello there");
+ println!("around the");
println!("world");
}
"#
.unindent(),
);
cx.update_editor(|editor, cx| {
editor.go_to_next_hunk(&GoToHunk, cx);
editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
});
executor.run_until_parked();
cx.assert_state_with_diff(
r#"
- use some::mod;
@@ -12295,7 +12187,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks(
executor.run_until_parked();
cx.update_editor(|editor, cx| {
editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
});
executor.run_until_parked();
cx.assert_state_with_diff(
@@ -12340,7 +12232,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks(
);
cx.update_editor(|editor, cx| {
editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
});
executor.run_until_parked();
cx.assert_state_with_diff(
@@ -12423,7 +12315,7 @@ async fn test_fold_unfold_diff_hunk(executor: BackgroundExecutor, cx: &mut gpui:
executor.run_until_parked();
cx.update_editor(|editor, cx| {
editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
});
executor.run_until_parked();
@@ -12616,7 +12508,9 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
cx,
)
});
editor.diff_map.add_change_set(change_set, cx)
editor.display_map.update(cx, |display_map, cx| {
display_map.add_change_set(change_set, cx)
});
}
})
.unwrap();
@@ -12726,14 +12620,16 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext
let buffer = buffer.read(cx).text_snapshot();
let change_set = cx
.new_model(|cx| BufferChangeSet::new_with_base_text(base.to_string(), buffer, cx));
editor.diff_map.add_change_set(change_set, cx)
editor.display_map.update(cx, |display_map, cx| {
display_map.add_change_set(change_set, cx)
})
})
.unwrap();
let mut cx = EditorTestContext::for_editor(editor, cx).await;
cx.run_until_parked();
cx.update_editor(|editor, cx| editor.expand_all_hunk_diffs(&Default::default(), cx));
cx.update_editor(|editor, cx| editor.expand_all_diff_hunks(&Default::default(), cx));
cx.executor().run_until_parked();
cx.assert_state_with_diff(
@@ -12797,7 +12693,7 @@ async fn test_edits_around_expanded_insertion_hunks(
executor.run_until_parked();
cx.update_editor(|editor, cx| {
editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
});
executor.run_until_parked();
@@ -12816,7 +12712,7 @@ async fn test_edits_around_expanded_insertion_hunks(
println!("world");
}
"#
"#
.unindent(),
);
@@ -12839,7 +12735,7 @@ async fn test_edits_around_expanded_insertion_hunks(
println!("world");
}
"#
"#
.unindent(),
);
@@ -12863,7 +12759,7 @@ async fn test_edits_around_expanded_insertion_hunks(
println!("world");
}
"#
"#
.unindent(),
);
@@ -12888,7 +12784,7 @@ async fn test_edits_around_expanded_insertion_hunks(
println!("world");
}
"#
"#
.unindent(),
);
@@ -12914,7 +12810,7 @@ async fn test_edits_around_expanded_insertion_hunks(
println!("world");
}
"#
"#
.unindent(),
);
@@ -12935,7 +12831,7 @@ async fn test_edits_around_expanded_insertion_hunks(
println!("world");
}
"#
"#
.unindent(),
);
}
@@ -12988,7 +12884,7 @@ async fn test_edits_around_expanded_deletion_hunks(
executor.run_until_parked();
cx.update_editor(|editor, cx| {
editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
});
executor.run_until_parked();
@@ -13007,7 +12903,7 @@ async fn test_edits_around_expanded_deletion_hunks(
println!("world");
}
"#
"#
.unindent(),
);
@@ -13030,7 +12926,7 @@ async fn test_edits_around_expanded_deletion_hunks(
println!("world");
}
"#
"#
.unindent(),
);
@@ -13053,7 +12949,7 @@ async fn test_edits_around_expanded_deletion_hunks(
println!("world");
}
"#
"#
.unindent(),
);
@@ -13077,7 +12973,7 @@ async fn test_edits_around_expanded_deletion_hunks(
println!("world");
}
"#
"#
.unindent(),
);
}
@@ -13130,7 +13026,7 @@ async fn test_edit_after_expanded_modification_hunk(
cx.set_diff_base(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, cx| {
editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
});
executor.run_until_parked();
@@ -14498,7 +14394,7 @@ async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppCon
fn test_inline_completion_text(cx: &mut TestAppContext) {
init_test(cx, |_| {});
// Simple insertion
// Test case 1: Simple insertion
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
@@ -14514,7 +14410,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
let edits = vec![(edit_range, " beautiful".to_string())];
let InlineCompletionText::Edit { text, highlights } =
inline_completion_edit_text(&snapshot, &edits, false, cx)
inline_completion_edit_text(&snapshot, &edits, cx)
else {
panic!("Failed to generate inline completion text");
};
@@ -14530,7 +14426,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
.unwrap();
}
// Replacement
// Test case 2: Replacement
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("This is a test.", cx);
@@ -14548,7 +14444,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
)];
let InlineCompletionText::Edit { text, highlights } =
inline_completion_edit_text(&snapshot, &edits, false, cx)
inline_completion_edit_text(&snapshot, &edits, cx)
else {
panic!("Failed to generate inline completion text");
};
@@ -14564,7 +14460,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
.unwrap();
}
// Multiple edits
// Test case 3: Multiple edits
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
@@ -14589,7 +14485,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
];
let InlineCompletionText::Edit { text, highlights } =
inline_completion_edit_text(&snapshot, &edits, false, cx)
inline_completion_edit_text(&snapshot, &edits, cx)
else {
panic!("Failed to generate inline completion text");
};
@@ -14610,7 +14506,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
.unwrap();
}
// Multiple lines with edits
// Test case 4: Multiple lines with edits
{
let window = cx.add_window(|cx| {
let buffer =
@@ -14641,7 +14537,7 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
];
let InlineCompletionText::Edit { text, highlights } =
inline_completion_edit_text(&snapshot, &edits, false, cx)
inline_completion_edit_text(&snapshot, &edits, cx)
else {
panic!("Failed to generate inline completion text");
};
@@ -14663,131 +14559,6 @@ fn test_inline_completion_text(cx: &mut TestAppContext) {
}
}
#[gpui::test]
fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
init_test(cx, |_| {});
// Deletion
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 5))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 11));
let edits = vec![(edit_range, "".to_string())];
let InlineCompletionText::Edit { text, highlights } =
inline_completion_edit_text(&snapshot, &edits, true, cx)
else {
panic!("Failed to generate inline completion text");
};
assert_eq!(text, "Hello, world!");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 5..11);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().deleted_background)
);
})
.unwrap();
}
// Insertion
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
let edits = vec![(edit_range, " digital".to_string())];
let InlineCompletionText::Edit { text, highlights } =
inline_completion_edit_text(&snapshot, &edits, true, cx)
else {
panic!("Failed to generate inline completion text");
};
assert_eq!(text, "Hello, digital world!");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 6..14);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
}
#[gpui::test]
async fn test_rename_with_duplicate_edits(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
cx.set_state(indoc! {"
struct Fˇoo {}
"});
cx.update_editor(|editor, cx| {
let highlight_range = Point::new(0, 7)..Point::new(0, 10);
let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
editor.highlight_background::<DocumentHighlightRead>(
&[highlight_range],
|c| c.editor_document_highlight_read_background,
cx,
);
});
cx.update_editor(|e, cx| e.rename(&Rename, cx))
.expect("Rename was not started")
.await
.expect("Rename failed");
let mut rename_handler =
cx.handle_request::<lsp::request::Rename, _, _>(move |url, _, _| async move {
let edit = lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 0,
character: 7,
},
end: lsp::Position {
line: 0,
character: 10,
},
},
new_text: "FooRenamed".to_string(),
};
Ok(Some(lsp::WorkspaceEdit::new(
// Specify the same edit twice
std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
)))
});
cx.update_editor(|e, cx| e.confirm_rename(&ConfirmRename, cx))
.expect("Confirm rename was not started")
.await
.expect("Confirm rename failed");
rename_handler.next().await.unwrap();
cx.run_until_parked();
// Despite two edits, only one is actually applied as those are identical
cx.assert_editor_state(indoc! {"
struct FooRenamedˇ {}
"});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point
@@ -14955,8 +14726,7 @@ fn assert_hunk_revert(
let reverted_hunk_statuses = cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let reverted_hunk_statuses = snapshot
.diff_map
.diff_hunks_in_range(0..snapshot.buffer_snapshot.len(), &snapshot.buffer_snapshot)
.diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
.map(|hunk| hunk_status(&hunk))
.collect::<Vec<_>>();

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,13 @@ use git::{
use gpui::{Model, ModelContext, Subscription, Task};
use http_client::HttpClient;
use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown};
use multi_buffer::MultiBufferRow;
use project::{Project, ProjectItem};
use smallvec::SmallVec;
use sum_tree::SumTree;
use url::Url;
use crate::RowInfo;
#[derive(Clone, Debug, Default)]
pub struct GitBlameEntry {
pub rows: u32,
@@ -194,15 +195,15 @@ impl GitBlame {
pub fn blame_for_rows<'a>(
&'a mut self,
rows: impl 'a + IntoIterator<Item = Option<MultiBufferRow>>,
rows: &'a [RowInfo],
cx: &mut ModelContext<Self>,
) -> impl 'a + Iterator<Item = Option<BlameEntry>> {
self.sync(cx);
let mut cursor = self.entries.cursor::<u32>(&());
rows.into_iter().map(move |row| {
let row = row?;
cursor.seek_forward(&row.0, Bias::Right, &());
rows.into_iter().map(move |info| {
let row = info.buffer_row?;
cursor.seek_forward(&row, Bias::Right, &());
cursor.item()?.blame.clone()
})
}
@@ -563,15 +564,38 @@ mod tests {
use unindent::Unindent as _;
use util::RandomCharIter;
macro_rules! assert_blame_rows {
($blame:expr, $rows:expr, $expected:expr, $cx:expr) => {
assert_eq!(
$blame
.blame_for_rows($rows.map(MultiBufferRow).map(Some), $cx)
.collect::<Vec<_>>(),
$expected
);
};
// macro_rules! assert_blame_rows {
// ($blame:expr, $rows:expr, $expected:expr, $cx:expr) => {
// assert_eq!(
// $blame
// .blame_for_rows($rows.map(MultiBufferRow).map(Some), $cx)
// .collect::<Vec<_>>(),
// $expected
// );
// };
// }
#[track_caller]
fn assert_blame_rows(
blame: &mut GitBlame,
rows: Range<u32>,
expected: Vec<Option<BlameEntry>>,
cx: &mut ModelContext<GitBlame>,
) {
assert_eq!(
blame
.blame_for_rows(
&rows
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
cx
)
.collect::<Vec<_>>(),
expected
);
}
fn init_test(cx: &mut gpui::TestAppContext) {
@@ -634,7 +658,15 @@ mod tests {
blame.update(cx, |blame, cx| {
assert_eq!(
blame
.blame_for_rows((0..1).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(0..1)
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
cx
)
.collect::<Vec<_>>(),
vec![None]
);
@@ -698,7 +730,15 @@ mod tests {
// All lines
assert_eq!(
blame
.blame_for_rows((0..8).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(0..8)
.map(|buffer_row| RowInfo {
buffer_row: Some(buffer_row),
..Default::default()
})
.collect::<Vec<_>>(),
cx
)
.collect::<Vec<_>>(),
vec![
Some(blame_entry("1b1b1b", 0..1)),
@@ -714,7 +754,15 @@ mod tests {
// Subset of lines
assert_eq!(
blame
.blame_for_rows((1..4).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(1..4)
.map(|buffer_row| RowInfo {
buffer_row: Some(buffer_row),
..Default::default()
})
.collect::<Vec<_>>(),
cx
)
.collect::<Vec<_>>(),
vec![
Some(blame_entry("0d0d0d", 1..2)),
@@ -725,7 +773,17 @@ mod tests {
// Subset of lines, with some not displayed
assert_eq!(
blame
.blame_for_rows(vec![Some(MultiBufferRow(1)), None, None], cx)
.blame_for_rows(
&[
RowInfo {
buffer_row: Some(1),
..Default::default()
},
Default::default(),
Default::default(),
],
cx
)
.collect::<Vec<_>>(),
vec![Some(blame_entry("0d0d0d", 1..2)), None, None]
);
@@ -777,16 +835,16 @@ mod tests {
git_blame.update(cx, |blame, cx| {
// Sanity check before edits: make sure that we get the same blame entry for all
// lines.
assert_blame_rows!(
assert_blame_rows(
blame,
(0..4),
0..4,
vec![
Some(blame_entry("1b1b1b", 0..4)),
Some(blame_entry("1b1b1b", 0..4)),
Some(blame_entry("1b1b1b", 0..4)),
Some(blame_entry("1b1b1b", 0..4)),
],
cx
cx,
);
});
@@ -795,11 +853,11 @@ mod tests {
buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "X")], None, cx);
});
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(
assert_blame_rows(
blame,
(0..2),
0..2,
vec![None, Some(blame_entry("1b1b1b", 0..4))],
cx
cx,
);
});
// Modify a single line, in the middle of the line
@@ -807,21 +865,21 @@ mod tests {
buffer.edit([(Point::new(1, 2)..Point::new(1, 2), "X")], None, cx);
});
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(
assert_blame_rows(
blame,
(1..4),
1..4,
vec![
None,
Some(blame_entry("1b1b1b", 0..4)),
Some(blame_entry("1b1b1b", 0..4))
Some(blame_entry("1b1b1b", 0..4)),
],
cx
cx,
);
});
// Before we insert a newline at the end, sanity check:
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(blame, (3..4), vec![Some(blame_entry("1b1b1b", 0..4))], cx);
assert_blame_rows(blame, 3..4, vec![Some(blame_entry("1b1b1b", 0..4))], cx);
});
// Insert a newline at the end
buffer.update(cx, |buffer, cx| {
@@ -829,17 +887,17 @@ mod tests {
});
// Only the new line is marked as edited:
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(
assert_blame_rows(
blame,
(3..5),
3..5,
vec![Some(blame_entry("1b1b1b", 0..4)), None],
cx
cx,
);
});
// Before we insert a newline at the start, sanity check:
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(blame, (2..3), vec![Some(blame_entry("1b1b1b", 0..4)),], cx);
assert_blame_rows(blame, 2..3, vec![Some(blame_entry("1b1b1b", 0..4))], cx);
});
// Usage example
@@ -849,11 +907,11 @@ mod tests {
});
// Only the new line is marked as edited:
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(
assert_blame_rows(
blame,
(2..4),
vec![None, Some(blame_entry("1b1b1b", 0..4)),],
cx
2..4,
vec![None, Some(blame_entry("1b1b1b", 0..4))],
cx,
);
});
}

View File

@@ -150,7 +150,7 @@ impl ProjectDiffEditor {
let editor = cx.new_view(|cx| {
let mut diff_display_editor =
Editor::for_multibuffer(excerpts.clone(), Some(project.clone()), true, cx);
diff_display_editor.set_expand_all_diff_hunks();
diff_display_editor.set_expand_all_diff_hunks(cx);
diff_display_editor
});
@@ -311,9 +311,11 @@ impl ProjectDiffEditor {
.update(&mut cx, |project_diff_editor, cx| {
project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx);
project_diff_editor.editor.update(cx, |editor, cx| {
for change_set in change_sets {
editor.diff_map.add_change_set(change_set, cx)
}
editor.display_map.update(cx, |display_map, cx| {
for change_set in change_sets {
display_map.add_change_set(change_set, cx)
}
});
});
})
.ok();
@@ -1193,9 +1195,9 @@ mod tests {
cx,
)
});
file_a_editor
.diff_map
.add_change_set(change_set.clone(), cx);
file_a_editor.display_map.update(cx, |display_map, cx| {
display_map.add_change_set(change_set.clone(), cx)
});
project.update(cx, |project, cx| {
project.buffer_store().update(cx, |buffer_store, cx| {
buffer_store.set_change_set(

View File

@@ -266,7 +266,7 @@ pub fn update_inlay_link_and_hover_points(
editor: &mut Editor,
secondary_held: bool,
shift_held: bool,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) {
let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))

View File

@@ -429,7 +429,7 @@ fn show_hover(
})
.or_else(|| {
let snapshot = &snapshot.buffer_snapshot;
let offset_range = snapshot.syntax_ancestor(anchor..anchor)?.1;
let offset_range = snapshot.range_for_syntax_ancestor(anchor..anchor)?;
Some(
snapshot.anchor_before(offset_range.start)
..snapshot.anchor_after(offset_range.end),

View File

@@ -6,11 +6,10 @@ use gpui::{
use language::{Buffer, BufferId, Point};
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
MultiBufferSnapshot, ToOffset, ToPoint,
MultiBufferSnapshot, ToPoint,
};
use project::buffer_store::BufferChangeSet;
use std::{ops::Range, sync::Arc};
use sum_tree::TreeMap;
use text::OffsetRangeExt;
use ui::{
prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement,
@@ -20,10 +19,10 @@ use util::RangeExt;
use workspace::Item;
use crate::{
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks,
ApplyDiffHunk, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight,
DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk,
RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
editor_settings::CurrentLineHighlight, hunk_status, ApplyAllDiffHunks, ApplyDiffHunk,
BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
DisplaySnapshot, Editor, EditorElement, GoToHunk, GoToPrevHunk, RevertFile,
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
};
#[derive(Debug, Clone)]
@@ -37,7 +36,6 @@ pub(super) struct HoveredHunk {
pub(super) struct DiffMap {
pub(crate) hunks: Vec<ExpandedHunk>,
pub(crate) diff_bases: HashMap<BufferId, DiffBaseState>,
pub(crate) snapshot: DiffMapSnapshot,
hunk_update_tasks: HashMap<Option<BufferId>, Task<()>>,
expand_all: bool,
}
@@ -51,9 +49,6 @@ pub(super) struct ExpandedHunk {
pub folded: bool,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct DiffMapSnapshot(TreeMap<BufferId, git::diff::BufferDiff>);
pub(crate) struct DiffBaseState {
pub(crate) change_set: Model<BufferChangeSet>,
pub(crate) last_version: Option<usize>,
@@ -74,133 +69,7 @@ pub enum DisplayDiffHunk {
},
}
impl DiffMap {
pub fn snapshot(&self) -> DiffMapSnapshot {
self.snapshot.clone()
}
pub fn add_change_set(
&mut self,
change_set: Model<BufferChangeSet>,
cx: &mut ViewContext<Editor>,
) {
let buffer_id = change_set.read(cx).buffer_id;
self.snapshot
.0
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
self.diff_bases.insert(
buffer_id,
DiffBaseState {
last_version: None,
_subscription: cx.observe(&change_set, move |editor, change_set, cx| {
editor
.diff_map
.snapshot
.0
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, cx);
}),
change_set,
},
);
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
}
pub fn hunks(&self, include_folded: bool) -> impl Iterator<Item = &ExpandedHunk> {
self.hunks
.iter()
.filter(move |hunk| include_folded || !hunk.folded)
}
}
impl DiffMapSnapshot {
pub fn is_empty(&self) -> bool {
self.0.values().all(|diff| diff.is_empty())
}
pub fn diff_hunks<'a>(
&'a self,
buffer_snapshot: &'a MultiBufferSnapshot,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
self.diff_hunks_in_range(0..buffer_snapshot.len(), buffer_snapshot)
}
pub fn diff_hunks_in_range<'a, T: ToOffset>(
&'a self,
range: Range<T>,
buffer_snapshot: &'a MultiBufferSnapshot,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot);
buffer_snapshot
.excerpts_for_range(range.clone())
.filter_map(move |excerpt| {
let buffer = excerpt.buffer();
let buffer_id = buffer.remote_id();
let diff = self.0.get(&buffer_id)?;
let buffer_range = excerpt.map_range_to_buffer(range.clone());
let buffer_range =
buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end);
Some(
diff.hunks_intersecting_range(buffer_range, excerpt.buffer())
.map(move |hunk| {
let start =
excerpt.map_point_from_buffer(Point::new(hunk.row_range.start, 0));
let end =
excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0));
MultiBufferDiffHunk {
row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row),
buffer_id,
buffer_range: hunk.buffer_range.clone(),
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
}
}),
)
})
.flatten()
}
pub fn diff_hunks_in_range_rev<'a, T: ToOffset>(
&'a self,
range: Range<T>,
buffer_snapshot: &'a MultiBufferSnapshot,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot);
buffer_snapshot
.excerpts_for_range_rev(range.clone())
.filter_map(move |excerpt| {
let buffer = excerpt.buffer();
let buffer_id = buffer.remote_id();
let diff = self.0.get(&buffer_id)?;
let buffer_range = excerpt.map_range_to_buffer(range.clone());
let buffer_range =
buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end);
Some(
diff.hunks_intersecting_range_rev(buffer_range, excerpt.buffer())
.map(move |hunk| {
let start_row = excerpt
.map_point_from_buffer(Point::new(hunk.row_range.start, 0))
.row;
let end_row = excerpt
.map_point_from_buffer(Point::new(hunk.row_range.end, 0))
.row;
MultiBufferDiffHunk {
row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row),
buffer_id,
buffer_range: hunk.buffer_range.clone(),
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
}
}),
)
})
.flatten()
}
}
impl Editor {
pub fn set_expand_all_diff_hunks(&mut self) {
self.diff_map.expand_all = true;
}
pub(super) fn toggle_hovered_hunk(
&mut self,
hovered_hunk: &HoveredHunk,
@@ -213,47 +82,6 @@ impl Editor {
}
}
pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext<Self>) {
let snapshot = self.snapshot(cx);
let selections = self.selections.all(cx);
self.toggle_hunks_expanded(hunks_for_selections(&snapshot, &selections), cx);
}
pub fn expand_all_hunk_diffs(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext<Self>) {
let snapshot = self.snapshot(cx);
let display_rows_with_expanded_hunks = self
.diff_map
.hunks(false)
.map(|hunk| &hunk.hunk_range)
.map(|anchor_range| {
(
anchor_range
.start
.to_display_point(&snapshot.display_snapshot)
.row(),
anchor_range
.end
.to_display_point(&snapshot.display_snapshot)
.row(),
)
})
.collect::<HashMap<_, _>>();
let hunks = self
.diff_map
.snapshot
.diff_hunks(&snapshot.display_snapshot.buffer_snapshot)
.filter(|hunk| {
let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0)
.to_display_point(&snapshot.display_snapshot)
..Point::new(hunk.row_range.end.0, 0)
.to_display_point(&snapshot.display_snapshot);
let row_range_end =
display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row());
row_range_end.is_none() || row_range_end != Some(&hunk_display_row_range.end.row())
});
self.toggle_hunks_expanded(hunks.collect(), cx);
}
fn toggle_hunks_expanded(
&mut self,
hunks_to_toggle: Vec<MultiBufferDiffHunk>,
@@ -365,7 +193,7 @@ impl Editor {
&mut self,
diff_base_buffer: Option<Model<Buffer>>,
hunk: &HoveredHunk,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) -> Option<()> {
let buffer = self.buffer.clone();
let multi_buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -454,7 +282,7 @@ impl Editor {
fn apply_diff_hunks_in_range(
&mut self,
range: Range<Anchor>,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) -> Option<()> {
let (buffer, range, _) = self
.buffer
@@ -497,7 +325,8 @@ impl Editor {
cx: &mut ViewContext<Self>,
) {
let snapshot = self.snapshot(cx);
let hunks = hunks_for_selections(&snapshot, &self.selections.all(cx));
let ranges = self.selections.all(cx).into_iter().map(|s| s.range());
let hunks = snapshot.hunks_for_ranges(ranges);
let mut ranges_by_buffer = HashMap::default();
self.transact(cx, |editor, cx| {
for hunk in hunks {
@@ -521,16 +350,14 @@ impl Editor {
}
}
fn has_multiple_hunks(&self, cx: &AppContext) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut hunks = self.diff_map.snapshot.diff_hunks(&snapshot);
hunks.nth(1).is_some()
fn has_multiple_hunks(&self, cx: &mut WindowContext) -> bool {
self.display_map.read(cx).has_multiple_hunks(cx)
}
fn hunk_header_block(
&self,
hunk: &HoveredHunk,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) -> BlockProperties<Anchor> {
let is_branch_buffer = self
.buffer
@@ -801,7 +628,7 @@ impl Editor {
hunk: &HoveredHunk,
diff_base_buffer: Model<Buffer>,
deleted_text_height: u32,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) -> BlockProperties<Anchor> {
let gutter_color = match hunk.status {
DiffHunkStatus::Added => unreachable!(),
@@ -864,30 +691,22 @@ impl Editor {
}
}
pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<Editor>) -> bool {
if self.diff_map.expand_all {
return false;
}
self.diff_map.hunk_update_tasks.clear();
self.clear_row_highlights::<DiffRowHighlight>();
let to_remove = self
.diff_map
.hunks
.drain(..)
.flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter())
.collect::<HashSet<_>>();
if to_remove.is_empty() {
false
} else {
self.remove_blocks(to_remove, None, cx);
true
}
pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
self.display_map.update(cx, |diff_map, cx| {
let ranges = vec![Anchor::min()..Anchor::max()];
if diff_map.has_expanded_diff_hunks_in_ranges(&ranges, cx) {
diff_map.collapse_diff_hunks(ranges, cx);
true
} else {
false
}
})
}
pub(super) fn sync_expanded_diff_hunks(
diff_map: &mut DiffMap,
buffer_id: BufferId,
cx: &mut ViewContext<Self>,
cx: &mut ViewContext<'_, Self>,
) {
let diff_base_state = diff_map.diff_bases.get_mut(&buffer_id);
let mut diff_base_buffer = None;
@@ -909,8 +728,7 @@ impl Editor {
.update(&mut cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let mut recalculated_hunks = snapshot
.diff_map
.diff_hunks(&snapshot.buffer_snapshot)
.diff_hunks()
.filter(|hunk| hunk.buffer_id == buffer_id)
.fuse()
.peekable();
@@ -1134,7 +952,7 @@ fn editor_with_deleted_text(
diff_base_buffer: Model<Buffer>,
deleted_color: Hsla,
hunk: &HoveredHunk,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) -> (u32, View<Editor>) {
let parent_editor = cx.view().downgrade();
let editor = cx.new_view(|cx| {
@@ -1155,11 +973,6 @@ fn editor_with_deleted_text(
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_gutter(false, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_scrollbars(false, cx);
editor.set_show_runnables(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_read_only(true);
editor.set_show_inline_completions(Some(false), cx);
@@ -1171,7 +984,7 @@ fn editor_with_deleted_text(
false,
cx,
);
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
editor.set_current_line_highlight(Some(CurrentLineHighlight::None)); //
editor
._subscriptions
.extend([cx.on_blur(&editor.focus_handle, |editor, cx| {
@@ -1423,7 +1236,9 @@ mod tests {
cx,
)
});
editor.diff_map.add_change_set(change_set, cx)
editor.display_map.update(cx, |display_map, cx| {
display_map.add_change_set(change_set, cx)
});
}
})
.unwrap();
@@ -1475,8 +1290,7 @@ mod tests {
assert_eq!(
snapshot
.diff_map
.diff_hunks_in_range(Point::zero()..Point::new(12, 0), &snapshot.buffer_snapshot)
.diff_hunks_in_range(Point::zero()..Point::new(12, 0))
.map(|hunk| (hunk_status(&hunk), hunk.row_range))
.collect::<Vec<_>>(),
&expected,
@@ -1484,11 +1298,7 @@ mod tests {
assert_eq!(
snapshot
.diff_map
.diff_hunks_in_range_rev(
Point::zero()..Point::new(12, 0),
&snapshot.buffer_snapshot
)
.diff_hunks_in_range_rev(Point::zero()..Point::new(12, 0))
.map(|hunk| (hunk_status(&hunk), hunk.row_range))
.collect::<Vec<_>>(),
expected

View File

@@ -579,7 +579,7 @@ impl InlayHintCache {
buffer_id: BufferId,
excerpt_id: ExcerptId,
id: InlayId,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) {
if let Some(excerpt_hints) = self.hints.get(&excerpt_id) {
let mut guard = excerpt_hints.write();
@@ -640,7 +640,7 @@ fn spawn_new_update_tasks(
excerpts_to_query: HashMap<ExcerptId, (Model<Buffer>, Global, Range<usize>)>,
invalidate: InvalidationStrategy,
update_cache_version: usize,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) {
for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in
excerpts_to_query
@@ -797,7 +797,7 @@ fn new_update_task(
query: ExcerptQuery,
query_ranges: QueryRanges,
excerpt_buffer: Model<Buffer>,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) -> Task<()> {
cx.spawn(move |editor, mut cx| async move {
let visible_range_update_results = future::join_all(
@@ -1129,7 +1129,7 @@ fn apply_hint_update(
invalidate: bool,
buffer_snapshot: BufferSnapshot,
multi_buffer_snapshot: MultiBufferSnapshot,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) {
let cached_excerpt_hints = editor
.inlay_hint_cache
@@ -3434,7 +3434,7 @@ pub mod tests {
labels
}
pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<Editor>) -> Vec<String> {
pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, Editor>) -> Vec<String> {
let mut hints = editor
.visible_inlay_hints(cx)
.into_iter()

View File

@@ -321,10 +321,6 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
"Fake Completion Provider"
}
fn show_completions_in_menu() -> bool {
false
}
fn is_enabled(
&self,
_buffer: &gpui::Model<language::Buffer>,

View File

@@ -1,8 +1,6 @@
use std::collections::hash_map::Entry;
use std::sync::Arc;
use crate::Editor;
use collections::HashMap;
use gpui::{Model, WindowContext};
use language::Buffer;
use language::Language;
@@ -22,7 +20,6 @@ where
return None;
};
let multibuffer = editor.buffer().read(cx);
let mut language_servers_for = HashMap::default();
editor
.selections
.disjoint_anchors()
@@ -31,36 +28,27 @@ where
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
.filter_map(|(buffer_id, trigger_anchor)| {
let buffer = multibuffer.buffer(buffer_id)?;
let server_id = *match language_servers_for.entry(buffer_id) {
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
Entry::Vacant(vacant_entry) => {
let language_server_id = project
.read(cx)
.language_servers_for_local_buffer(buffer.read(cx), cx)
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == language_server_name {
Some(server.server_id())
} else {
None
}
});
vacant_entry.insert(language_server_id)
}
}
.as_ref()?;
Some((buffer, trigger_anchor, server_id))
})
.find_map(|(buffer, trigger_anchor, server_id)| {
let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
if !filter_language(&language) {
return None;
}
Some((
trigger_anchor,
Arc::clone(&language),
server_id,
buffer.clone(),
))
Some((trigger_anchor, language, buffer))
})
.find_map(|(trigger_anchor, language, buffer)| {
project
.read(cx)
.language_servers_for_local_buffer(buffer.read(cx), cx)
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == language_server_name {
Some((
trigger_anchor,
Arc::clone(&language),
server.server_id(),
buffer.clone(),
))
} else {
None
}
})
})
}

View File

@@ -61,7 +61,7 @@ impl ProposedChangesEditor {
let mut this = Self {
editor: cx.new_view(|cx| {
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
editor.set_expand_all_diff_hunks();
editor.set_expand_all_diff_hunks(cx);
editor.set_completion_provider(None);
editor.clear_code_action_providers();
editor.set_semantics_provider(
@@ -223,9 +223,11 @@ impl ProposedChangesEditor {
self.buffer_entries = buffer_entries;
self.editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |selections| selections.refresh());
for change_set in new_change_sets {
editor.diff_map.add_change_set(change_set, cx)
}
editor.display_map.update(cx, |display_map, cx| {
for change_set in new_change_sets {
display_map.add_change_set(change_set, cx)
}
})
});
}
@@ -288,7 +290,7 @@ impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
impl Item for ProposedChangesEditor {
type Event = EditorEvent;
fn tab_icon(&self, _cx: &WindowContext) -> Option<Icon> {
fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
Some(Icon::new(IconName::Diff))
}

View File

@@ -33,7 +33,7 @@ pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
pub fn expand_macro_recursively(
editor: &mut Editor,
_: &ExpandMacroRecursively,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<'_, Editor>,
) {
if editor.selections.count() == 0 {
return;
@@ -98,7 +98,7 @@ pub fn expand_macro_recursively(
.detach_and_log_err(cx);
}
pub fn open_docs(editor: &mut Editor, _: &OpenDocs, cx: &mut ViewContext<Editor>) {
pub fn open_docs(editor: &mut Editor, _: &OpenDocs, cx: &mut ViewContext<'_, Editor>) {
if editor.selections.count() == 0 {
return;
}

View File

@@ -4,7 +4,7 @@ use crate::{
ScrollAnchor, ScrollCursorBottom, ScrollCursorCenter, ScrollCursorCenterTopBottom,
ScrollCursorTop, SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT,
};
use gpui::{AsyncWindowContext, Point, ViewContext};
use gpui::{Point, ViewContext};
impl Editor {
pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) {
@@ -75,7 +75,7 @@ impl Editor {
self.next_scroll_position = self.next_scroll_position.next();
self._scroll_cursor_center_top_bottom_task =
cx.spawn(|editor, mut cx: AsyncWindowContext| async move {
cx.spawn(|editor, mut cx: gpui::AsyncWindowContext| async move {
cx.background_executor()
.timer(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT)
.await;

View File

@@ -391,7 +391,7 @@ impl SelectionsCollection {
}
}
pub fn change_with<R>(
pub(crate) fn change_with<R>(
&mut self,
cx: &mut AppContext,
change: impl FnOnce(&mut MutableSelectionsCollection) -> R,
@@ -764,7 +764,7 @@ impl<'a> MutableSelectionsCollection<'a> {
pub fn replace_cursors_with(
&mut self,
find_replacement_cursors: impl FnOnce(&DisplaySnapshot) -> Vec<DisplayPoint>,
mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,
) {
let display_map = self.display_map();
let new_selections = find_replacement_cursors(&display_map)

View File

@@ -8,7 +8,7 @@ use workspace::Workspace;
fn task_context_with_editor(
editor: &mut Editor,
cx: &mut WindowContext,
cx: &mut WindowContext<'_>,
) -> AsyncTask<Option<TaskContext>> {
let Some(project) = editor.project.clone() else {
return AsyncTask::ready(None);
@@ -74,7 +74,7 @@ fn task_context_with_editor(
})
}
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext) -> AsyncTask<TaskContext> {
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> AsyncTask<TaskContext> {
let Some(editor) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))

View File

@@ -1,6 +1,6 @@
use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DiffRowHighlight, DisplayPoint,
Editor, MultiBuffer, RowExt,
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
RowExt,
};
use collections::BTreeMap;
use futures::Future;
@@ -11,11 +11,12 @@ use gpui::{
};
use itertools::Itertools;
use language::{Buffer, BufferSnapshot, LanguageRegistry};
use multi_buffer::{ExcerptRange, ToPoint};
use multi_buffer::ExcerptRange;
use parking_lot::RwLock;
use project::{FakeFs, Project};
use std::{
any::TypeId,
cmp,
ops::{Deref, DerefMut, Range},
path::Path,
sync::{
@@ -23,6 +24,7 @@ use std::{
Arc,
},
};
use text::Bias;
use util::{
assert_set_eq,
test::{generate_marked_text, marked_text_ranges},
@@ -332,83 +334,51 @@ impl EditorTestContext {
///
/// Diff hunks are indicated by lines starting with `+` and `-`.
#[track_caller]
pub fn assert_state_with_diff(&mut self, expected_diff: String) {
let has_diff_markers = expected_diff
.lines()
.any(|line| line.starts_with("+") || line.starts_with("-"));
let expected_diff_text = expected_diff
.split('\n')
.map(|line| {
let trimmed = line.trim();
if trimmed.is_empty() {
String::new()
} else if has_diff_markers {
line.to_string()
} else {
format!(" {line}")
}
})
.join("\n");
pub fn assert_state_with_diff(&mut self, expected_diff_text: String) {
let (snapshot, selections) = self
.editor
.update(&mut self.cx, |editor, cx| editor.selections.all_display(cx));
let actual_selections = self.editor_selections();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
let diff_snapshot = snapshot.diff_snapshot();
let diff_offsets = selections
.into_iter()
.map(|s| {
let start = snapshot.display_point_to_diff_offset(s.start, Bias::Left).0;
let end = snapshot.display_point_to_diff_offset(s.end, Bias::Left).0;
cmp::min(start, end)..cmp::max(start, end)
})
.collect::<Vec<_>>();
let actual_marked_text = generate_marked_text(&diff_snapshot.text(), &diff_offsets, true);
// Read the actual diff from the editor's row highlights and block
// decorations.
let actual_diff = self.editor.update(&mut self.cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let insertions = editor
.highlighted_rows::<DiffRowHighlight>()
.map(|(range, _)| {
let start = range.start.to_point(&snapshot.buffer_snapshot);
let end = range.end.to_point(&snapshot.buffer_snapshot);
start.row..end.row
})
.collect::<Vec<_>>();
let deletions = editor
.diff_map
.hunks
.iter()
.filter_map(|hunk| {
if hunk.blocks.is_empty() {
return None;
let line_infos = diff_snapshot.row_infos(0).collect::<Vec<_>>();
let has_diff = line_infos.iter().any(|info| info.diff_status.is_some());
let actual_diff = actual_marked_text
.split('\n')
.zip(line_infos)
.map(|(line, info)| {
let mut marker = match info.diff_status {
Some(DiffHunkStatus::Added) => "+ ",
Some(DiffHunkStatus::Removed) => "- ",
Some(DiffHunkStatus::Modified) => unreachable!(),
None => {
if has_diff {
" "
} else {
""
}
}
let row = hunk
.hunk_range
.start
.to_point(&snapshot.buffer_snapshot)
.row;
let (_, buffer, _) = editor
.buffer()
.read(cx)
.excerpt_containing(hunk.hunk_range.start, cx)
.expect("no excerpt for expanded buffer's hunk start");
let buffer_id = buffer.read(cx).remote_id();
let change_set = &editor
.diff_map
.diff_bases
.get(&buffer_id)
.expect("should have a diff base for expanded hunk")
.change_set;
let deleted_text = change_set
.read(cx)
.base_text
.as_ref()
.expect("no base text for expanded hunk")
.read(cx)
.as_rope()
.slice(hunk.diff_base_byte_range.clone())
.to_string();
if let DiffHunkStatus::Modified | DiffHunkStatus::Removed = hunk.status {
Some((row, deleted_text))
} else {
None
}
})
.collect::<Vec<_>>();
format_diff(actual_marked_text, deletions, insertions)
});
};
if line.is_empty() {
marker = marker.trim();
}
format!("{marker}{line}")
})
.collect::<Vec<_>>()
.join("\n");
pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state");
}
@@ -503,46 +473,6 @@ impl EditorTestContext {
}
}
fn format_diff(
text: String,
actual_deletions: Vec<(u32, String)>,
actual_insertions: Vec<Range<u32>>,
) -> String {
let mut diff = String::new();
for (row, line) in text.split('\n').enumerate() {
let row = row as u32;
if row > 0 {
diff.push('\n');
}
if let Some(text) = actual_deletions
.iter()
.find_map(|(deletion_row, deleted_text)| {
if *deletion_row == row {
Some(deleted_text)
} else {
None
}
})
{
for line in text.lines() {
diff.push('-');
if !line.is_empty() {
diff.push(' ');
diff.push_str(line);
}
diff.push('\n');
}
}
let marker = if actual_insertions.iter().any(|range| range.contains(&row)) {
"+ "
} else {
" "
};
diff.push_str(format!("{marker}{line}").trim_end());
}
diff
}
impl Deref for EditorTestContext {
type Target = gpui::VisualTestContext;

View File

@@ -30,7 +30,6 @@ semantic_version.workspace = true
serde.workspace = true
settings.workspace = true
smallvec.workspace = true
telemetry.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true

View File

@@ -48,8 +48,7 @@ impl RenderOnce for ExtensionCard {
.absolute()
.top_0()
.left_0()
.block_mouse_down()
.cursor_default()
.occlude()
.size_full()
.items_center()
.justify_center()

View File

@@ -1,3 +1,6 @@
use std::sync::Arc;
use client::telemetry::Telemetry;
use gpui::{AnyElement, Div, StyleRefinement};
use smallvec::SmallVec;
use ui::{prelude::*, ButtonLike};
@@ -5,15 +8,17 @@ use ui::{prelude::*, ButtonLike};
#[derive(IntoElement)]
pub struct FeatureUpsell {
base: Div,
telemetry: Arc<Telemetry>,
text: SharedString,
docs_url: Option<SharedString>,
children: SmallVec<[AnyElement; 2]>,
}
impl FeatureUpsell {
pub fn new(text: impl Into<SharedString>) -> Self {
pub fn new(telemetry: Arc<Telemetry>, text: impl Into<SharedString>) -> Self {
Self {
base: h_flex(),
telemetry,
text: text.into(),
docs_url: None,
children: SmallVec::new(),
@@ -62,13 +67,12 @@ impl RenderOnce for FeatureUpsell {
.child(Icon::new(IconName::ArrowUpRight)),
)
.on_click({
let telemetry = self.telemetry.clone();
let docs_url = docs_url.clone();
move |_event, cx| {
telemetry::event!(
"Documentation Viewed",
source = "Feature Upsell",
url = docs_url,
);
telemetry.report_app_event(format!(
"feature upsell: viewed docs ({docs_url})"
));
cx.open_url(&docs_url)
}
}),

Some files were not shown because too many files have changed in this diff Show More