Compare commits
43 Commits
v0.201.0-p
...
namespace_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76ab2d7a5a | ||
|
|
77c3a8c808 | ||
|
|
132daef9f6 | ||
|
|
4bee06e507 | ||
|
|
f23314bef4 | ||
|
|
697a39c251 | ||
|
|
d9ea97ee9c | ||
|
|
d8fc779a67 | ||
|
|
001ec97c0e | ||
|
|
2781a30971 | ||
|
|
e0613cbd0f | ||
|
|
1dd237139c | ||
|
|
f63d8e4c53 | ||
|
|
ad64a71f04 | ||
|
|
f435af2fde | ||
|
|
c5ee3f3e2e | ||
|
|
7f1bd2f15e | ||
|
|
62f2ef86dc | ||
|
|
fda6eda3c2 | ||
|
|
ed84767c9d | ||
|
|
cde0a5dd27 | ||
|
|
68f97d6069 | ||
|
|
5dcb90858e | ||
|
|
c731bb6d91 | ||
|
|
4b03d791b5 | ||
|
|
9a3e4c47d0 | ||
|
|
568e1d0a42 | ||
|
|
6f242772cc | ||
|
|
8ef9ecc91f | ||
|
|
3dd362978a | ||
|
|
74c0ba980b | ||
|
|
c20233e0b4 | ||
|
|
ffb995181e | ||
|
|
5120b6b7f9 | ||
|
|
c9c708ff08 | ||
|
|
9e34bb3f05 | ||
|
|
595cf1c6c3 | ||
|
|
d1820b183a | ||
|
|
fb7edbfb46 | ||
|
|
02dabbb9fa | ||
|
|
fa8bef1496 | ||
|
|
739e4551da | ||
|
|
b0bef3a9a2 |
3
.github/actionlint.yml
vendored
3
.github/actionlint.yml
vendored
@@ -19,11 +19,12 @@ self-hosted-runner:
|
||||
- namespace-profile-16x32-ubuntu-2004-arm
|
||||
- namespace-profile-32x64-ubuntu-2004-arm
|
||||
# Namespace Ubuntu 22.04 (Everything else)
|
||||
- namespace-profile-2x4-ubuntu-2204
|
||||
- namespace-profile-4x8-ubuntu-2204
|
||||
- namespace-profile-8x16-ubuntu-2204
|
||||
- namespace-profile-16x32-ubuntu-2204
|
||||
- namespace-profile-32x64-ubuntu-2204
|
||||
# Namespace Ubuntu 24.04 (like ubuntu-latest)
|
||||
- namespace-profile-2x4-ubuntu-2404
|
||||
# Namespace Limited Preview
|
||||
- namespace-profile-8x16-ubuntu-2004-arm-m4
|
||||
- namespace-profile-8x32-ubuntu-2004-arm-m4
|
||||
|
||||
2
.github/workflows/bump_collab_staging.yml
vendored
2
.github/workflows/bump_collab_staging.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
update-collab-staging-tag:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
run_nix: ${{ steps.filter.outputs.run_nix }}
|
||||
run_actionlint: ${{ steps.filter.outputs.run_actionlint }}
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
- namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
uses: ./.github/actions/build_docs
|
||||
|
||||
actionlint:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true'
|
||||
needs: [job_spec]
|
||||
steps:
|
||||
@@ -458,7 +458,7 @@ jobs:
|
||||
|
||||
tests_pass:
|
||||
name: Tests Pass
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
needs:
|
||||
- job_spec
|
||||
- style
|
||||
|
||||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
jobs:
|
||||
danger:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
6
.github/workflows/release_nightly.yml
vendored
6
.github/workflows/release_nightly.yml
vendored
@@ -206,9 +206,6 @@ jobs:
|
||||
runs-on: github-8vcpu-ubuntu-2404
|
||||
needs: tests
|
||||
name: Build Zed on FreeBSD
|
||||
# env:
|
||||
# MYTOKEN : ${{ secrets.MYTOKEN }}
|
||||
# MYTOKEN2: "value2"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build FreeBSD remote-server
|
||||
@@ -243,7 +240,6 @@ jobs:
|
||||
|
||||
bundle-nix:
|
||||
name: Build and cache Nix package
|
||||
if: false
|
||||
needs: tests
|
||||
secrets: inherit
|
||||
uses: ./.github/workflows/nix.yml
|
||||
@@ -294,7 +290,7 @@ jobs:
|
||||
update-nightly-tag:
|
||||
name: Update nightly tag
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
needs:
|
||||
- bundle-mac
|
||||
- bundle-linux-x86
|
||||
|
||||
2
.github/workflows/script_checks.yml
vendored
2
.github/workflows/script_checks.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
shellcheck:
|
||||
name: "ShellCheck Scripts"
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -171,9 +171,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.0.28"
|
||||
version = "0.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c887e795097665ab95119580534e7cc1335b59e1a7fec296943e534b970f4ed"
|
||||
checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures 0.3.31",
|
||||
@@ -268,11 +268,14 @@ dependencies = [
|
||||
"agent_settings",
|
||||
"agentic-coding-protocol",
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"context_server",
|
||||
"env_logger 0.11.8",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
@@ -284,6 +287,7 @@ dependencies = [
|
||||
"paths",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"reqwest_client",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
@@ -1375,10 +1379,11 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"derive_more 0.99.19",
|
||||
"gpui",
|
||||
"parking_lot",
|
||||
"rodio",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -9617,6 +9622,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"audio",
|
||||
"collections",
|
||||
"core-foundation 0.10.0",
|
||||
"core-video",
|
||||
@@ -9639,6 +9645,7 @@ dependencies = [
|
||||
"scap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sha2",
|
||||
"simplelog",
|
||||
"smallvec",
|
||||
@@ -20387,7 +20394,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.201.0"
|
||||
version = "0.202.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
|
||||
157
Cargo.toml
157
Cargo.toml
@@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
#
|
||||
|
||||
agentic-coding-protocol = "0.0.10"
|
||||
agent-client-protocol = "0.0.28"
|
||||
agent-client-protocol = "0.0.30"
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
@@ -802,147 +802,32 @@ unexpected_cfgs = { level = "allow" }
|
||||
dbg_macro = "deny"
|
||||
todo = "deny"
|
||||
|
||||
# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so
|
||||
# warning on this rule produces a lot of noise.
|
||||
single_range_in_vec_init = "allow"
|
||||
|
||||
redundant_clone = "warn"
|
||||
# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
|
||||
# Remove when the lint gets promoted to `suspicious`.
|
||||
declare_interior_mutable_const = "deny"
|
||||
|
||||
# These are all of the rules that currently have violations in the Zed
|
||||
# codebase.
|
||||
#
|
||||
# We'll want to drive this list down by either:
|
||||
# 1. fixing violations of the rule and begin enforcing it
|
||||
# 2. deciding we want to allow the rule permanently, at which point
|
||||
# we should codify that separately above.
|
||||
#
|
||||
# This list shouldn't be added to; it should only get shorter.
|
||||
# =============================================================================
|
||||
redundant_clone = "deny"
|
||||
|
||||
# There are a bunch of rules currently failing in the `style` group, so
|
||||
# allow all of those, for now.
|
||||
# We currently do not restrict any style rules
|
||||
# as it slows down shipping code to Zed.
|
||||
#
|
||||
# Running ./script/clippy can take several minutes, and so it's
|
||||
# common to skip that step and let CI do it. Any unexpected failures
|
||||
# (which also take minutes to discover) thus require switching back
|
||||
# to an old branch, manual fixing, and re-pushing.
|
||||
#
|
||||
# In the future we could improve this by either making sure
|
||||
# Zed can surface clippy errors in diagnostics (in addition to the
|
||||
# rust-analyzer errors), or by having CI fix style nits automatically.
|
||||
style = { level = "allow", priority = -1 }
|
||||
|
||||
# Temporary list of style lints that we've fixed so far.
|
||||
# Progress is being tracked in #36577
|
||||
blocks_in_conditions = "warn"
|
||||
bool_assert_comparison = "warn"
|
||||
borrow_interior_mutable_const = "warn"
|
||||
box_default = "warn"
|
||||
builtin_type_shadow = "warn"
|
||||
bytes_nth = "warn"
|
||||
chars_next_cmp = "warn"
|
||||
cmp_null = "warn"
|
||||
collapsible_else_if = "warn"
|
||||
collapsible_if = "warn"
|
||||
comparison_to_empty = "warn"
|
||||
default_instead_of_iter_empty = "warn"
|
||||
disallowed_macros = "warn"
|
||||
disallowed_methods = "warn"
|
||||
disallowed_names = "warn"
|
||||
disallowed_types = "warn"
|
||||
doc_lazy_continuation = "warn"
|
||||
doc_overindented_list_items = "warn"
|
||||
duplicate_underscore_argument = "warn"
|
||||
err_expect = "warn"
|
||||
fn_to_numeric_cast = "warn"
|
||||
fn_to_numeric_cast_with_truncation = "warn"
|
||||
for_kv_map = "warn"
|
||||
implicit_saturating_add = "warn"
|
||||
implicit_saturating_sub = "warn"
|
||||
inconsistent_digit_grouping = "warn"
|
||||
infallible_destructuring_match = "warn"
|
||||
inherent_to_string = "warn"
|
||||
init_numbered_fields = "warn"
|
||||
into_iter_on_ref = "warn"
|
||||
io_other_error = "warn"
|
||||
items_after_test_module = "warn"
|
||||
iter_cloned_collect = "warn"
|
||||
iter_next_slice = "warn"
|
||||
iter_nth = "warn"
|
||||
iter_nth_zero = "warn"
|
||||
iter_skip_next = "warn"
|
||||
just_underscores_and_digits = "warn"
|
||||
len_zero = "warn"
|
||||
let_and_return = "warn"
|
||||
main_recursion = "warn"
|
||||
manual_bits = "warn"
|
||||
manual_dangling_ptr = "warn"
|
||||
manual_is_ascii_check = "warn"
|
||||
manual_is_finite = "warn"
|
||||
manual_is_infinite = "warn"
|
||||
manual_map = "warn"
|
||||
manual_next_back = "warn"
|
||||
manual_non_exhaustive = "warn"
|
||||
manual_ok_or = "warn"
|
||||
manual_pattern_char_comparison = "warn"
|
||||
manual_rotate = "warn"
|
||||
manual_slice_fill = "warn"
|
||||
manual_while_let_some = "warn"
|
||||
map_clone = "warn"
|
||||
map_collect_result_unit = "warn"
|
||||
match_like_matches_macro = "warn"
|
||||
match_overlapping_arm = "warn"
|
||||
mem_replace_option_with_none = "warn"
|
||||
mem_replace_option_with_some = "warn"
|
||||
missing_enforced_import_renames = "warn"
|
||||
missing_safety_doc = "warn"
|
||||
mixed_attributes_style = "warn"
|
||||
mixed_case_hex_literals = "warn"
|
||||
module_inception = "warn"
|
||||
must_use_unit = "warn"
|
||||
mut_mutex_lock = "warn"
|
||||
needless_borrow = "warn"
|
||||
needless_doctest_main = "warn"
|
||||
needless_else = "warn"
|
||||
needless_parens_on_range_literals = "warn"
|
||||
needless_pub_self = "warn"
|
||||
needless_return = "warn"
|
||||
needless_return_with_question_mark = "warn"
|
||||
non_minimal_cfg = "warn"
|
||||
ok_expect = "warn"
|
||||
owned_cow = "warn"
|
||||
print_literal = "warn"
|
||||
print_with_newline = "warn"
|
||||
println_empty_string = "warn"
|
||||
ptr_eq = "warn"
|
||||
question_mark = "warn"
|
||||
redundant_closure = "warn"
|
||||
redundant_field_names = "warn"
|
||||
redundant_pattern_matching = "warn"
|
||||
redundant_static_lifetimes = "warn"
|
||||
result_map_or_into_option = "warn"
|
||||
self_named_constructors = "warn"
|
||||
single_match = "warn"
|
||||
tabs_in_doc_comments = "warn"
|
||||
to_digit_is_some = "warn"
|
||||
toplevel_ref_arg = "warn"
|
||||
unnecessary_fold = "warn"
|
||||
unnecessary_map_or = "warn"
|
||||
unnecessary_mut_passed = "warn"
|
||||
unnecessary_owned_empty_strings = "warn"
|
||||
unneeded_struct_pattern = "warn"
|
||||
unsafe_removed_from_name = "warn"
|
||||
unused_unit = "warn"
|
||||
unusual_byte_groupings = "warn"
|
||||
while_let_on_iterator = "warn"
|
||||
write_literal = "warn"
|
||||
write_with_newline = "warn"
|
||||
writeln_empty_string = "warn"
|
||||
wrong_self_convention = "warn"
|
||||
zero_ptr = "warn"
|
||||
|
||||
# Individual rules that have violations in the codebase:
|
||||
type_complexity = "allow"
|
||||
# We often return trait objects from `new` functions.
|
||||
new_ret_no_self = { level = "allow" }
|
||||
# We have a few `next` functions that differ in lifetimes
|
||||
# compared to Iterator::next. Yet, clippy complains about those.
|
||||
should_implement_trait = { level = "allow" }
|
||||
let_underscore_future = "allow"
|
||||
# It doesn't make sense to implement `Default` unilaterally.
|
||||
new_without_default = "allow"
|
||||
|
||||
# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so
|
||||
# warning on this rule produces a lot of noise.
|
||||
single_range_in_vec_init = "allow"
|
||||
|
||||
# in Rust it can be very tedious to reduce argument count without
|
||||
# running afoul of the borrow checker.
|
||||
@@ -951,10 +836,6 @@ too_many_arguments = "allow"
|
||||
# We often have large enum variants yet we rarely actually bother with splitting them up.
|
||||
large_enum_variant = "allow"
|
||||
|
||||
# `enum_variant_names` fires for all enums, even when they derive serde traits.
|
||||
# Adhering to this lint would be a breaking change.
|
||||
enum_variant_names = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = [
|
||||
"bindgen",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.333 10H8M13.333 6H2.66701" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.66699 8H10.667M2.66699 4H13.333M2.66699 12H7.99999" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 251 B |
@@ -1,27 +1,27 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 8.75V10.5C8.93097 10.5 8.06903 10.5 6 10.5V10L11 6V5.5H6V7.25" stroke="black" stroke-width="1.2"/>
|
||||
<path d="M11 8.75V10.5C8.93097 10.5 8.06903 10.5 6 10.5V10L11 6V5.5H6V7.25" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M2 8.5C2.27614 8.5 2.5 8.27614 2.5 8C2.5 7.72386 2.27614 7.5 2 7.5C1.72386 7.5 1.5 7.72386 1.5 8C1.5 8.27614 1.72386 8.5 2 8.5Z" fill="black"/>
|
||||
<path d="M2.99976 6.33002C3.2759 6.33002 3.49976 6.10616 3.49976 5.83002C3.49976 5.55387 3.2759 5.33002 2.99976 5.33002C2.72361 5.33002 2.49976 5.55387 2.49976 5.83002C2.49976 6.10616 2.72361 6.33002 2.99976 6.33002Z" fill="black"/>
|
||||
<path d="M2.99976 10.66C3.2759 10.66 3.49976 10.4361 3.49976 10.16C3.49976 9.88383 3.2759 9.65997 2.99976 9.65997C2.72361 9.65997 2.49976 9.88383 2.49976 10.16C2.49976 10.4361 2.72361 10.66 2.99976 10.66Z" fill="black"/>
|
||||
<path opacity="0.6" d="M2.99976 6.33002C3.2759 6.33002 3.49976 6.10616 3.49976 5.83002C3.49976 5.55387 3.2759 5.33002 2.99976 5.33002C2.72361 5.33002 2.49976 5.55387 2.49976 5.83002C2.49976 6.10616 2.72361 6.33002 2.99976 6.33002Z" fill="black"/>
|
||||
<path opacity="0.6" d="M2.99976 10.66C3.2759 10.66 3.49976 10.4361 3.49976 10.16C3.49976 9.88383 3.2759 9.65997 2.99976 9.65997C2.72361 9.65997 2.49976 9.88383 2.49976 10.16C2.49976 10.4361 2.72361 10.66 2.99976 10.66Z" fill="black"/>
|
||||
<path d="M15 8.5C15.2761 8.5 15.5 8.27614 15.5 8C15.5 7.72386 15.2761 7.5 15 7.5C14.7239 7.5 14.5 7.72386 14.5 8C14.5 8.27614 14.7239 8.5 15 8.5Z" fill="black"/>
|
||||
<path d="M14 6.33002C14.2761 6.33002 14.5 6.10616 14.5 5.83002C14.5 5.55387 14.2761 5.33002 14 5.33002C13.7239 5.33002 13.5 5.55387 13.5 5.83002C13.5 6.10616 13.7239 6.33002 14 6.33002Z" fill="black"/>
|
||||
<path d="M14 10.66C14.2761 10.66 14.5 10.4361 14.5 10.16C14.5 9.88383 14.2761 9.65997 14 9.65997C13.7239 9.65997 13.5 9.88383 13.5 10.16C13.5 10.4361 13.7239 10.66 14 10.66Z" fill="black"/>
|
||||
<path opacity="0.6" d="M14 6.33002C14.2761 6.33002 14.5 6.10616 14.5 5.83002C14.5 5.55387 14.2761 5.33002 14 5.33002C13.7239 5.33002 13.5 5.55387 13.5 5.83002C13.5 6.10616 13.7239 6.33002 14 6.33002Z" fill="black"/>
|
||||
<path opacity="0.6" d="M14 10.66C14.2761 10.66 14.5 10.4361 14.5 10.16C14.5 9.88383 14.2761 9.65997 14 9.65997C13.7239 9.65997 13.5 9.88383 13.5 10.16C13.5 10.4361 13.7239 10.66 14 10.66Z" fill="black"/>
|
||||
<path d="M8.49219 2C8.76833 2 8.99219 1.77614 8.99219 1.5C8.99219 1.22386 8.76833 1 8.49219 1C8.21605 1 7.99219 1.22386 7.99219 1.5C7.99219 1.77614 8.21605 2 8.49219 2Z" fill="black"/>
|
||||
<path d="M6 3C6.27614 3 6.5 2.77614 6.5 2.5C6.5 2.22386 6.27614 2 6 2C5.72386 2 5.5 2.22386 5.5 2.5C5.5 2.77614 5.72386 3 6 3Z" fill="black"/>
|
||||
<path opacity="0.6" d="M6 3C6.27614 3 6.5 2.77614 6.5 2.5C6.5 2.22386 6.27614 2 6 2C5.72386 2 5.5 2.22386 5.5 2.5C5.5 2.77614 5.72386 3 6 3Z" fill="black"/>
|
||||
<path d="M4 4C4.27614 4 4.5 3.77614 4.5 3.5C4.5 3.22386 4.27614 3 4 3C3.72386 3 3.5 3.22386 3.5 3.5C3.5 3.77614 3.72386 4 4 4Z" fill="black"/>
|
||||
<path d="M3.99976 13C4.2759 13 4.49976 12.7761 4.49976 12.5C4.49976 12.2239 4.2759 12 3.99976 12C3.72361 12 3.49976 12.2239 3.49976 12.5C3.49976 12.7761 3.72361 13 3.99976 13Z" fill="black"/>
|
||||
<path d="M2 12.5C2.27614 12.5 2.5 12.2761 2.5 12C2.5 11.7239 2.27614 11.5 2 11.5C1.72386 11.5 1.5 11.7239 1.5 12C1.5 12.2761 1.72386 12.5 2 12.5Z" fill="black"/>
|
||||
<path d="M2 4.5C2.27614 4.5 2.5 4.27614 2.5 4C2.5 3.72386 2.27614 3.5 2 3.5C1.72386 3.5 1.5 3.72386 1.5 4C1.5 4.27614 1.72386 4.5 2 4.5Z" fill="black"/>
|
||||
<path d="M15 12.5C15.2761 12.5 15.5 12.2761 15.5 12C15.5 11.7239 15.2761 11.5 15 11.5C14.7239 11.5 14.5 11.7239 14.5 12C14.5 12.2761 14.7239 12.5 15 12.5Z" fill="black"/>
|
||||
<path d="M15 4.5C15.2761 4.5 15.5 4.27614 15.5 4C15.5 3.72386 15.2761 3.5 15 3.5C14.7239 3.5 14.5 3.72386 14.5 4C14.5 4.27614 14.7239 4.5 15 4.5Z" fill="black"/>
|
||||
<path d="M3.99976 15C4.2759 15 4.49976 14.7761 4.49976 14.5C4.49976 14.2239 4.2759 14 3.99976 14C3.72361 14 3.49976 14.2239 3.49976 14.5C3.49976 14.7761 3.72361 15 3.99976 15Z" fill="black"/>
|
||||
<path d="M4 2C4.27614 2 4.5 1.77614 4.5 1.5C4.5 1.22386 4.27614 1 4 1C3.72386 1 3.5 1.22386 3.5 1.5C3.5 1.77614 3.72386 2 4 2Z" fill="black"/>
|
||||
<path d="M13 15C13.2761 15 13.5 14.7761 13.5 14.5C13.5 14.2239 13.2761 14 13 14C12.7239 14 12.5 14.2239 12.5 14.5C12.5 14.7761 12.7239 15 13 15Z" fill="black"/>
|
||||
<path d="M13 2C13.2761 2 13.5 1.77614 13.5 1.5C13.5 1.22386 13.2761 1 13 1C12.7239 1 12.5 1.22386 12.5 1.5C12.5 1.77614 12.7239 2 13 2Z" fill="black"/>
|
||||
<path opacity="0.2" d="M2 12.5C2.27614 12.5 2.5 12.2761 2.5 12C2.5 11.7239 2.27614 11.5 2 11.5C1.72386 11.5 1.5 11.7239 1.5 12C1.5 12.2761 1.72386 12.5 2 12.5Z" fill="black"/>
|
||||
<path opacity="0.2" d="M2 4.5C2.27614 4.5 2.5 4.27614 2.5 4C2.5 3.72386 2.27614 3.5 2 3.5C1.72386 3.5 1.5 3.72386 1.5 4C1.5 4.27614 1.72386 4.5 2 4.5Z" fill="black"/>
|
||||
<path opacity="0.2" d="M15 12.5C15.2761 12.5 15.5 12.2761 15.5 12C15.5 11.7239 15.2761 11.5 15 11.5C14.7239 11.5 14.5 11.7239 14.5 12C14.5 12.2761 14.7239 12.5 15 12.5Z" fill="black"/>
|
||||
<path opacity="0.2" d="M15 4.5C15.2761 4.5 15.5 4.27614 15.5 4C15.5 3.72386 15.2761 3.5 15 3.5C14.7239 3.5 14.5 3.72386 14.5 4C14.5 4.27614 14.7239 4.5 15 4.5Z" fill="black"/>
|
||||
<path opacity="0.5" d="M3.99976 15C4.2759 15 4.49976 14.7761 4.49976 14.5C4.49976 14.2239 4.2759 14 3.99976 14C3.72361 14 3.49976 14.2239 3.49976 14.5C3.49976 14.7761 3.72361 15 3.99976 15Z" fill="black"/>
|
||||
<path opacity="0.5" d="M4 2C4.27614 2 4.5 1.77614 4.5 1.5C4.5 1.22386 4.27614 1 4 1C3.72386 1 3.5 1.22386 3.5 1.5C3.5 1.77614 3.72386 2 4 2Z" fill="black"/>
|
||||
<path opacity="0.5" d="M13 15C13.2761 15 13.5 14.7761 13.5 14.5C13.5 14.2239 13.2761 14 13 14C12.7239 14 12.5 14.2239 12.5 14.5C12.5 14.7761 12.7239 15 13 15Z" fill="black"/>
|
||||
<path opacity="0.5" d="M13 2C13.2761 2 13.5 1.77614 13.5 1.5C13.5 1.22386 13.2761 1 13 1C12.7239 1 12.5 1.22386 12.5 1.5C12.5 1.77614 12.7239 2 13 2Z" fill="black"/>
|
||||
<path d="M13 4C13.2761 4 13.5 3.77614 13.5 3.5C13.5 3.22386 13.2761 3 13 3C12.7239 3 12.5 3.22386 12.5 3.5C12.5 3.77614 12.7239 4 13 4Z" fill="black"/>
|
||||
<path d="M13 13C13.2761 13 13.5 12.7761 13.5 12.5C13.5 12.2239 13.2761 12 13 12C12.7239 12 12.5 12.2239 12.5 12.5C12.5 12.7761 12.7239 13 13 13Z" fill="black"/>
|
||||
<path d="M11 3C11.2761 3 11.5 2.77614 11.5 2.5C11.5 2.22386 11.2761 2 11 2C10.7239 2 10.5 2.22386 10.5 2.5C10.5 2.77614 10.7239 3 11 3Z" fill="black"/>
|
||||
<path opacity="0.6" d="M11 3C11.2761 3 11.5 2.77614 11.5 2.5C11.5 2.22386 11.2761 2 11 2C10.7239 2 10.5 2.22386 10.5 2.5C10.5 2.77614 10.7239 3 11 3Z" fill="black"/>
|
||||
<path d="M8.5 15C8.77614 15 9 14.7761 9 14.5C9 14.2239 8.77614 14 8.5 14C8.22386 14 8 14.2239 8 14.5C8 14.7761 8.22386 15 8.5 15Z" fill="black"/>
|
||||
<path d="M6 14C6.27614 14 6.5 13.7761 6.5 13.5C6.5 13.2239 6.27614 13 6 13C5.72386 13 5.5 13.2239 5.5 13.5C5.5 13.7761 5.72386 14 6 14Z" fill="black"/>
|
||||
<path d="M11 14C11.2761 14 11.5 13.7761 11.5 13.5C11.5 13.2239 11.2761 13 11 13C10.7239 13 10.5 13.2239 10.5 13.5C10.5 13.7761 10.7239 14 11 14Z" fill="black"/>
|
||||
<path opacity="0.6" d="M6 14C6.27614 14 6.5 13.7761 6.5 13.5C6.5 13.2239 6.27614 13 6 13C5.72386 13 5.5 13.2239 5.5 13.5C5.5 13.7761 5.72386 14 6 14Z" fill="black"/>
|
||||
<path opacity="0.6" d="M11 14C11.2761 14 11.5 13.7761 11.5 13.5C11.5 13.2239 11.2761 13 11 13C10.7239 13 10.5 13.2239 10.5 13.5C10.5 13.7761 10.7239 14 11 14Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -1,5 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 2.93652L6.9243 6.20697C6.86924 6.37435 6.77565 6.52646 6.65105 6.65105C6.52646 6.77565 6.37435 6.86924 6.20697 6.9243L2.93652 8L6.20697 9.0757C6.37435 9.13076 6.52646 9.22435 6.65105 9.34895C6.77565 9.47354 6.86924 9.62565 6.9243 9.79306L8 13.0635L9.0757 9.79306C9.13076 9.62565 9.22435 9.47354 9.34895 9.34895C9.47354 9.22435 9.62565 9.13076 9.79306 9.0757L13.0635 8L9.79306 6.9243C9.62565 6.86924 9.47354 6.77565 9.34895 6.65105C9.22435 6.52646 9.13076 6.37435 9.0757 6.20697L8 2.93652Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -138,7 +138,7 @@
|
||||
"find": "buffer_search::Deploy",
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
"ctrl-h": "buffer_search::DeployReplace",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl->": "agent::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
@@ -241,7 +241,7 @@
|
||||
"ctrl-shift-i": "agent::ToggleOptionsMenu",
|
||||
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl->": "agent::QuoteSelection",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-enter": "agent::ContinueThread",
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
"cmd-alt-f": "buffer_search::DeployReplace",
|
||||
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
|
||||
"cmd-e": ["buffer_search::Deploy", { "focus": false }],
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd->": "agent::QuoteSelection",
|
||||
"cmd-<": "assistant::InsertIntoEditor",
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
@@ -281,7 +281,7 @@
|
||||
"cmd-shift-i": "agent::ToggleOptionsMenu",
|
||||
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd->": "agent::QuoteSelection",
|
||||
"cmd-alt-e": "agent::RemoveAllContext",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-ctrl-b": "agent::ToggleBurnMode",
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"bindings": {
|
||||
"ctrl-i": "agent::ToggleFocus",
|
||||
"ctrl-shift-i": "agent::ToggleFocus",
|
||||
"ctrl-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode
|
||||
"ctrl-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode
|
||||
"ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
|
||||
"ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
|
||||
"ctrl-k": "assistant::InlineAssist",
|
||||
"ctrl-shift-k": "assistant::InsertIntoEditor"
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"bindings": {
|
||||
"cmd-i": "agent::ToggleFocus",
|
||||
"cmd-shift-i": "agent::ToggleFocus",
|
||||
"cmd-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode
|
||||
"cmd-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode
|
||||
"cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
|
||||
"cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
|
||||
"cmd-k": "assistant::InlineAssist",
|
||||
"cmd-shift-k": "assistant::InsertIntoEditor"
|
||||
}
|
||||
|
||||
@@ -1381,7 +1381,7 @@ impl AcpThread {
|
||||
let canceled = matches!(
|
||||
result,
|
||||
Ok(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Canceled
|
||||
stop_reason: acp::StopReason::Cancelled
|
||||
}))
|
||||
);
|
||||
|
||||
|
||||
@@ -420,7 +420,7 @@ mod test_support {
|
||||
.response_tx
|
||||
.take()
|
||||
{
|
||||
end_turn_tx.send(acp::StopReason::Canceled).unwrap();
|
||||
end_turn_tx.send(acp::StopReason::Cancelled).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,7 @@ impl Diff {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
|
||||
|
||||
let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
|
||||
let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx));
|
||||
let new_buffer_snapshot = new_buffer.read(cx).text_snapshot();
|
||||
let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx));
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local(new_text, cx));
|
||||
let task = cx.spawn({
|
||||
let multibuffer = multibuffer.clone();
|
||||
let path = path.clone();
|
||||
@@ -43,42 +38,34 @@ impl Diff {
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?;
|
||||
buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?;
|
||||
|
||||
let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_language(language, cx);
|
||||
buffer.snapshot()
|
||||
})?;
|
||||
|
||||
buffer_diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.set_base_text(
|
||||
old_buffer_snapshot,
|
||||
Some(language_registry),
|
||||
new_buffer_snapshot,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
let diff = build_buffer_diff(
|
||||
old_text.unwrap_or("".into()).into(),
|
||||
&buffer,
|
||||
Some(language_registry.clone()),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
multibuffer
|
||||
.update(cx, |multibuffer, cx| {
|
||||
let hunk_ranges = {
|
||||
let buffer = new_buffer.read(cx);
|
||||
let diff = buffer_diff.read(cx);
|
||||
let buffer = buffer.read(cx);
|
||||
let diff = diff.read(cx);
|
||||
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&new_buffer, cx),
|
||||
new_buffer.clone(),
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer.clone(),
|
||||
hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(buffer_diff, cx);
|
||||
multibuffer.add_diff(diff, cx);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
@@ -106,6 +93,15 @@ impl Diff {
|
||||
text_snapshot,
|
||||
cx,
|
||||
);
|
||||
let snapshot = diff.snapshot(cx);
|
||||
|
||||
let secondary_diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer_snapshot, cx);
|
||||
diff.set_snapshot(snapshot, &buffer_snapshot, cx);
|
||||
diff
|
||||
});
|
||||
diff.set_secondary_diff(secondary_diff);
|
||||
|
||||
diff
|
||||
});
|
||||
|
||||
@@ -204,7 +200,10 @@ impl PendingDiff {
|
||||
)
|
||||
.await?;
|
||||
buffer_diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(diff_snapshot, &text_snapshot, cx)
|
||||
diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
|
||||
diff.secondary_diff().unwrap().update(cx, |diff, cx| {
|
||||
diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
|
||||
});
|
||||
})?;
|
||||
diff.update(cx, |diff, cx| {
|
||||
if let Diff::Pending(diff) = diff {
|
||||
|
||||
@@ -10,6 +10,7 @@ path = "src/agent2.rs"
|
||||
|
||||
[features]
|
||||
test-support = ["db/test-support"]
|
||||
e2e = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -72,6 +73,7 @@ zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
agent = { workspace = true, "features" = ["test-support"] }
|
||||
agent_servers = { workspace = true, "features" = ["test-support"] }
|
||||
assistant_context = { workspace = true, "features" = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
client = { workspace = true, "features" = ["test-support"] }
|
||||
|
||||
@@ -1269,18 +1269,12 @@ mod tests {
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let summary_model = Arc::new(FakeLanguageModel::default());
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_model(model, cx);
|
||||
thread.set_summarization_model(Some(summary_model), cx);
|
||||
thread.set_model(model.clone(), cx);
|
||||
thread.set_summarization_model(Some(summary_model.clone()), cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(history_entries(&history_store, cx), vec![]);
|
||||
|
||||
let model = thread.read_with(cx, |thread, _| thread.model().unwrap().clone());
|
||||
let model = model.as_fake();
|
||||
let summary_model = thread.read_with(cx, |thread, _| {
|
||||
thread.summarization_model().unwrap().clone()
|
||||
});
|
||||
let summary_model = summary_model.as_fake();
|
||||
let send = acp_thread.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
vec![
|
||||
|
||||
@@ -10,6 +10,7 @@ use itertools::Itertools;
|
||||
use paths::contexts_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
|
||||
use ui::ElementId;
|
||||
use util::ResultExt as _;
|
||||
|
||||
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
|
||||
@@ -68,6 +69,15 @@ pub enum HistoryEntryId {
|
||||
TextThread(Arc<Path>),
|
||||
}
|
||||
|
||||
impl Into<ElementId> for HistoryEntryId {
|
||||
fn into(self) -> ElementId {
|
||||
match self {
|
||||
HistoryEntryId::AcpThread(session_id) => ElementId::Name(session_id.0.into()),
|
||||
HistoryEntryId::TextThread(path) => ElementId::Path(path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum SerializedRecentOpen {
|
||||
AcpThread(String),
|
||||
@@ -345,4 +355,8 @@ impl HistoryStore {
|
||||
.retain(|old_entry| old_entry != entry);
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
|
||||
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
||||
self.entries(cx).into_iter().take(limit).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ impl AgentServer for NativeAgentServer {
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
""
|
||||
"Welcome to the Agent Panel"
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
@@ -73,3 +73,52 @@ impl AgentServer for NativeAgentServer {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use assistant_context::ContextStore;
|
||||
use gpui::AppContext;
|
||||
|
||||
agent_servers::e2e_tests::common_e2e_tests!(
|
||||
async |fs, project, cx| {
|
||||
let auth = cx.update(|cx| {
|
||||
prompt_store::init(cx);
|
||||
terminal::init(cx);
|
||||
|
||||
let registry = language_model::LanguageModelRegistry::read_global(cx);
|
||||
let auth = registry
|
||||
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
|
||||
.unwrap()
|
||||
.authenticate(cx);
|
||||
|
||||
cx.spawn(async move |_| auth.await)
|
||||
});
|
||||
|
||||
auth.await.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let registry = language_model::LanguageModelRegistry::global(cx);
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.select_default_model(
|
||||
Some(&language_model::SelectedModel {
|
||||
provider: language_model::ANTHROPIC_PROVIDER_ID,
|
||||
model: language_model::LanguageModelId("claude-sonnet-4-latest".into()),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
let history = cx.update(|cx| {
|
||||
let context_store = cx.new(move |cx| ContextStore::fake(project.clone(), cx));
|
||||
cx.new(move |cx| HistoryStore::new(context_store, cx))
|
||||
});
|
||||
|
||||
NativeAgentServer::new(fs.clone(), history)
|
||||
},
|
||||
allow_option_id = "allow"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -975,7 +975,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
assert!(
|
||||
matches!(
|
||||
last_event,
|
||||
Some(Ok(ThreadEvent::Stop(acp::StopReason::Canceled)))
|
||||
Some(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled)))
|
||||
),
|
||||
"unexpected event {last_event:?}"
|
||||
);
|
||||
@@ -1029,7 +1029,7 @@ async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
||||
fake_model.end_last_completion_stream();
|
||||
|
||||
let events_1 = events_1.collect::<Vec<_>>().await;
|
||||
assert_eq!(stop_events(events_1), vec![acp::StopReason::Canceled]);
|
||||
assert_eq!(stop_events(events_1), vec![acp::StopReason::Cancelled]);
|
||||
let events_2 = events_2.collect::<Vec<_>>().await;
|
||||
assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]);
|
||||
}
|
||||
|
||||
@@ -2248,7 +2248,7 @@ impl ThreadEventStream {
|
||||
|
||||
fn send_canceled(&self) {
|
||||
self.0
|
||||
.unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Canceled)))
|
||||
.unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled)))
|
||||
.ok();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,16 +8,11 @@ use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
/// Copies a file or directory in the project, and returns confirmation that the
|
||||
/// copy succeeded.
|
||||
///
|
||||
/// Copies a file or directory in the project, and returns confirmation that the copy succeeded.
|
||||
/// Directory contents will be copied recursively (like `cp -r`).
|
||||
///
|
||||
/// This tool should be used when it's desirable to create a copy of a file or
|
||||
/// directory without modifying the original. It's much more efficient than
|
||||
/// doing this by separately reading and then writing the file or directory's
|
||||
/// contents, so this tool should be preferred over that approach whenever
|
||||
/// copying is the goal.
|
||||
/// This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
|
||||
/// It's much more efficient than doing this by separately reading and then writing the file or directory's contents, so this tool should be preferred over that approach whenever copying is the goal.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CopyPathToolInput {
|
||||
/// The source path of the file or directory to copy.
|
||||
@@ -33,12 +28,10 @@ pub struct CopyPathToolInput {
|
||||
/// You can copy the first file by providing a source_path of "directory1/a/something.txt"
|
||||
/// </example>
|
||||
pub source_path: String,
|
||||
|
||||
/// The destination path where the file or directory should be copied to.
|
||||
///
|
||||
/// <example>
|
||||
/// To copy "directory1/a/something.txt" to "directory2/b/copy.txt",
|
||||
/// provide a destination_path of "directory2/b/copy.txt"
|
||||
/// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", provide a destination_path of "directory2/b/copy.txt"
|
||||
/// </example>
|
||||
pub destination_path: String,
|
||||
}
|
||||
|
||||
@@ -9,12 +9,9 @@ use util::markdown::MarkdownInlineCode;
|
||||
|
||||
use crate::{AgentTool, ToolCallEventStream};
|
||||
|
||||
/// Creates a new directory at the specified path within the project. Returns
|
||||
/// confirmation that the directory was created.
|
||||
/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
|
||||
///
|
||||
/// This tool creates a directory and all necessary parent directories (similar
|
||||
/// to `mkdir -p`). It should be used whenever you need to create new
|
||||
/// directories within the project.
|
||||
/// This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateDirectoryToolInput {
|
||||
/// The path of the new directory.
|
||||
|
||||
@@ -9,8 +9,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Deletes the file or directory (and the directory's contents, recursively) at
|
||||
/// the specified path in the project, and returns confirmation of the deletion.
|
||||
/// Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DeletePathToolInput {
|
||||
/// The path of the file or directory to delete.
|
||||
|
||||
@@ -34,25 +34,21 @@ const DEFAULT_UI_TEXT: &str = "Editing file";
|
||||
/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditFileToolInput {
|
||||
/// A one-line, user-friendly markdown description of the edit. This will be
|
||||
/// shown in the UI and also passed to another model to perform the edit.
|
||||
/// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit.
|
||||
///
|
||||
/// Be terse, but also descriptive in what you want to achieve with this
|
||||
/// edit. Avoid generic instructions.
|
||||
/// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions.
|
||||
///
|
||||
/// NEVER mention the file path in this description.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year in `page_footer`</example>
|
||||
///
|
||||
/// Make sure to include this field before all the others in the input object
|
||||
/// so that we can display it immediately.
|
||||
/// Make sure to include this field before all the others in the input object so that we can display it immediately.
|
||||
pub display_description: String,
|
||||
|
||||
/// The full path of the file to create or modify in the project.
|
||||
///
|
||||
/// WARNING: When specifying which file path need changing, you MUST
|
||||
/// start each path with one of the project's root directories.
|
||||
/// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories.
|
||||
///
|
||||
/// The following examples assume we have two root directories in the project:
|
||||
/// - /a/b/backend
|
||||
@@ -61,22 +57,19 @@ pub struct EditFileToolInput {
|
||||
/// <example>
|
||||
/// `backend/src/main.rs`
|
||||
///
|
||||
/// Notice how the file path starts with `backend`. Without that, the path
|
||||
/// would be ambiguous and the call would fail!
|
||||
/// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail!
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// `frontend/db.js`
|
||||
/// </example>
|
||||
pub path: PathBuf,
|
||||
|
||||
/// The mode of operation on the file. Possible values:
|
||||
/// - 'edit': Make granular edits to an existing file.
|
||||
/// - 'create': Create a new file if it doesn't exist.
|
||||
/// - 'overwrite': Replace the entire contents of an existing file.
|
||||
///
|
||||
/// When a file already exists or you just created it, prefer editing
|
||||
/// it as opposed to recreating it from scratch.
|
||||
/// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch.
|
||||
pub mode: EditFileMode,
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ pub struct FindPathToolInput {
|
||||
/// You can get back the first two paths by providing a glob of "*thing*.txt"
|
||||
/// </example>
|
||||
pub glob: String,
|
||||
|
||||
/// Optional starting position for paginated results (0-based).
|
||||
/// When not provided, starts from the beginning.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -27,8 +27,7 @@ use util::paths::PathMatcher;
|
||||
/// - DO NOT use HTML entities solely to escape characters in the tool parameters.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GrepToolInput {
|
||||
/// A regex pattern to search for in the entire project. Note that the regex
|
||||
/// will be parsed by the Rust `regex` crate.
|
||||
/// A regex pattern to search for in the entire project. Note that the regex will be parsed by the Rust `regex` crate.
|
||||
///
|
||||
/// Do NOT specify a path here! This will only be matched against the code **content**.
|
||||
pub regex: String,
|
||||
|
||||
@@ -10,14 +10,12 @@ use std::fmt::Write;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
/// Lists files and directories in a given path. Prefer the `grep` or
|
||||
/// `find_path` tools when searching the codebase.
|
||||
/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ListDirectoryToolInput {
|
||||
/// The fully-qualified path of the directory to list in the project.
|
||||
///
|
||||
/// This path should never be absolute, and the first component
|
||||
/// of the path should always be a root directory in a project.
|
||||
/// This path should never be absolute, and the first component of the path should always be a root directory in a project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
|
||||
@@ -8,14 +8,11 @@ use serde::{Deserialize, Serialize};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
/// Moves or rename a file or directory in the project, and returns confirmation
|
||||
/// that the move succeeded.
|
||||
/// Moves or rename a file or directory in the project, and returns confirmation that the move succeeded.
|
||||
///
|
||||
/// If the source and destination directories are the same, but the filename is
|
||||
/// different, this performs a rename. Otherwise, it performs a move.
|
||||
/// If the source and destination directories are the same, but the filename is different, this performs a rename. Otherwise, it performs a move.
|
||||
///
|
||||
/// This tool should be used when it's desirable to move or rename a file or
|
||||
/// directory without changing its contents at all.
|
||||
/// This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MovePathToolInput {
|
||||
/// The source path of the file or directory to move/rename.
|
||||
|
||||
@@ -8,19 +8,15 @@ use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use util::markdown::MarkdownEscaped;
|
||||
|
||||
/// This tool opens a file or URL with the default application associated with
|
||||
/// it on the user's operating system:
|
||||
/// This tool opens a file or URL with the default application associated with it on the user's operating system:
|
||||
///
|
||||
/// - On macOS, it's equivalent to the `open` command
|
||||
/// - On Windows, it's equivalent to `start`
|
||||
/// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate
|
||||
///
|
||||
/// For example, it can open a web browser with a URL, open a PDF file with the
|
||||
/// default PDF viewer, etc.
|
||||
/// For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc.
|
||||
///
|
||||
/// You MUST ONLY use this tool when the user has explicitly requested opening
|
||||
/// something. You MUST NEVER assume that the user would like for you to use
|
||||
/// this tool.
|
||||
/// You MUST ONLY use this tool when the user has explicitly requested opening something. You MUST NEVER assume that the user would like for you to use this tool.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct OpenToolInput {
|
||||
/// The path or URL to open with the default application.
|
||||
|
||||
@@ -21,8 +21,7 @@ use crate::{AgentTool, ToolCallEventStream};
|
||||
pub struct ReadFileToolInput {
|
||||
/// The relative path of the file to read.
|
||||
///
|
||||
/// This path should never be absolute, and the first component
|
||||
/// of the path should always be a root directory in a project.
|
||||
/// This path should never be absolute, and the first component of the path should always be a root directory in a project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
@@ -34,11 +33,9 @@ pub struct ReadFileToolInput {
|
||||
/// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.
|
||||
/// </example>
|
||||
pub path: String,
|
||||
|
||||
/// Optional line number to start reading on (1-based index)
|
||||
#[serde(default)]
|
||||
pub start_line: Option<u32>,
|
||||
|
||||
/// Optional line number to end reading on (1-based index, inclusive)
|
||||
#[serde(default)]
|
||||
pub end_line: Option<u32>,
|
||||
|
||||
@@ -11,8 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
|
||||
/// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ThinkingToolInput {
|
||||
/// Content to think about. This should be a description of what to think about or
|
||||
/// a problem to solve.
|
||||
/// Content to think about. This should be a description of what to think about or a problem to solve.
|
||||
content: String,
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use ui::prelude::*;
|
||||
use web_search::WebSearchRegistry;
|
||||
|
||||
/// Search the web for information using your query.
|
||||
/// Use this when you need real-time information, facts, or data that might not be in your training. \
|
||||
/// Use this when you need real-time information, facts, or data that might not be in your training.
|
||||
/// Results will include snippets and links from relevant web pages.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WebSearchToolInput {
|
||||
|
||||
@@ -6,7 +6,7 @@ publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[features]
|
||||
test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"]
|
||||
test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
|
||||
e2e = []
|
||||
|
||||
[lints]
|
||||
@@ -23,10 +23,14 @@ agent-client-protocol.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
client = { workspace = true, optional = true }
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
env_logger = { workspace = true, optional = true }
|
||||
fs = { workspace = true, optional = true }
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
gpui_tokio = { workspace = true, optional = true }
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
@@ -36,6 +40,7 @@ log.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest_client = { workspace = true, optional = true }
|
||||
schemars.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -57,8 +62,12 @@ libc.workspace = true
|
||||
nix.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
fs.workspace = true
|
||||
language.workspace = true
|
||||
indoc.workspace = true
|
||||
acp_thread = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
gpui_tokio.workspace = true
|
||||
reqwest_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, Agent as _};
|
||||
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use futures::AsyncBufReadExt as _;
|
||||
use futures::channel::oneshot;
|
||||
use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, cell::RefCell};
|
||||
@@ -27,6 +28,7 @@ pub struct AcpConnection {
|
||||
|
||||
pub struct AcpSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
suppress_abort_err: bool,
|
||||
}
|
||||
|
||||
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
|
||||
@@ -171,6 +173,7 @@ impl AgentConnection for AcpConnection {
|
||||
|
||||
let session = AcpSession {
|
||||
thread: thread.downgrade(),
|
||||
suppress_abort_err: false,
|
||||
};
|
||||
sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
@@ -202,9 +205,53 @@ impl AgentConnection for AcpConnection {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let session_id = params.session_id.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let response = conn.prompt(params).await?;
|
||||
Ok(response)
|
||||
let result = conn.prompt(params).await;
|
||||
|
||||
let mut suppress_abort_err = false;
|
||||
|
||||
if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
|
||||
suppress_abort_err = session.suppress_abort_err;
|
||||
session.suppress_abort_err = false;
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) => {
|
||||
if err.code != ErrorCode::INTERNAL_ERROR.code {
|
||||
anyhow::bail!(err)
|
||||
}
|
||||
|
||||
let Some(data) = &err.data else {
|
||||
anyhow::bail!(err)
|
||||
};
|
||||
|
||||
// Temporary workaround until the following PR is generally available:
|
||||
// https://github.com/google-gemini/gemini-cli/pull/6656
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ErrorDetails {
|
||||
details: Box<str>,
|
||||
}
|
||||
|
||||
match serde_json::from_value(data.clone()) {
|
||||
Ok(ErrorDetails { details }) => {
|
||||
if suppress_abort_err && details.contains("This operation was aborted")
|
||||
{
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(details))
|
||||
}
|
||||
}
|
||||
Err(_) => Err(anyhow!(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,6 +260,9 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
||||
session.suppress_abort_err = true;
|
||||
}
|
||||
let conn = self.connection.clone();
|
||||
let params = acp::CancelNotification {
|
||||
session_id: session_id.clone(),
|
||||
@@ -252,7 +302,7 @@ impl acp::Client for ClientDelegate {
|
||||
|
||||
let outcome = match result {
|
||||
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
|
||||
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled,
|
||||
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
|
||||
};
|
||||
|
||||
Ok(acp::RequestPermissionResponse { outcome })
|
||||
|
||||
@@ -3,8 +3,8 @@ mod claude;
|
||||
mod gemini;
|
||||
mod settings;
|
||||
|
||||
#[cfg(test)]
|
||||
mod e2e_tests;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod e2e_tests;
|
||||
|
||||
pub use claude::*;
|
||||
pub use gemini::*;
|
||||
|
||||
@@ -705,7 +705,7 @@ impl ClaudeAgentSession {
|
||||
let stop_reason = match subtype {
|
||||
ResultErrorType::Success => acp::StopReason::EndTurn,
|
||||
ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests,
|
||||
ResultErrorType::ErrorDuringExecution => acp::StopReason::Canceled,
|
||||
ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled,
|
||||
};
|
||||
end_turn_tx
|
||||
.send(Ok(acp::PromptResponse { stop_reason }))
|
||||
@@ -1093,7 +1093,7 @@ pub(crate) mod tests {
|
||||
use gpui::TestAppContext;
|
||||
use serde_json::json;
|
||||
|
||||
crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow");
|
||||
crate::common_e2e_tests!(async |_, _, _| ClaudeCode, allow_option_id = "allow");
|
||||
|
||||
pub fn local_command() -> AgentServerCommand {
|
||||
AgentServerCommand {
|
||||
|
||||
@@ -4,21 +4,30 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings};
|
||||
use crate::AgentServer;
|
||||
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
|
||||
use agent_client_protocol as acp;
|
||||
|
||||
use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
||||
use gpui::{Entity, TestAppContext};
|
||||
use gpui::{AppContext, Entity, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Project};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use util::path;
|
||||
|
||||
pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let fs = init_test(cx).await;
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
|
||||
where
|
||||
T: AgentServer + 'static,
|
||||
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
|
||||
{
|
||||
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
let thread = new_test_thread(
|
||||
server(&fs, &project, cx).await,
|
||||
project.clone(),
|
||||
"/private/tmp",
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx))
|
||||
@@ -42,8 +51,12 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let _fs = init_test(cx).await;
|
||||
pub async fn test_path_mentions<T, F>(server: F, cx: &mut TestAppContext)
|
||||
where
|
||||
T: AgentServer + 'static,
|
||||
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
|
||||
{
|
||||
let fs = init_test(cx).await as _;
|
||||
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
@@ -56,7 +69,13 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
|
||||
)
|
||||
.expect("failed to write file");
|
||||
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
|
||||
let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await;
|
||||
let thread = new_test_thread(
|
||||
server(&fs, &project, cx).await,
|
||||
project.clone(),
|
||||
tempdir.path(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
@@ -110,15 +129,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
|
||||
drop(tempdir);
|
||||
}
|
||||
|
||||
pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let _fs = init_test(cx).await;
|
||||
pub async fn test_tool_call<T, F>(server: F, cx: &mut TestAppContext)
|
||||
where
|
||||
T: AgentServer + 'static,
|
||||
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
|
||||
{
|
||||
let fs = init_test(cx).await as _;
|
||||
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let foo_path = tempdir.path().join("foo");
|
||||
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
|
||||
|
||||
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
let thread = new_test_thread(
|
||||
server(&fs, &project, cx).await,
|
||||
project.clone(),
|
||||
"/private/tmp",
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
@@ -152,14 +181,23 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
|
||||
drop(tempdir);
|
||||
}
|
||||
|
||||
pub async fn test_tool_call_with_permission(
|
||||
server: impl AgentServer + 'static,
|
||||
pub async fn test_tool_call_with_permission<T, F>(
|
||||
server: F,
|
||||
allow_option_id: acp::PermissionOptionId,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let fs = init_test(cx).await;
|
||||
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
) where
|
||||
T: AgentServer + 'static,
|
||||
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
|
||||
{
|
||||
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
|
||||
let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await;
|
||||
let thread = new_test_thread(
|
||||
server(&fs, &project, cx).await,
|
||||
project.clone(),
|
||||
"/private/tmp",
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let full_turn = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||
@@ -247,11 +285,21 @@ pub async fn test_tool_call_with_permission(
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let fs = init_test(cx).await;
|
||||
pub async fn test_cancel<T, F>(server: F, cx: &mut TestAppContext)
|
||||
where
|
||||
T: AgentServer + 'static,
|
||||
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
|
||||
{
|
||||
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
|
||||
|
||||
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await;
|
||||
let thread = new_test_thread(
|
||||
server(&fs, &project, cx).await,
|
||||
project.clone(),
|
||||
"/private/tmp",
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let _ = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||
@@ -316,10 +364,20 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let fs = init_test(cx).await;
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
pub async fn test_thread_drop<T, F>(server: F, cx: &mut TestAppContext)
|
||||
where
|
||||
T: AgentServer + 'static,
|
||||
F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
|
||||
{
|
||||
let fs = init_test(cx).await as Arc<dyn fs::Fs>;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
let thread = new_test_thread(
|
||||
server(&fs, &project, cx).await,
|
||||
project.clone(),
|
||||
"/private/tmp",
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.send_raw("Hello from test!", cx))
|
||||
@@ -386,25 +444,39 @@ macro_rules! common_e2e_tests {
|
||||
}
|
||||
};
|
||||
}
|
||||
pub use common_e2e_tests;
|
||||
|
||||
// Helpers
|
||||
|
||||
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
#[cfg(test)]
|
||||
use settings::Settings;
|
||||
|
||||
env_logger::try_init().ok();
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
let settings_store = settings::SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
Project::init_settings(cx);
|
||||
language::init(cx);
|
||||
gpui_tokio::init(cx);
|
||||
let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
client::init_settings(cx);
|
||||
let client = client::Client::production(cx);
|
||||
let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store, client, cx);
|
||||
agent_settings::init(cx);
|
||||
crate::settings::init(cx);
|
||||
|
||||
#[cfg(test)]
|
||||
crate::AllAgentServersSettings::override_global(
|
||||
AllAgentServersSettings {
|
||||
claude: Some(AgentServerSettings {
|
||||
crate::AllAgentServersSettings {
|
||||
claude: Some(crate::AgentServerSettings {
|
||||
command: crate::claude::tests::local_command(),
|
||||
}),
|
||||
gemini: Some(AgentServerSettings {
|
||||
gemini: Some(crate::AgentServerSettings {
|
||||
command: crate::gemini::tests::local_command(),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{AgentServer, AgentServerCommand};
|
||||
use acp_thread::{AgentConnection, LoadError};
|
||||
use anyhow::Result;
|
||||
use gpui::{Entity, Task};
|
||||
use language_models::provider::google::GoogleLanguageModelProvider;
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use ui::App;
|
||||
@@ -18,11 +19,11 @@ const ACP_ARG: &str = "--experimental-acp";
|
||||
|
||||
impl AgentServer for Gemini {
|
||||
fn name(&self) -> &'static str {
|
||||
"Gemini"
|
||||
"Gemini CLI"
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
"Welcome to Gemini"
|
||||
"Welcome to Gemini CLI"
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
@@ -47,16 +48,20 @@ impl AgentServer for Gemini {
|
||||
settings.get::<AllAgentServersSettings>(None).gemini.clone()
|
||||
})?;
|
||||
|
||||
let Some(command) =
|
||||
let Some(mut command) =
|
||||
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await
|
||||
else {
|
||||
return Err(LoadError::NotInstalled {
|
||||
error_message: "Failed to find Gemini CLI binary".into(),
|
||||
install_message: "Install Gemini CLI".into(),
|
||||
install_command: "npm install -g @google/gemini-cli@latest".into()
|
||||
install_command: "npm install -g @google/gemini-cli@preview".into()
|
||||
}.into());
|
||||
};
|
||||
|
||||
if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
|
||||
command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key);
|
||||
}
|
||||
|
||||
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
|
||||
if result.is_err() {
|
||||
let version_fut = util::command::new_smol_command(&command.path)
|
||||
@@ -84,7 +89,7 @@ impl AgentServer for Gemini {
|
||||
current_version
|
||||
).into(),
|
||||
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
||||
upgrade_command: "npm install -g @google/gemini-cli@latest".into(),
|
||||
upgrade_command: "npm install -g @google/gemini-cli@preview".into(),
|
||||
}.into())
|
||||
}
|
||||
}
|
||||
@@ -103,7 +108,7 @@ pub(crate) mod tests {
|
||||
use crate::AgentServerCommand;
|
||||
use std::path::Path;
|
||||
|
||||
crate::common_e2e_tests!(Gemini, allow_option_id = "proceed_once");
|
||||
crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once");
|
||||
|
||||
pub fn local_command() -> AgentServerCommand {
|
||||
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
|
||||
@@ -108,62 +108,7 @@ impl ContextPickerCompletionProvider {
|
||||
confirm: Some(Arc::new(|_, _, _| true)),
|
||||
}),
|
||||
ContextPickerEntry::Action(action) => {
|
||||
let (new_text, on_action) = match action {
|
||||
ContextPickerAction::AddSelections => {
|
||||
const PLACEHOLDER: &str = "selection ";
|
||||
let selections = selection_ranges(workspace, cx)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, (buffer, range))| {
|
||||
(
|
||||
buffer,
|
||||
range,
|
||||
(PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let new_text: String = PLACEHOLDER.repeat(selections.len());
|
||||
|
||||
let callback = Arc::new({
|
||||
let source_range = source_range.clone();
|
||||
move |_, window: &mut Window, cx: &mut App| {
|
||||
let selections = selections.clone();
|
||||
let message_editor = message_editor.clone();
|
||||
let source_range = source_range.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
message_editor
|
||||
.update(cx, |message_editor, cx| {
|
||||
message_editor.confirm_mention_for_selection(
|
||||
source_range,
|
||||
selections,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
(new_text, callback)
|
||||
}
|
||||
};
|
||||
|
||||
Some(Completion {
|
||||
replace_range: source_range,
|
||||
new_text,
|
||||
label: CodeLabel::plain(action.label().to_string(), None),
|
||||
icon_path: Some(action.icon().path().into()),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
// This ensures that when a user accepts this completion, the
|
||||
// completion menu will still be shown after "@category " is
|
||||
// inserted
|
||||
confirm: Some(on_action),
|
||||
})
|
||||
Self::completion_for_action(action, source_range, message_editor, workspace, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,6 +304,71 @@ impl ContextPickerCompletionProvider {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn completion_for_action(
|
||||
action: ContextPickerAction,
|
||||
source_range: Range<Anchor>,
|
||||
message_editor: WeakEntity<MessageEditor>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Option<Completion> {
|
||||
let (new_text, on_action) = match action {
|
||||
ContextPickerAction::AddSelections => {
|
||||
const PLACEHOLDER: &str = "selection ";
|
||||
let selections = selection_ranges(workspace, cx)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, (buffer, range))| {
|
||||
(
|
||||
buffer,
|
||||
range,
|
||||
(PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let new_text: String = PLACEHOLDER.repeat(selections.len());
|
||||
|
||||
let callback = Arc::new({
|
||||
let source_range = source_range.clone();
|
||||
move |_, window: &mut Window, cx: &mut App| {
|
||||
let selections = selections.clone();
|
||||
let message_editor = message_editor.clone();
|
||||
let source_range = source_range.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
message_editor
|
||||
.update(cx, |message_editor, cx| {
|
||||
message_editor.confirm_mention_for_selection(
|
||||
source_range,
|
||||
selections,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
(new_text, callback)
|
||||
}
|
||||
};
|
||||
|
||||
Some(Completion {
|
||||
replace_range: source_range,
|
||||
new_text,
|
||||
label: CodeLabel::plain(action.label().to_string(), None),
|
||||
icon_path: Some(action.icon().path().into()),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
// This ensures that when a user accepts this completion, the
|
||||
// completion menu will still be shown after "@category " is
|
||||
// inserted
|
||||
confirm: Some(on_action),
|
||||
})
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
mode: Option<ContextPickerMode>,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use acp_thread::{AcpThread, AgentThreadEntry};
|
||||
use agent_client_protocol::ToolCallId;
|
||||
use agent2::HistoryStore;
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||
@@ -106,6 +107,7 @@ impl EntryViewState {
|
||||
}
|
||||
}
|
||||
AgentThreadEntry::ToolCall(tool_call) => {
|
||||
let id = tool_call.id.clone();
|
||||
let terminals = tool_call.terminals().cloned().collect::<Vec<_>>();
|
||||
let diffs = tool_call.diffs().cloned().collect::<Vec<_>>();
|
||||
|
||||
@@ -121,21 +123,31 @@ impl EntryViewState {
|
||||
|
||||
for terminal in terminals {
|
||||
views.entry(terminal.entity_id()).or_insert_with(|| {
|
||||
create_terminal(
|
||||
let element = create_terminal(
|
||||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
terminal.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
.into_any();
|
||||
cx.emit(EntryViewEvent {
|
||||
entry_index: index,
|
||||
view_event: ViewEvent::NewTerminal(id.clone()),
|
||||
});
|
||||
element
|
||||
});
|
||||
}
|
||||
|
||||
for diff in diffs {
|
||||
views
|
||||
.entry(diff.entity_id())
|
||||
.or_insert_with(|| create_editor_diff(diff.clone(), window, cx).into_any());
|
||||
views.entry(diff.entity_id()).or_insert_with(|| {
|
||||
let element = create_editor_diff(diff.clone(), window, cx).into_any();
|
||||
cx.emit(EntryViewEvent {
|
||||
entry_index: index,
|
||||
view_event: ViewEvent::NewDiff(id.clone()),
|
||||
});
|
||||
element
|
||||
});
|
||||
}
|
||||
}
|
||||
AgentThreadEntry::AssistantMessage(_) => {
|
||||
@@ -187,6 +199,8 @@ pub struct EntryViewEvent {
|
||||
}
|
||||
|
||||
pub enum ViewEvent {
|
||||
NewDiff(ToolCallId),
|
||||
NewTerminal(ToolCallId),
|
||||
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
acp::completion_provider::ContextPickerCompletionProvider,
|
||||
context_picker::fetch_context_picker::fetch_url_content,
|
||||
context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
|
||||
};
|
||||
use acp_thread::{MentionUri, selection_name};
|
||||
use agent_client_protocol as acp;
|
||||
@@ -27,7 +27,7 @@ use gpui::{
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language_model::LanguageModelImage;
|
||||
use project::{Project, ProjectPath, Worktree};
|
||||
use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
@@ -561,21 +561,24 @@ impl MessageEditor {
|
||||
let range = snapshot.anchor_after(offset + range_to_fold.start)
|
||||
..snapshot.anchor_after(offset + range_to_fold.end);
|
||||
|
||||
let path = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf());
|
||||
// TODO support selections from buffers with no path
|
||||
let Some(project_path) = buffer.read(cx).project_path(cx) else {
|
||||
continue;
|
||||
};
|
||||
let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
|
||||
continue;
|
||||
};
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let point_range = selection_range.to_point(&snapshot);
|
||||
let line_range = point_range.start.row..point_range.end.row;
|
||||
|
||||
let uri = MentionUri::Selection {
|
||||
path: path.clone(),
|
||||
path: abs_path.clone(),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
let crease = crate::context_picker::crease_for_mention(
|
||||
selection_name(&path, &line_range).into(),
|
||||
selection_name(&abs_path, &line_range).into(),
|
||||
uri.icon_path(cx),
|
||||
range,
|
||||
self.editor.downgrade(),
|
||||
@@ -587,8 +590,7 @@ impl MessageEditor {
|
||||
crease_ids.first().copied().unwrap()
|
||||
});
|
||||
|
||||
self.mention_set
|
||||
.insert_uri(crease_id, MentionUri::Selection { path, line_range });
|
||||
self.mention_set.insert_uri(crease_id, uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,15 +629,11 @@ impl MessageEditor {
|
||||
.shared();
|
||||
|
||||
self.mention_set.insert_thread(id.clone(), task.clone());
|
||||
self.mention_set.insert_uri(crease_id, uri);
|
||||
|
||||
let editor = self.editor.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if task.await.notify_async_err(cx).is_some() {
|
||||
this.update(cx, |this, _| {
|
||||
this.mention_set.insert_uri(crease_id, uri);
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
if task.await.notify_async_err(cx).is_none() {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.display_map.update(cx, |display_map, cx| {
|
||||
@@ -646,6 +644,7 @@ impl MessageEditor {
|
||||
.ok();
|
||||
this.update(cx, |this, _| {
|
||||
this.mention_set.thread_summaries.remove(&id);
|
||||
this.mention_set.uri_by_crease_id.remove(&crease_id);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -710,9 +709,13 @@ impl MessageEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
|
||||
let contents =
|
||||
self.mention_set
|
||||
.contents(&self.project, self.prompt_store.as_ref(), window, cx);
|
||||
let contents = self.mention_set.contents(
|
||||
&self.project,
|
||||
self.prompt_store.as_ref(),
|
||||
&self.prompt_capabilities.get(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let editor = self.editor.clone();
|
||||
let prevent_slash_commands = self.prevent_slash_commands;
|
||||
|
||||
@@ -777,6 +780,17 @@ impl MessageEditor {
|
||||
.map(|path| format!("file://{}", path.display())),
|
||||
})
|
||||
}
|
||||
Mention::UriOnly(uri) => {
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
name: uri.name(),
|
||||
uri: uri.to_uri().to_string(),
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
})
|
||||
}
|
||||
};
|
||||
chunks.push(chunk);
|
||||
ix = crease_range.end;
|
||||
@@ -948,6 +962,38 @@ impl MessageEditor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let buffer = self.editor.read(cx).buffer().clone();
|
||||
let Some(buffer) = buffer.read(cx).as_singleton() else {
|
||||
return;
|
||||
};
|
||||
let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(completion) = ContextPickerCompletionProvider::completion_for_action(
|
||||
ContextPickerAction::AddSelections,
|
||||
anchor..anchor,
|
||||
cx.weak_entity(),
|
||||
&workspace,
|
||||
cx,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
self.editor.update(cx, |message_editor, cx| {
|
||||
message_editor.edit(
|
||||
[(
|
||||
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
|
||||
completion.new_text,
|
||||
)],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
if let Some(confirm) = completion.confirm {
|
||||
confirm(CompletionIntent::Complete, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |message_editor, cx| {
|
||||
message_editor.set_read_only(read_only);
|
||||
@@ -1387,6 +1433,7 @@ pub enum Mention {
|
||||
tracked_buffers: Vec<Entity<Buffer>>,
|
||||
},
|
||||
Image(MentionImage),
|
||||
UriOnly(MentionUri),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -1450,9 +1497,20 @@ impl MentionSet {
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
prompt_store: Option<&Entity<PromptStore>>,
|
||||
prompt_capabilities: &acp::PromptCapabilities,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<HashMap<CreaseId, Mention>>> {
|
||||
if !prompt_capabilities.embedded_context {
|
||||
let mentions = self
|
||||
.uri_by_crease_id
|
||||
.iter()
|
||||
.map(|(crease_id, uri)| (*crease_id, Mention::UriOnly(uri.clone())))
|
||||
.collect();
|
||||
|
||||
return Task::ready(Ok(mentions));
|
||||
}
|
||||
|
||||
let mut processed_image_creases = HashSet::default();
|
||||
|
||||
let mut contents = self
|
||||
@@ -1657,7 +1715,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider {
|
||||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Vec<project::Hover>>> {
|
||||
) -> Option<Task<Option<Vec<project::Hover>>>> {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let offset = position.to_offset(&snapshot);
|
||||
let (start, end) = self.range.get()?;
|
||||
@@ -1665,14 +1723,14 @@ impl SemanticsProvider for SlashCommandSemanticsProvider {
|
||||
return None;
|
||||
}
|
||||
let range = snapshot.anchor_after(start)..snapshot.anchor_after(end);
|
||||
Some(Task::ready(vec![project::Hover {
|
||||
Some(Task::ready(Some(vec![project::Hover {
|
||||
contents: vec![project::HoverBlock {
|
||||
text: "Slash commands are not supported".into(),
|
||||
kind: project::HoverBlockKind::PlainText,
|
||||
}],
|
||||
range: Some(range),
|
||||
language: None,
|
||||
}]))
|
||||
}])))
|
||||
}
|
||||
|
||||
fn inline_values(
|
||||
@@ -1722,7 +1780,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider {
|
||||
_position: text::Anchor,
|
||||
_kind: editor::GotoDefinitionKind,
|
||||
_cx: &mut App,
|
||||
) -> Option<Task<Result<Vec<project::LocationLink>>>> {
|
||||
) -> Option<Task<Result<Option<Vec<project::LocationLink>>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -2149,11 +2207,21 @@ mod tests {
|
||||
assert_eq!(fold_ranges(editor, cx).len(), 1);
|
||||
});
|
||||
|
||||
let all_prompt_capabilities = acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
};
|
||||
|
||||
let contents = message_editor
|
||||
.update_in(&mut cx, |message_editor, window, cx| {
|
||||
message_editor
|
||||
.mention_set()
|
||||
.contents(&project, None, window, cx)
|
||||
message_editor.mention_set().contents(
|
||||
&project,
|
||||
None,
|
||||
&all_prompt_capabilities,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -2168,6 +2236,28 @@ mod tests {
|
||||
pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
|
||||
}
|
||||
|
||||
let contents = message_editor
|
||||
.update_in(&mut cx, |message_editor, window, cx| {
|
||||
message_editor.mention_set().contents(
|
||||
&project,
|
||||
None,
|
||||
&acp::PromptCapabilities::default(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_values()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
{
|
||||
let [Mention::UriOnly(uri)] = contents.as_slice() else {
|
||||
panic!("Unexpected mentions");
|
||||
};
|
||||
pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
|
||||
}
|
||||
|
||||
cx.simulate_input(" ");
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
@@ -2203,9 +2293,13 @@ mod tests {
|
||||
|
||||
let contents = message_editor
|
||||
.update_in(&mut cx, |message_editor, window, cx| {
|
||||
message_editor
|
||||
.mention_set()
|
||||
.contents(&project, None, window, cx)
|
||||
message_editor.mention_set().contents(
|
||||
&project,
|
||||
None,
|
||||
&all_prompt_capabilities,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -2313,9 +2407,13 @@ mod tests {
|
||||
|
||||
let contents = message_editor
|
||||
.update_in(&mut cx, |message_editor, window, cx| {
|
||||
message_editor
|
||||
.mention_set()
|
||||
.contents(&project, None, window, cx)
|
||||
message_editor.mention_set().contents(
|
||||
&project,
|
||||
None,
|
||||
&all_prompt_capabilities,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use crate::RemoveSelectedThread;
|
||||
use crate::acp::AcpThreadView;
|
||||
use crate::{AgentPanel, RemoveSelectedThread};
|
||||
use agent2::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
|
||||
UniformListScrollHandle, Window, uniform_list,
|
||||
UniformListScrollHandle, WeakEntity, Window, uniform_list,
|
||||
};
|
||||
use std::{fmt::Display, ops::Range, sync::Arc};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
@@ -639,6 +640,141 @@ impl Render for AcpThreadHistory {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct AcpHistoryEntryElement {
|
||||
entry: HistoryEntry,
|
||||
thread_view: WeakEntity<AcpThreadView>,
|
||||
selected: bool,
|
||||
hovered: bool,
|
||||
on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl AcpHistoryEntryElement {
|
||||
pub fn new(entry: HistoryEntry, thread_view: WeakEntity<AcpThreadView>) -> Self {
|
||||
Self {
|
||||
entry,
|
||||
thread_view,
|
||||
selected: false,
|
||||
hovered: false,
|
||||
on_hover: Box::new(|_, _, _| {}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hovered(mut self, hovered: bool) -> Self {
|
||||
self.hovered = hovered;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_hover = Box::new(on_hover);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AcpHistoryEntryElement {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let id = self.entry.id();
|
||||
let title = self.entry.title();
|
||||
let timestamp = self.entry.updated_at();
|
||||
|
||||
let formatted_time = {
|
||||
let now = chrono::Utc::now();
|
||||
let duration = now.signed_duration_since(timestamp);
|
||||
|
||||
if duration.num_days() > 0 {
|
||||
format!("{}d", duration.num_days())
|
||||
} else if duration.num_hours() > 0 {
|
||||
format!("{}h ago", duration.num_hours())
|
||||
} else if duration.num_minutes() > 0 {
|
||||
format!("{}m ago", duration.num_minutes())
|
||||
} else {
|
||||
"Just now".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
ListItem::new(id)
|
||||
.rounded()
|
||||
.toggle_state(self.selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new(title).size(LabelSize::Small).truncate())
|
||||
.child(
|
||||
Label::new(formatted_time)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.on_hover(self.on_hover)
|
||||
.end_slot::<IconButton>(if self.hovered || self.selected {
|
||||
Some(
|
||||
IconButton::new("delete", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
||||
})
|
||||
.on_click({
|
||||
let thread_view = self.thread_view.clone();
|
||||
let entry = self.entry.clone();
|
||||
|
||||
move |_event, _window, cx| {
|
||||
if let Some(thread_view) = thread_view.upgrade() {
|
||||
thread_view.update(cx, |thread_view, cx| {
|
||||
thread_view.delete_history_entry(entry.clone(), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.on_click({
|
||||
let thread_view = self.thread_view.clone();
|
||||
let entry = self.entry;
|
||||
|
||||
move |_event, window, cx| {
|
||||
if let Some(workspace) = thread_view
|
||||
.upgrade()
|
||||
.and_then(|view| view.read(cx).workspace().upgrade())
|
||||
{
|
||||
match &entry {
|
||||
HistoryEntry::AcpThread(thread_metadata) => {
|
||||
if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.load_agent_thread(
|
||||
thread_metadata.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
HistoryEntry::TextThread(context) => {
|
||||
if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.open_saved_prompt_editor(
|
||||
context.path.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum EntryTimeFormat {
|
||||
DateAndTime,
|
||||
|
||||
@@ -8,7 +8,7 @@ use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp};
|
||||
use agent_servers::{AgentServer, ClaudeCode};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
|
||||
use agent2::{DbThreadMetadata, HistoryEntryId, HistoryStore};
|
||||
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore};
|
||||
use anyhow::bail;
|
||||
use audio::{Audio, Sound};
|
||||
use buffer_diff::BufferDiff;
|
||||
@@ -54,11 +54,12 @@ use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent};
|
||||
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::agent_diff::AgentDiff;
|
||||
use crate::profile_selector::{ProfileProvider, ProfileSelector};
|
||||
|
||||
use crate::ui::preview::UsageCallout;
|
||||
use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip};
|
||||
use crate::{
|
||||
AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow,
|
||||
KeepAll, OpenAgentDiff, RejectAll, ToggleBurnMode, ToggleProfileSelector,
|
||||
KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
|
||||
};
|
||||
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
@@ -75,11 +76,12 @@ enum ThreadError {
|
||||
PaymentRequired,
|
||||
ModelRequestLimitReached(cloud_llm_client::Plan),
|
||||
ToolUseLimitReached,
|
||||
AuthenticationRequired(SharedString),
|
||||
Other(SharedString),
|
||||
}
|
||||
|
||||
impl ThreadError {
|
||||
fn from_err(error: anyhow::Error) -> Self {
|
||||
fn from_err(error: anyhow::Error, agent: &Rc<dyn AgentServer>) -> Self {
|
||||
if error.is::<language_model::PaymentRequiredError>() {
|
||||
Self::PaymentRequired
|
||||
} else if error.is::<language_model::ToolUseLimitReachedError>() {
|
||||
@@ -89,7 +91,17 @@ impl ThreadError {
|
||||
{
|
||||
Self::ModelRequestLimitReached(error.plan)
|
||||
} else {
|
||||
Self::Other(error.to_string().into())
|
||||
let string = error.to_string();
|
||||
// TODO: we should have Gemini return better errors here.
|
||||
if agent.clone().downcast::<agent_servers::Gemini>().is_some()
|
||||
&& string.contains("Could not load the default credentials")
|
||||
|| string.contains("API key not valid")
|
||||
|| string.contains("Request had invalid authentication credentials")
|
||||
{
|
||||
Self::AuthenticationRequired(string.into())
|
||||
} else {
|
||||
Self::Other(error.to_string().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,6 +252,7 @@ pub struct AcpThreadView {
|
||||
project: Entity<Project>,
|
||||
thread_state: ThreadState,
|
||||
history_store: Entity<HistoryStore>,
|
||||
hovered_recent_history_item: Option<usize>,
|
||||
entry_view_state: Entity<EntryViewState>,
|
||||
message_editor: Entity<MessageEditor>,
|
||||
model_selector: Option<Entity<AcpModelSelectorPopover>>,
|
||||
@@ -257,7 +270,6 @@ pub struct AcpThreadView {
|
||||
edits_expanded: bool,
|
||||
plan_expanded: bool,
|
||||
editor_expanded: bool,
|
||||
terminal_expanded: bool,
|
||||
editing_message: Option<usize>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 3],
|
||||
@@ -276,6 +288,7 @@ enum ThreadState {
|
||||
connection: Rc<dyn AgentConnection>,
|
||||
description: Option<Entity<Markdown>>,
|
||||
configuration_view: Option<AnyView>,
|
||||
pending_auth_method: Option<acp::AuthMethodId>,
|
||||
_subscription: Option<Subscription>,
|
||||
},
|
||||
}
|
||||
@@ -355,8 +368,8 @@ impl AcpThreadView {
|
||||
edits_expanded: false,
|
||||
plan_expanded: false,
|
||||
editor_expanded: false,
|
||||
terminal_expanded: true,
|
||||
history_store,
|
||||
hovered_recent_history_item: None,
|
||||
_subscriptions: subscriptions,
|
||||
_cancel_task: None,
|
||||
}
|
||||
@@ -560,6 +573,7 @@ impl AcpThreadView {
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.thread_state = ThreadState::Unauthenticated {
|
||||
pending_auth_method: None,
|
||||
connection,
|
||||
configuration_view,
|
||||
description: err
|
||||
@@ -582,6 +596,10 @@ impl AcpThreadView {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn workspace(&self) -> &WeakEntity<Workspace> {
|
||||
&self.workspace
|
||||
}
|
||||
|
||||
pub fn thread(&self) -> Option<&Entity<AcpThread>> {
|
||||
match &self.thread_state {
|
||||
ThreadState::Ready { thread, .. } => Some(thread),
|
||||
@@ -668,9 +686,25 @@ impl AcpThreadView {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match &event.view_event {
|
||||
ViewEvent::NewDiff(tool_call_id) => {
|
||||
if AgentSettings::get_global(cx).expand_edit_card {
|
||||
self.expanded_tool_calls.insert(tool_call_id.clone());
|
||||
}
|
||||
}
|
||||
ViewEvent::NewTerminal(tool_call_id) => {
|
||||
if AgentSettings::get_global(cx).expand_terminal_card {
|
||||
self.expanded_tool_calls.insert(tool_call_id.clone());
|
||||
}
|
||||
}
|
||||
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
|
||||
self.editing_message = Some(event.entry_index);
|
||||
cx.notify();
|
||||
if let Some(thread) = self.thread()
|
||||
&& let Some(AgentThreadEntry::UserMessage(user_message)) =
|
||||
thread.read(cx).entries().get(event.entry_index)
|
||||
&& user_message.id.is_some()
|
||||
{
|
||||
self.editing_message = Some(event.entry_index);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
|
||||
self.regenerate(event.entry_index, editor, window, cx);
|
||||
@@ -907,7 +941,7 @@ impl AcpThreadView {
|
||||
}
|
||||
|
||||
fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) {
|
||||
self.thread_error = Some(ThreadError::from_err(error));
|
||||
self.thread_error = Some(ThreadError::from_err(error, &self.agent));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -986,12 +1020,74 @@ impl AcpThreadView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let ThreadState::Unauthenticated { ref connection, .. } = self.thread_state else {
|
||||
let ThreadState::Unauthenticated {
|
||||
connection,
|
||||
pending_auth_method,
|
||||
configuration_view,
|
||||
..
|
||||
} = &mut self.thread_state
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if method.0.as_ref() == "gemini-api-key" {
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
let provider = registry
|
||||
.read(cx)
|
||||
.provider(&language_model::GOOGLE_PROVIDER_ID)
|
||||
.unwrap();
|
||||
if !provider.is_authenticated(cx) {
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
description: Some("GEMINI_API_KEY must be set".to_owned()),
|
||||
provider_id: Some(language_model::GOOGLE_PROVIDER_ID),
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if method.0.as_ref() == "vertex-ai"
|
||||
&& std::env::var("GOOGLE_API_KEY").is_err()
|
||||
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
|
||||
|| (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()))
|
||||
{
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
description: Some(
|
||||
"GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed."
|
||||
.to_owned(),
|
||||
),
|
||||
provider_id: None,
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
self.thread_error.take();
|
||||
configuration_view.take();
|
||||
pending_auth_method.replace(method.clone());
|
||||
let authenticate = connection.authenticate(method, cx);
|
||||
cx.notify();
|
||||
self.auth_task = Some(cx.spawn_in(window, {
|
||||
let project = self.project.clone();
|
||||
let agent = self.agent.clone();
|
||||
@@ -1116,16 +1212,18 @@ impl AcpThreadView {
|
||||
.when(editing && !editor_focus, |this| this.border_dashed())
|
||||
.border_color(cx.theme().colors().border)
|
||||
.map(|this|{
|
||||
if editor_focus {
|
||||
if editing && editor_focus {
|
||||
this.border_color(focus_border)
|
||||
} else {
|
||||
} else if message.id.is_some() {
|
||||
this.hover(|s| s.border_color(focus_border.opacity(0.8)))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.text_xs()
|
||||
.child(editor.clone().into_any_element()),
|
||||
)
|
||||
.when(editor_focus, |this|
|
||||
.when(editing && editor_focus, |this|
|
||||
this.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
@@ -1382,19 +1480,26 @@ impl AcpThreadView {
|
||||
tool_call: &ToolCall,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
let tool_icon = Icon::new(match tool_call.kind {
|
||||
acp::ToolKind::Read => IconName::ToolRead,
|
||||
acp::ToolKind::Edit => IconName::ToolPencil,
|
||||
acp::ToolKind::Delete => IconName::ToolDeleteFile,
|
||||
acp::ToolKind::Move => IconName::ArrowRightLeft,
|
||||
acp::ToolKind::Search => IconName::ToolSearch,
|
||||
acp::ToolKind::Execute => IconName::ToolTerminal,
|
||||
acp::ToolKind::Think => IconName::ToolThink,
|
||||
acp::ToolKind::Fetch => IconName::ToolWeb,
|
||||
acp::ToolKind::Other => IconName::ToolHammer,
|
||||
})
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted);
|
||||
let tool_icon =
|
||||
if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 {
|
||||
FileIcons::get_icon(&tool_call.locations[0].path, cx)
|
||||
.map(Icon::from_path)
|
||||
.unwrap_or(Icon::new(IconName::ToolPencil))
|
||||
} else {
|
||||
Icon::new(match tool_call.kind {
|
||||
acp::ToolKind::Read => IconName::ToolRead,
|
||||
acp::ToolKind::Edit => IconName::ToolPencil,
|
||||
acp::ToolKind::Delete => IconName::ToolDeleteFile,
|
||||
acp::ToolKind::Move => IconName::ArrowRightLeft,
|
||||
acp::ToolKind::Search => IconName::ToolSearch,
|
||||
acp::ToolKind::Execute => IconName::ToolTerminal,
|
||||
acp::ToolKind::Think => IconName::ToolThink,
|
||||
acp::ToolKind::Fetch => IconName::ToolWeb,
|
||||
acp::ToolKind::Other => IconName::ToolHammer,
|
||||
})
|
||||
}
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted);
|
||||
|
||||
let base_container = h_flex().size_4().justify_center();
|
||||
|
||||
@@ -1475,10 +1580,9 @@ impl AcpThreadView {
|
||||
matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
|
||||
let use_card_layout = needs_confirmation || is_edit;
|
||||
|
||||
let is_collapsible = !tool_call.content.is_empty() && !use_card_layout;
|
||||
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
||||
|
||||
let is_open =
|
||||
needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let gradient_overlay = |color: Hsla| {
|
||||
div()
|
||||
@@ -1930,6 +2034,8 @@ impl AcpThreadView {
|
||||
.map(|path| format!("{}", path.display()))
|
||||
.unwrap_or_else(|| "current directory".to_string());
|
||||
|
||||
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let header = h_flex()
|
||||
.id(SharedString::from(format!(
|
||||
"terminal-tool-header-{}",
|
||||
@@ -2063,12 +2169,19 @@ impl AcpThreadView {
|
||||
"terminal-tool-disclosure-{}",
|
||||
terminal.entity_id()
|
||||
)),
|
||||
self.terminal_expanded,
|
||||
is_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener(move |this, _event, _window, _cx| {
|
||||
this.terminal_expanded = !this.terminal_expanded;
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
if is_expanded {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
}
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -2077,7 +2190,7 @@ impl AcpThreadView {
|
||||
.read(cx)
|
||||
.entry(entry_ix)
|
||||
.and_then(|entry| entry.terminal(terminal));
|
||||
let show_output = self.terminal_expanded && terminal_view.is_some();
|
||||
let show_output = is_expanded && terminal_view.is_some();
|
||||
|
||||
v_flex()
|
||||
.mb_2()
|
||||
@@ -2276,51 +2389,137 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_empty_state(&self, cx: &App) -> AnyElement {
|
||||
fn render_empty_state_section_header(
|
||||
&self,
|
||||
label: impl Into<SharedString>,
|
||||
action_slot: Option<AnyElement>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
div().pl_1().pr_1p5().child(
|
||||
h_flex()
|
||||
.mt_2()
|
||||
.pl_1p5()
|
||||
.pb_1()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Label::new(label.into())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(action_slot),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_empty_state(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let loading = matches!(&self.thread_state, ThreadState::Loading { .. });
|
||||
let render_history = self
|
||||
.agent
|
||||
.clone()
|
||||
.downcast::<agent2::NativeAgentServer>()
|
||||
.is_some()
|
||||
&& self
|
||||
.history_store
|
||||
.update(cx, |history_store, cx| !history_store.is_empty(cx));
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(if loading {
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.child(self.render_agent_logo())
|
||||
.with_animation(
|
||||
"pulsating_icon",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 1.0)),
|
||||
|icon, delta| icon.opacity(delta),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
self.render_agent_logo().into_any_element()
|
||||
})
|
||||
.child(h_flex().mt_4().mb_1().justify_center().child(if loading {
|
||||
div()
|
||||
.child(LoadingLabel::new("").size(LabelSize::Large))
|
||||
.into_any_element()
|
||||
} else {
|
||||
Headline::new(self.agent.empty_state_headline())
|
||||
.size(HeadlineSize::Medium)
|
||||
.into_any_element()
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.max_w_1_2()
|
||||
.text_sm()
|
||||
.text_center()
|
||||
.map(|this| {
|
||||
if loading {
|
||||
this.invisible()
|
||||
.when(!render_history, |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(if loading {
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.child(self.render_agent_logo())
|
||||
.with_animation(
|
||||
"pulsating_icon",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 1.0)),
|
||||
|icon, delta| icon.opacity(delta),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
this.text_color(cx.theme().colors().text_muted)
|
||||
}
|
||||
})
|
||||
.child(self.agent.empty_state_message()),
|
||||
)
|
||||
self.render_agent_logo().into_any_element()
|
||||
})
|
||||
.child(h_flex().mt_4().mb_2().justify_center().child(if loading {
|
||||
div()
|
||||
.child(LoadingLabel::new("").size(LabelSize::Large))
|
||||
.into_any_element()
|
||||
} else {
|
||||
Headline::new(self.agent.empty_state_headline())
|
||||
.size(HeadlineSize::Medium)
|
||||
.into_any_element()
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(render_history, |this| {
|
||||
let recent_history = self
|
||||
.history_store
|
||||
.update(cx, |history_store, cx| history_store.recent_entries(3, cx));
|
||||
this.justify_end().child(
|
||||
v_flex()
|
||||
.child(
|
||||
self.render_empty_state_section_header(
|
||||
"Recent",
|
||||
Some(
|
||||
Button::new("view-history", "View All")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenHistory,
|
||||
&self.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(OpenHistory.boxed_clone(), cx);
|
||||
})
|
||||
.into_any_element(),
|
||||
),
|
||||
cx,
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex().p_1().pr_1p5().gap_1().children(
|
||||
recent_history
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, entry)| {
|
||||
// TODO: Add keyboard navigation.
|
||||
let is_hovered =
|
||||
self.hovered_recent_history_item == Some(index);
|
||||
crate::acp::thread_history::AcpHistoryEntryElement::new(
|
||||
entry,
|
||||
cx.entity().downgrade(),
|
||||
)
|
||||
.hovered(is_hovered)
|
||||
.on_hover(cx.listener(
|
||||
move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_recent_history_item = Some(index);
|
||||
} else if this.hovered_recent_history_item
|
||||
== Some(index)
|
||||
{
|
||||
this.hovered_recent_history_item = None;
|
||||
}
|
||||
cx.notify();
|
||||
},
|
||||
))
|
||||
.into_any_element()
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -2329,6 +2528,7 @@ impl AcpThreadView {
|
||||
connection: &Rc<dyn AgentConnection>,
|
||||
description: Option<&Entity<Markdown>>,
|
||||
configuration_view: Option<&AnyView>,
|
||||
pending_auth_method: Option<&acp::AuthMethodId>,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
@@ -2343,9 +2543,11 @@ impl AcpThreadView {
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(self.render_error_agent_logo())
|
||||
.child(h_flex().mt_4().mb_1().justify_center().child(
|
||||
Headline::new(self.agent.empty_state_headline()).size(HeadlineSize::Medium),
|
||||
))
|
||||
.child(
|
||||
h_flex().mt_4().mb_1().justify_center().child(
|
||||
Headline::new("Authentication Required").size(HeadlineSize::Medium),
|
||||
),
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
.children(description.map(|desc| {
|
||||
@@ -2358,17 +2560,80 @@ impl AcpThreadView {
|
||||
.cloned()
|
||||
.map(|view| div().px_4().w_full().max_w_128().child(view)),
|
||||
)
|
||||
.child(h_flex().mt_1p5().justify_center().children(
|
||||
connection.auth_methods().iter().map(|method| {
|
||||
Button::new(SharedString::from(method.id.0.clone()), method.name.clone())
|
||||
.on_click({
|
||||
let method_id = method.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.authenticate(method_id.clone(), window, cx)
|
||||
.when(
|
||||
configuration_view.is_none()
|
||||
&& description.is_none()
|
||||
&& pending_auth_method.is_none(),
|
||||
|el| {
|
||||
el.child(
|
||||
div()
|
||||
.text_ui(cx)
|
||||
.text_center()
|
||||
.px_4()
|
||||
.w_full()
|
||||
.max_w_128()
|
||||
.child(Label::new("Authentication required")),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when_some(pending_auth_method, |el, _| {
|
||||
let spinner_icon = div()
|
||||
.px_0p5()
|
||||
.id("generating")
|
||||
.tooltip(Tooltip::text("Generating Changes…"))
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any();
|
||||
el.child(
|
||||
h_flex()
|
||||
.text_ui(cx)
|
||||
.text_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.px_4()
|
||||
.w_full()
|
||||
.max_w_128()
|
||||
.child(Label::new("Authenticating..."))
|
||||
.child(spinner_icon),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.mt_1p5()
|
||||
.gap_1()
|
||||
.flex_wrap()
|
||||
.justify_center()
|
||||
.children(connection.auth_methods().iter().enumerate().rev().map(
|
||||
|(ix, method)| {
|
||||
Button::new(
|
||||
SharedString::from(method.id.0.clone()),
|
||||
method.name.clone(),
|
||||
)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.when(ix == 0, |el| {
|
||||
el.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
})
|
||||
})
|
||||
}),
|
||||
))
|
||||
.size(ButtonSize::Medium)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let method_id = method.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.authenticate(method_id.clone(), window, cx)
|
||||
})
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
|
||||
@@ -2453,6 +2718,8 @@ impl AcpThreadView {
|
||||
let install_command = install_command.clone();
|
||||
container = container.child(
|
||||
Button::new("install", install_message)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.size(ButtonSize::Medium)
|
||||
.tooltip(Tooltip::text(install_command.clone()))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
let task = this
|
||||
@@ -3717,7 +3984,11 @@ impl AcpThreadView {
|
||||
.flex_wrap()
|
||||
.justify_end();
|
||||
|
||||
if AgentSettings::get_global(cx).enable_feedback {
|
||||
if AgentSettings::get_global(cx).enable_feedback
|
||||
&& self
|
||||
.thread()
|
||||
.is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
|
||||
{
|
||||
let feedback = self.thread_feedback.feedback;
|
||||
container = container.child(
|
||||
div().visible_on_hover("thread-controls-container").child(
|
||||
@@ -3999,6 +4270,12 @@ impl AcpThreadView {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.message_editor.update(cx, |message_editor, cx| {
|
||||
message_editor.insert_selections(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn render_thread_retry_status_callout(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
@@ -4044,6 +4321,9 @@ impl AcpThreadView {
|
||||
fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
let content = match self.thread_error.as_ref()? {
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
|
||||
ThreadError::AuthenticationRequired(error) => {
|
||||
self.render_authentication_required_error(error.clone(), cx)
|
||||
}
|
||||
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
|
||||
ThreadError::ModelRequestLimitReached(plan) => {
|
||||
self.render_model_request_limit_reached_error(*plan, cx)
|
||||
@@ -4082,6 +4362,24 @@ impl AcpThreadView {
|
||||
.dismiss_action(self.dismiss_error_button(cx))
|
||||
}
|
||||
|
||||
fn render_authentication_required_error(
|
||||
&self,
|
||||
error: SharedString,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Callout {
|
||||
Callout::new()
|
||||
.severity(Severity::Error)
|
||||
.title("Authentication Required")
|
||||
.description(error.clone())
|
||||
.actions_slot(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(self.authenticate_button(cx))
|
||||
.child(self.create_copy_button(error)),
|
||||
)
|
||||
.dismiss_action(self.dismiss_error_button(cx))
|
||||
}
|
||||
|
||||
fn render_model_request_limit_reached_error(
|
||||
&self,
|
||||
plan: cloud_llm_client::Plan,
|
||||
@@ -4203,6 +4501,31 @@ impl AcpThreadView {
|
||||
}))
|
||||
}
|
||||
|
||||
fn authenticate_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
Button::new("authenticate", "Authenticate")
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener({
|
||||
move |this, _, window, cx| {
|
||||
let agent = this.agent.clone();
|
||||
let ThreadState::Ready { thread, .. } = &this.thread_state else {
|
||||
return;
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let err = AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
};
|
||||
this.clear_thread_error(cx);
|
||||
let this = cx.weak_entity();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
Button::new("upgrade", "Upgrade")
|
||||
.label_size(LabelSize::Small)
|
||||
@@ -4226,6 +4549,18 @@ impl AcpThreadView {
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn delete_history_entry(&mut self, entry: HistoryEntry, cx: &mut Context<Self>) {
|
||||
let task = match entry {
|
||||
HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| {
|
||||
history.delete_thread(thread.id.clone(), cx)
|
||||
}),
|
||||
HistoryEntry::TextThread(context) => self.history_store.update(cx, |history, cx| {
|
||||
history.delete_text_thread(context.path.clone(), cx)
|
||||
}),
|
||||
};
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AcpThreadView {
|
||||
@@ -4252,15 +4587,19 @@ impl Render for AcpThreadView {
|
||||
connection,
|
||||
description,
|
||||
configuration_view,
|
||||
pending_auth_method,
|
||||
..
|
||||
} => self.render_auth_required_state(
|
||||
connection,
|
||||
description.as_ref(),
|
||||
configuration_view.as_ref(),
|
||||
pending_auth_method.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)),
|
||||
ThreadState::Loading { .. } => {
|
||||
v_flex().flex_1().child(self.render_empty_state(window, cx))
|
||||
}
|
||||
ThreadState::LoadError(e) => v_flex()
|
||||
.p_2()
|
||||
.flex_1()
|
||||
@@ -4302,7 +4641,7 @@ impl Render for AcpThreadView {
|
||||
},
|
||||
)
|
||||
} else {
|
||||
this.child(self.render_empty_state(cx))
|
||||
this.child(self.render_empty_state(window, cx))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ impl AgentConfiguration {
|
||||
let is_signed_in = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.client().status().borrow().is_connected()
|
||||
!workspace.client().status().borrow().is_signed_out()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
|
||||
@@ -903,6 +903,16 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
|
||||
match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => Some(thread_view),
|
||||
ActiveView::Thread { .. }
|
||||
| ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
|
||||
return self.new_agent_thread(AgentType::NativeAgent, window, cx);
|
||||
@@ -3882,7 +3892,11 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
// Wait to create a new context until the workspace is no longer
|
||||
// being updated.
|
||||
cx.defer_in(window, move |panel, window, cx| {
|
||||
if let Some(message_editor) = panel.active_message_editor() {
|
||||
if let Some(thread_view) = panel.active_thread_view() {
|
||||
thread_view.update(cx, |thread_view, cx| {
|
||||
thread_view.insert_selections(window, cx);
|
||||
});
|
||||
} else if let Some(message_editor) = panel.active_message_editor() {
|
||||
message_editor.update(cx, |message_editor, cx| {
|
||||
message_editor.context_store().update(cx, |store, cx| {
|
||||
let buffer = buffer.read(cx);
|
||||
|
||||
@@ -128,6 +128,12 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Action)]
|
||||
#[action(namespace = agent)]
|
||||
#[action(deprecated_aliases = ["assistant::QuoteSelection"])]
|
||||
/// Quotes the current selection in the agent panel's message editor.
|
||||
pub struct QuoteSelection;
|
||||
|
||||
/// Creates a new conversation thread, optionally based on an existing thread.
|
||||
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
QuoteSelection,
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
ui::BurnModeTooltip,
|
||||
};
|
||||
@@ -89,8 +90,6 @@ actions!(
|
||||
CycleMessageRole,
|
||||
/// Inserts the selected text into the active editor.
|
||||
InsertIntoEditor,
|
||||
/// Quotes the current selection in the assistant conversation.
|
||||
QuoteSelection,
|
||||
/// Splits the conversation at the current cursor position.
|
||||
Split,
|
||||
]
|
||||
|
||||
@@ -2,7 +2,6 @@ mod agent_notification;
|
||||
mod burn_mode_tooltip;
|
||||
mod context_pill;
|
||||
mod end_trial_upsell;
|
||||
// mod new_thread_button;
|
||||
mod onboarding_modal;
|
||||
pub mod preview;
|
||||
|
||||
@@ -10,5 +9,4 @@ pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use context_pill::*;
|
||||
pub use end_trial_upsell::*;
|
||||
// pub use new_thread_button::*;
|
||||
pub use onboarding_modal::*;
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled};
|
||||
use ui::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct NewThreadButton {
|
||||
id: ElementId,
|
||||
label: SharedString,
|
||||
icon: IconName,
|
||||
keybinding: Option<ui::KeyBinding>,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl NewThreadButton {
|
||||
fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
icon,
|
||||
keybinding: None,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self {
|
||||
self.keybinding = keybinding;
|
||||
self
|
||||
}
|
||||
|
||||
fn on_click<F>(mut self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) + 'static,
|
||||
{
|
||||
self.on_click = Some(Box::new(
|
||||
move |_: &ClickEvent, window: &mut Window, cx: &mut App| handler(window, cx),
|
||||
));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for NewThreadButton {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.w_full()
|
||||
.py_1p5()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.4))
|
||||
.bg(cx.theme().colors().element_active.opacity(0.2))
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(cx.theme().colors().element_hover)
|
||||
.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(self.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(self.label).size(LabelSize::Small)),
|
||||
)
|
||||
.when_some(self.keybinding, |this, keybinding| {
|
||||
this.child(keybinding.size(rems_from_px(10.)))
|
||||
})
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,10 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
parking_lot.workspace = true
|
||||
settings.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] }
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
use std::{io::Cursor, sync::Arc};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AssetSource, Global};
|
||||
use rodio::{Decoder, Source, source::Buffered};
|
||||
|
||||
type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
|
||||
|
||||
pub struct SoundRegistry {
|
||||
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
|
||||
assets: Box<dyn AssetSource>,
|
||||
}
|
||||
|
||||
struct GlobalSoundRegistry(Arc<SoundRegistry>);
|
||||
|
||||
impl Global for GlobalSoundRegistry {}
|
||||
|
||||
impl SoundRegistry {
|
||||
pub fn new(source: impl AssetSource) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
cache: Default::default(),
|
||||
assets: Box::new(source),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn global(cx: &App) -> Arc<Self> {
|
||||
cx.global::<GlobalSoundRegistry>().0.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) {
|
||||
cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source)));
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Result<impl Source<Item = f32> + use<>> {
|
||||
if let Some(wav) = self.cache.lock().get(name) {
|
||||
return Ok(wav.clone());
|
||||
}
|
||||
|
||||
let path = format!("sounds/{}.wav", name);
|
||||
let bytes = self
|
||||
.assets
|
||||
.load(&path)?
|
||||
.map(anyhow::Ok)
|
||||
.with_context(|| format!("No asset available for path {path}"))??
|
||||
.into_owned();
|
||||
let cursor = Cursor::new(bytes);
|
||||
let source = Decoder::new(cursor)?.buffered();
|
||||
|
||||
self.cache.lock().insert(name.to_string(), source.clone());
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
use assets::SoundRegistry;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use gpui::{App, AssetSource, BorrowAppContext, Global};
|
||||
use rodio::{OutputStream, OutputStreamBuilder};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, BorrowAppContext, Global};
|
||||
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered};
|
||||
use settings::Settings;
|
||||
use std::io::Cursor;
|
||||
use util::ResultExt;
|
||||
|
||||
mod assets;
|
||||
mod audio_settings;
|
||||
pub use audio_settings::AudioSettings;
|
||||
|
||||
pub fn init(source: impl AssetSource, cx: &mut App) {
|
||||
SoundRegistry::set_global(source, cx);
|
||||
cx.set_global(GlobalAudio(Audio::new()));
|
||||
pub fn init(cx: &mut App) {
|
||||
AudioSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
|
||||
pub enum Sound {
|
||||
Joined,
|
||||
Leave,
|
||||
@@ -38,18 +41,12 @@ impl Sound {
|
||||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
output_handle: Option<OutputStream>,
|
||||
source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut)]
|
||||
struct GlobalAudio(Audio);
|
||||
|
||||
impl Global for GlobalAudio {}
|
||||
impl Global for Audio {}
|
||||
|
||||
impl Audio {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
|
||||
if self.output_handle.is_none() {
|
||||
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
|
||||
@@ -58,26 +55,51 @@ impl Audio {
|
||||
self.output_handle.as_ref()
|
||||
}
|
||||
|
||||
pub fn play_sound(sound: Sound, cx: &mut App) {
|
||||
if !cx.has_global::<GlobalAudio>() {
|
||||
return;
|
||||
}
|
||||
pub fn play_source(
|
||||
source: impl rodio::Source + Send + 'static,
|
||||
cx: &mut App,
|
||||
) -> anyhow::Result<()> {
|
||||
cx.update_default_global(|this: &mut Self, _cx| {
|
||||
let output_handle = this
|
||||
.ensure_output_exists()
|
||||
.ok_or_else(|| anyhow!("Could not open audio output"))?;
|
||||
output_handle.mixer().add(source);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
cx.update_global::<GlobalAudio, _>(|this, cx| {
|
||||
pub fn play_sound(sound: Sound, cx: &mut App) {
|
||||
cx.update_default_global(|this: &mut Self, cx| {
|
||||
let source = this.sound_source(sound, cx).log_err()?;
|
||||
let output_handle = this.ensure_output_exists()?;
|
||||
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
|
||||
output_handle.mixer().add(source);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn end_call(cx: &mut App) {
|
||||
if !cx.has_global::<GlobalAudio>() {
|
||||
return;
|
||||
}
|
||||
|
||||
cx.update_global::<GlobalAudio, _>(|this, _| {
|
||||
cx.update_default_global(|this: &mut Self, _cx| {
|
||||
this.output_handle.take();
|
||||
});
|
||||
}
|
||||
|
||||
fn sound_source(&mut self, sound: Sound, cx: &App) -> Result<impl Source + use<>> {
|
||||
if let Some(wav) = self.source_cache.get(&sound) {
|
||||
return Ok(wav.clone());
|
||||
}
|
||||
|
||||
let path = format!("sounds/{}.wav", sound.file());
|
||||
let bytes = cx
|
||||
.asset_source()
|
||||
.load(&path)?
|
||||
.map(anyhow::Ok)
|
||||
.with_context(|| format!("No asset available for path {path}"))??
|
||||
.into_owned();
|
||||
let cursor = Cursor::new(bytes);
|
||||
let source = Decoder::new(cursor)?.buffered();
|
||||
|
||||
self.source_cache.insert(sound, source.clone());
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
}
|
||||
|
||||
33
crates/audio/src/audio_settings.rs
Normal file
33
crates/audio/src/audio_settings.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AudioSettings {
|
||||
/// Opt into the new audio system.
|
||||
#[serde(rename = "experimental.rodio_audio", default)]
|
||||
pub rodio_audio: bool, // default is false
|
||||
}
|
||||
|
||||
/// Configuration of audio in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct AudioSettingsContent {
|
||||
/// Whether to use the experimental audio system
|
||||
#[serde(rename = "experimental.rodio_audio", default)]
|
||||
pub rodio_audio: bool,
|
||||
}
|
||||
|
||||
impl Settings for AudioSettings {
|
||||
const KEY: Option<&'static str> = Some("audio");
|
||||
|
||||
type FileContent = AudioSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
alter table billing_subscriptions
|
||||
add column orb_subscription_status text,
|
||||
add column orb_current_billing_period_start_date timestamp without time zone,
|
||||
add column orb_current_billing_period_end_date timestamp without time zone;
|
||||
@@ -400,6 +400,8 @@ impl Server {
|
||||
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::BlameBuffer>)
|
||||
.add_request_handler(multi_lsp_query)
|
||||
.add_request_handler(lsp_query)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::LspQueryResponse>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::RestartLanguageServers>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::StopLanguageServers>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::LinkedEditingRange>)
|
||||
@@ -910,7 +912,9 @@ impl Server {
|
||||
user_id=field::Empty,
|
||||
login=field::Empty,
|
||||
impersonator=field::Empty,
|
||||
// todo(lsp) remove after Zed Stable hits v0.204.x
|
||||
multi_lsp_query_request=field::Empty,
|
||||
lsp_query_request=field::Empty,
|
||||
release_channel=field::Empty,
|
||||
{ TOTAL_DURATION_MS }=field::Empty,
|
||||
{ PROCESSING_DURATION_MS }=field::Empty,
|
||||
@@ -2356,6 +2360,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// todo(lsp) remove after Zed Stable hits v0.204.x
|
||||
async fn multi_lsp_query(
|
||||
request: MultiLspQuery,
|
||||
response: Response<MultiLspQuery>,
|
||||
@@ -2366,6 +2371,21 @@ async fn multi_lsp_query(
|
||||
forward_mutating_project_request(request, response, session).await
|
||||
}
|
||||
|
||||
async fn lsp_query(
|
||||
request: proto::LspQuery,
|
||||
response: Response<proto::LspQuery>,
|
||||
session: MessageContext,
|
||||
) -> Result<()> {
|
||||
let (name, should_write) = request.query_name_and_write_permissions();
|
||||
tracing::Span::current().record("lsp_query_request", name);
|
||||
tracing::info!("lsp_query message received");
|
||||
if should_write {
|
||||
forward_mutating_project_request(request, response, session).await
|
||||
} else {
|
||||
forward_read_only_project_request(request, response, session).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify other participants that a new buffer has been created
|
||||
async fn create_buffer_for_peer(
|
||||
request: proto::CreateBufferForPeer,
|
||||
|
||||
@@ -15,13 +15,14 @@ use editor::{
|
||||
},
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{StreamExt, lock::Mutex};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
|
||||
use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
FakeLspAdapter,
|
||||
language_settings::{AllLanguageSettings, InlayHintSettings},
|
||||
};
|
||||
use lsp::LSP_REQUEST_TIMEOUT;
|
||||
use project::{
|
||||
ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
|
||||
lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
|
||||
@@ -1017,6 +1018,211 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
let command_name = "test_command";
|
||||
let capabilities = lsp::ServerCapabilities {
|
||||
code_lens_provider: Some(lsp::CodeLensOptions {
|
||||
resolve_provider: None,
|
||||
}),
|
||||
execute_command_provider: Some(lsp::ExecuteCommandOptions {
|
||||
commands: vec![command_name.to_string()],
|
||||
..lsp::ExecuteCommandOptions::default()
|
||||
}),
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
client_b.language_registry().add(rust_lang());
|
||||
client_b.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities,
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"one.rs": "const ONE: usize = 1;"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| {
|
||||
let lsp_store = editor.project().unwrap().read(cx).lsp_store();
|
||||
let buffer = editor.buffer().read(cx).as_singleton().unwrap();
|
||||
(lsp_store, buffer)
|
||||
});
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
let long_request_time = LSP_REQUEST_TIMEOUT / 2;
|
||||
let (request_started_tx, mut request_started_rx) = mpsc::unbounded();
|
||||
let requests_started = Arc::new(AtomicUsize::new(0));
|
||||
let requests_completed = Arc::new(AtomicUsize::new(0));
|
||||
let _lens_requests = fake_language_server
|
||||
.set_request_handler::<lsp::request::CodeLensRequest, _, _>({
|
||||
let request_started_tx = request_started_tx.clone();
|
||||
let requests_started = requests_started.clone();
|
||||
let requests_completed = requests_completed.clone();
|
||||
move |params, cx| {
|
||||
let mut request_started_tx = request_started_tx.clone();
|
||||
let requests_started = requests_started.clone();
|
||||
let requests_completed = requests_completed.clone();
|
||||
async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri.as_str(),
|
||||
uri!("file:///dir/one.rs")
|
||||
);
|
||||
requests_started.fetch_add(1, atomic::Ordering::Release);
|
||||
request_started_tx.send(()).await.unwrap();
|
||||
cx.background_executor().timer(long_request_time).await;
|
||||
let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1;
|
||||
Ok(Some(vec![lsp::CodeLens {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)),
|
||||
command: Some(lsp::Command {
|
||||
title: format!("LSP Command {i}"),
|
||||
command: command_name.to_string(),
|
||||
arguments: None,
|
||||
}),
|
||||
data: None,
|
||||
}]))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Move cursor to a location, this should trigger the code lens call.
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([7..7])
|
||||
});
|
||||
});
|
||||
let () = request_started_rx.next().await.unwrap();
|
||||
assert_eq!(
|
||||
requests_started.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"Selection change should have initiated the first request"
|
||||
);
|
||||
assert_eq!(
|
||||
requests_completed.load(atomic::Ordering::Acquire),
|
||||
0,
|
||||
"Slow requests should be running still"
|
||||
);
|
||||
let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
|
||||
lsp_store
|
||||
.forget_code_lens_task(buffer_b.read(cx).remote_id())
|
||||
.expect("Should have the fetch task started")
|
||||
});
|
||||
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([1..1])
|
||||
});
|
||||
});
|
||||
let () = request_started_rx.next().await.unwrap();
|
||||
assert_eq!(
|
||||
requests_started.load(atomic::Ordering::Acquire),
|
||||
2,
|
||||
"Selection change should have initiated the second request"
|
||||
);
|
||||
assert_eq!(
|
||||
requests_completed.load(atomic::Ordering::Acquire),
|
||||
0,
|
||||
"Slow requests should be running still"
|
||||
);
|
||||
let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
|
||||
lsp_store
|
||||
.forget_code_lens_task(buffer_b.read(cx).remote_id())
|
||||
.expect("Should have the fetch task started for the 2nd time")
|
||||
});
|
||||
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([2..2])
|
||||
});
|
||||
});
|
||||
let () = request_started_rx.next().await.unwrap();
|
||||
assert_eq!(
|
||||
requests_started.load(atomic::Ordering::Acquire),
|
||||
3,
|
||||
"Selection change should have initiated the third request"
|
||||
);
|
||||
assert_eq!(
|
||||
requests_completed.load(atomic::Ordering::Acquire),
|
||||
0,
|
||||
"Slow requests should be running still"
|
||||
);
|
||||
|
||||
_first_task.await.unwrap();
|
||||
_second_task.await.unwrap();
|
||||
cx_b.run_until_parked();
|
||||
assert_eq!(
|
||||
requests_started.load(atomic::Ordering::Acquire),
|
||||
3,
|
||||
"No selection changes should trigger no more code lens requests"
|
||||
);
|
||||
assert_eq!(
|
||||
requests_completed.load(atomic::Ordering::Acquire),
|
||||
3,
|
||||
"After enough time, all 3 LSP requests should have been served by the language server"
|
||||
);
|
||||
let resulting_lens_actions = editor_b
|
||||
.update(cx_b, |editor, cx| {
|
||||
let lsp_store = editor.project().unwrap().read(cx).lsp_store();
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.code_lens_actions(&buffer_b, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resulting_lens_actions.len(),
|
||||
1,
|
||||
"Should have fetched one code lens action, but got: {resulting_lens_actions:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
resulting_lens_actions.first().unwrap().lsp_action.title(),
|
||||
"LSP Command 3",
|
||||
"Only the final code lens action should be in the data"
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
|
||||
@@ -4850,6 +4850,7 @@ async fn test_definition(
|
||||
let definitions_1 = project_b
|
||||
.update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
assert_eq!(
|
||||
@@ -4885,6 +4886,7 @@ async fn test_definition(
|
||||
let definitions_2 = project_b
|
||||
.update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
assert_eq!(definitions_2.len(), 1);
|
||||
@@ -4922,6 +4924,7 @@ async fn test_definition(
|
||||
let type_definitions = project_b
|
||||
.update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
assert_eq!(
|
||||
@@ -5060,7 +5063,7 @@ async fn test_references(
|
||||
])))
|
||||
.unwrap();
|
||||
|
||||
let references = references.await.unwrap();
|
||||
let references = references.await.unwrap().unwrap();
|
||||
executor.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
// User is informed that a request is no longer pending.
|
||||
@@ -5104,7 +5107,7 @@ async fn test_references(
|
||||
lsp_response_tx
|
||||
.unbounded_send(Err(anyhow!("can't find references")))
|
||||
.unwrap();
|
||||
assert_eq!(references.await.unwrap(), []);
|
||||
assert_eq!(references.await.unwrap().unwrap(), []);
|
||||
|
||||
// User is informed that the request is no longer pending.
|
||||
executor.run_until_parked();
|
||||
@@ -5505,7 +5508,8 @@ async fn test_lsp_hover(
|
||||
// Request hover information as the guest.
|
||||
let mut hovers = project_b
|
||||
.update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
hovers.len(),
|
||||
2,
|
||||
@@ -5764,7 +5768,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
|
||||
}
|
||||
|
||||
let definitions = definitions.await.unwrap();
|
||||
let definitions = definitions.await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
definitions.len(),
|
||||
1,
|
||||
|
||||
@@ -4,6 +4,8 @@ use minidumper::{Client, LoopAction, MinidumpBinary};
|
||||
use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, File},
|
||||
@@ -26,6 +28,9 @@ pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false);
|
||||
const CRASH_HANDLER_PING_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
static PANIC_THREAD_ID: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
pub async fn init(crash_init: InitCrashHandler) {
|
||||
if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() {
|
||||
return;
|
||||
@@ -110,9 +115,10 @@ unsafe fn suspend_all_other_threads() {
|
||||
mach2::task::task_threads(task, &raw mut threads, &raw mut count);
|
||||
}
|
||||
let current = unsafe { mach2::mach_init::mach_thread_self() };
|
||||
let panic_thread = PANIC_THREAD_ID.load(Ordering::SeqCst);
|
||||
for i in 0..count {
|
||||
let t = unsafe { *threads.add(i as usize) };
|
||||
if t != current {
|
||||
if t != current && t != panic_thread {
|
||||
unsafe { mach2::thread_act::thread_suspend(t) };
|
||||
}
|
||||
}
|
||||
@@ -238,6 +244,13 @@ pub fn handle_panic(message: String, span: Option<&Location>) {
|
||||
)
|
||||
.ok();
|
||||
log::error!("triggering a crash to generate a minidump...");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
PANIC_THREAD_ID.store(
|
||||
unsafe { mach2::mach_init::mach_thread_self() },
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
|
||||
@@ -99,6 +99,7 @@ fn handle_preprocessing() -> Result<()> {
|
||||
let mut errors = HashSet::<PreprocessorError>::new();
|
||||
|
||||
handle_frontmatter(&mut book, &mut errors);
|
||||
template_big_table_of_actions(&mut book);
|
||||
template_and_validate_keybindings(&mut book, &mut errors);
|
||||
template_and_validate_actions(&mut book, &mut errors);
|
||||
|
||||
@@ -147,6 +148,18 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>)
|
||||
});
|
||||
}
|
||||
|
||||
fn template_big_table_of_actions(book: &mut Book) {
|
||||
for_each_chapter_mut(book, |chapter| {
|
||||
let needle = "{#ACTIONS_TABLE#}";
|
||||
if let Some(start) = chapter.content.rfind(needle) {
|
||||
chapter.content.replace_range(
|
||||
start..start + needle.len(),
|
||||
&generate_big_table_of_actions(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||
let regex = Regex::new(r"\{#kb (.*?)\}").unwrap();
|
||||
|
||||
@@ -277,6 +290,7 @@ struct ActionDef {
|
||||
name: &'static str,
|
||||
human_name: String,
|
||||
deprecated_aliases: &'static [&'static str],
|
||||
docs: Option<&'static str>,
|
||||
}
|
||||
|
||||
fn dump_all_gpui_actions() -> Vec<ActionDef> {
|
||||
@@ -285,6 +299,7 @@ fn dump_all_gpui_actions() -> Vec<ActionDef> {
|
||||
name: action.name,
|
||||
human_name: command_palette::humanize_action_name(action.name),
|
||||
deprecated_aliases: action.deprecated_aliases,
|
||||
docs: action.documentation,
|
||||
})
|
||||
.collect::<Vec<ActionDef>>();
|
||||
|
||||
@@ -418,3 +433,54 @@ fn title_regex() -> &'static Regex {
|
||||
static TITLE_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap())
|
||||
}
|
||||
|
||||
fn generate_big_table_of_actions() -> String {
|
||||
let actions = &*ALL_ACTIONS;
|
||||
let mut output = String::new();
|
||||
|
||||
let mut actions_sorted = actions.iter().collect::<Vec<_>>();
|
||||
actions_sorted.sort_by_key(|a| a.name);
|
||||
|
||||
// Start the definition list with custom styling for better spacing
|
||||
output.push_str("<dl style=\"line-height: 1.8;\">\n");
|
||||
|
||||
for action in actions_sorted.into_iter() {
|
||||
// Add the humanized action name as the term with margin
|
||||
output.push_str(
|
||||
"<dt style=\"margin-top: 1.5em; margin-bottom: 0.5em; font-weight: bold;\"><code>",
|
||||
);
|
||||
output.push_str(&action.human_name);
|
||||
output.push_str("</code></dt>\n");
|
||||
|
||||
// Add the definition with keymap name and description
|
||||
output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
|
||||
|
||||
// Add the description, escaping HTML if needed
|
||||
if let Some(description) = action.docs {
|
||||
output.push_str(
|
||||
&description
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">"),
|
||||
);
|
||||
output.push_str("<br>\n");
|
||||
}
|
||||
output.push_str("Keymap Name: <code>");
|
||||
output.push_str(action.name);
|
||||
output.push_str("</code><br>\n");
|
||||
if !action.deprecated_aliases.is_empty() {
|
||||
output.push_str("Deprecated Aliases:");
|
||||
for alias in action.deprecated_aliases.iter() {
|
||||
output.push_str("<code>");
|
||||
output.push_str(alias);
|
||||
output.push_str("</code>, ");
|
||||
}
|
||||
}
|
||||
output.push_str("\n</dd>\n");
|
||||
}
|
||||
|
||||
// Close the definition list
|
||||
output.push_str("</dl>\n");
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
@@ -290,7 +290,10 @@ pub enum Block {
|
||||
ExcerptBoundary {
|
||||
excerpt: ExcerptInfo,
|
||||
height: u32,
|
||||
starts_new_buffer: bool,
|
||||
},
|
||||
BufferHeader {
|
||||
excerpt: ExcerptInfo,
|
||||
height: u32,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -303,27 +306,37 @@ impl Block {
|
||||
..
|
||||
} => BlockId::ExcerptBoundary(next_excerpt.id),
|
||||
Block::FoldedBuffer { first_excerpt, .. } => BlockId::FoldedBuffer(first_excerpt.id),
|
||||
Block::BufferHeader {
|
||||
excerpt: next_excerpt,
|
||||
..
|
||||
} => BlockId::ExcerptBoundary(next_excerpt.id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_height(&self) -> bool {
|
||||
match self {
|
||||
Block::Custom(block) => block.height.is_some(),
|
||||
Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => true,
|
||||
Block::ExcerptBoundary { .. }
|
||||
| Block::FoldedBuffer { .. }
|
||||
| Block::BufferHeader { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u32 {
|
||||
match self {
|
||||
Block::Custom(block) => block.height.unwrap_or(0),
|
||||
Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } => *height,
|
||||
Block::ExcerptBoundary { height, .. }
|
||||
| Block::FoldedBuffer { height, .. }
|
||||
| Block::BufferHeader { height, .. } => *height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(&self) -> BlockStyle {
|
||||
match self {
|
||||
Block::Custom(block) => block.style,
|
||||
Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => BlockStyle::Sticky,
|
||||
Block::ExcerptBoundary { .. }
|
||||
| Block::FoldedBuffer { .. }
|
||||
| Block::BufferHeader { .. } => BlockStyle::Sticky,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +345,7 @@ impl Block {
|
||||
Block::Custom(block) => matches!(block.placement, BlockPlacement::Above(_)),
|
||||
Block::FoldedBuffer { .. } => false,
|
||||
Block::ExcerptBoundary { .. } => true,
|
||||
Block::BufferHeader { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +354,7 @@ impl Block {
|
||||
Block::Custom(block) => matches!(block.placement, BlockPlacement::Near(_)),
|
||||
Block::FoldedBuffer { .. } => false,
|
||||
Block::ExcerptBoundary { .. } => false,
|
||||
Block::BufferHeader { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +366,7 @@ impl Block {
|
||||
),
|
||||
Block::FoldedBuffer { .. } => false,
|
||||
Block::ExcerptBoundary { .. } => false,
|
||||
Block::BufferHeader { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,6 +375,7 @@ impl Block {
|
||||
Block::Custom(block) => matches!(block.placement, BlockPlacement::Replace(_)),
|
||||
Block::FoldedBuffer { .. } => true,
|
||||
Block::ExcerptBoundary { .. } => false,
|
||||
Block::BufferHeader { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +384,7 @@ impl Block {
|
||||
Block::Custom(_) => false,
|
||||
Block::FoldedBuffer { .. } => true,
|
||||
Block::ExcerptBoundary { .. } => true,
|
||||
Block::BufferHeader { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,9 +392,8 @@ impl Block {
|
||||
match self {
|
||||
Block::Custom(_) => false,
|
||||
Block::FoldedBuffer { .. } => true,
|
||||
Block::ExcerptBoundary {
|
||||
starts_new_buffer, ..
|
||||
} => *starts_new_buffer,
|
||||
Block::ExcerptBoundary { .. } => false,
|
||||
Block::BufferHeader { .. } => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,14 +410,14 @@ impl Debug for Block {
|
||||
.field("first_excerpt", &first_excerpt)
|
||||
.field("height", height)
|
||||
.finish(),
|
||||
Self::ExcerptBoundary {
|
||||
starts_new_buffer,
|
||||
excerpt,
|
||||
height,
|
||||
} => f
|
||||
Self::ExcerptBoundary { excerpt, height } => f
|
||||
.debug_struct("ExcerptBoundary")
|
||||
.field("excerpt", excerpt)
|
||||
.field("starts_new_buffer", starts_new_buffer)
|
||||
.field("height", height)
|
||||
.finish(),
|
||||
Self::BufferHeader { excerpt, height } => f
|
||||
.debug_struct("BufferHeader")
|
||||
.field("excerpt", excerpt)
|
||||
.field("height", height)
|
||||
.finish(),
|
||||
}
|
||||
@@ -662,13 +679,11 @@ impl BlockMap {
|
||||
}),
|
||||
);
|
||||
|
||||
if buffer.show_headers() {
|
||||
blocks_in_edit.extend(self.header_and_footer_blocks(
|
||||
buffer,
|
||||
(start_bound, end_bound),
|
||||
wrap_snapshot,
|
||||
));
|
||||
}
|
||||
blocks_in_edit.extend(self.header_and_footer_blocks(
|
||||
buffer,
|
||||
(start_bound, end_bound),
|
||||
wrap_snapshot,
|
||||
));
|
||||
|
||||
BlockMap::sort_blocks(&mut blocks_in_edit);
|
||||
|
||||
@@ -771,7 +786,7 @@ impl BlockMap {
|
||||
if self.buffers_with_disabled_headers.contains(&new_buffer_id) {
|
||||
continue;
|
||||
}
|
||||
if self.folded_buffers.contains(&new_buffer_id) {
|
||||
if self.folded_buffers.contains(&new_buffer_id) && buffer.show_headers() {
|
||||
let mut last_excerpt_end_row = first_excerpt.end_row;
|
||||
|
||||
while let Some(next_boundary) = boundaries.peek() {
|
||||
@@ -804,20 +819,24 @@ impl BlockMap {
|
||||
}
|
||||
}
|
||||
|
||||
if new_buffer_id.is_some() {
|
||||
let starts_new_buffer = new_buffer_id.is_some();
|
||||
let block = if starts_new_buffer && buffer.show_headers() {
|
||||
height += self.buffer_header_height;
|
||||
} else {
|
||||
Block::BufferHeader {
|
||||
excerpt: excerpt_boundary.next,
|
||||
height,
|
||||
}
|
||||
} else if excerpt_boundary.prev.is_some() {
|
||||
height += self.excerpt_header_height;
|
||||
}
|
||||
|
||||
return Some((
|
||||
BlockPlacement::Above(WrapRow(wrap_row)),
|
||||
Block::ExcerptBoundary {
|
||||
excerpt: excerpt_boundary.next,
|
||||
height,
|
||||
starts_new_buffer: new_buffer_id.is_some(),
|
||||
},
|
||||
));
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
return Some((BlockPlacement::Above(WrapRow(wrap_row)), block));
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -842,13 +861,25 @@ impl BlockMap {
|
||||
(
|
||||
Block::ExcerptBoundary {
|
||||
excerpt: excerpt_a, ..
|
||||
}
|
||||
| Block::BufferHeader {
|
||||
excerpt: excerpt_a, ..
|
||||
},
|
||||
Block::ExcerptBoundary {
|
||||
excerpt: excerpt_b, ..
|
||||
}
|
||||
| Block::BufferHeader {
|
||||
excerpt: excerpt_b, ..
|
||||
},
|
||||
) => Some(excerpt_a.id).cmp(&Some(excerpt_b.id)),
|
||||
(Block::ExcerptBoundary { .. }, Block::Custom(_)) => Ordering::Less,
|
||||
(Block::Custom(_), Block::ExcerptBoundary { .. }) => Ordering::Greater,
|
||||
(
|
||||
Block::ExcerptBoundary { .. } | Block::BufferHeader { .. },
|
||||
Block::Custom(_),
|
||||
) => Ordering::Less,
|
||||
(
|
||||
Block::Custom(_),
|
||||
Block::ExcerptBoundary { .. } | Block::BufferHeader { .. },
|
||||
) => Ordering::Greater,
|
||||
(Block::Custom(block_a), Block::Custom(block_b)) => block_a
|
||||
.priority
|
||||
.cmp(&block_b.priority)
|
||||
@@ -1377,7 +1408,9 @@ impl BlockSnapshot {
|
||||
|
||||
while let Some(transform) = cursor.item() {
|
||||
match &transform.block {
|
||||
Some(Block::ExcerptBoundary { excerpt, .. }) => {
|
||||
Some(
|
||||
Block::ExcerptBoundary { excerpt, .. } | Block::BufferHeader { excerpt, .. },
|
||||
) => {
|
||||
return Some(StickyHeaderExcerpt { excerpt });
|
||||
}
|
||||
Some(block) if block.is_buffer_header() => return None,
|
||||
|
||||
@@ -1900,6 +1900,60 @@ impl Editor {
|
||||
editor.update_lsp_data(false, Some(*buffer_id), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
project::Event::EntryRenamed(transaction) => {
|
||||
let Some(workspace) = editor.workspace() else {
|
||||
return;
|
||||
};
|
||||
let Some(active_editor) = workspace.read(cx).active_item_as::<Self>(cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if active_editor.entity_id() == cx.entity_id() {
|
||||
let edited_buffers_already_open = {
|
||||
let other_editors: Vec<Entity<Editor>> = workspace
|
||||
.read(cx)
|
||||
.panes()
|
||||
.iter()
|
||||
.flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
|
||||
.filter(|editor| editor.entity_id() != cx.entity_id())
|
||||
.collect();
|
||||
|
||||
transaction.0.keys().all(|buffer| {
|
||||
other_editors.iter().any(|editor| {
|
||||
let multi_buffer = editor.read(cx).buffer();
|
||||
multi_buffer.read(cx).is_singleton()
|
||||
&& multi_buffer.read(cx).as_singleton().map_or(
|
||||
false,
|
||||
|singleton| {
|
||||
singleton.entity_id() == buffer.entity_id()
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
if !edited_buffers_already_open {
|
||||
let workspace = workspace.downgrade();
|
||||
let transaction = transaction.clone();
|
||||
cx.defer_in(window, move |_, window, cx| {
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
Self::open_project_transaction(
|
||||
&editor,
|
||||
workspace,
|
||||
transaction,
|
||||
"Rename".to_string(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
));
|
||||
@@ -6282,7 +6336,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub async fn open_project_transaction(
|
||||
this: &WeakEntity<Editor>,
|
||||
editor: &WeakEntity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
transaction: ProjectTransaction,
|
||||
title: String,
|
||||
@@ -6300,7 +6354,7 @@ impl Editor {
|
||||
|
||||
if let Some((buffer, transaction)) = entries.first() {
|
||||
if entries.len() == 1 {
|
||||
let excerpt = this.update(cx, |editor, cx| {
|
||||
let excerpt = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
@@ -6697,7 +6751,6 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer_id = cursor_position.buffer_id;
|
||||
let buffer = this.buffer.read(cx);
|
||||
if buffer
|
||||
.text_anchor_for_position(cursor_position, cx)
|
||||
@@ -6710,8 +6763,8 @@ impl Editor {
|
||||
let mut write_ranges = Vec::new();
|
||||
let mut read_ranges = Vec::new();
|
||||
for highlight in highlights {
|
||||
for (excerpt_id, excerpt_range) in
|
||||
buffer.excerpts_for_buffer(cursor_buffer.read(cx).remote_id(), cx)
|
||||
let buffer_id = cursor_buffer.read(cx).remote_id();
|
||||
for (excerpt_id, excerpt_range) in buffer.excerpts_for_buffer(buffer_id, cx)
|
||||
{
|
||||
let start = highlight
|
||||
.range
|
||||
@@ -6726,12 +6779,12 @@ impl Editor {
|
||||
}
|
||||
|
||||
let range = Anchor {
|
||||
buffer_id,
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id,
|
||||
text_anchor: start,
|
||||
diff_base_anchor: None,
|
||||
}..Anchor {
|
||||
buffer_id,
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id,
|
||||
text_anchor: end,
|
||||
diff_base_anchor: None,
|
||||
@@ -9496,17 +9549,21 @@ impl Editor {
|
||||
selection: Range<Anchor>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let buffer_id = match (&selection.start.buffer_id, &selection.end.buffer_id) {
|
||||
(Some(a), Some(b)) if a == b => a,
|
||||
_ => {
|
||||
log::error!("expected anchor range to have matching buffer IDs");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let multi_buffer = self.buffer().read(cx);
|
||||
let Some(buffer) = multi_buffer.buffer(*buffer_id) else {
|
||||
let Some((_, buffer, _)) = self
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.excerpt_containing(selection.start, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some((_, end_buffer, _)) = self.buffer().read(cx).excerpt_containing(selection.end, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if buffer != end_buffer {
|
||||
log::error!("expected anchor range to have matching buffer IDs");
|
||||
return;
|
||||
}
|
||||
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
|
||||
@@ -10593,16 +10650,12 @@ impl Editor {
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<(Anchor, Breakpoint)> {
|
||||
let project = self.project.clone()?;
|
||||
|
||||
let buffer_id = breakpoint_position.buffer_id.or_else(|| {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_id_for_excerpt(breakpoint_position.excerpt_id)
|
||||
})?;
|
||||
let buffer = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.buffer_for_anchor(breakpoint_position, cx)?;
|
||||
|
||||
let enclosing_excerpt = breakpoint_position.excerpt_id;
|
||||
let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?;
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let row = buffer_snapshot
|
||||
@@ -10775,21 +10828,11 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(buffer_id) = breakpoint_position.buffer_id.or_else(|| {
|
||||
if breakpoint_position == Anchor::min() {
|
||||
self.buffer()
|
||||
.read(cx)
|
||||
.excerpt_buffer_ids()
|
||||
.into_iter()
|
||||
.next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
|
||||
let Some(buffer) = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.buffer_for_anchor(breakpoint_position, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -15432,7 +15475,8 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else {
|
||||
let next_diagnostic_start = buffer.anchor_after(next_diagnostic.range.start);
|
||||
let Some(buffer_id) = buffer.buffer_id_for_anchor(next_diagnostic_start) else {
|
||||
return;
|
||||
};
|
||||
self.change_selections(Default::default(), window, cx, |s| {
|
||||
@@ -15710,7 +15754,9 @@ impl Editor {
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
let definitions = definitions.await?;
|
||||
let Some(definitions) = definitions.await? else {
|
||||
return Ok(Navigated::No);
|
||||
};
|
||||
let navigated = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.navigate_to_hover_links(
|
||||
@@ -16052,7 +16098,9 @@ impl Editor {
|
||||
}
|
||||
});
|
||||
|
||||
let locations = references.await?;
|
||||
let Some(locations) = references.await? else {
|
||||
return anyhow::Ok(Navigated::No);
|
||||
};
|
||||
if locations.is_empty() {
|
||||
return anyhow::Ok(Navigated::No);
|
||||
}
|
||||
@@ -20421,11 +20469,8 @@ impl Editor {
|
||||
.range_to_buffer_ranges_with_deleted_hunks(selection.range())
|
||||
{
|
||||
if let Some(anchor) = anchor {
|
||||
// selection is in a deleted hunk
|
||||
let Some(buffer_id) = anchor.buffer_id else {
|
||||
continue;
|
||||
};
|
||||
let Some(buffer_handle) = multi_buffer.buffer(buffer_id) else {
|
||||
let Some(buffer_handle) = multi_buffer.buffer_for_anchor(anchor, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let offset = text::ToOffset::to_offset(
|
||||
@@ -21837,7 +21882,7 @@ pub trait SemanticsProvider {
|
||||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Vec<project::Hover>>>;
|
||||
) -> Option<Task<Option<Vec<project::Hover>>>>;
|
||||
|
||||
fn inline_values(
|
||||
&self,
|
||||
@@ -21876,7 +21921,7 @@ pub trait SemanticsProvider {
|
||||
position: text::Anchor,
|
||||
kind: GotoDefinitionKind,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<Vec<LocationLink>>>>;
|
||||
) -> Option<Task<Result<Option<Vec<LocationLink>>>>>;
|
||||
|
||||
fn range_for_rename(
|
||||
&self,
|
||||
@@ -21989,7 +22034,13 @@ impl CodeActionProvider for Entity<Project> {
|
||||
Ok(code_lens_actions
|
||||
.context("code lens fetch")?
|
||||
.into_iter()
|
||||
.chain(code_actions.context("code action fetch")?)
|
||||
.flatten()
|
||||
.chain(
|
||||
code_actions
|
||||
.context("code action fetch")?
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.collect())
|
||||
})
|
||||
})
|
||||
@@ -22284,7 +22335,7 @@ impl SemanticsProvider for Entity<Project> {
|
||||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Vec<project::Hover>>> {
|
||||
) -> Option<Task<Option<Vec<project::Hover>>>> {
|
||||
Some(self.update(cx, |project, cx| project.hover(buffer, position, cx)))
|
||||
}
|
||||
|
||||
@@ -22305,7 +22356,7 @@ impl SemanticsProvider for Entity<Project> {
|
||||
position: text::Anchor,
|
||||
kind: GotoDefinitionKind,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<Vec<LocationLink>>>> {
|
||||
) -> Option<Task<Result<Option<Vec<LocationLink>>>>> {
|
||||
Some(self.update(cx, |project, cx| match kind {
|
||||
GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx),
|
||||
GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx),
|
||||
|
||||
@@ -2749,7 +2749,10 @@ impl EditorElement {
|
||||
let mut block_offset = 0;
|
||||
let mut found_excerpt_header = false;
|
||||
for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
|
||||
if matches!(block, Block::ExcerptBoundary { .. }) {
|
||||
if matches!(
|
||||
block,
|
||||
Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
|
||||
) {
|
||||
found_excerpt_header = true;
|
||||
break;
|
||||
}
|
||||
@@ -2766,7 +2769,10 @@ impl EditorElement {
|
||||
let mut block_height = 0;
|
||||
let mut found_excerpt_header = false;
|
||||
for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
|
||||
if matches!(block, Block::ExcerptBoundary { .. }) {
|
||||
if matches!(
|
||||
block,
|
||||
Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
|
||||
) {
|
||||
found_excerpt_header = true;
|
||||
}
|
||||
block_height += block.height();
|
||||
@@ -3452,42 +3458,41 @@ impl EditorElement {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
Block::ExcerptBoundary {
|
||||
excerpt,
|
||||
height,
|
||||
starts_new_buffer,
|
||||
..
|
||||
} => {
|
||||
Block::ExcerptBoundary { .. } => {
|
||||
let color = cx.theme().colors().clone();
|
||||
let mut result = v_flex().id(block_id).w_full();
|
||||
|
||||
result = result.child(
|
||||
h_flex().relative().child(
|
||||
div()
|
||||
.top(line_height / 2.)
|
||||
.absolute()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(color.border_variant),
|
||||
),
|
||||
);
|
||||
|
||||
result.into_any()
|
||||
}
|
||||
|
||||
Block::BufferHeader { excerpt, height } => {
|
||||
let mut result = v_flex().id(block_id).w_full();
|
||||
|
||||
let jump_data = header_jump_data(snapshot, block_row_start, *height, excerpt);
|
||||
|
||||
if *starts_new_buffer {
|
||||
if sticky_header_excerpt_id != Some(excerpt.id) {
|
||||
let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
|
||||
if sticky_header_excerpt_id != Some(excerpt.id) {
|
||||
let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
|
||||
|
||||
result = result.child(div().pr(editor_margins.right).child(
|
||||
self.render_buffer_header(
|
||||
excerpt, false, selected, false, jump_data, window, cx,
|
||||
),
|
||||
));
|
||||
} else {
|
||||
result =
|
||||
result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
|
||||
}
|
||||
} else {
|
||||
result = result.child(
|
||||
h_flex().relative().child(
|
||||
div()
|
||||
.top(line_height / 2.)
|
||||
.absolute()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(color.border_variant),
|
||||
result = result.child(div().pr(editor_margins.right).child(
|
||||
self.render_buffer_header(
|
||||
excerpt, false, selected, false, jump_data, window, cx,
|
||||
),
|
||||
);
|
||||
};
|
||||
));
|
||||
} else {
|
||||
result =
|
||||
result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
|
||||
}
|
||||
|
||||
result.into_any()
|
||||
}
|
||||
@@ -5708,7 +5713,10 @@ impl EditorElement {
|
||||
let end_row_in_current_excerpt = snapshot
|
||||
.blocks_in_range(start_row..end_row)
|
||||
.find_map(|(start_row, block)| {
|
||||
if matches!(block, Block::ExcerptBoundary { .. }) {
|
||||
if matches!(
|
||||
block,
|
||||
Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
|
||||
) {
|
||||
Some(start_row)
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{Editor, RangeToAnchorExt};
|
||||
use gpui::{Context, Window};
|
||||
use gpui::{Context, HighlightStyle, Window};
|
||||
use language::CursorShape;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
enum MatchingBracketHighlight {}
|
||||
|
||||
@@ -9,7 +10,7 @@ pub fn refresh_matching_bracket_highlights(
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
|
||||
editor.clear_highlights::<MatchingBracketHighlight>(cx);
|
||||
|
||||
let newest_selection = editor.selections.newest::<usize>(cx);
|
||||
// Don't highlight brackets if the selection isn't empty
|
||||
@@ -35,12 +36,19 @@ pub fn refresh_matching_bracket_highlights(
|
||||
.buffer_snapshot
|
||||
.innermost_enclosing_bracket_ranges(head..tail, None)
|
||||
{
|
||||
editor.highlight_background::<MatchingBracketHighlight>(
|
||||
&[
|
||||
editor.highlight_text::<MatchingBracketHighlight>(
|
||||
vec![
|
||||
opening_range.to_anchors(&snapshot.buffer_snapshot),
|
||||
closing_range.to_anchors(&snapshot.buffer_snapshot),
|
||||
],
|
||||
|theme| theme.colors().editor_document_highlight_bracket_background,
|
||||
HighlightStyle {
|
||||
background_color: Some(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_document_highlight_bracket_background,
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -104,7 +112,7 @@ mod tests {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test«(»"Test argument"«)» {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
@@ -115,7 +123,7 @@ mod tests {
|
||||
another_test(1, ˇ2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") {
|
||||
another_test«(»1, 2, 3«)»;
|
||||
}
|
||||
@@ -126,7 +134,7 @@ mod tests {
|
||||
anotherˇ_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") «{»
|
||||
another_test(1, 2, 3);
|
||||
«}»
|
||||
@@ -138,7 +146,7 @@ mod tests {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
@@ -150,8 +158,8 @@ mod tests {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") {
|
||||
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test«("Test argument") {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
|
||||
@@ -321,7 +321,10 @@ pub fn update_inlay_link_and_hover_points(
|
||||
if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
|
||||
match cached_hint.resolve_state {
|
||||
ResolveState::CanResolve(_, _) => {
|
||||
if let Some(buffer_id) = previous_valid_anchor.buffer_id {
|
||||
if let Some(buffer_id) = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_id_for_anchor(previous_valid_anchor)
|
||||
{
|
||||
inlay_hint_cache.spawn_hint_resolve(
|
||||
buffer_id,
|
||||
excerpt_id,
|
||||
@@ -559,7 +562,7 @@ pub fn show_link_definition(
|
||||
provider.definitions(&buffer, buffer_position, preferred_kind, cx)
|
||||
})?;
|
||||
if let Some(task) = task {
|
||||
task.await.ok().map(|definition_result| {
|
||||
task.await.ok().flatten().map(|definition_result| {
|
||||
(
|
||||
definition_result.iter().find_map(|link| {
|
||||
link.origin.as_ref().and_then(|origin| {
|
||||
|
||||
@@ -428,7 +428,7 @@ fn show_hover(
|
||||
};
|
||||
|
||||
let hovers_response = if let Some(hover_request) = hover_request {
|
||||
hover_request.await
|
||||
hover_request.await.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
@@ -475,10 +475,7 @@ impl InlayHintCache {
|
||||
let excerpt_cached_hints = excerpt_cached_hints.read();
|
||||
let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
|
||||
shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
|
||||
let Some(buffer) = shown_anchor
|
||||
.buffer_id
|
||||
.and_then(|buffer_id| multi_buffer.buffer(buffer_id))
|
||||
else {
|
||||
let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else {
|
||||
return false;
|
||||
};
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
@@ -72,7 +72,7 @@ pub(super) fn refresh_linked_ranges(
|
||||
// Throw away selections spanning multiple buffers.
|
||||
continue;
|
||||
}
|
||||
if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
|
||||
if let Some(buffer) = buffer.buffer_for_anchor(end_position, cx) {
|
||||
applicable_selections.push((
|
||||
buffer,
|
||||
start_position.text_anchor,
|
||||
|
||||
@@ -190,14 +190,16 @@ pub fn deploy_context_menu(
|
||||
.all::<PointUtf16>(cx)
|
||||
.into_iter()
|
||||
.any(|s| !s.is_empty());
|
||||
let has_git_repo = anchor.buffer_id.is_some_and(|buffer_id| {
|
||||
project
|
||||
.read(cx)
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer_id, cx)
|
||||
.is_some()
|
||||
});
|
||||
let has_git_repo = buffer
|
||||
.buffer_id_for_anchor(anchor)
|
||||
.is_some_and(|buffer_id| {
|
||||
project
|
||||
.read(cx)
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer_id, cx)
|
||||
.is_some()
|
||||
});
|
||||
|
||||
let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
|
||||
let run_to_cursor = window.is_action_available(&RunToCursor, cx);
|
||||
|
||||
@@ -431,7 +431,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
|
||||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Vec<project::Hover>>> {
|
||||
) -> Option<Task<Option<Vec<project::Hover>>>> {
|
||||
let buffer = self.to_base(buffer, &[position], cx)?;
|
||||
self.0.hover(&buffer, position, cx)
|
||||
}
|
||||
@@ -490,7 +490,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
|
||||
position: text::Anchor,
|
||||
kind: crate::GotoDefinitionKind,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<anyhow::Result<Vec<project::LocationLink>>>> {
|
||||
) -> Option<Task<anyhow::Result<Option<Vec<project::LocationLink>>>>> {
|
||||
let buffer = self.to_base(buffer, &[position], cx)?;
|
||||
self.0.definitions(&buffer, position, kind, cx)
|
||||
}
|
||||
|
||||
@@ -182,7 +182,9 @@ impl Editor {
|
||||
let signature_help = task.await;
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let Some(mut signature_help) = signature_help.into_iter().next() else {
|
||||
let Some(mut signature_help) =
|
||||
signature_help.unwrap_or_default().into_iter().next()
|
||||
else {
|
||||
editor
|
||||
.signature_help_state
|
||||
.hide(SignatureHelpHiddenBy::AutoClose);
|
||||
|
||||
@@ -230,26 +230,23 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
|
||||
lines[row as usize].push_str("§ -----");
|
||||
}
|
||||
}
|
||||
Block::ExcerptBoundary {
|
||||
excerpt,
|
||||
height,
|
||||
starts_new_buffer,
|
||||
} => {
|
||||
if starts_new_buffer {
|
||||
lines[row.0 as usize].push_str(&cx.update(|_, cx| {
|
||||
format!(
|
||||
"§ {}",
|
||||
excerpt
|
||||
.buffer
|
||||
.file()
|
||||
.unwrap()
|
||||
.file_name(cx)
|
||||
.to_string_lossy()
|
||||
)
|
||||
}));
|
||||
} else {
|
||||
lines[row.0 as usize].push_str("§ -----")
|
||||
Block::ExcerptBoundary { height, .. } => {
|
||||
for row in row.0..row.0 + height {
|
||||
lines[row as usize].push_str("§ -----");
|
||||
}
|
||||
}
|
||||
Block::BufferHeader { excerpt, height } => {
|
||||
lines[row.0 as usize].push_str(&cx.update(|_, cx| {
|
||||
format!(
|
||||
"§ {}",
|
||||
excerpt
|
||||
.buffer
|
||||
.file()
|
||||
.unwrap()
|
||||
.file_name(cx)
|
||||
.to_string_lossy()
|
||||
)
|
||||
}));
|
||||
for row in row.0 + 1..row.0 + height {
|
||||
lines[row as usize].push_str("§ -----");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ffi::OsStr,
|
||||
mem::ManuallyDrop,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
@@ -460,13 +461,15 @@ impl Platform for WindowsPlatform {
|
||||
}
|
||||
|
||||
fn open_url(&self, url: &str) {
|
||||
if url.is_empty() {
|
||||
return;
|
||||
}
|
||||
let url_string = url.to_string();
|
||||
self.background_executor()
|
||||
.spawn(async move {
|
||||
if url_string.is_empty() {
|
||||
return;
|
||||
}
|
||||
open_target(url_string.as_str());
|
||||
open_target(&url_string)
|
||||
.with_context(|| format!("Opening url: {}", url_string))
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -514,37 +517,29 @@ impl Platform for WindowsPlatform {
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: &Path) {
|
||||
let Ok(file_full_path) = path.canonicalize() else {
|
||||
log::error!("unable to parse file path");
|
||||
if path.as_os_str().is_empty() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
let path = path.to_path_buf();
|
||||
self.background_executor()
|
||||
.spawn(async move {
|
||||
let Some(path) = file_full_path.to_str() else {
|
||||
return;
|
||||
};
|
||||
if path.is_empty() {
|
||||
return;
|
||||
}
|
||||
open_target_in_explorer(path);
|
||||
open_target_in_explorer(&path)
|
||||
.with_context(|| format!("Revealing path {} in explorer", path.display()))
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn open_with_system(&self, path: &Path) {
|
||||
let Ok(full_path) = path.canonicalize() else {
|
||||
log::error!("unable to parse file full path: {}", path.display());
|
||||
if path.as_os_str().is_empty() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
let path = path.to_path_buf();
|
||||
self.background_executor()
|
||||
.spawn(async move {
|
||||
let Some(full_path_str) = full_path.to_str() else {
|
||||
return;
|
||||
};
|
||||
if full_path_str.is_empty() {
|
||||
return;
|
||||
};
|
||||
open_target(full_path_str);
|
||||
open_target(&path)
|
||||
.with_context(|| format!("Opening {} with system", path.display()))
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -735,39 +730,67 @@ pub(crate) struct WindowCreationInfo {
|
||||
pub(crate) disable_direct_composition: bool,
|
||||
}
|
||||
|
||||
fn open_target(target: &str) {
|
||||
unsafe {
|
||||
let ret = ShellExecuteW(
|
||||
fn open_target(target: impl AsRef<OsStr>) -> Result<()> {
|
||||
let target = target.as_ref();
|
||||
let ret = unsafe {
|
||||
ShellExecuteW(
|
||||
None,
|
||||
windows::core::w!("open"),
|
||||
&HSTRING::from(target),
|
||||
None,
|
||||
None,
|
||||
SW_SHOWDEFAULT,
|
||||
);
|
||||
if ret.0 as isize <= 32 {
|
||||
log::error!("Unable to open target: {}", std::io::Error::last_os_error());
|
||||
}
|
||||
)
|
||||
};
|
||||
if ret.0 as isize <= 32 {
|
||||
Err(anyhow::anyhow!(
|
||||
"Unable to open target: {}",
|
||||
std::io::Error::last_os_error()
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn open_target_in_explorer(target: &str) {
|
||||
fn open_target_in_explorer(target: &Path) -> Result<()> {
|
||||
let dir = target.parent().context("No parent folder found")?;
|
||||
let desktop = unsafe { SHGetDesktopFolder()? };
|
||||
|
||||
let mut dir_item = std::ptr::null_mut();
|
||||
unsafe {
|
||||
let ret = ShellExecuteW(
|
||||
desktop.ParseDisplayName(
|
||||
HWND::default(),
|
||||
None,
|
||||
windows::core::w!("open"),
|
||||
windows::core::w!("explorer.exe"),
|
||||
&HSTRING::from(format!("/select,{}", target).as_str()),
|
||||
&HSTRING::from(dir),
|
||||
None,
|
||||
SW_SHOWDEFAULT,
|
||||
);
|
||||
if ret.0 as isize <= 32 {
|
||||
log::error!(
|
||||
"Unable to open target in explorer: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
&mut dir_item,
|
||||
std::ptr::null_mut(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut file_item = std::ptr::null_mut();
|
||||
unsafe {
|
||||
desktop.ParseDisplayName(
|
||||
HWND::default(),
|
||||
None,
|
||||
&HSTRING::from(target),
|
||||
None,
|
||||
&mut file_item,
|
||||
std::ptr::null_mut(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let highlight = [file_item as *const _];
|
||||
unsafe { SHOpenFolderAndSelectItems(dir_item as _, Some(&highlight), 0) }.or_else(|err| {
|
||||
if err.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 {
|
||||
// On some systems, the above call mysteriously fails with "file not
|
||||
// found" even though the file is there. In these cases, ShellExecute()
|
||||
// seems to work as a fallback (although it won't select the file).
|
||||
open_target(dir).context("Opening target parent folder")
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Can not open target path: {}", err))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn file_open_dialog(
|
||||
|
||||
@@ -5,7 +5,7 @@ use anyhow::Result;
|
||||
use collections::{FxHashMap, HashMap, HashSet};
|
||||
use ec4rs::{
|
||||
Properties as EditorconfigProperties,
|
||||
property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs},
|
||||
property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs},
|
||||
};
|
||||
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||
use gpui::{App, Modifiers};
|
||||
@@ -1131,6 +1131,10 @@ impl AllLanguageSettings {
|
||||
}
|
||||
|
||||
fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
|
||||
let preferred_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
|
||||
MaxLineLen::Value(u) => Some(u as u32),
|
||||
MaxLineLen::Off => None,
|
||||
});
|
||||
let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
|
||||
IndentSize::Value(u) => NonZeroU32::new(u as u32),
|
||||
IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
|
||||
@@ -1158,6 +1162,7 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr
|
||||
*target = value;
|
||||
}
|
||||
}
|
||||
merge(&mut settings.preferred_line_length, preferred_line_length);
|
||||
merge(&mut settings.tab_size, tab_size);
|
||||
merge(&mut settings.hard_tabs, hard_tabs);
|
||||
merge(
|
||||
|
||||
@@ -96,7 +96,7 @@ impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
|
||||
}
|
||||
|
||||
type DefaultIndex = usize;
|
||||
#[derive(Default, Clone)]
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct ToolchainList {
|
||||
pub toolchains: Vec<Toolchain>,
|
||||
pub default: Option<DefaultIndex>,
|
||||
|
||||
@@ -12,9 +12,9 @@ use gpui::{
|
||||
};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelToolChoice, LanguageModelToolSchemaFormat, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, MessageContent, StopReason,
|
||||
AuthenticateError, ConfigurationViewTargetAgent, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason,
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
@@ -37,6 +37,8 @@ use util::ResultExt;
|
||||
use crate::AllLanguageModelSettings;
|
||||
use crate::ui::InstructionListItem;
|
||||
|
||||
use super::anthropic::ApiKey;
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID;
|
||||
const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME;
|
||||
|
||||
@@ -198,6 +200,33 @@ impl GoogleLanguageModelProvider {
|
||||
request_limiter: RateLimiter::new(4),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn api_key(cx: &mut App) -> Task<Result<ApiKey>> {
|
||||
let credentials_provider = <dyn CredentialsProvider>::global(cx);
|
||||
let api_url = AllLanguageModelSettings::get_global(cx)
|
||||
.google
|
||||
.api_url
|
||||
.clone();
|
||||
|
||||
if let Ok(key) = std::env::var(GEMINI_API_KEY_VAR) {
|
||||
Task::ready(Ok(ApiKey {
|
||||
key,
|
||||
from_env: true,
|
||||
}))
|
||||
} else {
|
||||
cx.spawn(async move |cx| {
|
||||
let (_, api_key) = credentials_provider
|
||||
.read_credentials(&api_url, cx)
|
||||
.await?
|
||||
.ok_or(AuthenticateError::CredentialsNotFound)?;
|
||||
|
||||
Ok(ApiKey {
|
||||
key: String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?,
|
||||
from_env: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelProviderState for GoogleLanguageModelProvider {
|
||||
@@ -279,11 +308,11 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
|
||||
|
||||
fn configuration_view(
|
||||
&self,
|
||||
_target_agent: language_model::ConfigurationViewTargetAgent,
|
||||
target_agent: language_model::ConfigurationViewTargetAgent,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyView {
|
||||
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
|
||||
cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx))
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -776,11 +805,17 @@ fn convert_usage(usage: &UsageMetadata) -> language_model::TokenUsage {
|
||||
struct ConfigurationView {
|
||||
api_key_editor: Entity<Editor>,
|
||||
state: gpui::Entity<State>,
|
||||
target_agent: language_model::ConfigurationViewTargetAgent,
|
||||
load_credentials_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
fn new(
|
||||
state: gpui::Entity<State>,
|
||||
target_agent: language_model::ConfigurationViewTargetAgent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&state, |_, _, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
@@ -810,6 +845,7 @@ impl ConfigurationView {
|
||||
editor.set_placeholder_text("AIzaSy...", cx);
|
||||
editor
|
||||
}),
|
||||
target_agent,
|
||||
state,
|
||||
load_credentials_task,
|
||||
}
|
||||
@@ -885,7 +921,10 @@ impl Render for ConfigurationView {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::save_api_key))
|
||||
.child(Label::new("To use Zed's agent with Google AI, you need to add an API key. Follow these steps:"))
|
||||
.child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent {
|
||||
ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI",
|
||||
ConfigurationViewTargetAgent::Other(agent) => agent,
|
||||
})))
|
||||
.child(
|
||||
List::new()
|
||||
.child(InstructionListItem::new(
|
||||
|
||||
@@ -25,6 +25,7 @@ async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
cpal.workspace = true
|
||||
futures.workspace = true
|
||||
audio.workspace = true
|
||||
gpui = { workspace = true, features = ["screen-capture", "x11", "wayland", "windows-manifest"] }
|
||||
gpui_tokio.workspace = true
|
||||
http_client_tls.workspace = true
|
||||
@@ -35,6 +36,7 @@ nanoid.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
smallvec.workspace = true
|
||||
settings.workspace = true
|
||||
tokio-tungstenite.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -24,8 +24,11 @@ mod livekit_client;
|
||||
)))]
|
||||
pub use livekit_client::*;
|
||||
|
||||
// If you need proper LSP in livekit_client you've got to comment out
|
||||
// the mocks and test
|
||||
// If you need proper LSP in livekit_client you've got to comment
|
||||
// - the cfg blocks above
|
||||
// - the mods: mock_client & test and their conditional blocks
|
||||
// - the pub use mock_client::* and their conditional blocks
|
||||
|
||||
#[cfg(any(
|
||||
test,
|
||||
feature = "test-support",
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use audio::AudioSettings;
|
||||
use collections::HashMap;
|
||||
use futures::{SinkExt, channel::mpsc};
|
||||
use gpui::{App, AsyncApp, ScreenCaptureSource, ScreenCaptureStream, Task};
|
||||
use gpui_tokio::Tokio;
|
||||
use log::info;
|
||||
use playback::capture_local_video_track;
|
||||
use settings::Settings;
|
||||
|
||||
mod playback;
|
||||
#[cfg(feature = "record-microphone")]
|
||||
mod record;
|
||||
|
||||
use crate::{LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication};
|
||||
pub use playback::AudioStream;
|
||||
@@ -125,9 +126,14 @@ impl Room {
|
||||
pub fn play_remote_audio_track(
|
||||
&self,
|
||||
track: &RemoteAudioTrack,
|
||||
_cx: &App,
|
||||
cx: &mut App,
|
||||
) -> Result<playback::AudioStream> {
|
||||
Ok(self.playback.play_remote_audio_track(&track.0))
|
||||
if AudioSettings::get_global(cx).rodio_audio {
|
||||
info!("Using experimental.rodio_audio audio pipeline");
|
||||
playback::play_remote_audio_track(&track.0, cx)
|
||||
} else {
|
||||
Ok(self.playback.play_remote_audio_track(&track.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,13 +18,16 @@ use livekit::webrtc::{
|
||||
video_stream::native::NativeVideoStream,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use rodio::Source;
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Weak;
|
||||
use std::sync::atomic::{self, AtomicI32};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::{borrow::Cow, collections::VecDeque, sync::Arc, thread};
|
||||
use util::{ResultExt as _, maybe};
|
||||
|
||||
mod source;
|
||||
|
||||
pub(crate) struct AudioStack {
|
||||
executor: BackgroundExecutor,
|
||||
apm: Arc<Mutex<apm::AudioProcessingModule>>,
|
||||
@@ -40,6 +43,29 @@ pub(crate) struct AudioStack {
|
||||
const SAMPLE_RATE: u32 = 48000;
|
||||
const NUM_CHANNELS: u32 = 2;
|
||||
|
||||
pub(crate) fn play_remote_audio_track(
|
||||
track: &livekit::track::RemoteAudioTrack,
|
||||
cx: &mut gpui::App,
|
||||
) -> Result<AudioStream> {
|
||||
let stop_handle = Arc::new(AtomicBool::new(false));
|
||||
let stop_handle_clone = stop_handle.clone();
|
||||
let stream = source::LiveKitStream::new(cx.background_executor(), track)
|
||||
.stoppable()
|
||||
.periodic_access(Duration::from_millis(50), move |s| {
|
||||
if stop_handle.load(Ordering::Relaxed) {
|
||||
s.stop();
|
||||
}
|
||||
});
|
||||
audio::Audio::play_source(stream, cx).context("Could not play audio")?;
|
||||
|
||||
let on_drop = util::defer(move || {
|
||||
stop_handle_clone.store(true, Ordering::Relaxed);
|
||||
});
|
||||
Ok(AudioStream::Output {
|
||||
_drop: Box::new(on_drop),
|
||||
})
|
||||
}
|
||||
|
||||
impl AudioStack {
|
||||
pub(crate) fn new(executor: BackgroundExecutor) -> Self {
|
||||
let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new(
|
||||
@@ -61,7 +87,7 @@ impl AudioStack {
|
||||
) -> AudioStream {
|
||||
let output_task = self.start_output();
|
||||
|
||||
let next_ssrc = self.next_ssrc.fetch_add(1, atomic::Ordering::Relaxed);
|
||||
let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed);
|
||||
let source = AudioMixerSource {
|
||||
ssrc: next_ssrc,
|
||||
sample_rate: SAMPLE_RATE,
|
||||
@@ -97,6 +123,23 @@ impl AudioStack {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_output(&self) -> Arc<Task<()>> {
|
||||
if let Some(task) = self._output_task.borrow().upgrade() {
|
||||
return task;
|
||||
}
|
||||
let task = Arc::new(self.executor.spawn({
|
||||
let apm = self.apm.clone();
|
||||
let mixer = self.mixer.clone();
|
||||
async move {
|
||||
Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
*self._output_task.borrow_mut() = Arc::downgrade(&task);
|
||||
task
|
||||
}
|
||||
|
||||
pub(crate) fn capture_local_microphone_track(
|
||||
&self,
|
||||
) -> Result<(crate::LocalAudioTrack, AudioStream)> {
|
||||
@@ -139,23 +182,6 @@ impl AudioStack {
|
||||
))
|
||||
}
|
||||
|
||||
fn start_output(&self) -> Arc<Task<()>> {
|
||||
if let Some(task) = self._output_task.borrow().upgrade() {
|
||||
return task;
|
||||
}
|
||||
let task = Arc::new(self.executor.spawn({
|
||||
let apm = self.apm.clone();
|
||||
let mixer = self.mixer.clone();
|
||||
async move {
|
||||
Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
*self._output_task.borrow_mut() = Arc::downgrade(&task);
|
||||
task
|
||||
}
|
||||
|
||||
async fn play_output(
|
||||
apm: Arc<Mutex<apm::AudioProcessingModule>>,
|
||||
mixer: Arc<Mutex<audio_mixer::AudioMixer>>,
|
||||
|
||||
67
crates/livekit_client/src/livekit_client/playback/source.rs
Normal file
67
crates/livekit_client/src/livekit_client/playback/source.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use futures::StreamExt;
|
||||
use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame};
|
||||
use livekit::track::RemoteAudioTrack;
|
||||
use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter};
|
||||
|
||||
use crate::livekit_client::playback::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
|
||||
fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer {
|
||||
let samples = frame.data.iter().copied();
|
||||
let samples = SampleTypeConverter::<_, _>::new(samples);
|
||||
let samples: Vec<f32> = samples.collect();
|
||||
SamplesBuffer::new(frame.num_channels as u16, frame.sample_rate, samples)
|
||||
}
|
||||
|
||||
pub struct LiveKitStream {
|
||||
// shared_buffer: SharedBuffer,
|
||||
inner: rodio::queue::SourcesQueueOutput,
|
||||
_receiver_task: gpui::Task<()>,
|
||||
}
|
||||
|
||||
impl LiveKitStream {
|
||||
pub fn new(executor: &gpui::BackgroundExecutor, track: &RemoteAudioTrack) -> Self {
|
||||
let mut stream =
|
||||
NativeAudioStream::new(track.rtc_track(), SAMPLE_RATE as i32, NUM_CHANNELS as i32);
|
||||
let (queue_input, queue_output) = rodio::queue::queue(true);
|
||||
// spawn rtc stream
|
||||
let receiver_task = executor.spawn({
|
||||
async move {
|
||||
while let Some(frame) = stream.next().await {
|
||||
let samples = frame_to_samplesbuffer(frame);
|
||||
queue_input.append(samples);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
LiveKitStream {
|
||||
_receiver_task: receiver_task,
|
||||
inner: queue_output,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for LiveKitStream {
|
||||
type Item = rodio::Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl Source for LiveKitStream {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
self.inner.current_span_len()
|
||||
}
|
||||
|
||||
fn channels(&self) -> rodio::ChannelCount {
|
||||
self.inner.channels()
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> rodio::SampleRate {
|
||||
self.inner.sample_rate()
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<std::time::Duration> {
|
||||
self.inner.total_duration()
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact};
|
||||
const JSON_RPC_VERSION: &str = "2.0";
|
||||
const CONTENT_LEN_HEADER: &str = "Content-Length: ";
|
||||
|
||||
const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
|
||||
pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
|
||||
const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
type NotificationHandler = Box<dyn Send + FnMut(Option<RequestId>, Value, &mut AsyncApp)>;
|
||||
|
||||
@@ -6998,20 +6998,19 @@ impl Excerpt {
|
||||
}
|
||||
|
||||
fn contains(&self, anchor: &Anchor) -> bool {
|
||||
anchor.buffer_id == None
|
||||
|| anchor.buffer_id == Some(self.buffer_id)
|
||||
&& self
|
||||
.range
|
||||
.context
|
||||
.start
|
||||
.cmp(&anchor.text_anchor, &self.buffer)
|
||||
.is_le()
|
||||
&& self
|
||||
.range
|
||||
.context
|
||||
.end
|
||||
.cmp(&anchor.text_anchor, &self.buffer)
|
||||
.is_ge()
|
||||
(anchor.buffer_id == None || anchor.buffer_id == Some(self.buffer_id))
|
||||
&& self
|
||||
.range
|
||||
.context
|
||||
.start
|
||||
.cmp(&anchor.text_anchor, &self.buffer)
|
||||
.is_le()
|
||||
&& self
|
||||
.range
|
||||
.context
|
||||
.end
|
||||
.cmp(&anchor.text_anchor, &self.buffer)
|
||||
.is_ge()
|
||||
}
|
||||
|
||||
/// The [`Excerpt`]'s start offset in its [`Buffer`]
|
||||
|
||||
@@ -4393,12 +4393,13 @@ impl OutlinePanel {
|
||||
})
|
||||
.filter(|(match_range, _)| {
|
||||
let editor = active_editor.read(cx);
|
||||
if let Some(buffer_id) = match_range.start.buffer_id
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.start)
|
||||
&& editor.is_buffer_folded(buffer_id, cx)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(buffer_id) = match_range.start.buffer_id
|
||||
if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.end)
|
||||
&& editor.is_buffer_folded(buffer_id, cx)
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -88,9 +88,18 @@ pub enum BufferStoreEvent {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct ProjectTransaction(pub HashMap<Entity<Buffer>, language::Transaction>);
|
||||
|
||||
impl PartialEq for ProjectTransaction {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.len() == other.0.len()
|
||||
&& self.0.iter().all(|(buffer, transaction)| {
|
||||
other.0.get(buffer).is_some_and(|t| t.id == transaction.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<BufferStoreEvent> for BufferStore {}
|
||||
|
||||
impl RemoteBufferStore {
|
||||
|
||||
@@ -3444,8 +3444,7 @@ impl LspCommand for GetCodeLens {
|
||||
capabilities
|
||||
.server_capabilities
|
||||
.code_lens_provider
|
||||
.as_ref()
|
||||
.is_some_and(|code_lens_options| code_lens_options.resolve_provider.unwrap_or(false))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -181,6 +181,7 @@ impl LanguageServerTree {
|
||||
&root_path.path,
|
||||
language_name.clone(),
|
||||
);
|
||||
|
||||
(
|
||||
Arc::new(InnerTreeNode::new(
|
||||
adapter.name(),
|
||||
@@ -408,6 +409,7 @@ impl ServerTreeRebase {
|
||||
if live_node.id.get().is_some() {
|
||||
return Some(node);
|
||||
}
|
||||
|
||||
let disposition = &live_node.disposition;
|
||||
let Some((existing_node, _)) = self
|
||||
.old_contents
|
||||
|
||||
@@ -327,6 +327,7 @@ pub enum Event {
|
||||
RevealInProjectPanel(ProjectEntryId),
|
||||
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
|
||||
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
|
||||
EntryRenamed(ProjectTransaction),
|
||||
AgentLocationChanged,
|
||||
}
|
||||
|
||||
@@ -2119,7 +2120,7 @@ impl Project {
|
||||
let is_root_entry = self.entry_is_worktree_root(entry_id, cx);
|
||||
|
||||
let lsp_store = self.lsp_store().downgrade();
|
||||
cx.spawn(async move |_, cx| {
|
||||
cx.spawn(async move |project, cx| {
|
||||
let (old_abs_path, new_abs_path) = {
|
||||
let root_path = worktree.read_with(cx, |this, _| this.abs_path())?;
|
||||
let new_abs_path = if is_root_entry {
|
||||
@@ -2129,7 +2130,7 @@ impl Project {
|
||||
};
|
||||
(root_path.join(&old_path), new_abs_path)
|
||||
};
|
||||
LspStore::will_rename_entry(
|
||||
let transaction = LspStore::will_rename_entry(
|
||||
lsp_store.clone(),
|
||||
worktree_id,
|
||||
&old_abs_path,
|
||||
@@ -2145,6 +2146,12 @@ impl Project {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
project
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(Event::EntryRenamed(transaction));
|
||||
})
|
||||
.ok();
|
||||
|
||||
lsp_store
|
||||
.read_with(cx, |this, _| {
|
||||
this.did_rename_entry(worktree_id, &old_abs_path, &new_abs_path, is_dir);
|
||||
@@ -3415,7 +3422,7 @@ impl Project {
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<LocationLink>>> {
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
let guard = self.retain_remotely_created_models(cx);
|
||||
let task = self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
@@ -3433,7 +3440,7 @@ impl Project {
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<LocationLink>>> {
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
let guard = self.retain_remotely_created_models(cx);
|
||||
let task = self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
@@ -3451,7 +3458,7 @@ impl Project {
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<LocationLink>>> {
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
let guard = self.retain_remotely_created_models(cx);
|
||||
let task = self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
@@ -3469,7 +3476,7 @@ impl Project {
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<LocationLink>>> {
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
let guard = self.retain_remotely_created_models(cx);
|
||||
let task = self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
@@ -3487,7 +3494,7 @@ impl Project {
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<Location>>> {
|
||||
) -> Task<Result<Option<Vec<Location>>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
let guard = self.retain_remotely_created_models(cx);
|
||||
let task = self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
@@ -3585,23 +3592,12 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn signature_help<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Vec<SignatureHelp>> {
|
||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.signature_help(buffer, position, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hover<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Vec<Hover>> {
|
||||
) -> Task<Option<Vec<Hover>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.lsp_store
|
||||
.update(cx, |lsp_store, cx| lsp_store.hover(buffer, position, cx))
|
||||
@@ -3637,7 +3633,7 @@ impl Project {
|
||||
range: Range<T>,
|
||||
kinds: Option<Vec<CodeActionKind>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
) -> Task<Result<Option<Vec<CodeAction>>>> {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
|
||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
@@ -3650,7 +3646,7 @@ impl Project {
|
||||
buffer: &Entity<Buffer>,
|
||||
range: Range<T>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
) -> Task<Result<Option<Vec<CodeAction>>>> {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let range = range.to_point(&snapshot);
|
||||
let range_start = snapshot.anchor_before(range.start);
|
||||
@@ -3668,16 +3664,18 @@ impl Project {
|
||||
let mut code_lens_actions = code_lens_actions
|
||||
.await
|
||||
.map_err(|e| anyhow!("code lens fetch failed: {e:#}"))?;
|
||||
code_lens_actions.retain(|code_lens_action| {
|
||||
range
|
||||
.start
|
||||
.cmp(&code_lens_action.range.start, &snapshot)
|
||||
.is_ge()
|
||||
&& range
|
||||
.end
|
||||
.cmp(&code_lens_action.range.end, &snapshot)
|
||||
.is_le()
|
||||
});
|
||||
if let Some(code_lens_actions) = &mut code_lens_actions {
|
||||
code_lens_actions.retain(|code_lens_action| {
|
||||
range
|
||||
.start
|
||||
.cmp(&code_lens_action.range.start, &snapshot)
|
||||
.is_ge()
|
||||
&& range
|
||||
.end
|
||||
.cmp(&code_lens_action.range.end, &snapshot)
|
||||
.is_le()
|
||||
});
|
||||
}
|
||||
Ok(code_lens_actions)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation,
|
||||
*,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use buffer_diff::{
|
||||
BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus,
|
||||
DiffHunkStatusKind, assert_hunks,
|
||||
@@ -21,7 +22,8 @@ use http_client::Url;
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter,
|
||||
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
|
||||
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider,
|
||||
ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainLister,
|
||||
language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings},
|
||||
tree_sitter_rust, tree_sitter_typescript,
|
||||
};
|
||||
@@ -140,8 +142,10 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
max_line_length = 120
|
||||
[*.js]
|
||||
tab_width = 10
|
||||
max_line_length = off
|
||||
"#,
|
||||
".zed": {
|
||||
"settings.json": r#"{
|
||||
@@ -149,7 +153,8 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
|
||||
"hard_tabs": false,
|
||||
"ensure_final_newline_on_save": false,
|
||||
"remove_trailing_whitespace_on_save": false,
|
||||
"soft_wrap": "editor_width"
|
||||
"preferred_line_length": 64,
|
||||
"soft_wrap": "editor_width",
|
||||
}"#,
|
||||
},
|
||||
"a.rs": "fn a() {\n A\n}",
|
||||
@@ -157,6 +162,7 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
|
||||
".editorconfig": r#"
|
||||
[*.rs]
|
||||
indent_size = 2
|
||||
max_line_length = off,
|
||||
"#,
|
||||
"b.rs": "fn b() {\n B\n}",
|
||||
},
|
||||
@@ -205,6 +211,7 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
|
||||
assert_eq!(settings_a.hard_tabs, true);
|
||||
assert_eq!(settings_a.ensure_final_newline_on_save, true);
|
||||
assert_eq!(settings_a.remove_trailing_whitespace_on_save, true);
|
||||
assert_eq!(settings_a.preferred_line_length, 120);
|
||||
|
||||
// .editorconfig in b/ overrides .editorconfig in root
|
||||
assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2));
|
||||
@@ -212,6 +219,10 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
|
||||
// "indent_size" is not set, so "tab_width" is used
|
||||
assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10));
|
||||
|
||||
// When max_line_length is "off", default to .zed/settings.json
|
||||
assert_eq!(settings_b.preferred_line_length, 64);
|
||||
assert_eq!(settings_c.preferred_line_length, 64);
|
||||
|
||||
// README.md should not be affected by .editorconfig's globe "*.rs"
|
||||
assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8));
|
||||
});
|
||||
@@ -587,6 +598,203 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
pub(crate) struct PyprojectTomlManifestProvider;
|
||||
|
||||
impl ManifestProvider for PyprojectTomlManifestProvider {
|
||||
fn name(&self) -> ManifestName {
|
||||
SharedString::new_static("pyproject.toml").into()
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
ManifestQuery {
|
||||
path,
|
||||
depth,
|
||||
delegate,
|
||||
}: ManifestQuery,
|
||||
) -> Option<Arc<Path>> {
|
||||
for path in path.ancestors().take(depth) {
|
||||
let p = path.join("pyproject.toml");
|
||||
if delegate.exists(&p, Some(false)) {
|
||||
return Some(path.into());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/the-root"),
|
||||
json!({
|
||||
".zed": {
|
||||
"settings.json": r#"
|
||||
{
|
||||
"languages": {
|
||||
"Python": {
|
||||
"language_servers": ["ty"]
|
||||
}
|
||||
}
|
||||
}"#
|
||||
},
|
||||
"project-a": {
|
||||
".venv": {},
|
||||
"file.py": "",
|
||||
"pyproject.toml": ""
|
||||
},
|
||||
"project-b": {
|
||||
".venv": {},
|
||||
"source_file.py":"",
|
||||
"another_file.py": "",
|
||||
"pyproject.toml": ""
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
cx.update(|cx| {
|
||||
ManifestProvidersStore::global(cx).register(Arc::new(PyprojectTomlManifestProvider))
|
||||
});
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let _fake_python_server = language_registry.register_fake_lsp(
|
||||
"Python",
|
||||
FakeLspAdapter {
|
||||
name: "ty",
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
language_registry.add(python_lang(fs.clone()));
|
||||
let (first_buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp(path!("/the-root/project-a/file.py"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
let servers = project.update(cx, |project, cx| {
|
||||
project.lsp_store.update(cx, |this, cx| {
|
||||
first_buffer.update(cx, |buffer, cx| {
|
||||
this.language_servers_for_local_buffer(buffer, cx)
|
||||
.map(|(adapter, server)| (adapter.clone(), server.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(servers.len(), 1);
|
||||
let (adapter, server) = servers.into_iter().next().unwrap();
|
||||
assert_eq!(adapter.name(), LanguageServerName::new_static("ty"));
|
||||
assert_eq!(server.server_id(), LanguageServerId(0));
|
||||
// `workspace_folders` are set to the rooting point.
|
||||
assert_eq!(
|
||||
server.workspace_folders(),
|
||||
BTreeSet::from_iter(
|
||||
[Url::from_file_path(path!("/the-root/project-a")).unwrap()].into_iter()
|
||||
)
|
||||
);
|
||||
|
||||
let (second_project_buffer, _other_handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp(path!("/the-root/project-b/source_file.py"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
let servers = project.update(cx, |project, cx| {
|
||||
project.lsp_store.update(cx, |this, cx| {
|
||||
second_project_buffer.update(cx, |buffer, cx| {
|
||||
this.language_servers_for_local_buffer(buffer, cx)
|
||||
.map(|(adapter, server)| (adapter.clone(), server.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(servers.len(), 1);
|
||||
let (adapter, server) = servers.into_iter().next().unwrap();
|
||||
assert_eq!(adapter.name(), LanguageServerName::new_static("ty"));
|
||||
// We're not using venvs at all here, so both folders should fall under the same root.
|
||||
assert_eq!(server.server_id(), LanguageServerId(0));
|
||||
// Now, let's select a different toolchain for one of subprojects.
|
||||
let (available_toolchains_for_b, root_path) = project
|
||||
.update(cx, |this, cx| {
|
||||
let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
|
||||
this.available_toolchains(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from("project-b/source_file.py".as_ref()),
|
||||
},
|
||||
LanguageName::new("Python"),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.expect("A toolchain to be discovered");
|
||||
assert_eq!(root_path.as_ref(), Path::new("project-b"));
|
||||
assert_eq!(available_toolchains_for_b.toolchains().len(), 1);
|
||||
let currently_active_toolchain = project
|
||||
.update(cx, |this, cx| {
|
||||
let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
|
||||
this.active_toolchain(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from("project-b/source_file.py".as_ref()),
|
||||
},
|
||||
LanguageName::new("Python"),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(currently_active_toolchain.is_none());
|
||||
let _ = project
|
||||
.update(cx, |this, cx| {
|
||||
let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
|
||||
this.activate_toolchain(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: root_path,
|
||||
},
|
||||
available_toolchains_for_b
|
||||
.toolchains
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
let servers = project.update(cx, |project, cx| {
|
||||
project.lsp_store.update(cx, |this, cx| {
|
||||
second_project_buffer.update(cx, |buffer, cx| {
|
||||
this.language_servers_for_local_buffer(buffer, cx)
|
||||
.map(|(adapter, server)| (adapter.clone(), server.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(servers.len(), 1);
|
||||
let (adapter, server) = servers.into_iter().next().unwrap();
|
||||
assert_eq!(adapter.name(), LanguageServerName::new_static("ty"));
|
||||
// There's a new language server in town.
|
||||
assert_eq!(server.server_id(), LanguageServerId(1));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -3005,6 +3213,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
|
||||
let mut definitions = project
|
||||
.update(cx, |project, cx| project.definitions(&buffer, 22, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// Assert no new language server started
|
||||
@@ -3519,7 +3728,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
|
||||
.next()
|
||||
.await;
|
||||
|
||||
let action = actions.await.unwrap()[0].clone();
|
||||
let action = actions.await.unwrap().unwrap()[0].clone();
|
||||
let apply = project.update(cx, |project, cx| {
|
||||
project.apply_code_action(buffer.clone(), action, true, cx)
|
||||
});
|
||||
@@ -6110,6 +6319,7 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
|
||||
hover_task
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
|
||||
.sorted()
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -6183,6 +6393,7 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
|
||||
hover_task
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
|
||||
.sorted()
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -6261,7 +6472,7 @@ async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) {
|
||||
.await
|
||||
.expect("The code action request should have been triggered");
|
||||
|
||||
let code_actions = code_actions_task.await.unwrap();
|
||||
let code_actions = code_actions_task.await.unwrap().unwrap();
|
||||
assert_eq!(code_actions.len(), 1);
|
||||
assert_eq!(
|
||||
code_actions[0].lsp_action.action_kind(),
|
||||
@@ -6420,6 +6631,7 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
|
||||
code_actions_task
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|code_action| code_action.lsp_action.title().to_owned())
|
||||
.sorted()
|
||||
@@ -8969,6 +9181,65 @@ fn rust_lang() -> Arc<Language> {
|
||||
))
|
||||
}
|
||||
|
||||
fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
|
||||
struct PythonMootToolchainLister(Arc<FakeFs>);
|
||||
#[async_trait]
|
||||
impl ToolchainLister for PythonMootToolchainLister {
|
||||
async fn list(
|
||||
&self,
|
||||
worktree_root: PathBuf,
|
||||
subroot_relative_path: Option<Arc<Path>>,
|
||||
_: Option<HashMap<String, String>>,
|
||||
) -> ToolchainList {
|
||||
// This lister will always return a path .venv directories within ancestors
|
||||
let ancestors = subroot_relative_path
|
||||
.into_iter()
|
||||
.flat_map(|path| path.ancestors().map(ToOwned::to_owned).collect::<Vec<_>>());
|
||||
let mut toolchains = vec![];
|
||||
for ancestor in ancestors {
|
||||
let venv_path = worktree_root.join(ancestor).join(".venv");
|
||||
if self.0.is_dir(&venv_path).await {
|
||||
toolchains.push(Toolchain {
|
||||
name: SharedString::new("Python Venv"),
|
||||
path: venv_path.to_string_lossy().into_owned().into(),
|
||||
language_name: LanguageName(SharedString::new_static("Python")),
|
||||
as_json: serde_json::Value::Null,
|
||||
})
|
||||
}
|
||||
}
|
||||
ToolchainList {
|
||||
toolchains,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
// Returns a term which we should use in UI to refer to a toolchain.
|
||||
fn term(&self) -> SharedString {
|
||||
SharedString::new_static("virtual environment")
|
||||
}
|
||||
/// Returns the name of the manifest file for this toolchain.
|
||||
fn manifest_name(&self) -> ManifestName {
|
||||
SharedString::new_static("pyproject.toml").into()
|
||||
}
|
||||
}
|
||||
Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Python".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["py".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
None, // We're not testing Python parsing with this language.
|
||||
)
|
||||
.with_manifest(Some(ManifestName::from(SharedString::new_static(
|
||||
"pyproject.toml",
|
||||
))))
|
||||
.with_toolchain_lister(Some(Arc::new(PythonMootToolchainLister(fs)))),
|
||||
)
|
||||
}
|
||||
|
||||
fn typescript_lang() -> Arc<Language> {
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
|
||||
@@ -34,7 +34,10 @@ enum ToolchainStoreInner {
|
||||
Entity<LocalToolchainStore>,
|
||||
#[allow(dead_code)] Subscription,
|
||||
),
|
||||
Remote(Entity<RemoteToolchainStore>),
|
||||
Remote(
|
||||
Entity<RemoteToolchainStore>,
|
||||
#[allow(dead_code)] Subscription,
|
||||
),
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolchainStoreEvent> for ToolchainStore {}
|
||||
@@ -65,10 +68,12 @@ impl ToolchainStore {
|
||||
Self(ToolchainStoreInner::Local(entity, subscription))
|
||||
}
|
||||
|
||||
pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut App) -> Self {
|
||||
Self(ToolchainStoreInner::Remote(
|
||||
cx.new(|_| RemoteToolchainStore { client, project_id }),
|
||||
))
|
||||
pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) -> Self {
|
||||
let entity = cx.new(|_| RemoteToolchainStore { client, project_id });
|
||||
let _subscription = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
|
||||
cx.emit(e.clone())
|
||||
});
|
||||
Self(ToolchainStoreInner::Remote(entity, _subscription))
|
||||
}
|
||||
pub(crate) fn activate_toolchain(
|
||||
&self,
|
||||
@@ -80,8 +85,8 @@ impl ToolchainStore {
|
||||
ToolchainStoreInner::Local(local, _) => {
|
||||
local.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx))
|
||||
}
|
||||
ToolchainStoreInner::Remote(remote) => {
|
||||
remote.read(cx).activate_toolchain(path, toolchain, cx)
|
||||
ToolchainStoreInner::Remote(remote, _) => {
|
||||
remote.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,7 +100,7 @@ impl ToolchainStore {
|
||||
ToolchainStoreInner::Local(local, _) => {
|
||||
local.update(cx, |this, cx| this.list_toolchains(path, language_name, cx))
|
||||
}
|
||||
ToolchainStoreInner::Remote(remote) => {
|
||||
ToolchainStoreInner::Remote(remote, _) => {
|
||||
remote.read(cx).list_toolchains(path, language_name, cx)
|
||||
}
|
||||
}
|
||||
@@ -112,7 +117,7 @@ impl ToolchainStore {
|
||||
&path.path,
|
||||
language_name,
|
||||
)),
|
||||
ToolchainStoreInner::Remote(remote) => {
|
||||
ToolchainStoreInner::Remote(remote, _) => {
|
||||
remote.read(cx).active_toolchain(path, language_name, cx)
|
||||
}
|
||||
}
|
||||
@@ -234,13 +239,13 @@ impl ToolchainStore {
|
||||
pub fn as_language_toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
|
||||
match &self.0 {
|
||||
ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())),
|
||||
ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())),
|
||||
ToolchainStoreInner::Remote(remote, _) => Arc::new(RemoteStore(remote.downgrade())),
|
||||
}
|
||||
}
|
||||
pub fn as_local_store(&self) -> Option<&Entity<LocalToolchainStore>> {
|
||||
match &self.0 {
|
||||
ToolchainStoreInner::Local(local, _) => Some(local),
|
||||
ToolchainStoreInner::Remote(_) => None,
|
||||
ToolchainStoreInner::Remote(_, _) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -415,6 +420,8 @@ impl LocalToolchainStore {
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolchainStoreEvent> for RemoteToolchainStore {}
|
||||
struct RemoteToolchainStore {
|
||||
client: AnyProtoClient,
|
||||
project_id: u64,
|
||||
@@ -425,27 +432,37 @@ impl RemoteToolchainStore {
|
||||
&self,
|
||||
project_path: ProjectPath,
|
||||
toolchain: Toolchain,
|
||||
cx: &App,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<()>> {
|
||||
let project_id = self.project_id;
|
||||
let client = self.client.clone();
|
||||
cx.background_spawn(async move {
|
||||
let path = PathBuf::from(toolchain.path.to_string());
|
||||
let _ = client
|
||||
.request(proto::ActivateToolchain {
|
||||
project_id,
|
||||
worktree_id: project_path.worktree_id.to_proto(),
|
||||
language_name: toolchain.language_name.into(),
|
||||
toolchain: Some(proto::Toolchain {
|
||||
name: toolchain.name.into(),
|
||||
path: path.to_proto(),
|
||||
raw_json: toolchain.as_json.to_string(),
|
||||
}),
|
||||
path: Some(project_path.path.to_string_lossy().into_owned()),
|
||||
cx.spawn(async move |this, cx| {
|
||||
let did_activate = cx
|
||||
.background_spawn(async move {
|
||||
let path = PathBuf::from(toolchain.path.to_string());
|
||||
let _ = client
|
||||
.request(proto::ActivateToolchain {
|
||||
project_id,
|
||||
worktree_id: project_path.worktree_id.to_proto(),
|
||||
language_name: toolchain.language_name.into(),
|
||||
toolchain: Some(proto::Toolchain {
|
||||
name: toolchain.name.into(),
|
||||
path: path.to_proto(),
|
||||
raw_json: toolchain.as_json.to_string(),
|
||||
}),
|
||||
path: Some(project_path.path.to_string_lossy().into_owned()),
|
||||
})
|
||||
.await
|
||||
.log_err()?;
|
||||
Some(())
|
||||
})
|
||||
.await
|
||||
.log_err()?;
|
||||
Some(())
|
||||
.await;
|
||||
did_activate.and_then(|_| {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(ToolchainStoreEvent::ToolchainActivated);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -753,28 +753,47 @@ message TextEdit {
|
||||
PointUtf16 lsp_range_end = 3;
|
||||
}
|
||||
|
||||
message MultiLspQuery {
|
||||
message LspQuery {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
repeated VectorClockEntry version = 3;
|
||||
oneof strategy {
|
||||
AllLanguageServers all = 4;
|
||||
}
|
||||
uint64 lsp_request_id = 2;
|
||||
oneof request {
|
||||
GetReferences get_references = 3;
|
||||
GetDocumentColor get_document_color = 4;
|
||||
GetHover get_hover = 5;
|
||||
GetCodeActions get_code_actions = 6;
|
||||
GetSignatureHelp get_signature_help = 7;
|
||||
GetCodeLens get_code_lens = 8;
|
||||
GetDocumentDiagnostics get_document_diagnostics = 9;
|
||||
GetDocumentColor get_document_color = 10;
|
||||
GetDefinition get_definition = 11;
|
||||
GetDeclaration get_declaration = 12;
|
||||
GetTypeDefinition get_type_definition = 13;
|
||||
GetImplementation get_implementation = 14;
|
||||
GetReferences get_references = 15;
|
||||
GetDefinition get_definition = 10;
|
||||
GetDeclaration get_declaration = 11;
|
||||
GetTypeDefinition get_type_definition = 12;
|
||||
GetImplementation get_implementation = 13;
|
||||
}
|
||||
}
|
||||
|
||||
message LspQueryResponse {
|
||||
uint64 project_id = 1;
|
||||
uint64 lsp_request_id = 2;
|
||||
repeated LspResponse responses = 3;
|
||||
}
|
||||
|
||||
message LspResponse {
|
||||
oneof response {
|
||||
GetHoverResponse get_hover_response = 1;
|
||||
GetCodeActionsResponse get_code_actions_response = 2;
|
||||
GetSignatureHelpResponse get_signature_help_response = 3;
|
||||
GetCodeLensResponse get_code_lens_response = 4;
|
||||
GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5;
|
||||
GetDocumentColorResponse get_document_color_response = 6;
|
||||
GetDefinitionResponse get_definition_response = 8;
|
||||
GetDeclarationResponse get_declaration_response = 9;
|
||||
GetTypeDefinitionResponse get_type_definition_response = 10;
|
||||
GetImplementationResponse get_implementation_response = 11;
|
||||
GetReferencesResponse get_references_response = 12;
|
||||
}
|
||||
uint64 server_id = 7;
|
||||
}
|
||||
|
||||
message AllLanguageServers {}
|
||||
|
||||
message LanguageServerSelector {
|
||||
@@ -798,27 +817,6 @@ message StopLanguageServers {
|
||||
bool all = 4;
|
||||
}
|
||||
|
||||
message MultiLspQueryResponse {
|
||||
repeated LspResponse responses = 1;
|
||||
}
|
||||
|
||||
message LspResponse {
|
||||
oneof response {
|
||||
GetHoverResponse get_hover_response = 1;
|
||||
GetCodeActionsResponse get_code_actions_response = 2;
|
||||
GetSignatureHelpResponse get_signature_help_response = 3;
|
||||
GetCodeLensResponse get_code_lens_response = 4;
|
||||
GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5;
|
||||
GetDocumentColorResponse get_document_color_response = 6;
|
||||
GetDefinitionResponse get_definition_response = 8;
|
||||
GetDeclarationResponse get_declaration_response = 9;
|
||||
GetTypeDefinitionResponse get_type_definition_response = 10;
|
||||
GetImplementationResponse get_implementation_response = 11;
|
||||
GetReferencesResponse get_references_response = 12;
|
||||
}
|
||||
uint64 server_id = 7;
|
||||
}
|
||||
|
||||
message LspExtRunnables {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
@@ -909,3 +907,30 @@ message PullWorkspaceDiagnostics {
|
||||
uint64 project_id = 1;
|
||||
uint64 server_id = 2;
|
||||
}
|
||||
|
||||
// todo(lsp) remove after Zed Stable hits v0.204.x
|
||||
message MultiLspQuery {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
repeated VectorClockEntry version = 3;
|
||||
oneof strategy {
|
||||
AllLanguageServers all = 4;
|
||||
}
|
||||
oneof request {
|
||||
GetHover get_hover = 5;
|
||||
GetCodeActions get_code_actions = 6;
|
||||
GetSignatureHelp get_signature_help = 7;
|
||||
GetCodeLens get_code_lens = 8;
|
||||
GetDocumentDiagnostics get_document_diagnostics = 9;
|
||||
GetDocumentColor get_document_color = 10;
|
||||
GetDefinition get_definition = 11;
|
||||
GetDeclaration get_declaration = 12;
|
||||
GetTypeDefinition get_type_definition = 13;
|
||||
GetImplementation get_implementation = 14;
|
||||
GetReferences get_references = 15;
|
||||
}
|
||||
}
|
||||
|
||||
message MultiLspQueryResponse {
|
||||
repeated LspResponse responses = 1;
|
||||
}
|
||||
|
||||
@@ -393,7 +393,10 @@ message Envelope {
|
||||
GetCrashFilesResponse get_crash_files_response = 362;
|
||||
|
||||
GitClone git_clone = 363;
|
||||
GitCloneResponse git_clone_response = 364; // current max
|
||||
GitCloneResponse git_clone_response = 364;
|
||||
|
||||
LspQuery lsp_query = 365;
|
||||
LspQueryResponse lsp_query_response = 366; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
|
||||
@@ -69,3 +69,32 @@ macro_rules! entity_messages {
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! lsp_messages {
|
||||
($(($request_name:ident, $response_name:ident, $stop_previous_requests:expr)),* $(,)?) => {
|
||||
$(impl LspRequestMessage for $request_name {
|
||||
type Response = $response_name;
|
||||
|
||||
fn to_proto_query(self) -> $crate::lsp_query::Request {
|
||||
$crate::lsp_query::Request::$request_name(self)
|
||||
}
|
||||
|
||||
fn response_to_proto_query(response: Self::Response) -> $crate::lsp_response::Response {
|
||||
$crate::lsp_response::Response::$response_name(response)
|
||||
}
|
||||
|
||||
fn buffer_id(&self) -> u64 {
|
||||
self.buffer_id
|
||||
}
|
||||
|
||||
fn buffer_version(&self) -> &[$crate::VectorClockEntry] {
|
||||
&self.version
|
||||
}
|
||||
|
||||
fn stop_previous_requests() -> bool {
|
||||
$stop_previous_requests
|
||||
}
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
@@ -169,6 +169,9 @@ messages!(
|
||||
(MarkNotificationRead, Foreground),
|
||||
(MoveChannel, Foreground),
|
||||
(ReorderChannel, Foreground),
|
||||
(LspQuery, Background),
|
||||
(LspQueryResponse, Background),
|
||||
// todo(lsp) remove after Zed Stable hits v0.204.x
|
||||
(MultiLspQuery, Background),
|
||||
(MultiLspQueryResponse, Background),
|
||||
(OnTypeFormatting, Background),
|
||||
@@ -426,7 +429,10 @@ request_messages!(
|
||||
(SetRoomParticipantRole, Ack),
|
||||
(BlameBuffer, BlameBufferResponse),
|
||||
(RejoinRemoteProjects, RejoinRemoteProjectsResponse),
|
||||
// todo(lsp) remove after Zed Stable hits v0.204.x
|
||||
(MultiLspQuery, MultiLspQueryResponse),
|
||||
(LspQuery, Ack),
|
||||
(LspQueryResponse, Ack),
|
||||
(RestartLanguageServers, Ack),
|
||||
(StopLanguageServers, Ack),
|
||||
(OpenContext, OpenContextResponse),
|
||||
@@ -478,6 +484,20 @@ request_messages!(
|
||||
(GitClone, GitCloneResponse)
|
||||
);
|
||||
|
||||
lsp_messages!(
|
||||
(GetReferences, GetReferencesResponse, true),
|
||||
(GetDocumentColor, GetDocumentColorResponse, true),
|
||||
(GetHover, GetHoverResponse, true),
|
||||
(GetCodeActions, GetCodeActionsResponse, true),
|
||||
(GetSignatureHelp, GetSignatureHelpResponse, true),
|
||||
(GetCodeLens, GetCodeLensResponse, true),
|
||||
(GetDocumentDiagnostics, GetDocumentDiagnosticsResponse, true),
|
||||
(GetDefinition, GetDefinitionResponse, true),
|
||||
(GetDeclaration, GetDeclarationResponse, true),
|
||||
(GetTypeDefinition, GetTypeDefinitionResponse, true),
|
||||
(GetImplementation, GetImplementationResponse, true),
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
{project_id, ShareProject},
|
||||
AddProjectCollaborator,
|
||||
@@ -520,6 +540,9 @@ entity_messages!(
|
||||
LeaveProject,
|
||||
LinkedEditingRange,
|
||||
LoadCommitDiff,
|
||||
LspQuery,
|
||||
LspQueryResponse,
|
||||
// todo(lsp) remove after Zed Stable hits v0.204.x
|
||||
MultiLspQuery,
|
||||
RestartLanguageServers,
|
||||
StopLanguageServers,
|
||||
@@ -777,6 +800,28 @@ pub fn split_repository_update(
|
||||
}])
|
||||
}
|
||||
|
||||
impl LspQuery {
|
||||
pub fn query_name_and_write_permissions(&self) -> (&str, bool) {
|
||||
match self.request {
|
||||
Some(lsp_query::Request::GetHover(_)) => ("GetHover", false),
|
||||
Some(lsp_query::Request::GetCodeActions(_)) => ("GetCodeActions", true),
|
||||
Some(lsp_query::Request::GetSignatureHelp(_)) => ("GetSignatureHelp", false),
|
||||
Some(lsp_query::Request::GetCodeLens(_)) => ("GetCodeLens", true),
|
||||
Some(lsp_query::Request::GetDocumentDiagnostics(_)) => {
|
||||
("GetDocumentDiagnostics", false)
|
||||
}
|
||||
Some(lsp_query::Request::GetDefinition(_)) => ("GetDefinition", false),
|
||||
Some(lsp_query::Request::GetDeclaration(_)) => ("GetDeclaration", false),
|
||||
Some(lsp_query::Request::GetTypeDefinition(_)) => ("GetTypeDefinition", false),
|
||||
Some(lsp_query::Request::GetImplementation(_)) => ("GetImplementation", false),
|
||||
Some(lsp_query::Request::GetReferences(_)) => ("GetReferences", false),
|
||||
Some(lsp_query::Request::GetDocumentColor(_)) => ("GetDocumentColor", false),
|
||||
None => ("<unknown>", true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo(lsp) remove after Zed Stable hits v0.204.x
|
||||
impl MultiLspQuery {
|
||||
pub fn request_str(&self) -> &str {
|
||||
match self.request {
|
||||
|
||||
@@ -31,6 +31,58 @@ pub trait RequestMessage: EnvelopedMessage {
|
||||
type Response: EnvelopedMessage;
|
||||
}
|
||||
|
||||
/// A trait to bind LSP request and responses for the proto layer.
|
||||
/// Should be used for every LSP request that has to traverse through the proto layer.
|
||||
///
|
||||
/// `lsp_messages` macro in the same crate provides a convenient way to implement this.
|
||||
pub trait LspRequestMessage: EnvelopedMessage {
|
||||
type Response: EnvelopedMessage;
|
||||
|
||||
fn to_proto_query(self) -> crate::lsp_query::Request;
|
||||
|
||||
fn response_to_proto_query(response: Self::Response) -> crate::lsp_response::Response;
|
||||
|
||||
fn buffer_id(&self) -> u64;
|
||||
|
||||
fn buffer_version(&self) -> &[crate::VectorClockEntry];
|
||||
|
||||
/// Whether to deduplicate the requests, or keep the previous ones running when another
|
||||
/// request of the same kind is processed.
|
||||
fn stop_previous_requests() -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct LspRequestId(pub u64);
|
||||
|
||||
/// A response from a single language server.
|
||||
/// There could be multiple responses for a single LSP request,
|
||||
/// from different servers.
|
||||
pub struct ProtoLspResponse<R> {
|
||||
pub server_id: u64,
|
||||
pub response: R,
|
||||
}
|
||||
|
||||
impl ProtoLspResponse<Box<dyn AnyTypedEnvelope>> {
|
||||
pub fn into_response<T: LspRequestMessage>(self) -> Result<ProtoLspResponse<T::Response>> {
|
||||
let envelope = self
|
||||
.response
|
||||
.into_any()
|
||||
.downcast::<TypedEnvelope<T::Response>>()
|
||||
.map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"cannot downcast LspResponse to {} for message {}",
|
||||
T::Response::NAME,
|
||||
T::NAME,
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(ProtoLspResponse {
|
||||
server_id: self.server_id,
|
||||
response: envelope.payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnyTypedEnvelope: Any + Send + Sync {
|
||||
fn payload_type_id(&self) -> TypeId;
|
||||
fn payload_type_name(&self) -> &'static str;
|
||||
|
||||
@@ -46,6 +46,9 @@ pub struct HeadlessProject {
|
||||
pub languages: Arc<LanguageRegistry>,
|
||||
pub extensions: Entity<HeadlessExtensionStore>,
|
||||
pub git_store: Entity<GitStore>,
|
||||
// Used mostly to keep alive the toolchain store for RPC handlers.
|
||||
// Local variant is used within LSP store, but that's a separate entity.
|
||||
pub _toolchain_store: Entity<ToolchainStore>,
|
||||
}
|
||||
|
||||
pub struct HeadlessAppState {
|
||||
@@ -269,6 +272,7 @@ impl HeadlessProject {
|
||||
languages,
|
||||
extensions,
|
||||
git_store,
|
||||
_toolchain_store: toolchain_store,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user