Compare commits

..

4 Commits

208 changed files with 2303 additions and 7941 deletions

View File

@@ -4,8 +4,10 @@ description: "Runs the tests"
runs:
using: "composite"
steps:
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Install Rust
shell: bash -euxo pipefail {0}
run: |
cargo install cargo-nextest --locked
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

View File

@@ -11,8 +11,9 @@ runs:
using: "composite"
steps:
- name: Install test runner
shell: powershell
working-directory: ${{ inputs.working-directory }}
uses: taiki-e/install-action@nextest
run: cargo install cargo-nextest --locked
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

View File

@@ -42,7 +42,7 @@ jobs:
exit 1
;;
esac
which cargo-set-version > /dev/null || cargo install cargo-edit -f --no-default-features --features "set-version"
which cargo-set-version > /dev/null || cargo install cargo-edit
output="$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')"
export GIT_COMMITTER_NAME="Zed Bot"
export GIT_COMMITTER_EMAIL="hi@zed.dev"

View File

@@ -26,4 +26,3 @@ jobs:
ascending: true
enable-statistics: true
stale-issue-label: "stale"
exempt-issue-labels: "never stale"

View File

@@ -39,7 +39,8 @@ jobs:
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::install_hyperfine
uses: taiki-e/install-action@hyperfine
run: cargo install hyperfine
shell: bash -euxo pipefail {0}
- name: steps::git_checkout
run: git fetch origin ${{ inputs.base }} && git checkout ${{ inputs.base }}
shell: bash -euxo pipefail {0}

View File

@@ -43,7 +43,9 @@ jobs:
fetch-depth: 0
- name: Install cargo nextest
uses: taiki-e/install-action@nextest
shell: bash -euxo pipefail {0}
run: |
cargo install cargo-nextest --locked
- name: Limit target directory size
shell: bash -euxo pipefail {0}

47
Cargo.lock generated
View File

@@ -322,7 +322,6 @@ dependencies = [
"assistant_slash_command",
"assistant_slash_commands",
"assistant_text_thread",
"async-fs",
"audio",
"buffer_diff",
"chrono",
@@ -344,7 +343,6 @@ dependencies = [
"gpui",
"html_to_markdown",
"http_client",
"image",
"indoc",
"itertools 0.14.0",
"jsonschema",
@@ -1240,15 +1238,15 @@ dependencies = [
[[package]]
name = "async_zip"
version = "0.0.18"
version = "0.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6"
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
dependencies = [
"async-compression",
"crc32fast",
"futures-lite 2.6.1",
"pin-project",
"thiserror 2.0.17",
"thiserror 1.0.69",
]
[[package]]
@@ -3211,7 +3209,6 @@ dependencies = [
"rustc-hash 2.1.1",
"schemars 1.0.4",
"serde",
"serde_json",
"strum 0.27.2",
]
@@ -3691,7 +3688,6 @@ dependencies = [
"collections",
"futures 0.3.31",
"gpui",
"http_client",
"log",
"net",
"parking_lot",
@@ -5316,7 +5312,6 @@ dependencies = [
"serde_json",
"settings",
"supermaven",
"sweep_ai",
"telemetry",
"theme",
"ui",
@@ -5866,7 +5861,6 @@ dependencies = [
"lsp",
"parking_lot",
"pretty_assertions",
"proto",
"semantic_version",
"serde",
"serde_json",
@@ -8730,6 +8724,7 @@ dependencies = [
"ui",
"ui_input",
"util",
"vim",
"workspace",
"zed_actions",
]
@@ -14039,7 +14034,6 @@ dependencies = [
"paths",
"pretty_assertions",
"project",
"prompt_store",
"proto",
"rayon",
"release_channel",
@@ -16592,33 +16586,6 @@ dependencies = [
"zeno",
]
[[package]]
name = "sweep_ai"
version = "0.1.0"
dependencies = [
"anyhow",
"arrayvec",
"brotli",
"client",
"collections",
"edit_prediction",
"feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
"indoc",
"language",
"project",
"release_channel",
"reqwest_client",
"serde",
"serde_json",
"tree-sitter-rust",
"util",
"workspace",
"zlog",
]
[[package]]
name = "symphonia"
version = "0.5.5"
@@ -21233,7 +21200,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.214.2"
version = "0.214.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -21246,6 +21213,7 @@ dependencies = [
"audio",
"auto_update",
"auto_update_ui",
"backtrace",
"bincode 1.3.3",
"breadcrumbs",
"call",
@@ -21310,6 +21278,7 @@ dependencies = [
"mimalloc",
"miniprofiler_ui",
"nc",
"nix 0.29.0",
"node_runtime",
"notifications",
"onboarding",
@@ -21345,13 +21314,13 @@ dependencies = [
"snippets_ui",
"supermaven",
"svg_preview",
"sweep_ai",
"sysinfo 0.37.2",
"system_specs",
"tab_switcher",
"task",
"tasks_ui",
"telemetry",
"telemetry_events",
"terminal_view",
"theme",
"theme_extension",

View File

@@ -165,7 +165,6 @@ members = [
"crates/sum_tree",
"crates/supermaven",
"crates/supermaven_api",
"crates/sweep_ai",
"crates/codestral",
"crates/svg_preview",
"crates/system_specs",
@@ -399,7 +398,6 @@ streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
sweep_ai = { path = "crates/sweep_ai" }
codestral = { path = "crates/codestral" }
system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" }
@@ -463,7 +461,7 @@ async-tar = "0.5.1"
async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.31.0"
async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] }
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.2", features = [
"hardcoded-credentials",
@@ -480,7 +478,6 @@ bitflags = "2.6.0"
blade-graphics = { version = "0.7.0" }
blade-macros = { version = "0.3.0" }
blade-util = { version = "0.3.0" }
brotli = "8.0.2"
bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.21"
@@ -634,7 +631,6 @@ scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197
schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0"
serde = { version = "1.0.221", features = ["derive", "rc"] }
serde_derive = "1.0.221"
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.2", features = [
"preserve_order",
@@ -728,7 +724,6 @@ yawc = "0.2.5"
zeroize = "1.8"
zstd = "0.11"
[workspace.dependencies.windows]
version = "0.61"
features = [
@@ -797,19 +792,6 @@ codegen-units = 16
codegen-units = 16
[profile.dev.package]
# proc-macros start
gpui_macros = { opt-level = 3 }
derive_refineable = { opt-level = 3 }
settings_macros = { opt-level = 3 }
sqlez_macros = { opt-level = 3, codegen-units = 1 }
ui_macros = { opt-level = 3 }
util_macros = { opt-level = 3 }
serde_derive = { opt-level = 3 }
quote = { opt-level = 3 }
syn = { opt-level = 3 }
proc-macro2 = { opt-level = 3 }
# proc-macros end
taffy = { opt-level = 3 }
cranelift-codegen = { opt-level = 3 }
cranelift-codegen-meta = { opt-level = 3 }
@@ -851,6 +833,7 @@ semantic_version = { codegen-units = 1 }
session = { codegen-units = 1 }
snippet = { codegen-units = 1 }
snippets_ui = { codegen-units = 1 }
sqlez_macros = { codegen-units = 1 }
story = { codegen-units = 1 }
supermaven_api = { codegen-units = 1 }
telemetry_events = { codegen-units = 1 }

View File

@@ -44,7 +44,6 @@ design
docs
= @probably-neb
= @miguelraz
extension
= @kubkon
@@ -99,9 +98,6 @@ settings_ui
= @danilo-leal
= @probably-neb
support
= @miguelraz
tasks
= @SomeoneToIgnore
= @Veykril

View File

@@ -1,32 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3348_16)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97419 6.27207C8.44653 6.29114 8.86622 6.27046 9.23628 6.22425C9.08884 7.48378 8.7346 8.72903 8.16697 9.90688C8.04459 9.83861 7.92582 9.76008 7.81193 9.67108C7.64539 9.54099 7.49799 9.39549 7.37015 9.23818C7.5282 9.54496 7.64901 9.86752 7.73175 10.1986C7.35693 10.6656 6.90663 11.0373 6.412 11.3101C5.01165 10.8075 4.03638 9.63089 4.03638 7.93001C4.03638 6.96185 4.35234 6.07053 4.88281 5.36157C5.34001 5.69449 6.30374 6.20455 7.97419 6.27207ZM8.27511 11.5815C10.3762 11.5349 11.8115 10.7826 12.8347 7.93001C11.6992 7.93001 11.4246 7.10731 11.1188 6.19149C11.0669 6.03596 11.0141 5.87771 10.956 5.72037C10.6733 5.86733 10.2753 6.02782 9.74834 6.13895C9.59658 7.49345 9.20592 8.83238 8.56821 10.0897C8.89933 10.2093 9.24674 10.262 9.5908 10.2502C9.08928 10.4803 8.62468 10.8066 8.22655 11.2255C8.2457 11.3438 8.26186 11.4625 8.27511 11.5815ZM6.62702 7.75422C6.62702 7.50604 6.82821 7.30485 7.07639 7.30485C7.32457 7.30485 7.52576 7.50604 7.52576 7.75422V8.23616C7.52576 8.48435 7.32457 8.68554 7.07639 8.68554C6.82821 8.68554 6.62702 8.48435 6.62702 8.23616V7.75422ZM5.27746 7.30485C5.05086 7.30485 4.86716 7.48854 4.86716 7.71513V8.27525C4.86716 8.50185 5.05086 8.68554 5.27746 8.68554C5.50406 8.68554 5.68776 8.50185 5.68776 8.27525V7.71513C5.68776 7.48854 5.50406 7.30485 5.27746 7.30485Z" fill="white"/>
<mask id="mask0_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="4" y="5" width="9" height="7">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97419 6.27207C8.44653 6.29114 8.86622 6.27046 9.23628 6.22425C9.08884 7.48378 8.7346 8.72903 8.16697 9.90688C8.04459 9.83861 7.92582 9.76008 7.81193 9.67108C7.64539 9.54099 7.49799 9.39549 7.37015 9.23818C7.5282 9.54496 7.64901 9.86752 7.73175 10.1986C7.35693 10.6656 6.90663 11.0373 6.412 11.3101C5.01165 10.8075 4.03638 9.63089 4.03638 7.93001C4.03638 6.96185 4.35234 6.07053 4.88281 5.36157C5.34001 5.69449 6.30374 6.20455 7.97419 6.27207ZM8.27511 11.5815C10.3762 11.5349 11.8115 10.7826 12.8347 7.93001C11.6992 7.93001 11.4246 7.10731 11.1188 6.19149C11.0669 6.03596 11.0141 5.87771 10.956 5.72037C10.6733 5.86733 10.2753 6.02782 9.74834 6.13895C9.59658 7.49345 9.20592 8.83238 8.56821 10.0897C8.89933 10.2093 9.24674 10.262 9.5908 10.2502C9.08928 10.4803 8.62468 10.8066 8.22655 11.2255C8.2457 11.3438 8.26186 11.4625 8.27511 11.5815ZM6.62702 7.75422C6.62702 7.50604 6.82821 7.30485 7.07639 7.30485C7.32457 7.30485 7.52576 7.50604 7.52576 7.75422V8.23616C7.52576 8.48435 7.32457 8.68554 7.07639 8.68554C6.82821 8.68554 6.62702 8.48435 6.62702 8.23616V7.75422ZM5.27746 7.30485C5.05086 7.30485 4.86716 7.48854 4.86716 7.71513V8.27525C4.86716 8.50185 5.05086 8.68554 5.27746 8.68554C5.50406 8.68554 5.68776 8.50185 5.68776 8.27525V7.71513C5.68776 7.48854 5.50406 7.30485 5.27746 7.30485Z" fill="white"/>
</mask>
<g mask="url(#mask0_3348_16)">
<path d="M9.23617 6.22425L9.39588 6.24293L9.41971 6.0393L9.21624 6.06471L9.23617 6.22425ZM8.16687 9.90688L8.08857 10.0473L8.23765 10.1305L8.31174 9.97669L8.16687 9.90688ZM7.37005 9.23819L7.49487 9.13676L7.22714 9.3118L7.37005 9.23819ZM7.73165 10.1986L7.85702 10.2993L7.90696 10.2371L7.88761 10.1597L7.73165 10.1986ZM6.41189 11.3101L6.35758 11.4615L6.42594 11.486L6.48954 11.4509L6.41189 11.3101ZM4.88271 5.36157L4.97736 5.23159L4.84905 5.13817L4.75397 5.26525L4.88271 5.36157ZM8.27501 11.5815L8.11523 11.5993L8.13151 11.7456L8.27859 11.7423L8.27501 11.5815ZM12.8346 7.93001L12.986 7.98428L13.0631 7.76921H12.8346V7.93001ZM10.9559 5.72037L11.1067 5.66469L11.0436 5.49354L10.8817 5.5777L10.9559 5.72037ZM9.74824 6.13896L9.71508 5.98161L9.60139 6.0056L9.58846 6.12102L9.74824 6.13896ZM8.56811 10.0897L8.42469 10.017L8.34242 10.1792L8.51348 10.241L8.56811 10.0897ZM9.5907 10.2502L9.65775 10.3964L9.58519 10.0896L9.5907 10.2502ZM8.22644 11.2255L8.10992 11.1147L8.05502 11.1725L8.06773 11.2512L8.22644 11.2255ZM9.21624 6.06471C8.85519 6.10978 8.44439 6.13015 7.98058 6.11139L7.96756 6.43272C8.44852 6.45215 8.87701 6.43111 9.25607 6.3838L9.21624 6.06471ZM8.31174 9.97669C8.88724 8.78244 9.2464 7.51988 9.39588 6.24293L9.07647 6.20557C8.93108 7.44772 8.58175 8.67563 8.02203 9.83708L8.31174 9.97669ZM8.2452 9.76645C8.12998 9.70219 8.01817 9.62826 7.91082 9.54438L7.71285 9.79779C7.8333 9.8919 7.95895 9.97503 8.08857 10.0473L8.2452 9.76645ZM7.91082 9.54438C7.75387 9.4218 7.61512 9.28479 7.49487 9.13676L7.24526 9.33957C7.38066 9.50619 7.53671 9.66023 7.71285 9.79779L7.91082 9.54438ZM7.22714 9.3118C7.37944 9.60746 7.49589 9.91837 7.57564 10.2376L7.88761 10.1597C7.80196 9.81663 7.67679 9.48248 7.513 9.16453L7.22714 9.3118ZM7.60624 10.098C7.24483 10.5482 6.81083 10.9065 6.33425 11.1693L6.48954 11.4509C7.00223 11.1682 7.46887 10.7829 7.85702 10.2993L7.60624 10.098ZM3.87549 7.93001C3.87548 9.7042 4.89861 10.9378 6.35758 11.4615L6.46622 11.1588C5.12449 10.6772 4.19707 9.55763 4.19707 7.93001H3.87549ZM4.75397 5.26525C4.20309 6.00147 3.87549 6.92646 3.87549 7.93001H4.19707C4.19707 6.99724 4.50139 6.13959 5.01145 5.45791L4.75397 5.26525ZM7.98058 6.11139C6.34236 6.04516 5.40922 5.54604 4.97736 5.23159L4.78806 5.49157C5.27058 5.84291 6.26491 6.3639 7.96756 6.43272L7.98058 6.11139ZM8.27859 11.7423C9.34696 11.7185 10.2682 11.515 11.0542 10.9376C11.8388 10.3612 12.4683 9.4273 12.986 7.98428L12.6833 7.8757C12.1776 9.28534 11.5779 10.1539 10.8638 10.6784C10.1511 11.202 9.30417 11.3978 8.27143 11.4208L8.27859 11.7423ZM12.8346 7.76921C12.3148 7.76921 12.0098 7.58516 11.7925 7.30552C11.5639 7.0114 11.4266 6.60587 11.2712 6.14061L10.9662 6.24242C11.1166 6.69294 11.2695 7.15667 11.5385 7.50285C11.8188 7.86347 12.2189 8.09078 12.8346 8.09078V7.76921ZM11.2712 6.14061C11.2195 5.98543 11.1658 5.82478 11.1067 5.66469L10.805 5.77606C10.8621 5.93065 10.9142 6.0865 10.9662 6.24242L11.2712 6.14061ZM10.8817 5.5777C10.6115 5.71821 10.2273 5.87362 9.71508 5.98161L9.78143 6.29626C10.3232 6.18206 10.735 6.0165 11.0301 5.86301L10.8817 5.5777ZM9.58846 6.12102C9.43882 7.45684 9.05355 8.77717 8.42469 10.017L8.71149 10.1625C9.35809 8.88764 9.75417 7.53011 9.90806 6.15685L9.58846 6.12102ZM9.58519 10.0896C9.26119 10.1006 8.93423 10.051 8.62269 9.93854L8.51348 10.241C8.86427 10.3677 9.23205 10.4234 9.5962 10.4109L9.58519 10.0896ZM8.34301 11.3363C8.72675 10.9325 9.17443 10.6181 9.65775 10.3964L9.52365 10.1041C9.00392 10.3425 8.52241 10.6807 8.10992 11.1147L8.34301 11.3363ZM8.43483 11.5638C8.4213 11.4421 8.40475 11.3207 8.3852 11.1998L8.06773 11.2512C8.08644 11.3668 8.10225 11.4829 8.11523 11.5993L8.43483 11.5638ZM7.07629 7.14405C6.73931 7.14405 6.46613 7.41724 6.46613 7.75423H6.7877C6.7877 7.59484 6.91691 7.46561 7.07629 7.46561V7.14405ZM7.68646 7.75423C7.68646 7.41724 7.41326 7.14405 7.07629 7.14405V7.46561C7.23567 7.46561 7.36489 7.59484 7.36489 7.75423H7.68646ZM7.68646 8.23616V7.75423H7.36489V8.23616H7.68646ZM7.07629 8.84634C7.41326 8.84634 7.68646 8.57315 7.68646 8.23616H7.36489C7.36489 8.39555 7.23567 8.52474 7.07629 8.52474V8.84634ZM6.46613 8.23616C6.46613 8.57315 6.73931 8.84634 7.07629 8.84634V8.52474C6.91691 8.52474 6.7877 8.39555 6.7877 8.23616H6.46613ZM6.46613 7.75423V8.23616H6.7877V7.75423H6.46613ZM5.02785 7.71514C5.02785 7.57734 5.13956 7.46561 5.27736 7.46561V7.14405C4.96196 7.14405 4.70627 7.39974 4.70627 7.71514H5.02785ZM5.02785 8.27525V7.71514H4.70627V8.27525H5.02785ZM5.27736 8.52474C5.13956 8.52474 5.02785 8.41305 5.02785 8.27525H4.70627C4.70627 8.59065 4.96196 8.84634 5.27736 8.84634V8.52474ZM5.52687 8.27525C5.52687 8.41305 5.41516 8.52474 5.27736 8.52474V8.84634C5.59277 8.84634 5.84845 8.59065 5.84845 8.27525H5.52687ZM5.52687 7.71514V8.27525H5.84845V7.71514H5.52687ZM5.27736 7.46561C5.41516 7.46561 5.52687 7.57734 5.52687 7.71514H5.84845C5.84845 7.39974 5.59277 7.14405 5.27736 7.14405V7.46561Z" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.12635 14.5901C7.22369 14.3749 7.3069 14.1501 7.37454 13.9167C7.54132 13.3412 7.5998 12.7599 7.56197 12.1948C7.53665 12.5349 7.47589 12.8775 7.37718 13.2181C7.23926 13.694 7.03667 14.1336 6.78174 14.5301C6.89605 14.5547 7.01101 14.5747 7.12635 14.5901Z" fill="white"/>
<path d="M9.71984 7.74796C9.50296 7.74796 9.29496 7.83412 9.14159 7.98745C8.98822 8.14082 8.9021 8.34882 8.9021 8.5657C8.9021 8.78258 8.98822 8.99057 9.14159 9.14394C9.29496 9.29728 9.50296 9.38344 9.71984 9.38344V8.5657V7.74796Z" fill="white"/>
<mask id="mask1_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="5" y="2" width="8" height="9">
<path d="M12.3783 2.9985H5.36792V10.3954H12.3783V2.9985Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.75733 3.61999C9.98577 5.80374 9.60089 8.05373 8.56819 10.0898C8.43122 10.0403 8.29704 9.9794 8.16699 9.90688C9.15325 7.86033 9.49538 5.61026 9.22757 3.43526C9.39923 3.51584 9.57682 3.57729 9.75733 3.61999Z" fill="black"/>
</mask>
<g mask="url(#mask1_3348_16)">
<path d="M8.56815 10.0898L8.67689 10.1449L8.62812 10.241L8.52678 10.2044L8.56815 10.0898ZM9.75728 3.61998L9.78536 3.50136L9.86952 3.52127L9.87853 3.6073L9.75728 3.61998ZM8.16695 9.90687L8.1076 10.0133L8.00732 9.9574L8.05715 9.85398L8.16695 9.90687ZM9.22753 3.43524L9.10656 3.45014L9.07958 3.23116L9.27932 3.32491L9.22753 3.43524ZM8.45945 10.0346C9.48122 8.02009 9.86217 5.79374 9.63608 3.63266L9.87853 3.6073C10.1093 5.81372 9.72048 8.0873 8.67689 10.1449L8.45945 10.0346ZM8.22633 9.80041C8.35056 9.86971 8.47876 9.92791 8.60956 9.97514L8.52678 10.2044C8.38363 10.1527 8.24344 10.0891 8.1076 10.0133L8.22633 9.80041ZM9.34849 3.42035C9.61905 5.61792 9.27346 7.89158 8.27675 9.9598L8.05715 9.85398C9.03298 7.82905 9.37158 5.60258 9.10656 3.45014L9.34849 3.42035ZM9.72925 3.7386C9.54064 3.69399 9.3551 3.62977 9.17573 3.54558L9.27932 3.32491C9.44327 3.40188 9.61288 3.46058 9.78536 3.50136L9.72925 3.7386Z" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4118 3.46925L11.2416 3.39926L11.1904 3.57611L11.349 3.62202C11.1904 3.57611 11.1904 3.57615 11.1904 3.5762L11.1903 3.57631L11.1902 3.57658L11.19 3.57741L11.1893 3.58009C11.1886 3.58233 11.1878 3.58548 11.1867 3.58949C11.1845 3.5975 11.1814 3.60897 11.1777 3.62359C11.1703 3.6528 11.1603 3.69464 11.1493 3.74656C11.1275 3.85017 11.102 3.99505 11.0869 4.16045C11.0573 4.4847 11.0653 4.91594 11.2489 5.26595C11.2613 5.28944 11.2643 5.31174 11.2625 5.32629C11.261 5.33849 11.2572 5.34226 11.2536 5.3449C11.0412 5.50026 10.5639 5.78997 9.76653 5.96607C9.76095 6.02373 9.75493 6.08134 9.74848 6.13895C10.601 5.95915 11.1161 5.65017 11.3511 5.4782C11.4413 5.41219 11.4471 5.28823 11.3952 5.18922C11.1546 4.73063 11.2477 4.08248 11.3103 3.78401C11.3314 3.68298 11.349 3.62202 11.349 3.62202C11.3745 3.6325 11.4002 3.63983 11.4259 3.64425C11.9083 3.72709 12.4185 2.78249 12.6294 2.33939C12.6852 2.22212 12.6234 2.08843 12.497 2.05837C11.2595 1.76399 5.46936 0.631807 4.57214 4.96989C4.55907 5.03307 4.57607 5.10106 4.62251 5.14584C4.87914 5.39322 5.86138 6.18665 7.9743 6.27207C8.44664 6.29114 8.86633 6.27046 9.23638 6.22425C9.24295 6.16797 9.24912 6.1117 9.25491 6.05534C8.88438 6.10391 8.46092 6.12641 7.98094 6.10702C5.91152 6.02337 4.96693 5.24843 4.73714 5.02692C4.73701 5.02679 4.73545 5.02525 4.73422 5.0208C4.73292 5.01611 4.73254 5.00987 4.73388 5.00334C4.94996 3.95861 5.4573 3.25195 6.11188 2.77714C6.77039 2.29947 7.58745 2.04983 8.42824 1.94075C10.1122 1.72228 11.8454 2.07312 12.4588 2.21906C12.4722 2.22225 12.4787 2.22927 12.4819 2.2362C12.4853 2.24342 12.4869 2.25443 12.4803 2.2684C12.3706 2.49879 12.183 2.85746 11.9656 3.13057C11.8564 3.26783 11.7479 3.37295 11.6469 3.43216C11.5491 3.48956 11.4752 3.49529 11.4118 3.46925Z" fill="white"/>
<mask id="mask2_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="9" width="7" height="6">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.22654 11.2255C8.62463 10.8066 9.08923 10.4803 9.59075 10.2502C8.97039 10.2715 8.33933 10.0831 7.81189 9.67109C7.64534 9.541 7.49795 9.39549 7.37014 9.23819C7.52815 9.54497 7.64896 9.86752 7.7317 10.1986C6.70151 11.4821 5.1007 12.0466 3.57739 11.8125C3.85909 12.527 4.32941 13.178 4.97849 13.6851C5.8625 14.3756 6.92544 14.6799 7.96392 14.6227C8.32513 13.5174 8.4085 12.351 8.22654 11.2255Z" fill="white"/>
</mask>
<g mask="url(#mask2_3348_16)">
<path d="M9.59085 10.2502L9.58389 10.0472L9.67556 10.4349L9.59085 10.2502ZM8.22663 11.2255L8.02607 11.258L8.00999 11.1585L8.07936 11.0856L8.22663 11.2255ZM7.37024 9.23819L7.18961 9.33119L7.52789 9.11006L7.37024 9.23819ZM7.7318 10.1986L7.92886 10.1494L7.95328 10.2472L7.8902 10.3258L7.7318 10.1986ZM3.57749 11.8125L3.3885 11.887L3.25879 11.5579L3.60835 11.6117L3.57749 11.8125ZM7.96402 14.6227L8.15711 14.6858L8.11397 14.8179L7.97519 14.8255L7.96402 14.6227ZM9.67556 10.4349C9.19708 10.6544 8.7538 10.9657 8.37387 11.3655L8.07936 11.0856C8.49566 10.6475 8.98161 10.3062 9.50614 10.0656L9.67556 10.4349ZM7.93704 9.51099C8.42551 9.89261 9.00942 10.0669 9.58389 10.0472L9.59781 10.4533C8.93151 10.4761 8.25334 10.2737 7.68693 9.83118L7.93704 9.51099ZM7.52789 9.11006C7.64615 9.25565 7.78261 9.39038 7.93704 9.51099L7.68693 9.83118C7.50827 9.69161 7.34994 9.53537 7.21254 9.36627L7.52789 9.11006ZM7.5347 10.2479C7.45573 9.93178 7.34043 9.62393 7.18961 9.33119L7.55082 9.14514C7.71611 9.466 7.84242 9.80326 7.92886 10.1494L7.5347 10.2479ZM3.60835 11.6117C5.06278 11.8352 6.59038 11.2962 7.57335 10.0715L7.8902 10.3258C6.81284 11.6681 5.1388 12.258 3.54663 12.0133L3.60835 11.6117ZM4.85352 13.8452C4.17512 13.3152 3.68312 12.6343 3.3885 11.887L3.76648 11.738C4.03524 12.4197 4.4839 13.0409 5.10364 13.525L4.85352 13.8452ZM7.97519 14.8255C6.8895 14.8853 5.77774 14.5672 4.85352 13.8452L5.10364 13.525C5.94745 14.1842 6.96157 14.4744 7.95285 14.4198L7.97519 14.8255ZM8.42716 11.1931C8.61419 12.3499 8.52858 13.5491 8.15711 14.6858L7.77093 14.5596C8.12191 13.4857 8.20296 12.352 8.02607 11.258L8.42716 11.1931Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_3348_16">
<rect width="9.63483" height="14" fill="white" transform="translate(3.19995 1.5)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -313,7 +313,7 @@
"use_key_equivalents": true,
"bindings": {
"cmd-n": "agent::NewTextThread",
"cmd-alt-n": "agent::NewExternalAgentThread"
"cmd-alt-t": "agent::NewThread"
}
},
{

View File

@@ -421,6 +421,12 @@
"ctrl-[": "editor::Cancel"
}
},
{
"context": "vim_mode == helix_select && !menu",
"bindings": {
"escape": "vim::SwitchToHelixNormalMode"
}
},
{
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
"bindings": {

View File

@@ -742,16 +742,6 @@
// "never"
"show": "always"
},
// Sort order for entries in the project panel.
// This setting can take three values:
//
// 1. Show directories first, then files:
// "directories_first"
// 2. Mix directories and files together:
// "mixed"
// 3. Show files first, then directories:
// "files_first"
"sort_mode": "directories_first",
// Whether to enable drag-and-drop operations in the project panel.
"drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window.
@@ -2059,18 +2049,6 @@
"dev": {
// "theme": "Andromeda"
},
// Settings overrides to use when using linux
"linux": {},
// Settings overrides to use when using macos
"macos": {},
// Settings overrides to use when using windows
"windows": {
"languages": {
"PHP": {
"language_servers": ["intelephense", "!phpactor", "..."]
}
}
},
// Whether to show full labels in line indicator or short ones
//
// Values:

View File

@@ -150,7 +150,6 @@ impl DbThread {
.unwrap_or_default(),
input: tool_use.input,
is_input_complete: true,
thought_signature: None,
},
));
}

View File

@@ -1108,7 +1108,6 @@ fn tool_use(
raw_input: serde_json::to_string_pretty(&input).unwrap(),
input: serde_json::to_value(input).unwrap(),
is_input_complete: true,
thought_signature: None,
})
}

View File

@@ -48,7 +48,7 @@ pub async fn get_buffer_content_or_outline(
if outline_items.is_empty() {
let text = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
let len = snapshot.len().min(snapshot.as_rope().floor_char_boundary(1024));
let len = snapshot.len().min(1024);
let content = snapshot.text_for_range(0..len).collect::<String>();
if let Some(path) = path {
format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}")
@@ -178,7 +178,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let content = "".repeat(100 * 1024); // 100KB
let content = "A".repeat(100 * 1024); // 100KB
let content_len = content.len();
let buffer = project
.update(cx, |project, cx| project.create_buffer(true, cx))
@@ -194,7 +194,7 @@ mod tests {
// Should contain some of the actual file content
assert!(
result.text.contains("⚡⚡⚡⚡⚡⚡⚡"),
result.text.contains("AAAAAAAAAA"),
"Result did not contain content subset"
);

View File

@@ -274,7 +274,6 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
};
fake_model
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
@@ -462,7 +461,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -472,7 +470,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -523,7 +520,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -558,7 +554,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -597,7 +592,6 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -627,7 +621,6 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
is_input_complete: true,
thought_signature: None,
};
fake_model
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
@@ -738,7 +731,6 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
is_input_complete: true,
thought_signature: None,
};
let tool_result = LanguageModelToolResult {
tool_use_id: "tool_id_1".into(),
@@ -1045,7 +1037,6 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -1089,7 +1080,6 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "mcp"}).to_string(),
input: json!({"text": "mcp"}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -1099,7 +1089,6 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "native"}).to_string(),
input: json!({"text": "native"}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -1799,7 +1788,6 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
};
let echo_tool_use = LanguageModelToolUse {
id: "tool_id_2".into(),
@@ -1807,7 +1795,6 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
};
fake_model.send_last_completion_stream_text_chunk("Hi!");
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -2013,7 +2000,6 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_input: input.to_string(),
input,
is_input_complete: false,
thought_signature: None,
},
));
@@ -2026,7 +2012,6 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_input: input.to_string(),
input,
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -2229,7 +2214,6 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
};
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
tool_use_1.clone(),

View File

@@ -607,8 +607,6 @@ pub struct Thread {
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
pub(crate) project: Entity<Project>,
pub(crate) action_log: Entity<ActionLog>,
/// Tracks the last time files were read by the agent, to detect external modifications
pub(crate) file_read_times: HashMap<PathBuf, fs::MTime>,
}
impl Thread {
@@ -667,7 +665,6 @@ impl Thread {
prompt_capabilities_rx,
project,
action_log,
file_read_times: HashMap::default(),
}
}
@@ -863,7 +860,6 @@ impl Thread {
updated_at: db_thread.updated_at,
prompt_capabilities_tx,
prompt_capabilities_rx,
file_read_times: HashMap::default(),
}
}
@@ -1003,7 +999,6 @@ impl Thread {
self.add_tool(NowTool);
self.add_tool(OpenTool::new(self.project.clone()));
self.add_tool(ReadFileTool::new(
cx.weak_entity(),
self.project.clone(),
self.action_log.clone(),
));

View File

@@ -309,40 +309,6 @@ impl AgentTool for EditFileTool {
})?
.await?;
// Check if the file has been modified since the agent last read it
if let Some(abs_path) = abs_path.as_ref() {
let (last_read_mtime, current_mtime, is_dirty) = self.thread.update(cx, |thread, cx| {
let last_read = thread.file_read_times.get(abs_path).copied();
let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime());
let dirty = buffer.read(cx).is_dirty();
(last_read, current, dirty)
})?;
// Check for unsaved changes first - these indicate modifications we don't know about
if is_dirty {
anyhow::bail!(
"This file cannot be written to because it has unsaved changes. \
Please end the current conversation immediately by telling the user you want to write to this file (mention its path explicitly) but you can't write to it because it has unsaved changes. \
Ask the user to save that buffer's changes and to inform you when it's ok to proceed."
);
}
// Check if the file was modified on disk since we last read it
if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) {
// MTime can be unreliable for comparisons, so our newtype intentionally
// doesn't support comparing them. If the mtime at all different
// (which could be because of a modification or because e.g. system clock changed),
// we pessimistically assume it was modified.
if current != last_read {
anyhow::bail!(
"The file {} has been modified since you last read it. \
Please read the file again to get the current state before editing it.",
input.path.display()
);
}
}
}
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
event_stream.update_diff(diff.clone());
let _finalize_diff = util::defer({
@@ -455,17 +421,6 @@ impl AgentTool for EditFileTool {
log.buffer_edited(buffer.clone(), cx);
})?;
// Update the recorded read time after a successful edit so consecutive edits work
if let Some(abs_path) = abs_path.as_ref() {
if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| {
buffer.file().and_then(|file| file.disk_state().mtime())
})? {
self.thread.update(cx, |thread, _| {
thread.file_read_times.insert(abs_path.to_path_buf(), new_mtime);
})?;
}
}
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let (new_text, unified_diff) = cx
.background_spawn({
@@ -1793,426 +1748,10 @@ mod tests {
}
}
#[gpui::test]
async fn test_file_read_times_tracking(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
// Initially, file_read_times should be empty
let is_empty = thread.read_with(cx, |thread, _| thread.file_read_times.is_empty());
assert!(is_empty, "file_read_times should start empty");
// Create read tool
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
// Read the file to record the read time
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Verify that file_read_times now contains an entry for the file
let has_entry = thread.read_with(cx, |thread, _| {
thread.file_read_times.len() == 1
&& thread
.file_read_times
.keys()
.any(|path| path.ends_with("test.txt"))
});
assert!(
has_entry,
"file_read_times should contain an entry after reading the file"
);
// Read the file again - should update the entry
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Should still have exactly one entry
let has_one_entry = thread.read_with(cx, |thread, _| thread.file_read_times.len() == 1);
assert!(
has_one_entry,
"file_read_times should still have one entry after re-reading"
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
});
}
#[gpui::test]
async fn test_consecutive_edits_work(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let languages = project.read_with(cx, |project, _| project.languages().clone());
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
let edit_tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages,
Templates::new(),
));
// Read the file first
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// First edit should work
let edit_result = {
let edit_task = cx.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "First edit".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
});
cx.executor().run_until_parked();
model.send_last_completion_stream_text_chunk(
"<old_text>original content</old_text><new_text>modified content</new_text>"
.to_string(),
);
model.end_last_completion_stream();
edit_task.await
};
assert!(
edit_result.is_ok(),
"First edit should succeed, got error: {:?}",
edit_result.as_ref().err()
);
// Second edit should also work because the edit updated the recorded read time
let edit_result = {
let edit_task = cx.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "Second edit".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
});
cx.executor().run_until_parked();
model.send_last_completion_stream_text_chunk(
"<old_text>modified content</old_text><new_text>further modified content</new_text>".to_string(),
);
model.end_last_completion_stream();
edit_task.await
};
assert!(
edit_result.is_ok(),
"Second consecutive edit should succeed, got error: {:?}",
edit_result.as_ref().err()
);
}
#[gpui::test]
async fn test_external_modification_detected(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let languages = project.read_with(cx, |project, _| project.languages().clone());
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
let edit_tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages,
Templates::new(),
));
// Read the file first
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Simulate external modification - advance time and save file
cx.background_executor
.advance_clock(std::time::Duration::from_secs(2));
fs.save(
path!("/root/test.txt").as_ref(),
&"externally modified content".into(),
language::LineEnding::Unix,
)
.await
.unwrap();
// Reload the buffer to pick up the new mtime
let project_path = project
.read_with(cx, |project, cx| {
project.find_project_path("root/test.txt", cx)
})
.expect("Should find project path");
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.await
.unwrap();
buffer
.update(cx, |buffer, cx| buffer.reload(cx))
.await
.unwrap();
cx.executor().run_until_parked();
// Try to edit - should fail because file was modified externally
let result = cx
.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "Edit after external change".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
})
.await;
assert!(
result.is_err(),
"Edit should fail after external modification"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("has been modified since you last read it"),
"Error should mention file modification, got: {}",
error_msg
);
}
#[gpui::test]
async fn test_dirty_buffer_detected(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let languages = project.read_with(cx, |project, _| project.languages().clone());
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
let edit_tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages,
Templates::new(),
));
// Read the file first
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Open the buffer and make it dirty by editing without saving
let project_path = project
.read_with(cx, |project, cx| {
project.find_project_path("root/test.txt", cx)
})
.expect("Should find project path");
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.await
.unwrap();
// Make an in-memory edit to the buffer (making it dirty)
buffer.update(cx, |buffer, cx| {
let end_point = buffer.max_point();
buffer.edit([(end_point..end_point, " added text")], None, cx);
});
// Verify buffer is dirty
let is_dirty = buffer.read_with(cx, |buffer, _| buffer.is_dirty());
assert!(is_dirty, "Buffer should be dirty after in-memory edit");
// Try to edit - should fail because buffer has unsaved changes
let result = cx
.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "Edit with dirty buffer".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
})
.await;
assert!(result.is_err(), "Edit should fail when buffer is dirty");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("cannot be written to because it has unsaved changes"),
"Error should mention unsaved changes, got: {}",
error_msg
);
}
}

View File

@@ -1,7 +1,7 @@
use action_log::ActionLog;
use agent_client_protocol::{self as acp, ToolCallUpdateFields};
use anyhow::{Context as _, Result, anyhow};
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use gpui::{App, Entity, SharedString, Task};
use indoc::formatdoc;
use language::Point;
use language_model::{LanguageModelImage, LanguageModelToolResultContent};
@@ -12,7 +12,7 @@ use settings::Settings;
use std::sync::Arc;
use util::markdown::MarkdownCodeBlock;
use crate::{AgentTool, Thread, ToolCallEventStream, outline};
use crate::{AgentTool, ToolCallEventStream, outline};
/// Reads the content of the given file in the project.
///
@@ -42,19 +42,13 @@ pub struct ReadFileToolInput {
}
pub struct ReadFileTool {
thread: WeakEntity<Thread>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
}
impl ReadFileTool {
pub fn new(
thread: WeakEntity<Thread>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
) -> Self {
pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
Self {
thread,
project,
action_log,
}
@@ -201,17 +195,6 @@ impl AgentTool for ReadFileTool {
anyhow::bail!("{file_path} not found");
}
// Record the file read time and mtime
if let Some(mtime) = buffer.read_with(cx, |buffer, _| {
buffer.file().and_then(|file| file.disk_state().mtime())
})? {
self.thread
.update(cx, |thread, _| {
thread.file_read_times.insert(abs_path.to_path_buf(), mtime);
})
.ok();
}
let mut anchor = None;
// Check if specific line ranges are provided
@@ -302,15 +285,11 @@ impl AgentTool for ReadFileTool {
#[cfg(test)]
mod test {
use super::*;
use crate::{ContextServerRegistry, Templates, Thread};
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
use language_model::fake_provider::FakeLanguageModel;
use project::{FakeFs, Project};
use prompt_store::ProjectContext;
use serde_json::json;
use settings::SettingsStore;
use std::sync::Arc;
use util::path;
#[gpui::test]
@@ -321,20 +300,7 @@ mod test {
fs.insert_tree(path!("/root"), json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let tool = Arc::new(ReadFileTool::new(project, action_log));
let (event_stream, _) = ToolCallEventStream::test();
let result = cx
@@ -367,20 +333,7 @@ mod test {
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let tool = Arc::new(ReadFileTool::new(project, action_log));
let result = cx
.update(|cx| {
let input = ReadFileToolInput {
@@ -410,20 +363,7 @@ mod test {
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(rust_lang()));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let tool = Arc::new(ReadFileTool::new(project, action_log));
let result = cx
.update(|cx| {
let input = ReadFileToolInput {
@@ -495,20 +435,7 @@ mod test {
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let tool = Arc::new(ReadFileTool::new(project, action_log));
let result = cx
.update(|cx| {
let input = ReadFileToolInput {
@@ -536,20 +463,7 @@ mod test {
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let tool = Arc::new(ReadFileTool::new(project, action_log));
// start_line of 0 should be treated as 1
let result = cx
@@ -693,20 +607,7 @@ mod test {
let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let tool = Arc::new(ReadFileTool::new(project, action_log));
// Reading a file outside the project worktree should fail
let result = cx
@@ -920,24 +821,7 @@ mod test {
.await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log.clone(),
));
let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone()));
// Test reading allowed files in worktree1
let result = cx

View File

@@ -247,58 +247,37 @@ impl AgentConnection for AcpConnection {
let default_mode = self.default_mode.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers =
if project.read(cx).is_local() {
context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
match &*configuration {
project::context_server_store::ContextServerConfiguration::Custom {
command,
..
}
| project::context_server_store::ContextServerConfiguration::Extension {
command,
..
} => Some(acp::McpServer::Stdio {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
meta: None,
})
.collect()
} else {
vec![]
},
}),
project::context_server_store::ContextServerConfiguration::Http {
url,
headers,
} => Some(acp::McpServer::Http {
name: id.0.to_string(),
url: url.to_string(),
headers: headers.iter().map(|(name, value)| acp::HttpHeader {
name: name.clone(),
value: value.clone(),
meta: None,
}).collect(),
}),
}
let mcp_servers = if project.read(cx).is_local() {
context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
let command = configuration.command();
Some(acp::McpServer::Stdio {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
meta: None,
})
.collect()
} else {
vec![]
},
})
.collect()
} else {
// In SSH projects, the external agent is running on the remote
// machine, and currently we only run MCP servers on the local
// machine. So don't pass any MCP servers to the agent in that case.
Vec::new()
};
})
.collect()
} else {
// In SSH projects, the external agent is running on the remote
// machine, and currently we only run MCP servers on the local
// machine. So don't pass any MCP servers to the agent in that case.
Vec::new()
};
cx.spawn(async move |cx| {
let response = conn

View File

@@ -98,8 +98,6 @@ util.workspace = true
watch.workspace = true
workspace.workspace = true
zed_actions.workspace = true
image.workspace = true
async-fs.workspace = true
[dev-dependencies]
acp_thread = { workspace = true, features = ["test-support"] }

View File

@@ -28,7 +28,6 @@ use gpui::{
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
Subscription, Task, TextStyle, WeakEntity, pulsating_between,
};
use itertools::Either;
use language::{Buffer, Language, language_settings::InlayHintKind};
use language_model::LanguageModelImage;
use postage::stream::Stream as _;
@@ -913,114 +912,74 @@ impl MessageEditor {
if !self.prompt_capabilities.borrow().image {
return;
}
let Some(clipboard) = cx.read_from_clipboard() else {
return;
};
cx.spawn_in(window, async move |this, cx| {
use itertools::Itertools;
let (mut images, paths) = clipboard
.into_entries()
.filter_map(|entry| match entry {
ClipboardEntry::Image(image) => Some(Either::Left(image)),
ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
_ => None,
})
.partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
if !paths.is_empty() {
images.extend(
cx.background_spawn(async move {
let mut images = vec![];
for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
let Ok(content) = async_fs::read(path).await else {
continue;
};
let Ok(format) = image::guess_format(&content) else {
continue;
};
images.push(gpui::Image::from_bytes(
match format {
image::ImageFormat::Png => gpui::ImageFormat::Png,
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
_ => continue,
},
content,
));
let images = cx
.read_from_clipboard()
.map(|item| {
item.into_entries()
.filter_map(|entry| {
if let ClipboardEntry::Image(image) = entry {
Some(image)
} else {
None
}
images
})
.await,
);
}
.collect::<Vec<_>>()
})
.unwrap_or_default();
if images.is_empty() {
return;
}
if images.is_empty() {
return;
}
cx.stop_propagation();
let replacement_text = MentionUri::PastedImage.as_link().to_string();
let Ok(editor) = this.update(cx, |this, cx| {
cx.stop_propagation();
this.editor.clone()
}) else {
return;
};
for image in images {
let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
editor.update_in(cx, |message_editor, window, cx| {
let snapshot = message_editor.snapshot(window, cx);
let (excerpt_id, _, buffer_snapshot) =
snapshot.buffer_snapshot().as_singleton().unwrap();
let replacement_text = MentionUri::PastedImage.as_link().to_string();
for image in images {
let (excerpt_id, text_anchor, multibuffer_anchor) =
self.editor.update(cx, |message_editor, cx| {
let snapshot = message_editor.snapshot(window, cx);
let (excerpt_id, _, buffer_snapshot) =
snapshot.buffer_snapshot().as_singleton().unwrap();
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
let multibuffer_anchor = snapshot
.buffer_snapshot()
.anchor_in_excerpt(*excerpt_id, text_anchor);
message_editor.edit(
[(
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
format!("{replacement_text} "),
)],
cx,
);
(*excerpt_id, text_anchor, multibuffer_anchor)
})
else {
break;
};
let content_len = replacement_text.len();
let Some(start_anchor) = multibuffer_anchor else {
continue;
};
let Ok(end_anchor) = editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
}) else {
continue;
};
let image = Arc::new(image);
let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
MentionUri::PastedImage.name().into(),
IconName::Image.path().into(),
Some(Task::ready(Ok(image.clone())).shared()),
editor.clone(),
window,
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
let multibuffer_anchor = snapshot
.buffer_snapshot()
.anchor_in_excerpt(*excerpt_id, text_anchor);
message_editor.edit(
[(
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
format!("{replacement_text} "),
)],
cx,
)
}) else {
continue;
};
let task = cx
.spawn(async move |cx| {
);
(*excerpt_id, text_anchor, multibuffer_anchor)
});
let content_len = replacement_text.len();
let Some(start_anchor) = multibuffer_anchor else {
continue;
};
let end_anchor = self.editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
});
let image = Arc::new(image);
let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
MentionUri::PastedImage.name().into(),
IconName::Image.path().into(),
Some(Task::ready(Ok(image.clone())).shared()),
self.editor.clone(),
window,
cx,
) else {
continue;
};
let task = cx
.spawn_in(window, {
async move |_, cx| {
let format = image.format;
let image = cx
.update(|_, cx| LanguageModelImage::from_image(image, cx))
@@ -1035,16 +994,15 @@ impl MessageEditor {
} else {
Err("Failed to convert image".into())
}
})
.shared();
this.update(cx, |this, _| {
this.mention_set
.mentions
.insert(crease_id, (MentionUri::PastedImage, task.clone()))
}
})
.ok();
.shared();
self.mention_set
.mentions
.insert(crease_id, (MentionUri::PastedImage, task.clone()));
cx.spawn_in(window, async move |this, cx| {
if task.await.notify_async_err(cx).is_none() {
this.update(cx, |this, cx| {
this.editor.update(cx, |editor, cx| {
@@ -1054,9 +1012,9 @@ impl MessageEditor {
})
.ok();
}
}
})
.detach();
})
.detach();
}
}
pub fn insert_dragged_files(

View File

@@ -56,10 +56,6 @@ impl ModeSelector {
self.set_mode(all_modes[next_index].id.clone(), cx);
}
pub fn mode(&self) -> acp::SessionModeId {
self.connection.current_mode()
}
pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) {
let task = self.connection.set_mode(mode, cx);
self.setting_mode = true;

View File

@@ -251,17 +251,17 @@ impl PickerDelegate for AcpModelPickerDelegate {
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.start_slot::<Icon>(model_info.icon.map(|icon| {
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small)
}))
.child(
h_flex()
.w_full()
.pl_0p5()
.gap_1p5()
.when_some(model_info.icon, |this, icon| {
this.child(
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small)
)
})
.w(px(240.))
.child(Label::new(model_info.name.clone()).truncate()),
)
.end_slot(div().pr_3().when(is_selected, |this| {

View File

@@ -51,7 +51,7 @@ use ui::{
PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, NewTerminal, Workspace};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, ToggleModelSelector};
use zed_actions::assistant::OpenRulesLibrary;
@@ -69,8 +69,8 @@ use crate::ui::{
};
use crate::{
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory,
RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, OpenHistory, RejectAll,
RejectOnce, ToggleBurnMode, ToggleProfileSelector,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -1135,7 +1135,6 @@ impl AcpThreadView {
self.is_loading_contents = true;
let model_id = self.current_model_id(cx);
let mode_id = self.current_mode_id(cx);
let guard = cx.new(|_| ());
cx.observe_release(&guard, |this, _guard, cx| {
this.is_loading_contents = false;
@@ -1170,8 +1169,7 @@ impl AcpThreadView {
"Agent Message Sent",
agent = agent_telemetry_id,
session = session_id,
model = model_id,
mode = mode_id
model = model_id
);
thread.send(contents, cx)
@@ -1184,7 +1182,6 @@ impl AcpThreadView {
agent = agent_telemetry_id,
session = session_id,
model = model_id,
mode = mode_id,
status,
turn_time_ms,
);
@@ -3147,7 +3144,7 @@ impl AcpThreadView {
.text_ui_sm(cx)
.h_full()
.children(terminal_view.map(|terminal_view| {
let element = if terminal_view
if terminal_view
.read(cx)
.content_mode(window, cx)
.is_scrollable()
@@ -3155,15 +3152,7 @@ impl AcpThreadView {
div().h_72().child(terminal_view).into_any_element()
} else {
terminal_view.into_any_element()
};
div()
.on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
window.dispatch_action(NewThread.boxed_clone(), cx);
cx.stop_propagation();
}))
.child(element)
.into_any_element()
}
})),
)
})
@@ -5408,16 +5397,6 @@ impl AcpThreadView {
)
}
fn current_mode_id(&self, cx: &App) -> Option<Arc<str>> {
if let Some(thread) = self.as_native_thread(cx) {
Some(thread.read(cx).profile().0.clone())
} else if let Some(mode_selector) = self.mode_selector() {
Some(mode_selector.read(cx).mode().0)
} else {
None
}
}
fn current_model_id(&self, cx: &App) -> Option<String> {
self.model_selector
.as_ref()

View File

@@ -1,5 +1,5 @@
mod add_llm_provider_modal;
pub mod configure_context_server_modal;
mod configure_context_server_modal;
mod configure_context_server_tools_modal;
mod manage_profiles_modal;
mod tool_picker;
@@ -46,8 +46,9 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::agent_configuration::add_llm_provider_modal::{
AddLlmProviderModal, LlmCompatibleProvider,
use crate::{
AddContextServer,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
};
pub struct AgentConfiguration {
@@ -552,9 +553,7 @@ impl AgentConfiguration {
move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.entry("Add Custom Server", None, {
|window, cx| {
window.dispatch_action(crate::AddContextServer.boxed_clone(), cx)
}
|window, cx| window.dispatch_action(AddContextServer.boxed_clone(), cx)
})
.entry("Install from Extensions", None, {
|window, cx| {
@@ -652,7 +651,7 @@ impl AgentConfiguration {
let is_running = matches!(server_status, ContextServerStatus::Running);
let item_id = SharedString::from(context_server_id.0.clone());
// Servers without a configuration can only be provided by extensions.
let provided_by_extension = server_configuration.as_ref().is_none_or(|config| {
let provided_by_extension = server_configuration.is_none_or(|config| {
matches!(
config.as_ref(),
ContextServerConfiguration::Extension { .. }
@@ -708,10 +707,7 @@ impl AgentConfiguration {
"Server is stopped.",
),
};
let is_remote = server_configuration
.as_ref()
.map(|config| matches!(config.as_ref(), ContextServerConfiguration::Http { .. }))
.unwrap_or(false);
let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
.trigger_with_tooltip(
IconButton::new("context-server-config-menu", IconName::Settings)
@@ -734,25 +730,14 @@ impl AgentConfiguration {
let language_registry = language_registry.clone();
let workspace = workspace.clone();
move |window, cx| {
if is_remote {
crate::agent_configuration::configure_context_server_modal::ConfigureContextServerModal::show_modal_for_existing_server(
context_server_id.clone(),
language_registry.clone(),
workspace.clone(),
window,
cx,
)
.detach();
} else {
ConfigureContextServerModal::show_modal_for_existing_server(
context_server_id.clone(),
language_registry.clone(),
workspace.clone(),
window,
cx,
)
.detach();
}
ConfigureContextServerModal::show_modal_for_existing_server(
context_server_id.clone(),
language_registry.clone(),
workspace.clone(),
window,
cx,
)
.detach_and_log_err(cx);
}
}).when(tool_count > 0, |this| this.entry("View Tools", None, {
let context_server_id = context_server_id.clone();

View File

@@ -3,42 +3,16 @@ use std::sync::Arc;
use anyhow::Result;
use collections::HashSet;
use fs::Fs;
use gpui::{
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, ScrollHandle, Task,
};
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
use language_model::LanguageModelRegistry;
use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities};
use settings::{OpenAiCompatibleSettingsContent, update_settings_file};
use ui::{
Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState,
WithScrollbar, prelude::*,
Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
};
use ui_input::InputField;
use workspace::{ModalView, Workspace};
fn single_line_input(
label: impl Into<SharedString>,
placeholder: impl Into<SharedString>,
text: Option<&str>,
tab_index: isize,
window: &mut Window,
cx: &mut App,
) -> Entity<InputField> {
cx.new(|cx| {
let input = InputField::new(window, cx, placeholder)
.label(label)
.tab_index(tab_index)
.tab_stop(true);
if let Some(text) = text {
input
.editor()
.update(cx, |editor, cx| editor.set_text(text, window, cx));
}
input
})
}
#[derive(Clone, Copy)]
pub enum LlmCompatibleProvider {
OpenAi,
@@ -67,14 +41,12 @@ struct AddLlmProviderInput {
impl AddLlmProviderInput {
fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self {
let provider_name =
single_line_input("Provider Name", provider.name(), None, 1, window, cx);
let api_url = single_line_input("API URL", provider.api_url(), None, 2, window, cx);
let provider_name = single_line_input("Provider Name", provider.name(), None, window, cx);
let api_url = single_line_input("API URL", provider.api_url(), None, window, cx);
let api_key = single_line_input(
"API Key",
"000000000000000000000000000000000000000000000000",
None,
3,
window,
cx,
);
@@ -83,13 +55,12 @@ impl AddLlmProviderInput {
provider_name,
api_url,
api_key,
models: vec![ModelInput::new(0, window, cx)],
models: vec![ModelInput::new(window, cx)],
}
}
fn add_model(&mut self, window: &mut Window, cx: &mut App) {
let model_index = self.models.len();
self.models.push(ModelInput::new(model_index, window, cx));
self.models.push(ModelInput::new(window, cx));
}
fn remove_model(&mut self, index: usize) {
@@ -113,14 +84,11 @@ struct ModelInput {
}
impl ModelInput {
fn new(model_index: usize, window: &mut Window, cx: &mut App) -> Self {
let base_tab_index = (3 + (model_index * 4)) as isize;
fn new(window: &mut Window, cx: &mut App) -> Self {
let model_name = single_line_input(
"Model Name",
"e.g. gpt-4o, claude-opus-4, gemini-2.5-pro",
None,
base_tab_index + 1,
window,
cx,
);
@@ -128,7 +96,6 @@ impl ModelInput {
"Max Completion Tokens",
"200000",
Some("200000"),
base_tab_index + 2,
window,
cx,
);
@@ -136,26 +103,16 @@ impl ModelInput {
"Max Output Tokens",
"Max Output Tokens",
Some("32000"),
base_tab_index + 3,
window,
cx,
);
let max_tokens = single_line_input(
"Max Tokens",
"Max Tokens",
Some("200000"),
base_tab_index + 4,
window,
cx,
);
let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
let ModelCapabilities {
tools,
images,
parallel_tool_calls,
prompt_cache_key,
} = ModelCapabilities::default();
Self {
name: model_name,
max_completion_tokens,
@@ -208,6 +165,24 @@ impl ModelInput {
}
}
fn single_line_input(
label: impl Into<SharedString>,
placeholder: impl Into<SharedString>,
text: Option<&str>,
window: &mut Window,
cx: &mut App,
) -> Entity<InputField> {
cx.new(|cx| {
let input = InputField::new(window, cx, placeholder).label(label);
if let Some(text) = text {
input
.editor()
.update(cx, |editor, cx| editor.set_text(text, window, cx));
}
input
})
}
fn save_provider_to_settings(
input: &AddLlmProviderInput,
cx: &mut App,
@@ -283,7 +258,6 @@ fn save_provider_to_settings(
pub struct AddLlmProviderModal {
provider: LlmCompatibleProvider,
input: AddLlmProviderInput,
scroll_handle: ScrollHandle,
focus_handle: FocusHandle,
last_error: Option<SharedString>,
}
@@ -304,7 +278,6 @@ impl AddLlmProviderModal {
provider,
last_error: None,
focus_handle: cx.focus_handle(),
scroll_handle: ScrollHandle::new(),
}
}
@@ -445,19 +418,6 @@ impl AddLlmProviderModal {
)
})
}
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
}
fn on_tab_prev(
&mut self,
_: &menu::SelectPrevious,
window: &mut Window,
_: &mut Context<Self>,
) {
window.focus_prev();
}
}
impl EventEmitter<DismissEvent> for AddLlmProviderModal {}
@@ -471,27 +431,15 @@ impl Focusable for AddLlmProviderModal {
impl ModalView for AddLlmProviderModal {}
impl Render for AddLlmProviderModal {
fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
let window_size = window.viewport_size();
let rem_size = window.rem_size();
let is_large_window = window_size.height / rem_size > rems_from_px(600.).0;
let modal_max_height = if is_large_window {
rems_from_px(450.)
} else {
rems_from_px(200.)
};
v_flex()
div()
.id("add-llm-provider-modal")
.key_context("AddLlmProviderModal")
.w(rems(34.))
.elevation_3(cx)
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
@@ -514,25 +462,17 @@ impl Render for AddLlmProviderModal {
)
})
.child(
div()
v_flex()
.id("modal_content")
.size_full()
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
.child(
v_flex()
.id("modal_content")
.size_full()
.tab_group()
.max_h(modal_max_height)
.pl_3()
.pr_4()
.gap_2()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.child(self.input.provider_name.clone())
.child(self.input.api_url.clone())
.child(self.input.api_key.clone())
.child(self.render_model_section(cx)),
),
.max_h_128()
.overflow_y_scroll()
.px(DynamicSpacing::Base12.rems(cx))
.gap(DynamicSpacing::Base04.rems(cx))
.child(self.input.provider_name.clone())
.child(self.input.api_url.clone())
.child(self.input.api_key.clone())
.child(self.render_model_section(cx)),
)
.footer(
ModalFooter::new().end_slot(
@@ -702,7 +642,7 @@ mod tests {
let cx = setup_test(cx).await;
cx.update(|window, cx| {
let model_input = ModelInput::new(0, window, cx);
let model_input = ModelInput::new(window, cx);
model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx);
@@ -738,7 +678,7 @@ mod tests {
let cx = setup_test(cx).await;
cx.update(|window, cx| {
let mut model_input = ModelInput::new(0, window, cx);
let mut model_input = ModelInput::new(window, cx);
model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx);
@@ -763,7 +703,7 @@ mod tests {
let cx = setup_test(cx).await;
cx.update(|window, cx| {
let mut model_input = ModelInput::new(0, window, cx);
let mut model_input = ModelInput::new(window, cx);
model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx);
@@ -827,7 +767,7 @@ mod tests {
models.iter().enumerate()
{
if i >= input.models.len() {
input.models.push(ModelInput::new(i, window, cx));
input.models.push(ModelInput::new(window, cx));
}
let model = &mut input.models[i];
set_text(&model.name, name, window, cx);

View File

@@ -4,7 +4,6 @@ use std::{
};
use anyhow::{Context as _, Result};
use collections::HashMap;
use context_server::{ContextServerCommand, ContextServerId};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
@@ -21,7 +20,6 @@ use project::{
project_settings::{ContextServerSettings, ProjectSettings},
worktree_store::WorktreeStore,
};
use serde::Deserialize;
use settings::{Settings as _, update_settings_file};
use theme::ThemeSettings;
use ui::{
@@ -39,11 +37,6 @@ enum ConfigurationTarget {
id: ContextServerId,
command: ContextServerCommand,
},
ExistingHttp {
id: ContextServerId,
url: String,
headers: HashMap<String, String>,
},
Extension {
id: ContextServerId,
repository_url: Option<SharedString>,
@@ -54,11 +47,9 @@ enum ConfigurationTarget {
enum ConfigurationSource {
New {
editor: Entity<Editor>,
is_http: bool,
},
Existing {
editor: Entity<Editor>,
is_http: bool,
},
Extension {
id: ContextServerId,
@@ -106,7 +97,6 @@ impl ConfigurationSource {
match target {
ConfigurationTarget::New => ConfigurationSource::New {
editor: create_editor(context_server_input(None), jsonc_language, window, cx),
is_http: false,
},
ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
editor: create_editor(
@@ -115,20 +105,6 @@ impl ConfigurationSource {
window,
cx,
),
is_http: false,
},
ConfigurationTarget::ExistingHttp {
id,
url,
headers: auth,
} => ConfigurationSource::Existing {
editor: create_editor(
context_server_http_input(Some((id, url, auth))),
jsonc_language,
window,
cx,
),
is_http: true,
},
ConfigurationTarget::Extension {
id,
@@ -165,30 +141,16 @@ impl ConfigurationSource {
fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
match self {
ConfigurationSource::New { editor, is_http }
| ConfigurationSource::Existing { editor, is_http } => {
if *is_http {
parse_http_input(&editor.read(cx).text(cx)).map(|(id, url, auth)| {
(
id,
ContextServerSettings::Http {
enabled: true,
url,
headers: auth,
},
)
})
} else {
parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
(
id,
ContextServerSettings::Custom {
enabled: true,
command,
},
)
})
}
ConfigurationSource::New { editor } | ConfigurationSource::Existing { editor } => {
parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
(
id,
ContextServerSettings::Custom {
enabled: true,
command,
},
)
})
}
ConfigurationSource::Extension {
id,
@@ -250,66 +212,6 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
)
}
fn context_server_http_input(
existing: Option<(ContextServerId, String, HashMap<String, String>)>,
) -> String {
let (name, url, headers) = match existing {
Some((id, url, headers)) => {
let header = if headers.is_empty() {
r#"// "Authorization": "Bearer <token>"#.to_string()
} else {
let json = serde_json::to_string_pretty(&headers).unwrap();
let mut lines = json.split("\n").collect::<Vec<_>>();
if lines.len() > 1 {
lines.remove(0);
lines.pop();
}
lines
.into_iter()
.map(|line| format!(" {}", line))
.collect::<String>()
};
(id.0.to_string(), url, header)
}
None => (
"some-remote-server".to_string(),
"https://example.com/mcp".to_string(),
r#"// "Authorization": "Bearer <token>"#.to_string(),
),
};
format!(
r#"{{
/// The name of your remote MCP server
"{name}": {{
/// The URL of the remote MCP server
"url": "{url}",
"headers": {{
/// Any headers to send along
{headers}
}}
}}
}}"#
)
}
fn parse_http_input(text: &str) -> Result<(ContextServerId, String, HashMap<String, String>)> {
#[derive(Deserialize)]
struct Temp {
url: String,
#[serde(default)]
headers: HashMap<String, String>,
}
let value: HashMap<String, Temp> = serde_json_lenient::from_str(text)?;
if value.len() != 1 {
anyhow::bail!("Expected exactly one context server configuration");
}
let (key, value) = value.into_iter().next().unwrap();
Ok((ContextServerId(key.into()), value.url, value.headers))
}
fn resolve_context_server_extension(
id: ContextServerId,
worktree_store: Entity<WorktreeStore>,
@@ -410,15 +312,6 @@ impl ConfigureContextServerModal {
id: server_id,
command,
}),
ContextServerSettings::Http {
enabled: _,
url,
headers,
} => Some(ConfigurationTarget::ExistingHttp {
id: server_id,
url,
headers,
}),
ContextServerSettings::Extension { .. } => {
match workspace
.update(cx, |workspace, cx| {
@@ -460,7 +353,6 @@ impl ConfigureContextServerModal {
state: State::Idle,
original_server_id: match &target {
ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
ConfigurationTarget::ExistingHttp { id, .. } => Some(id.clone()),
ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
ConfigurationTarget::New => None,
},
@@ -589,7 +481,7 @@ impl ModalView for ConfigureContextServerModal {}
impl Focusable for ConfigureContextServerModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.source {
ConfigurationSource::New { editor, .. } => editor.focus_handle(cx),
ConfigurationSource::New { editor } => editor.focus_handle(cx),
ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx),
ConfigurationSource::Extension { editor, .. } => editor
.as_ref()
@@ -635,10 +527,9 @@ impl ConfigureContextServerModal {
}
fn render_modal_content(&self, cx: &App) -> AnyElement {
// All variants now use single editor approach
let editor = match &self.source {
ConfigurationSource::New { editor, .. } => editor,
ConfigurationSource::Existing { editor, .. } => editor,
ConfigurationSource::New { editor } => editor,
ConfigurationSource::Existing { editor } => editor,
ConfigurationSource::Extension { editor, .. } => {
let Some(editor) = editor else {
return div().into_any_element();
@@ -710,36 +601,6 @@ impl ConfigureContextServerModal {
move |_, _, cx| cx.open_url(&repository_url)
}),
)
} else if let ConfigurationSource::New { is_http, .. } = &self.source {
let label = if *is_http {
"Run command"
} else {
"Connect via HTTP"
};
let tooltip = if *is_http {
"Configure an MCP serevr that runs on stdin/stdout."
} else {
"Configure an MCP server that you connect to over HTTP"
};
Some(
Button::new("toggle-kind", label)
.tooltip(Tooltip::text(tooltip))
.on_click(cx.listener(|this, _, window, cx| match &mut this.source {
ConfigurationSource::New { editor, is_http } => {
*is_http = !*is_http;
let new_text = if *is_http {
context_server_http_input(None)
} else {
context_server_input(None)
};
editor.update(cx, |editor, cx| {
editor.set_text(new_text, window, cx);
})
}
_ => {}
})),
)
} else {
None
},

View File

@@ -1892,9 +1892,6 @@ impl AgentPanel {
.anchor(Corner::TopRight)
.with_handle(self.new_thread_menu_handle.clone())
.menu({
let selected_agent = self.selected_agent.clone();
let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
let workspace = self.workspace.clone();
let is_via_collab = workspace
.update(cx, |workspace, cx| {
@@ -1932,9 +1929,7 @@ impl AgentPanel {
})
.item(
ContextMenuEntry::new("Zed Agent")
.when(is_agent_selected(AgentType::NativeAgent) | is_agent_selected(AgentType::TextThread) , |this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
})
.action(NewThread.boxed_clone())
.icon(IconName::ZedAgent)
.icon_color(Color::Muted)
.handler({
@@ -1960,9 +1955,9 @@ impl AgentPanel {
)
.item(
ContextMenuEntry::new("Text Thread")
.action(NewTextThread.boxed_clone())
.icon(IconName::TextThread)
.icon_color(Color::Muted)
.action(NewTextThread.boxed_clone())
.handler({
let workspace = workspace.clone();
move |window, cx| {
@@ -1988,9 +1983,6 @@ impl AgentPanel {
.header("External Agents")
.item(
ContextMenuEntry::new("Claude Code")
.when(is_agent_selected(AgentType::ClaudeCode), |this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
})
.icon(IconName::AiClaude)
.disabled(is_via_collab)
.icon_color(Color::Muted)
@@ -2017,9 +2009,6 @@ impl AgentPanel {
)
.item(
ContextMenuEntry::new("Codex CLI")
.when(is_agent_selected(AgentType::Codex), |this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
})
.icon(IconName::AiOpenAi)
.disabled(is_via_collab)
.icon_color(Color::Muted)
@@ -2046,9 +2035,6 @@ impl AgentPanel {
)
.item(
ContextMenuEntry::new("Gemini CLI")
.when(is_agent_selected(AgentType::Gemini), |this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
})
.icon(IconName::AiGemini)
.icon_color(Color::Muted)
.disabled(is_via_collab)
@@ -2074,8 +2060,8 @@ impl AgentPanel {
}),
)
.map(|mut menu| {
let agent_server_store = agent_server_store.read(cx);
let agent_names = agent_server_store
let agent_server_store_read = agent_server_store.read(cx);
let agent_names = agent_server_store_read
.external_agents()
.filter(|name| {
name.0 != GEMINI_NAME
@@ -2084,38 +2070,21 @@ impl AgentPanel {
})
.cloned()
.collect::<Vec<_>>();
let custom_settings = cx
.global::<SettingsStore>()
.get::<AllAgentServersSettings>(None)
.custom
.clone();
for agent_name in agent_names {
let icon_path = agent_server_store.agent_icon(&agent_name);
let mut entry = ContextMenuEntry::new(agent_name.clone());
let command = custom_settings
.get(&agent_name.0)
.map(|settings| settings.command.clone())
.unwrap_or(placeholder_command());
let icon_path = agent_server_store_read.agent_icon(&agent_name);
let mut entry =
ContextMenuEntry::new(format!("{}", agent_name));
if let Some(icon_path) = icon_path {
entry = entry.custom_icon_svg(icon_path);
} else {
entry = entry.icon(IconName::Terminal);
}
entry = entry
.when(
is_agent_selected(AgentType::Custom {
name: agent_name.0.clone(),
command: command.clone(),
}),
|this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
},
)
.icon_color(Color::Muted)
.disabled(is_via_collab)
.handler({
@@ -2155,7 +2124,6 @@ impl AgentPanel {
}
}
});
menu = menu.item(entry);
}
@@ -2188,7 +2156,7 @@ impl AgentPanel {
.id("selected_agent_icon")
.when_some(selected_agent_custom_icon, |this, icon_path| {
let label = selected_agent_label.clone();
this.px_1()
this.px(DynamicSpacing::Base02.rems(cx))
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
@@ -2197,7 +2165,7 @@ impl AgentPanel {
.when(!has_custom_icon, |this| {
this.when_some(self.selected_agent.icon(), |this, icon| {
let label = selected_agent_label.clone();
this.px_1()
this.px(DynamicSpacing::Base02.rems(cx))
.child(Icon::new(icon).color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)

View File

@@ -346,9 +346,7 @@ fn update_command_palette_filter(cx: &mut App) {
filter.show_namespace("supermaven");
filter.show_action_types(edit_prediction_actions.iter());
}
EditPredictionProvider::Zed
| EditPredictionProvider::Codestral
| EditPredictionProvider::Experimental(_) => {
EditPredictionProvider::Zed | EditPredictionProvider::Codestral => {
filter.show_namespace("edit_prediction");
filter.hide_namespace("copilot");
filter.hide_namespace("supermaven");

View File

@@ -492,15 +492,17 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.start_slot(
Icon::new(model_info.icon)
.color(model_icon_color)
.size(IconSize::Small),
)
.child(
h_flex()
.w_full()
.pl_0p5()
.gap_1p5()
.child(
Icon::new(model_info.icon)
.color(model_icon_color)
.size(IconSize::Small),
)
.w(px(240.))
.child(Label::new(model_info.model.name().0).truncate()),
)
.end_slot(div().pr_3().when(is_selected, |this| {

View File

@@ -1679,7 +1679,7 @@ impl TextThreadEditor {
) {
cx.stop_propagation();
let mut images = if let Some(item) = cx.read_from_clipboard() {
let images = if let Some(item) = cx.read_from_clipboard() {
item.into_entries()
.filter_map(|entry| {
if let ClipboardEntry::Image(image) = entry {
@@ -1693,40 +1693,6 @@ impl TextThreadEditor {
Vec::new()
};
if let Some(paths) = cx.read_from_clipboard() {
for path in paths
.into_entries()
.filter_map(|entry| {
if let ClipboardEntry::ExternalPaths(paths) = entry {
Some(paths.paths().to_owned())
} else {
None
}
})
.flatten()
{
let Ok(content) = std::fs::read(path) else {
continue;
};
let Ok(format) = image::guess_format(&content) else {
continue;
};
images.push(gpui::Image::from_bytes(
match format {
image::ImageFormat::Png => gpui::ImageFormat::Png,
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
_ => continue,
},
content,
));
}
}
let metadata = if let Some(item) = cx.read_from_clipboard() {
item.entries().first().and_then(|entry| {
if let ClipboardEntry::String(text) = entry {
@@ -2626,11 +2592,12 @@ impl SearchableItem for TextThreadEditor {
&mut self,
index: usize,
matches: &[Self::Match],
collapse: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.activate_match(index, matches, window, cx);
editor.activate_match(index, matches, collapse, window, cx);
});
}

View File

@@ -374,7 +374,7 @@ fn generate_askpass_script(
Ok(format!(
r#"
$ErrorActionPreference = 'Stop';
($args -join [char]0) | {askpass_program} --askpass={askpass_socket} 2> $null
($args -join [char]0) | & {askpass_program} --askpass={askpass_socket} 2> $null
"#,
))
}

View File

@@ -10,8 +10,8 @@ use paths::remote_servers_dir;
use release_channel::{AppCommitSha, ReleaseChannel};
use serde::{Deserialize, Serialize};
use settings::{RegisterSetting, Settings, SettingsStore};
use smol::fs::File;
use smol::{fs, io::AsyncReadExt};
use smol::{fs::File, process::Command};
use std::mem;
use std::{
env::{
@@ -23,7 +23,6 @@ use std::{
sync::Arc,
time::Duration,
};
use util::command::new_smol_command;
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
@@ -122,7 +121,7 @@ impl Drop for MacOsUnmounter<'_> {
let mount_path = mem::take(&mut self.mount_path);
self.background_executor
.spawn(async move {
let unmount_output = new_smol_command("hdiutil")
let unmount_output = Command::new("hdiutil")
.args(["detach", "-force"])
.arg(&mount_path)
.output()
@@ -800,7 +799,7 @@ async fn install_release_linux(
.await
.context("failed to create directory into which to extract update")?;
let output = new_smol_command("tar")
let output = Command::new("tar")
.arg("-xzf")
.arg(&downloaded_tar_gz)
.arg("-C")
@@ -835,7 +834,7 @@ async fn install_release_linux(
to = PathBuf::from(prefix);
}
let output = new_smol_command("rsync")
let output = Command::new("rsync")
.args(["-av", "--delete"])
.arg(&from)
.arg(&to)
@@ -867,7 +866,7 @@ async fn install_release_macos(
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
mounted_app_path.push("/");
let output = new_smol_command("hdiutil")
let output = Command::new("hdiutil")
.args(["attach", "-nobrowse"])
.arg(&downloaded_dmg)
.arg("-mountroot")
@@ -887,7 +886,7 @@ async fn install_release_macos(
background_executor: cx.background_executor(),
};
let output = new_smol_command("rsync")
let output = Command::new("rsync")
.args(["-av", "--delete"])
.arg(&mounted_app_path)
.arg(&running_app_path)
@@ -918,7 +917,7 @@ async fn cleanup_windows() -> Result<()> {
}
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
let output = new_smol_command(downloaded_installer)
let output = Command::new(downloaded_installer)
.arg("/verysilent")
.arg("/update=true")
.arg("!desktopicon")

View File

@@ -293,11 +293,10 @@ impl Telemetry {
}
pub fn metrics_enabled(self: &Arc<Self>) -> bool {
self.state.lock().settings.metrics
}
pub fn diagnostics_enabled(self: &Arc<Self>) -> bool {
self.state.lock().settings.diagnostics
let state = self.state.lock();
let enabled = state.settings.metrics;
drop(state);
enabled
}
pub fn set_authenticated_user_info(

View File

@@ -78,8 +78,6 @@ pub enum PromptFormat {
OnlySnippets,
/// One-sentence instructions used in fine-tuned models
Minimal,
/// One-sentence instructions + FIM-like template
MinimalQwen,
}
impl PromptFormat {
@@ -107,7 +105,6 @@ impl std::fmt::Display for PromptFormat {
PromptFormat::NumLinesUniDiff => write!(f, "Numbered Lines / Unified Diff"),
PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
PromptFormat::Minimal => write!(f, "Minimal"),
PromptFormat::MinimalQwen => write!(f, "Minimal + Qwen FIM"),
}
}
}

View File

@@ -19,5 +19,4 @@ ordered-float.workspace = true
rustc-hash.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
strum.workspace = true

View File

@@ -3,8 +3,7 @@ pub mod retrieval_prompt;
use anyhow::{Context as _, Result, anyhow};
use cloud_llm_client::predict_edits_v3::{
self, DiffPathFmt, Event, Excerpt, IncludedFile, Line, Point, PromptFormat,
ReferencedDeclaration,
self, DiffPathFmt, Excerpt, Line, Point, PromptFormat, ReferencedDeclaration,
};
use indoc::indoc;
use ordered_float::OrderedFloat;
@@ -167,21 +166,6 @@ const OLD_TEXT_NEW_TEXT_REMINDER: &str = indoc! {r#"
pub fn build_prompt(
request: &predict_edits_v3::PredictEditsRequest,
) -> Result<(String, SectionLabels)> {
let mut section_labels = Default::default();
match request.prompt_format {
PromptFormat::MinimalQwen => {
let prompt = MinimalQwenPrompt {
events: request.events.clone(),
cursor_point: request.cursor_point,
cursor_path: request.excerpt_path.clone(),
included_files: request.included_files.clone(),
};
return Ok((prompt.render(), section_labels));
}
_ => (),
};
let mut insertions = match request.prompt_format {
PromptFormat::MarkedExcerpt => vec![
(
@@ -207,7 +191,6 @@ pub fn build_prompt(
vec![(request.cursor_point, CURSOR_MARKER)]
}
PromptFormat::OnlySnippets => vec![],
PromptFormat::MinimalQwen => unreachable!(),
};
let mut prompt = match request.prompt_format {
@@ -217,7 +200,6 @@ pub fn build_prompt(
PromptFormat::OldTextNewText => XML_TAGS_INSTRUCTIONS.to_string(),
PromptFormat::OnlySnippets => String::new(),
PromptFormat::Minimal => STUDENT_MODEL_INSTRUCTIONS.to_string(),
PromptFormat::MinimalQwen => unreachable!(),
};
if request.events.is_empty() {
@@ -269,6 +251,8 @@ pub fn build_prompt(
prompt.push_str(excerpts_preamble);
prompt.push('\n');
let mut section_labels = Default::default();
if !request.referenced_declarations.is_empty() || !request.signatures.is_empty() {
let syntax_based_prompt = SyntaxBasedPrompt::populate(request)?;
section_labels = syntax_based_prompt.write(&mut insertions, &mut prompt)?;
@@ -785,7 +769,6 @@ impl<'a> SyntaxBasedPrompt<'a> {
writeln!(output, "<|section_{}|>", section_index).ok();
}
}
PromptFormat::MinimalQwen => unreachable!(),
}
let push_full_snippet = |output: &mut String| {
@@ -895,69 +878,3 @@ fn declaration_size(declaration: &ReferencedDeclaration, style: DeclarationStyle
DeclarationStyle::Declaration => declaration.text.len(),
}
}
struct MinimalQwenPrompt {
events: Vec<Event>,
cursor_point: Point,
cursor_path: Arc<Path>, // TODO: make a common struct with cursor_point
included_files: Vec<IncludedFile>,
}
impl MinimalQwenPrompt {
const INSTRUCTIONS: &str = "You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.\n";
fn render(&self) -> String {
let edit_history = self.fmt_edit_history();
let context = self.fmt_context();
format!(
"{instructions}\n\n{edit_history}\n\n{context}",
instructions = MinimalQwenPrompt::INSTRUCTIONS,
edit_history = edit_history,
context = context
)
}
fn fmt_edit_history(&self) -> String {
if self.events.is_empty() {
"(No edit history)\n\n".to_string()
} else {
let mut events_str = String::new();
push_events(&mut events_str, &self.events);
format!(
"The following are the latest edits made by the user, from earlier to later.\n\n{}",
events_str
)
}
}
fn fmt_context(&self) -> String {
let mut context = String::new();
let include_line_numbers = true;
for related_file in &self.included_files {
writeln!(context, "<|file_sep|>{}", DiffPathFmt(&related_file.path)).unwrap();
if related_file.path == self.cursor_path {
write!(context, "<|fim_prefix|>").unwrap();
write_excerpts(
&related_file.excerpts,
&[(self.cursor_point, "<|fim_suffix|>")],
related_file.max_row,
include_line_numbers,
&mut context,
);
writeln!(context, "<|fim_middle|>").unwrap();
} else {
write_excerpts(
&related_file.excerpts,
&[],
related_file.max_row,
include_line_numbers,
&mut context,
);
}
}
context
}
}

View File

@@ -40,47 +40,9 @@ pub fn build_prompt(request: predict_edits_v3::PlanContextRetrievalRequest) -> R
pub struct SearchToolInput {
/// An array of queries to run for gathering context relevant to the next prediction
#[schemars(length(max = 3))]
#[serde(deserialize_with = "deserialize_queries")]
pub queries: Box<[SearchToolQuery]>,
}
fn deserialize_queries<'de, D>(deserializer: D) -> Result<Box<[SearchToolQuery]>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
#[serde(untagged)]
enum QueryCollection {
Array(Box<[SearchToolQuery]>),
DoubleArray(Box<[Box<[SearchToolQuery]>]>),
Single(SearchToolQuery),
}
#[derive(Deserialize)]
#[serde(untagged)]
enum MaybeDoubleEncoded {
SingleEncoded(QueryCollection),
DoubleEncoded(String),
}
let result = MaybeDoubleEncoded::deserialize(deserializer)?;
let normalized = match result {
MaybeDoubleEncoded::SingleEncoded(value) => value,
MaybeDoubleEncoded::DoubleEncoded(value) => {
serde_json::from_str(&value).map_err(D::Error::custom)?
}
};
Ok(match normalized {
QueryCollection::Array(items) => items,
QueryCollection::Single(search_tool_query) => Box::new([search_tool_query]),
QueryCollection::DoubleArray(double_array) => double_array.into_iter().flatten().collect(),
})
}
/// Search for relevant code by path, syntax hierarchy, and content.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Hash)]
pub struct SearchToolQuery {
@@ -130,115 +92,3 @@ const TOOL_USE_REMINDER: &str = indoc! {"
--
Analyze the user's intent in one to two sentences, then call the `search` tool.
"};
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_deserialize_queries() {
let single_query_json = indoc! {r#"{
"queries": {
"glob": "**/*.rs",
"syntax_node": ["fn test"],
"content": "assert"
}
}"#};
let flat_input: SearchToolInput = serde_json::from_str(single_query_json).unwrap();
assert_eq!(flat_input.queries.len(), 1);
assert_eq!(flat_input.queries[0].glob, "**/*.rs");
assert_eq!(flat_input.queries[0].syntax_node, vec!["fn test"]);
assert_eq!(flat_input.queries[0].content, Some("assert".to_string()));
let flat_json = indoc! {r#"{
"queries": [
{
"glob": "**/*.rs",
"syntax_node": ["fn test"],
"content": "assert"
},
{
"glob": "**/*.ts",
"syntax_node": [],
"content": null
}
]
}"#};
let flat_input: SearchToolInput = serde_json::from_str(flat_json).unwrap();
assert_eq!(flat_input.queries.len(), 2);
assert_eq!(flat_input.queries[0].glob, "**/*.rs");
assert_eq!(flat_input.queries[0].syntax_node, vec!["fn test"]);
assert_eq!(flat_input.queries[0].content, Some("assert".to_string()));
assert_eq!(flat_input.queries[1].glob, "**/*.ts");
assert_eq!(flat_input.queries[1].syntax_node.len(), 0);
assert_eq!(flat_input.queries[1].content, None);
let nested_json = indoc! {r#"{
"queries": [
[
{
"glob": "**/*.rs",
"syntax_node": ["fn test"],
"content": "assert"
}
],
[
{
"glob": "**/*.ts",
"syntax_node": [],
"content": null
}
]
]
}"#};
let nested_input: SearchToolInput = serde_json::from_str(nested_json).unwrap();
assert_eq!(nested_input.queries.len(), 2);
assert_eq!(nested_input.queries[0].glob, "**/*.rs");
assert_eq!(nested_input.queries[0].syntax_node, vec!["fn test"]);
assert_eq!(nested_input.queries[0].content, Some("assert".to_string()));
assert_eq!(nested_input.queries[1].glob, "**/*.ts");
assert_eq!(nested_input.queries[1].syntax_node.len(), 0);
assert_eq!(nested_input.queries[1].content, None);
let double_encoded_queries = serde_json::to_string(&json!({
"queries": serde_json::to_string(&json!([
{
"glob": "**/*.rs",
"syntax_node": ["fn test"],
"content": "assert"
},
{
"glob": "**/*.ts",
"syntax_node": [],
"content": null
}
])).unwrap()
}))
.unwrap();
let double_encoded_input: SearchToolInput =
serde_json::from_str(&double_encoded_queries).unwrap();
assert_eq!(double_encoded_input.queries.len(), 2);
assert_eq!(double_encoded_input.queries[0].glob, "**/*.rs");
assert_eq!(double_encoded_input.queries[0].syntax_node, vec!["fn test"]);
assert_eq!(
double_encoded_input.queries[0].content,
Some("assert".to_string())
);
assert_eq!(double_encoded_input.queries[1].glob, "**/*.ts");
assert_eq!(double_encoded_input.queries[1].syntax_node.len(), 0);
assert_eq!(double_encoded_input.queries[1].content, None);
// ### ERROR Switching from var declarations to lexical declarations [RUN 073]
// invalid search json {"queries": ["express/lib/response.js", "var\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s*=.*;", "function.*\\(.*\\).*\\{.*\\}"]}
}
}

View File

@@ -1,4 +0,0 @@
alter table billing_customers
add column external_id text;
create unique index uix_billing_customers_on_external_id on billing_customers (external_id);

View File

@@ -453,7 +453,6 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::StashPop>)
.add_request_handler(forward_mutating_project_request::<proto::StashDrop>)
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
.add_request_handler(forward_mutating_project_request::<proto::RunGitHook>)
.add_request_handler(forward_mutating_project_request::<proto::GitInit>)
.add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)

View File

@@ -288,7 +288,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
"});
}
#[gpui::test]
#[gpui::test(iterations = 10)]
async fn test_collaborating_with_completion(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;
@@ -307,35 +307,17 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
..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_a.language_registry().register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "fake-analyzer",
capabilities: capabilities.clone(),
..FakeLspAdapter::default()
},
),
];
client_b.language_registry().add(rust_lang());
client_b.language_registry().register_fake_lsp_adapter(
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 {
name: "fake-analyzer",
capabilities,
..FakeLspAdapter::default()
},
@@ -370,8 +352,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)
});
let fake_language_server = fake_language_servers[0].next().await.unwrap();
let second_fake_language_server = fake_language_servers[1].next().await.unwrap();
let fake_language_server = fake_language_servers.next().await.unwrap();
cx_a.background_executor.run_until_parked();
buffer_b.read_with(cx_b, |buffer, _| {
@@ -433,11 +414,6 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.next()
.await
.unwrap();
second_fake_language_server
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) })
.next()
.await
.unwrap();
cx_a.executor().finish_waiting();
// Open the buffer on the host.
@@ -546,10 +522,6 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
])))
});
// Second language server also needs to handle the request (returns None)
let mut second_completion_response = second_fake_language_server
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
// The completion now gets a new `text_edit.new_text` when resolving the completion item
let mut resolve_completion_response = fake_language_server
.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {
@@ -573,7 +545,6 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
cx_b.executor().run_until_parked();
completion_response.next().await.unwrap();
second_completion_response.next().await.unwrap();
editor_b.update_in(cx_b, |editor, window, cx| {
assert!(editor.context_menu_visible());
@@ -592,77 +563,6 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
);
});
// Ensure buffer is synced before proceeding with the next test
cx_a.executor().run_until_parked();
cx_b.executor().run_until_parked();
// Test completions from the second fake language server
// Add another completion trigger to test the second language server
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([68..68])
});
editor.handle_input("; b", window, cx);
editor.handle_input(".", window, cx);
});
buffer_b.read_with(cx_b, |buffer, _| {
assert_eq!(
buffer.text(),
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b. }"
);
});
// Set up completion handlers for both language servers
let mut first_lsp_completion = fake_language_server
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
let mut second_lsp_completion = second_fake_language_server
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
lsp::Position::new(1, 54),
);
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
label: "analyzer_method(…)".into(),
detail: Some("fn(&self) -> Result<T>".into()),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
new_text: "analyzer_method()".to_string(),
range: lsp::Range::new(
lsp::Position::new(1, 54),
lsp::Position::new(1, 54),
),
})),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
..Default::default()
},
])))
});
cx_b.executor().run_until_parked();
// Await both language server responses
first_lsp_completion.next().await.unwrap();
second_lsp_completion.next().await.unwrap();
cx_b.executor().run_until_parked();
// Confirm the completion from the second language server works
editor_b.update_in(cx_b, |editor, window, cx| {
assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
assert_eq!(
editor.text(cx),
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b.analyzer_method() }"
);
});
}
#[gpui::test(iterations = 10)]
@@ -2269,28 +2169,16 @@ async fn test_inlay_hint_refresh_is_forwarded(
} else {
"initial hint"
};
Ok(Some(vec![
lsp::InlayHint {
position: lsp::Position::new(0, character),
label: lsp::InlayHintLabel::String(label.to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
lsp::InlayHint {
position: lsp::Position::new(1090, 1090),
label: lsp::InlayHintLabel::String("out-of-bounds hint".to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
]))
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, character),
label: lsp::InlayHintLabel::String(label.to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
}
})
.next()

View File

@@ -12,7 +12,7 @@ workspace = true
path = "src/context_server.rs"
[features]
test-support = ["gpui/test-support"]
test-support = []
[dependencies]
anyhow.workspace = true
@@ -20,7 +20,6 @@ async-trait.workspace = true
collections.workspace = true
futures.workspace = true
gpui.workspace = true
http_client = { workspace = true, features = ["test-support"] }
log.workspace = true
net.workspace = true
parking_lot.workspace = true
@@ -33,6 +32,3 @@ smol.workspace = true
tempfile.workspace = true
url = { workspace = true, features = ["serde"] }
util.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -6,8 +6,6 @@ pub mod test;
pub mod transport;
pub mod types;
use collections::HashMap;
use http_client::HttpClient;
use std::path::Path;
use std::sync::Arc;
use std::{fmt::Display, path::PathBuf};
@@ -17,9 +15,6 @@ use client::Client;
use gpui::AsyncApp;
use parking_lot::RwLock;
pub use settings::ContextServerCommand;
use url::Url;
use crate::transport::HttpTransport;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ContextServerId(pub Arc<str>);
@@ -57,25 +52,6 @@ impl ContextServer {
}
}
pub fn http(
id: ContextServerId,
endpoint: &Url,
headers: HashMap<String, String>,
http_client: Arc<dyn HttpClient>,
executor: gpui::BackgroundExecutor,
) -> Result<Self> {
let transport = match endpoint.scheme() {
"http" | "https" => {
log::info!("Using HTTP transport for {}", endpoint);
let transport =
HttpTransport::new(http_client, endpoint.to_string(), headers, executor);
Arc::new(transport) as _
}
_ => anyhow::bail!("unsupported MCP url scheme {}", endpoint.scheme()),
};
Ok(Self::new(id, transport))
}
pub fn new(id: ContextServerId, transport: Arc<dyn crate::transport::Transport>) -> Self {
Self {
id,

View File

@@ -1,12 +1,11 @@
pub mod http;
mod stdio_transport;
use std::pin::Pin;
use anyhow::Result;
use async_trait::async_trait;
use futures::Stream;
use std::pin::Pin;
pub use http::*;
pub use stdio_transport::*;
#[async_trait]

View File

@@ -1,259 +0,0 @@
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use collections::HashMap;
use futures::{Stream, StreamExt};
use gpui::BackgroundExecutor;
use http_client::{AsyncBody, HttpClient, Request, Response, http::Method};
use parking_lot::Mutex as SyncMutex;
use smol::channel;
use std::{pin::Pin, sync::Arc};
use crate::transport::Transport;
// Constants from MCP spec
const HEADER_SESSION_ID: &str = "Mcp-Session-Id";
const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream";
const JSON_MIME_TYPE: &str = "application/json";
/// HTTP Transport with session management and SSE support
pub struct HttpTransport {
http_client: Arc<dyn HttpClient>,
endpoint: String,
session_id: Arc<SyncMutex<Option<String>>>,
executor: BackgroundExecutor,
response_tx: channel::Sender<String>,
response_rx: channel::Receiver<String>,
error_tx: channel::Sender<String>,
error_rx: channel::Receiver<String>,
// Authentication headers to include in requests
headers: HashMap<String, String>,
}
impl HttpTransport {
pub fn new(
http_client: Arc<dyn HttpClient>,
endpoint: String,
headers: HashMap<String, String>,
executor: BackgroundExecutor,
) -> Self {
let (response_tx, response_rx) = channel::unbounded();
let (error_tx, error_rx) = channel::unbounded();
Self {
http_client,
executor,
endpoint,
session_id: Arc::new(SyncMutex::new(None)),
response_tx,
response_rx,
error_tx,
error_rx,
headers,
}
}
/// Send a message and handle the response based on content type
async fn send_message(&self, message: String) -> Result<()> {
let is_notification =
!message.contains("\"id\":") || message.contains("notifications/initialized");
let mut request_builder = Request::builder()
.method(Method::POST)
.uri(&self.endpoint)
.header("Content-Type", JSON_MIME_TYPE)
.header(
"Accept",
format!("{}, {}", JSON_MIME_TYPE, EVENT_STREAM_MIME_TYPE),
);
for (key, value) in &self.headers {
request_builder = request_builder.header(key.as_str(), value.as_str());
}
// Add session ID if we have one (except for initialize)
if let Some(ref session_id) = *self.session_id.lock() {
request_builder = request_builder.header(HEADER_SESSION_ID, session_id.as_str());
}
let request = request_builder.body(AsyncBody::from(message.into_bytes()))?;
let mut response = self.http_client.send(request).await?;
// Handle different response types based on status and content-type
match response.status() {
status if status.is_success() => {
// Check content type
let content_type = response
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok());
// Extract session ID from response headers if present
if let Some(session_id) = response
.headers()
.get(HEADER_SESSION_ID)
.and_then(|v| v.to_str().ok())
{
*self.session_id.lock() = Some(session_id.to_string());
log::debug!("Session ID set: {}", session_id);
}
match content_type {
Some(ct) if ct.starts_with(JSON_MIME_TYPE) => {
// JSON response - read and forward immediately
let mut body = String::new();
futures::AsyncReadExt::read_to_string(response.body_mut(), &mut body)
.await?;
// Only send non-empty responses
if !body.is_empty() {
self.response_tx
.send(body)
.await
.map_err(|_| anyhow!("Failed to send JSON response"))?;
}
}
Some(ct) if ct.starts_with(EVENT_STREAM_MIME_TYPE) => {
// SSE stream - set up streaming
self.setup_sse_stream(response).await?;
}
_ => {
// For notifications, 202 Accepted with no content type is ok
if is_notification && status.as_u16() == 202 {
log::debug!("Notification accepted");
} else {
return Err(anyhow!("Unexpected content type: {:?}", content_type));
}
}
}
}
status if status.as_u16() == 202 => {
// Accepted - notification acknowledged, no response needed
log::debug!("Notification accepted");
}
_ => {
let mut error_body = String::new();
futures::AsyncReadExt::read_to_string(response.body_mut(), &mut error_body).await?;
self.error_tx
.send(format!("HTTP {}: {}", response.status(), error_body))
.await
.map_err(|_| anyhow!("Failed to send error"))?;
}
}
Ok(())
}
/// Set up SSE streaming from the response
async fn setup_sse_stream(&self, mut response: Response<AsyncBody>) -> Result<()> {
let response_tx = self.response_tx.clone();
let error_tx = self.error_tx.clone();
// Spawn a task to handle the SSE stream
smol::spawn(async move {
let reader = futures::io::BufReader::new(response.body_mut());
let mut lines = futures::AsyncBufReadExt::lines(reader);
let mut data_buffer = Vec::new();
let mut in_message = false;
while let Some(line_result) = lines.next().await {
match line_result {
Ok(line) => {
if line.is_empty() {
// Empty line signals end of event
if !data_buffer.is_empty() {
let message = data_buffer.join("\n");
// Filter out ping messages and empty data
if !message.trim().is_empty() && message != "ping" {
if let Err(e) = response_tx.send(message).await {
log::error!("Failed to send SSE message: {}", e);
break;
}
}
data_buffer.clear();
}
in_message = false;
} else if let Some(data) = line.strip_prefix("data: ") {
// Handle data lines
let data = data.trim();
if !data.is_empty() {
// Check if this is a ping message
if data == "ping" {
log::trace!("Received SSE ping");
continue;
}
data_buffer.push(data.to_string());
in_message = true;
}
} else if line.starts_with("event:")
|| line.starts_with("id:")
|| line.starts_with("retry:")
{
// Ignore other SSE fields
continue;
} else if in_message {
// Continuation of data
data_buffer.push(line);
}
}
Err(e) => {
let _ = error_tx.send(format!("SSE stream error: {}", e)).await;
break;
}
}
}
})
.detach();
Ok(())
}
}
#[async_trait]
impl Transport for HttpTransport {
async fn send(&self, message: String) -> Result<()> {
self.send_message(message).await
}
fn receive(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
Box::pin(self.response_rx.clone())
}
fn receive_err(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
Box::pin(self.error_rx.clone())
}
}
impl Drop for HttpTransport {
fn drop(&mut self) {
// Try to cleanup session on drop
let http_client = self.http_client.clone();
let endpoint = self.endpoint.clone();
let session_id = self.session_id.lock().clone();
let headers = self.headers.clone();
if let Some(session_id) = session_id {
self.executor
.spawn(async move {
let mut request_builder = Request::builder()
.method(Method::DELETE)
.uri(&endpoint)
.header(HEADER_SESSION_ID, &session_id);
// Add authentication headers if present
for (key, value) in headers {
request_builder = request_builder.header(key.as_str(), value.as_str());
}
let request = request_builder.body(AsyncBody::empty());
if let Ok(request) = request {
let _ = http_client.send(request).await;
}
})
.detach();
}
}
}

View File

@@ -51,13 +51,11 @@ pub async fn init(crash_init: InitCrashHandler) {
unsafe { env::set_var("RUST_BACKTRACE", "1") };
old_hook(info);
// prevent the macOS crash dialog from popping up
if cfg!(target_os = "macos") {
std::process::exit(1);
}
std::process::exit(1);
}));
return;
}
_ => {
(Some(true), _) | (None, _) => {
panic::set_hook(Box::new(panic_hook));
}
}
@@ -291,29 +289,26 @@ impl minidumper::ServerHandler for CrashServer {
pub fn panic_hook(info: &PanicHookInfo) {
// Don't handle a panic on threads that are not relevant to the main execution.
if extension_host::wasm_host::IS_WASM_THREAD.with(|v| v.load(Ordering::Acquire)) {
log::error!("wasm thread panicked!");
return;
}
let message = info.payload_as_str().unwrap_or("Box<Any>").to_owned();
let message = info
.payload()
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| info.payload().downcast_ref::<String>().cloned())
.unwrap_or_else(|| "Box<Any>".to_string());
let span = info
.location()
.map(|loc| format!("{}:{}", loc.file(), loc.line()))
.unwrap_or_default();
let current_thread = std::thread::current();
let thread_name = current_thread.name().unwrap_or("<unnamed>");
// wait 500ms for the crash handler process to start up
// if it's still not there just write panic info and no minidump
let retry_frequency = Duration::from_millis(100);
for _ in 0..5 {
if let Some(client) = CRASH_HANDLER.get() {
let location = info
.location()
.map_or_else(|| "<unknown>".to_owned(), |location| location.to_string());
log::error!("thread '{thread_name}' panicked at {location}:\n{message}...");
client
.send_message(
2,

View File

@@ -324,7 +324,6 @@ pub async fn download_adapter_from_github(
extract_zip(&version_path, file)
.await
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
.inspect_err(|e| log::warn!("ZIP extraction error: {}. Ignoring...", e))
.ok();
util::fs::remove_matching(&adapter_path, |entry| {

View File

@@ -1029,11 +1029,13 @@ impl SearchableItem for DapLogView {
&mut self,
index: usize,
matches: &[Self::Match],
collapse: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor
.update(cx, |e, cx| e.activate_match(index, matches, window, cx))
self.editor.update(cx, |e, cx| {
e.activate_match(index, matches, collapse, window, cx)
})
}
fn select_matches(

View File

@@ -491,7 +491,7 @@ impl ProjectDiagnosticsEditor {
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let was_empty = self.multibuffer.read(cx).is_empty();
let buffer_snapshot = buffer.read(cx).snapshot();
let mut buffer_snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer_snapshot.remote_id();
let max_severity = if self.include_warnings {
@@ -602,6 +602,7 @@ impl ProjectDiagnosticsEditor {
cx,
)
.await;
buffer_snapshot = cx.update(|_, cx| buffer.read(cx).snapshot())?;
let initial_range = buffer_snapshot.anchor_after(b.initial_range.start)
..buffer_snapshot.anchor_before(b.initial_range.end);
let excerpt_range = ExcerptRange {
@@ -1009,14 +1010,11 @@ async fn heuristic_syntactic_expand(
snapshot: BufferSnapshot,
cx: &mut AsyncApp,
) -> Option<RangeInclusive<BufferRow>> {
let start = snapshot.clip_point(input_range.start, Bias::Right);
let end = snapshot.clip_point(input_range.end, Bias::Left);
let input_row_count = input_range.end.row - input_range.start.row;
if input_row_count > max_row_count {
return None;
}
let input_range = start..end;
// If the outline node contains the diagnostic and is small enough, just use that.
let outline_range = snapshot.outline_range_containing(input_range.clone());
if let Some(outline_range) = outline_range.clone() {

View File

@@ -18,19 +18,18 @@ client.workspace = true
cloud_llm_client.workspace = true
codestral.workspace = true
copilot.workspace = true
edit_prediction.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
gpui.workspace = true
indoc.workspace = true
edit_prediction.workspace = true
language.workspace = true
paths.workspace = true
project.workspace = true
regex.workspace = true
settings.workspace = true
supermaven.workspace = true
sweep_ai.workspace = true
telemetry.workspace = true
ui.workspace = true
workspace.workspace = true

View File

@@ -18,15 +18,12 @@ use language::{
};
use project::DisableAiSettings;
use regex::Regex;
use settings::{
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME, Settings, SettingsStore, update_settings_file,
};
use settings::{Settings, SettingsStore, update_settings_file};
use std::{
sync::{Arc, LazyLock},
time::Duration,
};
use supermaven::{AccountStatus, Supermaven};
use sweep_ai::SweepFeatureFlag;
use ui::{
Clickable, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton,
IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
@@ -81,7 +78,7 @@ impl Render for EditPredictionButton {
let all_language_settings = all_language_settings(None, cx);
match &all_language_settings.edit_predictions.provider {
match all_language_settings.edit_predictions.provider {
EditPredictionProvider::None => div().hidden(),
EditPredictionProvider::Copilot => {
@@ -300,15 +297,6 @@ impl Render for EditPredictionButton {
.with_handle(self.popover_menu_handle.clone()),
)
}
EditPredictionProvider::Experimental(provider_name) => {
if *provider_name == EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME
&& cx.has_flag::<SweepFeatureFlag>()
{
div().child(Icon::new(IconName::SweepAi))
} else {
div()
}
}
EditPredictionProvider::Zed => {
let enabled = self.editor_enabled.unwrap_or(true);
@@ -537,7 +525,7 @@ impl EditPredictionButton {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => continue,
EditPredictionProvider::None => continue,
};
}
}

View File

@@ -8,7 +8,6 @@ use gpui::{
use itertools::Itertools;
use language::CodeLabel;
use language::{Buffer, LanguageName, LanguageRegistry};
use lsp::CompletionItemTag;
use markdown::{Markdown, MarkdownElement};
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
@@ -841,16 +840,7 @@ impl CompletionsMenu {
if completion
.source
.lsp_completion(false)
.and_then(|lsp_completion| {
match (lsp_completion.deprecated, &lsp_completion.tags)
{
(Some(true), _) => Some(true),
(_, Some(tags)) => Some(
tags.contains(&CompletionItemTag::DEPRECATED),
),
_ => None,
}
})
.and_then(|lsp_completion| lsp_completion.deprecated)
.unwrap_or(false)
{
highlight.strikethrough = Some(StrikethroughStyle {

View File

@@ -1099,7 +1099,6 @@ pub struct Editor {
searchable: bool,
cursor_shape: CursorShape,
current_line_highlight: Option<CurrentLineHighlight>,
collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
workspace: Option<(WeakEntity<Workspace>, Option<WorkspaceId>)>,
input_enabled: bool,
@@ -2212,7 +2211,7 @@ impl Editor {
.unwrap_or_default(),
current_line_highlight: None,
autoindent_mode: Some(AutoindentMode::EachLine),
collapse_matches: false,
workspace: None,
input_enabled: !is_minimap,
use_modal_editing: full_mode,
@@ -2385,10 +2384,7 @@ impl Editor {
}
}
EditorEvent::Edited { .. } => {
let vim_mode = vim_mode_setting::VimModeSetting::try_get(cx)
.map(|vim_mode| vim_mode.0)
.unwrap_or(false);
if !vim_mode {
if vim_flavor(cx).is_none() {
let display_map = editor.display_snapshot(cx);
let selections = editor.selections.all_adjusted_display(&display_map);
let pop_state = editor
@@ -3017,21 +3013,17 @@ impl Editor {
self.current_line_highlight = current_line_highlight;
}
pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
self.collapse_matches = collapse_matches;
}
pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
if self.collapse_matches {
pub fn range_for_match<T: std::marker::Copy>(
&self,
range: &Range<T>,
collapse: bool,
) -> Range<T> {
if collapse {
return range.start..range.start;
}
range.clone()
}
pub fn clip_at_line_ends(&mut self, cx: &mut Context<Self>) -> bool {
self.display_map.read(cx).clip_at_line_ends
}
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut Context<Self>) {
if self.display_map.read(cx).clip_at_line_ends != clip {
self.display_map
@@ -16929,7 +16921,7 @@ impl Editor {
editor.update_in(cx, |editor, window, cx| {
let range = target_range.to_point(target_buffer.read(cx));
let range = editor.range_for_match(&range);
let range = editor.range_for_match(&range, false);
let range = collapse_multiline_range(range);
if !split
@@ -21769,9 +21761,7 @@ impl Editor {
.and_then(|e| e.to_str())
.map(|a| a.to_string()));
let vim_mode = vim_mode_setting::VimModeSetting::try_get(cx)
.map(|vim_mode| vim_mode.0)
.unwrap_or(false);
let vim_mode = vim_flavor(cx).is_some();
let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider;
let copilot_enabled = edit_predictions_provider
@@ -22406,6 +22396,28 @@ fn edit_for_markdown_paste<'a>(
(range, new_text)
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum VimFlavor {
Vim,
Helix,
}
pub fn vim_flavor(cx: &App) -> Option<VimFlavor> {
if vim_mode_setting::HelixModeSetting::try_get(cx)
.map(|helix_mode| helix_mode.0)
.unwrap_or(false)
{
Some(VimFlavor::Helix)
} else if vim_mode_setting::VimModeSetting::try_get(cx)
.map(|vim_mode| vim_mode.0)
.unwrap_or(false)
{
Some(VimFlavor::Vim)
} else {
None // neither vim nor helix mode
}
}
fn process_completion_for_edit(
completion: &Completion,
intent: CompletionIntent,

View File

@@ -1,5 +1,4 @@
use std::{
collections::hash_map,
ops::{ControlFlow, Range},
time::Duration,
};
@@ -779,7 +778,6 @@ impl Editor {
}
let excerpts = self.buffer.read(cx).excerpt_ids();
let mut inserted_hint_text = HashMap::default();
let hints_to_insert = new_hints
.into_iter()
.filter_map(|(chunk_range, hints_result)| {
@@ -806,35 +804,8 @@ impl Editor {
}
}
})
.flat_map(|new_hints| {
let mut hints_deduplicated = Vec::new();
if new_hints.len() > 1 {
for (server_id, new_hints) in new_hints {
for (new_id, new_hint) in new_hints {
let hints_text_for_position = inserted_hint_text
.entry(new_hint.position)
.or_insert_with(HashMap::default);
let insert =
match hints_text_for_position.entry(new_hint.text().to_string()) {
hash_map::Entry::Occupied(o) => o.get() == &server_id,
hash_map::Entry::Vacant(v) => {
v.insert(server_id);
true
}
};
if insert {
hints_deduplicated.push((new_id, new_hint));
}
}
}
} else {
hints_deduplicated.extend(new_hints.into_values().flatten());
}
hints_deduplicated
})
.flat_map(|hints| hints.into_values())
.flatten()
.filter_map(|(hint_id, lsp_hint)| {
if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
&& inlay_hints
@@ -3761,7 +3732,6 @@ let c = 3;"#
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "rust-analyzer",
capabilities: lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
@@ -3834,78 +3804,6 @@ let c = 3;"#
},
);
// Add another server that does send the same, duplicate hints back
let mut fake_servers_2 = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "CrabLang-ls",
capabilities: lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
initializer: Some(Box::new(move |fake_server| {
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
move |params, _| async move {
if params.text_document.uri
== lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
{
Ok(Some(vec![
lsp::InlayHint {
position: lsp::Position::new(1, 9),
label: lsp::InlayHintLabel::String(": i32".to_owned()),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
lsp::InlayHint {
position: lsp::Position::new(19, 9),
label: lsp::InlayHintLabel::String(": i33".to_owned()),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
]))
} else if params.text_document.uri
== lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
{
Ok(Some(vec![
lsp::InlayHint {
position: lsp::Position::new(1, 10),
label: lsp::InlayHintLabel::String(": i34".to_owned()),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
lsp::InlayHint {
position: lsp::Position::new(29, 10),
label: lsp::InlayHintLabel::String(": i35".to_owned()),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
]))
} else {
panic!("Unexpected file path {:?}", params.text_document.uri);
}
},
);
})),
..FakeLspAdapter::default()
},
);
let (buffer_1, _handle_1) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
@@ -3949,7 +3847,6 @@ let c = 3;"#
});
let fake_server = fake_servers.next().await.unwrap();
let _fake_server_2 = fake_servers_2.next().await.unwrap();
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
@@ -3958,16 +3855,11 @@ let c = 3;"#
assert_eq!(
vec![
": i32".to_string(),
": i32".to_string(),
": i33".to_string(),
": i33".to_string(),
": i34".to_string(),
": i34".to_string(),
": i35".to_string(),
": i35".to_string(),
],
sorted_cached_hint_labels(editor, cx),
"We receive duplicate hints from 2 servers and cache them all"
);
assert_eq!(
vec![
@@ -3977,7 +3869,7 @@ let c = 3;"#
": i33".to_string(),
],
visible_hint_labels(editor, cx),
"lib.rs is added before main.rs , so its excerpts should be visible first; hints should be deduplicated per label"
"lib.rs is added before main.rs , so its excerpts should be visible first"
);
})
.unwrap();
@@ -4027,12 +3919,8 @@ let c = 3;"#
assert_eq!(
vec![
": i32".to_string(),
": i32".to_string(),
": i33".to_string(),
": i33".to_string(),
": i34".to_string(),
": i34".to_string(),
": i35".to_string(),
": i35".to_string(),
],
sorted_cached_hint_labels(editor, cx),

View File

@@ -1586,11 +1586,12 @@ impl SearchableItem for Editor {
&mut self,
index: usize,
matches: &[Range<Anchor>],
collapse: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.unfold_ranges(&[matches[index].clone()], false, true, cx);
let range = self.range_for_match(&matches[index]);
let range = self.range_for_match(&matches[index], collapse);
let autoscroll = if EditorSettings::get_global(cx).search.center_on_match {
Autoscroll::center()
} else {

View File

@@ -25,7 +25,6 @@ language.workspace = true
log.workspace = true
lsp.workspace = true
parking_lot.workspace = true
proto.workspace = true
semantic_version.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -193,36 +193,6 @@ pub struct TargetConfig {
/// If not provided and the URL is a GitHub release, we'll attempt to fetch it from GitHub.
#[serde(default)]
pub sha256: Option<String>,
/// Environment variables to set when launching the agent server.
/// These target-specific env vars will override any env vars set at the agent level.
#[serde(default)]
pub env: HashMap<String, String>,
}
impl TargetConfig {
pub fn from_proto(proto: proto::ExternalExtensionAgentTarget) -> Self {
Self {
archive: proto.archive,
cmd: proto.cmd,
args: proto.args,
sha256: proto.sha256,
env: proto.env.into_iter().collect(),
}
}
pub fn to_proto(&self) -> proto::ExternalExtensionAgentTarget {
proto::ExternalExtensionAgentTarget {
archive: self.archive.clone(),
cmd: self.cmd.clone(),
args: self.args.clone(),
sha256: self.sha256.clone(),
env: self
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
@@ -295,26 +265,25 @@ impl ExtensionManifest {
.and_then(OsStr::to_str)
.context("invalid extension name")?;
let extension_manifest_path = extension_dir.join("extension.toml");
let mut extension_manifest_path = extension_dir.join("extension.json");
if fs.is_file(&extension_manifest_path).await {
let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| {
format!("loading {extension_name} extension.json, {extension_manifest_path:?}")
})?;
let manifest_json = serde_json::from_str::<OldExtensionManifest>(&manifest_content)
.with_context(|| {
format!("invalid extension.json for extension {extension_name}")
})?;
Ok(manifest_from_old_manifest(manifest_json, extension_name))
} else {
extension_manifest_path.set_extension("toml");
let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| {
format!("loading {extension_name} extension.toml, {extension_manifest_path:?}")
})?;
toml::from_str(&manifest_content).map_err(|err| {
anyhow!("Invalid extension.toml for extension {extension_name}:\n{err}")
})
} else if let extension_manifest_path = extension_manifest_path.with_extension("json")
&& fs.is_file(&extension_manifest_path).await
{
let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| {
format!("loading {extension_name} extension.json, {extension_manifest_path:?}")
})?;
serde_json::from_str::<OldExtensionManifest>(&manifest_content)
.with_context(|| format!("invalid extension.json for extension {extension_name}"))
.map(|manifest_json| manifest_from_old_manifest(manifest_json, extension_name))
} else {
anyhow::bail!("No extension manifest found for extension {extension_name}")
}
}
}

View File

@@ -11,7 +11,7 @@ use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use client::ExtensionProvides;
use client::{Client, ExtensionMetadata, GetExtensionsResponse, proto, telemetry::Telemetry};
use collections::{BTreeMap, BTreeSet, HashSet, btree_map};
use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map};
pub use extension::ExtensionManifest;
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
use extension::{
@@ -43,7 +43,7 @@ use language::{
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use release_channel::ReleaseChannel;
use remote::RemoteClient;
use remote::{RemoteClient, RemoteConnectionOptions};
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -123,7 +123,7 @@ pub struct ExtensionStore {
pub wasm_host: Arc<WasmHost>,
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
pub tasks: Vec<Task<()>>,
pub remote_clients: Vec<WeakEntity<RemoteClient>>,
pub remote_clients: HashMap<RemoteConnectionOptions, WeakEntity<RemoteClient>>,
pub ssh_registered_tx: UnboundedSender<()>,
}
@@ -274,7 +274,7 @@ impl ExtensionStore {
reload_tx,
tasks: Vec::new(),
remote_clients: Default::default(),
remote_clients: HashMap::default(),
ssh_registered_tx: connection_registered_tx,
};
@@ -343,12 +343,12 @@ impl ExtensionStore {
let index = this
.update(cx, |this, cx| this.rebuild_extension_index(cx))?
.await;
this.update(cx, |this, cx| this.extensions_updated(index, cx))?
this.update( cx, |this, cx| this.extensions_updated(index, cx))?
.await;
index_changed = false;
}
Self::update_remote_clients(&this, cx).await?;
Self::update_ssh_clients(&this, cx).await?;
}
_ = connection_registered_rx.next() => {
debounce_timer = cx
@@ -758,28 +758,29 @@ impl ExtensionStore {
if let Some(content_length) = content_length {
let actual_len = tar_gz_bytes.len();
if content_length != actual_len {
bail!(concat!(
"downloaded extension size {actual_len} ",
"does not match content length {content_length}"
));
bail!("downloaded extension size {actual_len} does not match content length {content_length}");
}
}
let decompressed_bytes = GzipDecoder::new(BufReader::new(tar_gz_bytes.as_slice()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(extension_dir).await?;
this.update(cx, |this, cx| this.reload(Some(extension_id.clone()), cx))?
.await;
this.update( cx, |this, cx| {
this.reload(Some(extension_id.clone()), cx)
})?
.await;
if let ExtensionOperation::Install = operation {
this.update(cx, |this, cx| {
this.update( cx, |this, cx| {
cx.emit(Event::ExtensionInstalled(extension_id.clone()));
if let Some(events) = ExtensionEvents::try_global(cx)
&& let Some(manifest) = this.extension_manifest_for_id(&extension_id)
{
events.update(cx, |this, cx| {
this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx)
});
}
&& let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
events.update(cx, |this, cx| {
this.emit(
extension::Event::ExtensionInstalled(manifest.clone()),
cx,
)
});
}
})
.ok();
}
@@ -1725,7 +1726,7 @@ impl ExtensionStore {
})
}
async fn sync_extensions_to_remotes(
async fn sync_extensions_over_ssh(
this: &WeakEntity<Self>,
client: WeakEntity<RemoteClient>,
cx: &mut AsyncApp,
@@ -1778,11 +1779,7 @@ impl ExtensionStore {
})?,
path_style,
);
log::info!(
"Uploading extension {} to {:?}",
missing_extension.clone().id,
dest_dir
);
log::info!("Uploading extension {}", missing_extension.clone().id);
client
.update(cx, |client, cx| {
@@ -1795,35 +1792,27 @@ impl ExtensionStore {
missing_extension.clone().id
);
let result = client
client
.update(cx, |client, _cx| {
client.proto_client().request(proto::InstallExtension {
tmp_dir: dest_dir.to_proto(),
extension: Some(missing_extension.clone()),
extension: Some(missing_extension),
})
})?
.await;
if let Err(e) = result {
log::error!(
"Failed to install extension {}: {}",
missing_extension.id,
e
);
}
.await?;
}
anyhow::Ok(())
}
pub async fn update_remote_clients(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
pub async fn update_ssh_clients(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
let clients = this.update(cx, |this, _cx| {
this.remote_clients.retain(|v| v.upgrade().is_some());
this.remote_clients.clone()
this.remote_clients.retain(|_k, v| v.upgrade().is_some());
this.remote_clients.values().cloned().collect::<Vec<_>>()
})?;
for client in clients {
Self::sync_extensions_to_remotes(this, client, cx)
Self::sync_extensions_over_ssh(this, client, cx)
.await
.log_err();
}
@@ -1831,12 +1820,16 @@ impl ExtensionStore {
anyhow::Ok(())
}
pub fn register_remote_client(
&mut self,
client: Entity<RemoteClient>,
_cx: &mut Context<Self>,
) {
self.remote_clients.push(client.downgrade());
pub fn register_remote_client(&mut self, client: Entity<RemoteClient>, cx: &mut Context<Self>) {
let options = client.read(cx).connection_options();
if let Some(existing_client) = self.remote_clients.get(&options)
&& existing_client.upgrade().is_some()
{
return;
}
self.remote_clients.insert(options, client.downgrade());
self.ssh_registered_tx.unbounded_send(()).ok();
}
}

View File

@@ -279,8 +279,7 @@ impl HeadlessExtensionStore {
}
fs.rename(&tmp_path, &path, RenameOptions::default())
.await
.context("Failed to rename {tmp_path:?} to {path:?}")?;
.await?;
Self::load_extension(this, extension, cx).await
})

View File

@@ -537,6 +537,7 @@ fn wasm_engine(executor: &BackgroundExecutor) -> wasmtime::Engine {
let engine_ref = engine.weak();
executor
.spawn(async move {
IS_WASM_THREAD.with(|v| v.store(true, Ordering::Release));
// Somewhat arbitrary interval, as it isn't a guaranteed interval.
// But this is a rough upper bound for how long the extension execution can block on
// `Future::poll`.
@@ -642,12 +643,6 @@ impl WasmHost {
let (tx, mut rx) = mpsc::unbounded::<ExtensionCall>();
let extension_task = async move {
// note: Setting the thread local here will slowly "poison" all tokio threads
// causing us to not record their panics any longer.
//
// This is fine though, the main zed binary only uses tokio for livekit and wasm extensions.
// Livekit seldom (if ever) panics 🤞 so the likelihood of us missing a panic in sentry is very low.
IS_WASM_THREAD.with(|v| v.store(true, Ordering::Release));
while let Some(call) = rx.next().await {
(call)(&mut extension, &mut store).await;
}
@@ -664,8 +659,8 @@ impl WasmHost {
cx.spawn(async move |cx| {
let (extension_task, manifest, work_dir, tx, zed_api_version) =
cx.background_executor().spawn(load_extension_task).await?;
// we need to run run the task in a tokio context as wasmtime_wasi may
// call into tokio, accessing its runtime handle when we trigger the `engine.increment_epoch()` above.
// we need to run run the task in an extension context as wasmtime_wasi may
// call into tokio, accessing its runtime handle
let task = Arc::new(gpui_tokio::Tokio::spawn(cx, extension_task)?);
Ok(WasmExtension {

View File

@@ -990,9 +990,6 @@ impl ExtensionImports for WasmState {
command: None,
settings: Some(settings),
})?),
project::project_settings::ContextServerSettings::Http { .. } => {
bail!("remote context server settings not supported in 0.6.0")
}
}
}
_ => {

View File

@@ -3,7 +3,7 @@ use anyhow::{Context as _, Result, bail};
use collections::{HashMap, HashSet};
use futures::future::{self, BoxFuture, join_all};
use git::{
Oid, RunHook,
Oid,
blame::Blame,
repository::{
AskPassDelegate, Branch, CommitDetails, CommitOptions, FetchOptions, GitRepository,
@@ -532,14 +532,6 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
fn run_hook(
&self,
_hook: RunHook,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
fn push(
&self,
_branch: String,

View File

@@ -395,19 +395,19 @@ mod tests {
thread::spawn(move || stream.run(move |events| tx.send(events.to_vec()).is_ok()));
fs::write(path.join("new-file"), "").unwrap();
let events = rx.recv_timeout(timeout()).unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("new-file"));
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
fs::remove_file(path.join("existing-file-5")).unwrap();
let mut events = rx.recv_timeout(timeout()).unwrap();
let mut events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let mut event = events.last().unwrap();
// we see this duplicate about 1/100 test runs.
if event.path == path.join("new-file")
&& event.flags.contains(StreamFlags::ITEM_CREATED)
{
events = rx.recv_timeout(timeout()).unwrap();
events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
event = events.last().unwrap();
}
assert_eq!(event.path, path.join("existing-file-5"));
@@ -440,13 +440,13 @@ mod tests {
});
fs::write(path.join("new-file"), "").unwrap();
let events = rx.recv_timeout(timeout()).unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("new-file"));
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
fs::remove_file(path.join("existing-file-5")).unwrap();
let events = rx.recv_timeout(timeout()).unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("existing-file-5"));
assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
@@ -477,11 +477,11 @@ mod tests {
});
fs::write(path.join("new-file"), "").unwrap();
assert_eq!(rx.recv_timeout(timeout()).unwrap(), "running");
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "running");
// Dropping the handle causes `EventStream::run` to return.
drop(handle);
assert_eq!(rx.recv_timeout(timeout()).unwrap(), "stopped");
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "stopped");
}
#[test]
@@ -500,14 +500,11 @@ mod tests {
}
fn flush_historical_events() {
thread::sleep(timeout());
}
fn timeout() -> Duration {
if std::env::var("CI").is_ok() {
Duration::from_secs(4)
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(2)
} else {
Duration::from_millis(500)
}
};
thread::sleep(duration);
}
}

View File

@@ -225,28 +225,3 @@ impl From<Oid> for usize {
u64::from_ne_bytes(u64_bytes) as usize
}
}
#[repr(i32)]
#[derive(Copy, Clone, Debug)]
pub enum RunHook {
PreCommit,
}
impl RunHook {
pub fn as_str(&self) -> &str {
match self {
Self::PreCommit => "pre-commit",
}
}
pub fn to_proto(&self) -> i32 {
*self as i32
}
pub fn from_proto(value: i32) -> Option<Self> {
match value {
0 => Some(Self::PreCommit),
_ => None,
}
}
}

View File

@@ -1,7 +1,7 @@
use crate::commit::parse_git_diff_name_status;
use crate::stash::GitStash;
use crate::status::{DiffTreeType, GitStatus, StatusCode, TreeDiff};
use crate::{Oid, RunHook, SHORT_SHA_LENGTH};
use crate::{Oid, SHORT_SHA_LENGTH};
use anyhow::{Context as _, Result, anyhow, bail};
use collections::HashMap;
use futures::future::BoxFuture;
@@ -485,12 +485,6 @@ pub trait GitRepository: Send + Sync {
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>>;
fn run_hook(
&self,
hook: RunHook,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>>;
fn commit(
&self,
message: SharedString,
@@ -1649,7 +1643,6 @@ impl GitRepository for RealGitRepository {
.args(["commit", "--quiet", "-m"])
.arg(&message.to_string())
.arg("--cleanup=strip")
.arg("--no-verify")
.stdout(smol::process::Stdio::piped())
.stderr(smol::process::Stdio::piped());
@@ -2044,26 +2037,6 @@ impl GitRepository for RealGitRepository {
})
.boxed()
}
fn run_hook(
&self,
hook: RunHook,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.any_git_binary_path.clone();
let executor = self.executor.clone();
self.executor
.spawn(async move {
let working_directory = working_directory?;
let git = GitBinary::new(git_binary_path, working_directory, executor)
.envs(HashMap::clone(&env));
git.run(&["hook", "run", "--ignore-missing", hook.as_str()])
.await?;
Ok(())
})
.boxed()
}
}
fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {

View File

@@ -373,7 +373,6 @@ impl GitPanel {
let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
if is_sort_by_path != was_sort_by_path {
this.entries.clear();
this.bulk_staging.take();
this.update_visible_entries(window, cx);
}
was_sort_by_path = is_sort_by_path
@@ -2714,8 +2713,14 @@ impl GitPanel {
self.single_staged_entry = single_staged_entry;
}
}
} else if repo.pending_ops_summary().item_summary.staging_count == 1 {
self.single_staged_entry = repo.pending_ops().find_map(|ops| {
} else if repo
.pending_ops_by_path
.summary()
.item_summary
.staging_count
== 1
{
self.single_staged_entry = repo.pending_ops_by_path.iter().find_map(|ops| {
if ops.staging() {
repo.status_for_path(&ops.repo_path)
.map(|status| GitStatusEntry {

View File

@@ -229,10 +229,6 @@ pub struct GenerativeContentBlob {
#[serde(rename_all = "camelCase")]
pub struct FunctionCallPart {
pub function_call: FunctionCall,
/// Thought signature returned by the model for function calls.
/// Only present on the first function call in parallel call scenarios.
#[serde(skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -521,8 +517,6 @@ pub enum Model {
alias = "gemini-2.5-pro-preview-06-05"
)]
Gemini25Pro,
#[serde(rename = "gemini-3-pro-preview")]
Gemini3ProPreview,
#[serde(rename = "custom")]
Custom {
name: String,
@@ -549,7 +543,6 @@ impl Model {
Self::Gemini25FlashLitePreview => "gemini-2.5-flash-lite-preview",
Self::Gemini25Flash => "gemini-2.5-flash",
Self::Gemini25Pro => "gemini-2.5-pro",
Self::Gemini3ProPreview => "gemini-3-pro-preview",
Self::Custom { name, .. } => name,
}
}
@@ -563,7 +556,6 @@ impl Model {
Self::Gemini25FlashLitePreview => "gemini-2.5-flash-lite-preview-06-17",
Self::Gemini25Flash => "gemini-2.5-flash",
Self::Gemini25Pro => "gemini-2.5-pro",
Self::Gemini3ProPreview => "gemini-3-pro-preview",
Self::Custom { name, .. } => name,
}
}
@@ -578,7 +570,6 @@ impl Model {
Self::Gemini25FlashLitePreview => "Gemini 2.5 Flash-Lite Preview",
Self::Gemini25Flash => "Gemini 2.5 Flash",
Self::Gemini25Pro => "Gemini 2.5 Pro",
Self::Gemini3ProPreview => "Gemini 3 Pro",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
@@ -595,7 +586,6 @@ impl Model {
Self::Gemini25FlashLitePreview => 1_000_000,
Self::Gemini25Flash => 1_048_576,
Self::Gemini25Pro => 1_048_576,
Self::Gemini3ProPreview => 1_048_576,
Self::Custom { max_tokens, .. } => *max_tokens,
}
}
@@ -610,7 +600,6 @@ impl Model {
Model::Gemini25FlashLitePreview => Some(64_000),
Model::Gemini25Flash => Some(65_536),
Model::Gemini25Pro => Some(65_536),
Model::Gemini3ProPreview => Some(65_536),
Model::Custom { .. } => None,
}
}
@@ -630,10 +619,7 @@ impl Model {
| Self::Gemini15Flash
| Self::Gemini20FlashLite
| Self::Gemini20Flash => GoogleModelMode::Default,
Self::Gemini25FlashLitePreview
| Self::Gemini25Flash
| Self::Gemini25Pro
| Self::Gemini3ProPreview => {
Self::Gemini25FlashLitePreview | Self::Gemini25Flash | Self::Gemini25Pro => {
GoogleModelMode::Thinking {
// By default these models are set to "auto", so we preserve that behavior
// but indicate they are capable of thinking mode
@@ -650,109 +636,3 @@ impl std::fmt::Display for Model {
write!(f, "{}", self.id())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_function_call_part_with_signature_serializes_correctly() {
let part = FunctionCallPart {
function_call: FunctionCall {
name: "test_function".to_string(),
args: json!({"arg": "value"}),
},
thought_signature: Some("test_signature".to_string()),
};
let serialized = serde_json::to_value(&part).unwrap();
assert_eq!(serialized["functionCall"]["name"], "test_function");
assert_eq!(serialized["functionCall"]["args"]["arg"], "value");
assert_eq!(serialized["thoughtSignature"], "test_signature");
}
#[test]
fn test_function_call_part_without_signature_omits_field() {
let part = FunctionCallPart {
function_call: FunctionCall {
name: "test_function".to_string(),
args: json!({"arg": "value"}),
},
thought_signature: None,
};
let serialized = serde_json::to_value(&part).unwrap();
assert_eq!(serialized["functionCall"]["name"], "test_function");
assert_eq!(serialized["functionCall"]["args"]["arg"], "value");
// thoughtSignature field should not be present when None
assert!(serialized.get("thoughtSignature").is_none());
}
#[test]
fn test_function_call_part_deserializes_with_signature() {
let json = json!({
"functionCall": {
"name": "test_function",
"args": {"arg": "value"}
},
"thoughtSignature": "test_signature"
});
let part: FunctionCallPart = serde_json::from_value(json).unwrap();
assert_eq!(part.function_call.name, "test_function");
assert_eq!(part.thought_signature, Some("test_signature".to_string()));
}
#[test]
fn test_function_call_part_deserializes_without_signature() {
let json = json!({
"functionCall": {
"name": "test_function",
"args": {"arg": "value"}
}
});
let part: FunctionCallPart = serde_json::from_value(json).unwrap();
assert_eq!(part.function_call.name, "test_function");
assert_eq!(part.thought_signature, None);
}
#[test]
fn test_function_call_part_round_trip() {
let original = FunctionCallPart {
function_call: FunctionCall {
name: "test_function".to_string(),
args: json!({"arg": "value", "nested": {"key": "val"}}),
},
thought_signature: Some("round_trip_signature".to_string()),
};
let serialized = serde_json::to_value(&original).unwrap();
let deserialized: FunctionCallPart = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.function_call.name, original.function_call.name);
assert_eq!(deserialized.function_call.args, original.function_call.args);
assert_eq!(deserialized.thought_signature, original.thought_signature);
}
#[test]
fn test_function_call_part_with_empty_signature_serializes() {
let part = FunctionCallPart {
function_call: FunctionCall {
name: "test_function".to_string(),
args: json!({"arg": "value"}),
},
thought_signature: Some("".to_string()),
};
let serialized = serde_json::to_value(&part).unwrap();
// Empty string should still be serialized (normalization happens at a higher level)
assert_eq!(serialized["thoughtSignature"], "");
}
}

View File

@@ -63,4 +63,4 @@ In addition to the systems above, GPUI provides a range of smaller services that
- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details.
Currently, the best way to learn about these APIs is to read the Zed source code or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).

View File

@@ -1410,7 +1410,7 @@ impl App {
let quit_on_empty = match cx.quit_mode {
QuitMode::Explicit => false,
QuitMode::LastWindowClosed => true,
QuitMode::Default => cfg!(not(target_os = "macos")),
QuitMode::Default => !cfg!(macos),
};
if quit_on_empty && cx.windows.is_empty() {
@@ -2400,6 +2400,10 @@ impl HttpClient for NullHttpClient {
fn proxy(&self) -> Option<&Url> {
None
}
fn type_name(&self) -> &'static str {
type_name::<Self>()
}
}
/// A mutable reference to an entity owned by GPUI

View File

@@ -509,7 +509,7 @@ impl Deref for MouseExitEvent {
}
/// A collection of paths from the platform, such as from a file drop.
#[derive(Debug, Clone, Default, Eq, PartialEq)]
#[derive(Debug, Clone, Default)]
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
impl ExternalPaths {

View File

@@ -1389,10 +1389,6 @@ pub enum WindowBackgroundAppearance {
///
/// Not always supported.
Blurred,
/// The Mica backdrop material, supported on Windows 11.
MicaBackdrop,
/// The Mica Alt backdrop material, supported on Windows 11.
MicaAltBackdrop,
}
/// The options that can be configured for a file dialog prompt
@@ -1577,8 +1573,6 @@ pub enum ClipboardEntry {
String(ClipboardString),
/// An image entry
Image(Image),
/// A file entry
ExternalPaths(crate::ExternalPaths),
}
impl ClipboardItem {
@@ -1619,29 +1613,16 @@ impl ClipboardItem {
/// Returns None if there were no ClipboardString entries.
pub fn text(&self) -> Option<String> {
let mut answer = String::new();
let mut any_entries = false;
for entry in self.entries.iter() {
if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
answer.push_str(text);
any_entries = true;
}
}
if answer.is_empty() {
for entry in self.entries.iter() {
if let ClipboardEntry::ExternalPaths(paths) = entry {
for path in &paths.0 {
use std::fmt::Write as _;
_ = write!(answer, "{}", path.display());
}
}
}
}
if !answer.is_empty() {
Some(answer)
} else {
None
}
if any_entries { Some(answer) } else { None }
}
/// If this item is one ClipboardEntry::String, returns its metadata.

View File

@@ -1,6 +1,7 @@
use std::{
env,
path::{Path, PathBuf},
process::Command,
rc::Rc,
sync::Arc,
};
@@ -17,7 +18,6 @@ use anyhow::{Context as _, anyhow};
use calloop::{LoopSignal, channel::Channel};
use futures::channel::oneshot;
use util::ResultExt as _;
use util::command::{new_smol_command, new_std_command};
#[cfg(any(feature = "wayland", feature = "x11"))]
use xkbcommon::xkb::{self, Keycode, Keysym, State};
@@ -215,7 +215,7 @@ impl<P: LinuxClient + 'static> Platform for P {
clippy::disallowed_methods,
reason = "We are restarting ourselves, using std command thus is fine"
)]
let restart_process = new_std_command("/usr/bin/env")
let restart_process = Command::new("/usr/bin/env")
.arg("bash")
.arg("-c")
.arg(script)
@@ -422,7 +422,7 @@ impl<P: LinuxClient + 'static> Platform for P {
let path = path.to_owned();
self.background_executor()
.spawn(async move {
let _ = new_smol_command("xdg-open")
let _ = smol::process::Command::new("xdg-open")
.arg(path)
.spawn()
.context("invoking xdg-open")

View File

@@ -53,16 +53,14 @@ use std::{
ffi::{CStr, OsStr, c_void},
os::{raw::c_char, unix::ffi::OsStrExt},
path::{Path, PathBuf},
process::Command,
ptr,
rc::Rc,
slice, str,
sync::{Arc, OnceLock},
};
use strum::IntoEnumIterator;
use util::{
ResultExt,
command::{new_smol_command, new_std_command},
};
use util::ResultExt;
#[allow(non_upper_case_globals)]
const NSUTF8StringEncoding: NSUInteger = 4;
@@ -554,7 +552,7 @@ impl Platform for MacPlatform {
clippy::disallowed_methods,
reason = "We are restarting ourselves, using std command thus is fine"
)]
let restart_process = new_std_command("/bin/bash")
let restart_process = Command::new("/bin/bash")
.arg("-c")
.arg(script)
.arg(app_pid)
@@ -869,7 +867,7 @@ impl Platform for MacPlatform {
.lock()
.background_executor
.spawn(async move {
if let Some(mut child) = new_smol_command("open")
if let Some(mut child) = smol::process::Command::new("open")
.arg(path)
.spawn()
.context("invoking open command")
@@ -1048,7 +1046,6 @@ impl Platform for MacPlatform {
ClipboardEntry::Image(image) => {
self.write_image_to_clipboard(image);
}
ClipboardEntry::ExternalPaths(_) => {}
},
None => {
// Writing an empty list of entries just clears the clipboard.
@@ -1135,7 +1132,32 @@ impl Platform for MacPlatform {
}
}
// If it wasn't a string, try the various supported image types.
// Next, check for URL flavors (including file URLs). Some tools only provide a URL
// with no plain text entry.
{
// Try the modern UTType identifiers first.
let file_url_type: id = ns_string("public.file-url");
let url_type: id = ns_string("public.url");
let url_data = if msg_send![types, containsObject: file_url_type] {
pasteboard.dataForType(file_url_type)
} else if msg_send![types, containsObject: url_type] {
pasteboard.dataForType(url_type)
} else {
nil
};
if url_data != nil && !url_data.bytes().is_null() {
let bytes = slice::from_raw_parts(
url_data.bytes() as *mut u8,
url_data.length() as usize,
);
return Some(self.read_string_from_clipboard(&state, bytes));
}
}
// If it wasn't a string or URL, try the various supported image types.
for format in ImageFormat::iter() {
if let Some(item) = try_clipboard_image(pasteboard, format) {
return Some(item);
@@ -1143,7 +1165,7 @@ impl Platform for MacPlatform {
}
}
// If it wasn't a string or a supported image type, give up.
// If it wasn't a string, URL, or a supported image type, give up.
None
}
@@ -1718,6 +1740,40 @@ mod tests {
);
}
#[test]
fn test_file_url_reads_as_url_string() {
let platform = build_platform();
// Create a file URL for an arbitrary test path and write it to the pasteboard.
// This path does not need to exist; we only validate URL→path conversion.
let mock_path = "/tmp/zed-clipboard-file-url-test";
unsafe {
// Build an NSURL from the file path
let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(mock_path)];
let abs: id = msg_send![url, absoluteString];
// Encode the URL string as UTF-8 bytes
let len: usize = msg_send![abs, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
let bytes_ptr = abs.UTF8String() as *const u8;
let data = NSData::dataWithBytes_length_(nil, bytes_ptr as *const c_void, len as u64);
// Write as public.file-url to the unique pasteboard
let file_url_type: id = ns_string("public.file-url");
platform
.0
.lock()
.pasteboard
.setData_forType(data, file_url_type);
}
// Ensure the clipboard read returns the URL string, not a converted path
let expected_url = format!("file://{}", mock_path);
assert_eq!(
platform.read_from_clipboard(),
Some(ClipboardItem::new_string(expected_url))
);
}
fn build_platform() -> MacPlatform {
let platform = MacPlatform::new(false);
platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };

View File

@@ -1,7 +1,7 @@
use std::sync::LazyLock;
use anyhow::Result;
use collections::FxHashMap;
use collections::{FxHashMap, FxHashSet};
use itertools::Itertools;
use windows::Win32::{
Foundation::{HANDLE, HGLOBAL},
@@ -18,9 +18,7 @@ use windows::Win32::{
};
use windows_core::PCWSTR;
use crate::{
ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash,
};
use crate::{ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, hash};
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
@@ -50,6 +48,16 @@ static FORMATS_MAP: LazyLock<FxHashMap<u32, ClipboardFormatType>> = LazyLock::ne
formats_map.insert(CF_HDROP.0 as u32, ClipboardFormatType::Files);
formats_map
});
static FORMATS_SET: LazyLock<FxHashSet<u32>> = LazyLock::new(|| {
let mut formats_map = FxHashSet::default();
formats_map.insert(CF_UNICODETEXT.0 as u32);
formats_map.insert(*CLIPBOARD_PNG_FORMAT);
formats_map.insert(*CLIPBOARD_GIF_FORMAT);
formats_map.insert(*CLIPBOARD_JPG_FORMAT);
formats_map.insert(*CLIPBOARD_SVG_FORMAT);
formats_map.insert(CF_HDROP.0 as u32);
formats_map
});
static IMAGE_FORMATS_MAP: LazyLock<FxHashMap<u32, ImageFormat>> = LazyLock::new(|| {
let mut formats_map = FxHashMap::default();
formats_map.insert(*CLIPBOARD_PNG_FORMAT, ImageFormat::Png);
@@ -130,11 +138,6 @@ fn register_clipboard_format(format: PCWSTR) -> u32 {
std::io::Error::last_os_error()
);
}
log::debug!(
"Registered clipboard format {} as {}",
unsafe { format.display() },
ret
);
ret
}
@@ -156,7 +159,6 @@ fn write_to_clipboard_inner(item: ClipboardItem) -> Result<()> {
ClipboardEntry::Image(image) => {
write_image_to_clipboard(image)?;
}
ClipboardEntry::ExternalPaths(_) => {}
},
None => {
// Writing an empty list of entries just clears the clipboard.
@@ -247,33 +249,19 @@ fn with_best_match_format<F>(f: F) -> Option<ClipboardItem>
where
F: Fn(u32) -> Option<ClipboardEntry>,
{
let mut text = None;
let mut image = None;
let mut files = None;
let count = unsafe { CountClipboardFormats() };
let mut clipboard_format = 0;
for _ in 0..count {
clipboard_format = unsafe { EnumClipboardFormats(clipboard_format) };
let Some(item_format) = FORMATS_MAP.get(&clipboard_format) else {
let Some(item_format) = FORMATS_SET.get(&clipboard_format) else {
continue;
};
let bucket = match item_format {
ClipboardFormatType::Text if text.is_none() => &mut text,
ClipboardFormatType::Image if image.is_none() => &mut image,
ClipboardFormatType::Files if files.is_none() => &mut files,
_ => continue,
};
if let Some(entry) = f(clipboard_format) {
*bucket = Some(entry);
if let Some(entry) = f(*item_format) {
return Some(ClipboardItem {
entries: vec![entry],
});
}
}
if let Some(entry) = [image, files, text].into_iter().flatten().next() {
return Some(ClipboardItem {
entries: vec![entry],
});
}
// log the formats that we don't support yet.
{
clipboard_format = 0;
@@ -358,17 +346,18 @@ fn read_image_for_type(format_number: u32, format: ImageFormat) -> Option<Clipbo
}
fn read_files_from_clipboard() -> Option<ClipboardEntry> {
let filenames = with_clipboard_data(CF_HDROP.0 as u32, |data_ptr, _size| {
let text = with_clipboard_data(CF_HDROP.0 as u32, |data_ptr, _size| {
let hdrop = HDROP(data_ptr);
let mut filenames = Vec::new();
let mut filenames = String::new();
with_file_names(hdrop, |file_name| {
filenames.push(std::path::PathBuf::from(file_name));
filenames.push_str(&file_name);
});
filenames
})?;
Some(ClipboardEntry::ExternalPaths(ExternalPaths(
filenames.into(),
)))
Some(ClipboardEntry::String(ClipboardString {
text,
metadata: None,
}))
}
fn with_clipboard_data<F, R>(format: u32, f: F) -> Option<R>

View File

@@ -18,7 +18,6 @@ use smallvec::SmallVec;
use windows::{
Win32::{
Foundation::*,
Graphics::Dwm::*,
Graphics::Gdi::*,
System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*},
UI::{Controls::*, HiDpi::*, Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
@@ -774,26 +773,20 @@ impl PlatformWindow for WindowsWindow {
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let hwnd = self.0.hwnd;
// using Dwm APIs for Mica and MicaAlt backdrops.
// others follow the set_window_composition_attribute approach
match background_appearance {
WindowBackgroundAppearance::Opaque => {
// ACCENT_DISABLED
set_window_composition_attribute(hwnd, None, 0);
}
WindowBackgroundAppearance::Transparent => {
// Use ACCENT_ENABLE_TRANSPARENTGRADIENT for transparent background
set_window_composition_attribute(hwnd, None, 2);
}
WindowBackgroundAppearance::Blurred => {
// Enable acrylic blur
// ACCENT_ENABLE_ACRYLICBLURBEHIND
set_window_composition_attribute(hwnd, Some((0, 0, 0, 0)), 4);
}
WindowBackgroundAppearance::MicaBackdrop => {
// DWMSBT_MAINWINDOW => MicaBase
dwm_set_window_composition_attribute(hwnd, 2);
}
WindowBackgroundAppearance::MicaAltBackdrop => {
// DWMSBT_TABBEDWINDOW => MicaAlt
dwm_set_window_composition_attribute(hwnd, 4);
}
}
}
@@ -1337,34 +1330,9 @@ fn retrieve_window_placement(
Ok(placement)
}
fn dwm_set_window_composition_attribute(hwnd: HWND, backdrop_type: u32) {
let mut version = unsafe { std::mem::zeroed() };
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut version) };
// DWMWA_SYSTEMBACKDROP_TYPE is available only on version 22621 or later
// using SetWindowCompositionAttributeType as a fallback
if !status.is_ok() || version.dwBuildNumber < 22621 {
return;
}
unsafe {
let result = DwmSetWindowAttribute(
hwnd,
DWMWA_SYSTEMBACKDROP_TYPE,
&backdrop_type as *const _ as *const _,
std::mem::size_of_val(&backdrop_type) as u32,
);
if !result.is_ok() {
return;
}
}
}
fn set_window_composition_attribute(hwnd: HWND, color: Option<Color>, state: u32) {
let mut version = unsafe { std::mem::zeroed() };
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut version) };
if !status.is_ok() || version.dwBuildNumber < 17763 {
return;
}

View File

@@ -13,9 +13,8 @@ const ELLIPSIS: SharedString = SharedString::new_static("…");
/// A trait for elements that can be styled.
/// Use this to opt-in to a utility CSS-like styling API.
// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv
#[cfg_attr(
all(any(feature = "inspector", debug_assertions), not(rust_analyzer)),
any(feature = "inspector", debug_assertions),
gpui_macros::derive_inspector_reflection
)]
pub trait Styled: Sized {

View File

@@ -1,7 +1,8 @@
//! This code was generated using Zed Agent with Claude Opus 4.
// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv
#[cfg_attr(not(rust_analyzer), gpui_macros::derive_inspector_reflection)]
use gpui_macros::derive_inspector_reflection;
#[derive_inspector_reflection]
trait Transform: Clone {
/// Doubles the value
fn double(self) -> Self;

View File

@@ -14,9 +14,9 @@ use futures::{
};
use parking_lot::Mutex;
use serde::Serialize;
use std::sync::Arc;
#[cfg(feature = "test-support")]
use std::{any::type_name, fmt};
use std::fmt;
use std::{any::type_name, sync::Arc};
pub use url::{Host, Url};
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
@@ -59,9 +59,9 @@ impl HttpRequestExt for http::request::Builder {
}
pub trait HttpClient: 'static + Send + Sync {
fn user_agent(&self) -> Option<&HeaderValue>;
fn type_name(&self) -> &'static str;
fn proxy(&self) -> Option<&Url>;
fn user_agent(&self) -> Option<&HeaderValue>;
fn send(
&self,
@@ -106,6 +106,8 @@ pub trait HttpClient: 'static + Send + Sync {
}
}
fn proxy(&self) -> Option<&Url>;
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
panic!("called as_fake on {}", type_name::<Self>())
@@ -161,6 +163,10 @@ impl HttpClient for HttpClientWithProxy {
self.proxy.as_ref()
}
fn type_name(&self) -> &'static str {
self.client.type_name()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
self.client.as_fake()
@@ -176,13 +182,19 @@ impl HttpClient for HttpClientWithProxy {
}
/// An [`HttpClient`] that has a base URL.
#[derive(Deref)]
pub struct HttpClientWithUrl {
base_url: Mutex<String>,
#[deref]
client: HttpClientWithProxy,
}
impl std::ops::Deref for HttpClientWithUrl {
type Target = HttpClientWithProxy;
fn deref(&self) -> &Self::Target {
&self.client
}
}
impl HttpClientWithUrl {
/// Returns a new [`HttpClientWithUrl`] with the given base URL.
pub fn new(
@@ -302,6 +314,10 @@ impl HttpClient for HttpClientWithUrl {
self.client.proxy.as_ref()
}
fn type_name(&self) -> &'static str {
self.client.type_name()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
self.client.as_fake()
@@ -368,6 +384,10 @@ impl HttpClient for BlockedHttpClient {
None
}
fn type_name(&self) -> &'static str {
type_name::<Self>()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
panic!("called as_fake on {}", type_name::<Self>())
@@ -462,6 +482,10 @@ impl HttpClient for FakeHttpClient {
None
}
fn type_name(&self) -> &'static str {
type_name::<Self>()
}
fn as_fake(&self) -> &FakeHttpClient {
self
}

View File

@@ -217,7 +217,6 @@ pub enum IconName {
SupermavenError,
SupermavenInit,
SwatchBook,
SweepAi,
Tab,
Terminal,
TerminalAlt,

View File

@@ -41,6 +41,7 @@ tree-sitter-rust.workspace = true
ui_input.workspace = true
ui.workspace = true
util.workspace = true
vim.workspace = true
workspace.workspace = true
zed_actions.workspace = true

View File

@@ -184,7 +184,7 @@ enum SearchMode {
impl SearchMode {
fn invert(&self) -> Self {
match self {
SearchMode::Normal => SearchMode::KeyStroke { exact_match: true },
SearchMode::Normal => SearchMode::KeyStroke { exact_match: false },
SearchMode::KeyStroke { .. } => SearchMode::Normal,
}
}
@@ -958,14 +958,12 @@ impl KeymapEditor {
let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.context(self.focus_handle.clone())
.when(selected_binding_is_unbound, |this| {
this.action("Create", Box::new(CreateBinding))
})
.action_disabled_when(
selected_binding_is_unbound,
"Edit",
Box::new(EditBinding),
)
.action("Create", Box::new(CreateBinding))
.action_disabled_when(
selected_binding_is_unbound,
"Delete",
@@ -1600,33 +1598,9 @@ impl Item for KeymapEditor {
impl Render for KeymapEditor {
fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
if let SearchMode::KeyStroke { exact_match } = self.search_mode {
let button = IconButton::new("keystrokes-exact-match", IconName::CaseSensitive)
.tooltip(move |_window, cx| {
Tooltip::for_action(
"Toggle Exact Match Mode",
&ToggleExactKeystrokeMatching,
cx,
)
})
.shape(IconButtonShape::Square)
.toggle_state(exact_match)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(ToggleExactKeystrokeMatching.boxed_clone(), cx);
}));
self.keystroke_editor.update(cx, |editor, _| {
editor.actions_slot = Some(button.into_any_element());
});
} else {
self.keystroke_editor.update(cx, |editor, _| {
editor.actions_slot = None;
});
}
let row_count = self.matches.len();
let focus_handle = &self.focus_handle;
let theme = cx.theme();
let focus_handle = &self.focus_handle;
v_flex()
.id("keymap-editor")
@@ -1769,7 +1743,7 @@ impl Render for KeymapEditor {
)
.action(
"Vim Bindings",
zed_actions::vim::OpenDefaultKeymap.boxed_clone(),
vim::OpenDefaultKeymap.boxed_clone(),
)
}))
})
@@ -1810,14 +1784,49 @@ impl Render for KeymapEditor {
)
),
)
.when(
matches!(self.search_mode, SearchMode::KeyStroke { .. }),
|this| {
.when_some(
match self.search_mode {
SearchMode::Normal => None,
SearchMode::KeyStroke { exact_match } => Some(exact_match),
},
|this, exact_match| {
this.child(
h_flex()
.gap_2()
.child(self.keystroke_editor.clone())
.child(div().min_w_64()), // Spacer div to align with the search input
.child(
h_flex()
.min_w_64()
.child(
IconButton::new(
"keystrokes-exact-match",
IconName::CaseSensitive,
)
.tooltip({
let keystroke_focus_handle =
self.keystroke_editor.read(cx).focus_handle(cx);
move |_window, cx| {
Tooltip::for_action_in(
"Toggle Exact Match Mode",
&ToggleExactKeystrokeMatching,
&keystroke_focus_handle,
cx,
)
}
})
.shape(IconButtonShape::Square)
.toggle_state(exact_match)
.on_click(
cx.listener(|_, _, window, cx| {
window.dispatch_action(
ToggleExactKeystrokeMatching.boxed_clone(),
cx,
);
}),
),
),
)
)
},
),

View File

@@ -64,7 +64,6 @@ pub struct KeystrokeInput {
clear_close_keystrokes_timer: Option<Task<()>>,
#[cfg(test)]
recording: bool,
pub actions_slot: Option<AnyElement>,
}
impl KeystrokeInput {
@@ -95,7 +94,6 @@ impl KeystrokeInput {
clear_close_keystrokes_timer: None,
#[cfg(test)]
recording: false,
actions_slot: None,
}
}
@@ -447,11 +445,6 @@ impl KeystrokeInput {
// not get de-synced
self.inner_focus_handle.is_focused(window)
}
pub fn actions_slot(mut self, action: impl IntoElement) -> Self {
self.actions_slot = Some(action.into_any_element());
self
}
}
impl EventEmitter<()> for KeystrokeInput {}
@@ -593,7 +586,7 @@ impl Render for KeystrokeInput {
.min_w_0()
.justify_center()
.flex_wrap()
.gap_1()
.gap(ui::DynamicSpacing::Base04.rems(cx))
.children(self.render_keystrokes(is_recording)),
)
.child(
@@ -643,25 +636,18 @@ impl Render for KeystrokeInput {
)
}
})
.when_some(self.actions_slot.take(), |this, action| this.child(action))
.when(is_recording, |this| {
this.child(
IconButton::new("clear-btn", IconName::Backspace)
.shape(IconButtonShape::Square)
.tooltip(move |_, cx| {
Tooltip::with_meta(
"Clear Keystrokes",
Some(&ClearKeystrokes),
"Hit it three times to execute",
cx,
)
})
.when(!is_focused, |this| this.icon_color(Color::Muted))
.on_click(cx.listener(|this, _event, window, cx| {
this.clear_keystrokes(&ClearKeystrokes, window, cx);
})),
)
}),
.child(
IconButton::new("clear-btn", IconName::Backspace)
.shape(IconButtonShape::Square)
.tooltip(Tooltip::for_action_title(
"Clear Keystrokes",
&ClearKeystrokes,
))
.when(!is_focused, |this| this.icon_color(Color::Muted))
.on_click(cx.listener(|this, _event, window, cx| {
this.clear_keystrokes(&ClearKeystrokes, window, cx);
})),
),
)
}
}

View File

@@ -515,9 +515,6 @@ pub struct LanguageModelToolUse {
pub raw_input: String,
pub input: serde_json::Value,
pub is_input_complete: bool,
/// Thought signature the model sent us. Some models require that this
/// signature be preserved and sent back in conversation history for validation.
pub thought_signature: Option<String>,
}
pub struct LanguageModelTextStream {
@@ -924,85 +921,4 @@ mod tests {
),
}
}
#[test]
fn test_language_model_tool_use_serializes_with_signature() {
use serde_json::json;
let tool_use = LanguageModelToolUse {
id: LanguageModelToolUseId::from("test_id"),
name: "test_tool".into(),
raw_input: json!({"arg": "value"}).to_string(),
input: json!({"arg": "value"}),
is_input_complete: true,
thought_signature: Some("test_signature".to_string()),
};
let serialized = serde_json::to_value(&tool_use).unwrap();
assert_eq!(serialized["id"], "test_id");
assert_eq!(serialized["name"], "test_tool");
assert_eq!(serialized["thought_signature"], "test_signature");
}
#[test]
fn test_language_model_tool_use_deserializes_with_missing_signature() {
use serde_json::json;
let json = json!({
"id": "test_id",
"name": "test_tool",
"raw_input": "{\"arg\":\"value\"}",
"input": {"arg": "value"},
"is_input_complete": true
});
let tool_use: LanguageModelToolUse = serde_json::from_value(json).unwrap();
assert_eq!(tool_use.id, LanguageModelToolUseId::from("test_id"));
assert_eq!(tool_use.name.as_ref(), "test_tool");
assert_eq!(tool_use.thought_signature, None);
}
#[test]
fn test_language_model_tool_use_round_trip_with_signature() {
use serde_json::json;
let original = LanguageModelToolUse {
id: LanguageModelToolUseId::from("round_trip_id"),
name: "round_trip_tool".into(),
raw_input: json!({"key": "value"}).to_string(),
input: json!({"key": "value"}),
is_input_complete: true,
thought_signature: Some("round_trip_sig".to_string()),
};
let serialized = serde_json::to_value(&original).unwrap();
let deserialized: LanguageModelToolUse = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.id, original.id);
assert_eq!(deserialized.name, original.name);
assert_eq!(deserialized.thought_signature, original.thought_signature);
}
#[test]
fn test_language_model_tool_use_round_trip_without_signature() {
use serde_json::json;
let original = LanguageModelToolUse {
id: LanguageModelToolUseId::from("no_sig_id"),
name: "no_sig_tool".into(),
raw_input: json!({"key": "value"}).to_string(),
input: json!({"key": "value"}),
is_input_complete: true,
thought_signature: None,
};
let serialized = serde_json::to_value(&original).unwrap();
let deserialized: LanguageModelToolUse = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.id, original.id);
assert_eq!(deserialized.name, original.name);
assert_eq!(deserialized.thought_signature, None);
}
}

View File

@@ -711,7 +711,6 @@ impl AnthropicEventMapper {
is_input_complete: false,
raw_input: tool_use.input_json.clone(),
input,
thought_signature: None,
},
))];
}
@@ -735,7 +734,6 @@ impl AnthropicEventMapper {
is_input_complete: true,
input,
raw_input: tool_use.input_json.clone(),
thought_signature: None,
},
)),
Err(json_parse_err) => {

View File

@@ -24,10 +24,7 @@ use bedrock::{
use collections::{BTreeMap, HashMap};
use credentials_provider::CredentialsProvider;
use futures::{FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream};
use gpui::{
AnyView, App, AsyncApp, Context, Entity, FocusHandle, FontWeight, Subscription, Task, Window,
actions,
};
use gpui::{AnyView, App, AsyncApp, Context, Entity, FontWeight, Subscription, Task};
use gpui_tokio::Tokio;
use http_client::HttpClient;
use language_model::{
@@ -50,8 +47,6 @@ use util::ResultExt;
use crate::AllLanguageModelSettings;
actions!(bedrock, [Tab, TabPrev]);
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("amazon-bedrock");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Amazon Bedrock");
@@ -970,7 +965,6 @@ pub fn map_to_language_model_completion_events(
is_input_complete: true,
raw_input: tool_use.input_json,
input,
thought_signature: None,
},
))
}),
@@ -1018,7 +1012,6 @@ struct ConfigurationView {
region_editor: Entity<InputField>,
state: Entity<State>,
load_credentials_task: Option<Task<()>>,
focus_handle: FocusHandle,
}
impl ConfigurationView {
@@ -1029,41 +1022,11 @@ impl ConfigurationView {
const PLACEHOLDER_REGION: &'static str = "us-east-1";
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
cx.observe(&state, |_, _, cx| {
cx.notify();
})
.detach();
let access_key_id_editor = cx.new(|cx| {
InputField::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT)
.label("Access Key ID")
.tab_index(0)
.tab_stop(true)
});
let secret_access_key_editor = cx.new(|cx| {
InputField::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT)
.label("Secret Access Key")
.tab_index(1)
.tab_stop(true)
});
let session_token_editor = cx.new(|cx| {
InputField::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT)
.label("Session Token (Optional)")
.tab_index(2)
.tab_stop(true)
});
let region_editor = cx.new(|cx| {
InputField::new(window, cx, Self::PLACEHOLDER_REGION)
.label("Region")
.tab_index(3)
.tab_stop(true)
});
let load_credentials_task = Some(cx.spawn({
let state = state.clone();
async move |this, cx| {
@@ -1083,13 +1046,22 @@ impl ConfigurationView {
}));
Self {
access_key_id_editor,
secret_access_key_editor,
session_token_editor,
region_editor,
access_key_id_editor: cx.new(|cx| {
InputField::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT)
.label("Access Key ID")
}),
secret_access_key_editor: cx.new(|cx| {
InputField::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT)
.label("Secret Access Key")
}),
session_token_editor: cx.new(|cx| {
InputField::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT)
.label("Session Token (Optional)")
}),
region_editor: cx
.new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_REGION).label("Region")),
state,
load_credentials_task,
focus_handle,
}
}
@@ -1169,19 +1141,6 @@ impl ConfigurationView {
fn should_render_editor(&self, cx: &Context<Self>) -> bool {
self.state.read(cx).is_authenticated()
}
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
}
fn on_tab_prev(
&mut self,
_: &menu::SelectPrevious,
window: &mut Window,
_: &mut Context<Self>,
) {
window.focus_prev();
}
}
impl Render for ConfigurationView {
@@ -1231,9 +1190,6 @@ impl Render for ConfigurationView {
v_flex()
.size_full()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.on_action(cx.listener(ConfigurationView::save_credentials))
.child(Label::new("To use Zed's agent with Bedrock, you can set a custom authentication strategy through the settings.json, or use static credentials."))
.child(Label::new("But, to access models on AWS, you need to:").mt_1())
@@ -1278,7 +1234,6 @@ impl ConfigurationView {
fn render_static_credentials_ui(&self) -> impl IntoElement {
v_flex()
.my_2()
.tab_group()
.gap_1p5()
.child(
Label::new("Static Keys")

View File

@@ -143,11 +143,9 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
};
let Some(copilot) = Copilot::global(cx) else {
return Task::ready(Err(anyhow!(concat!(
"Copilot must be enabled for Copilot Chat to work. ",
"Please enable Copilot and try again."
))
.into()));
return Task::ready( Err(anyhow!(
"Copilot must be enabled for Copilot Chat to work. Please enable Copilot and try again."
).into()));
};
let err = match copilot.read(cx).status() {
@@ -458,7 +456,6 @@ pub fn map_to_language_model_completion_events(
is_input_complete: true,
input,
raw_input: tool_call.arguments,
thought_signature: None,
},
)),
Err(error) => Ok(
@@ -561,7 +558,6 @@ impl CopilotResponsesEventMapper {
is_input_complete: true,
input,
raw_input: arguments.clone(),
thought_signature: None,
},
))),
Err(error) => {

View File

@@ -501,7 +501,6 @@ impl DeepSeekEventMapper {
is_input_complete: true,
input,
raw_input: tool_call.arguments.clone(),
thought_signature: None,
},
)),
Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {

View File

@@ -439,15 +439,11 @@ pub fn into_google(
})]
}
language_model::MessageContent::ToolUse(tool_use) => {
// Normalize empty string signatures to None
let thought_signature = tool_use.thought_signature.filter(|s| !s.is_empty());
vec![Part::FunctionCallPart(google_ai::FunctionCallPart {
function_call: google_ai::FunctionCall {
name: tool_use.name.to_string(),
args: tool_use.input,
},
thought_signature,
})]
}
language_model::MessageContent::ToolResult(tool_result) => {
@@ -659,11 +655,6 @@ impl GoogleEventMapper {
let id: LanguageModelToolUseId =
format!("{}-{}", name, next_tool_id).into();
// Normalize empty string signatures to None
let thought_signature = function_call_part
.thought_signature
.filter(|s| !s.is_empty());
events.push(Ok(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id,
@@ -671,7 +662,6 @@ impl GoogleEventMapper {
is_input_complete: true,
raw_input: function_call_part.function_call.args.to_string(),
input: function_call_part.function_call.args,
thought_signature,
},
)));
}
@@ -901,424 +891,3 @@ impl Render for ConfigurationView {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use google_ai::{
Content, FunctionCall, FunctionCallPart, GenerateContentCandidate, GenerateContentResponse,
Part, Role as GoogleRole, TextPart,
};
use language_model::{LanguageModelToolUseId, MessageContent, Role};
use serde_json::json;
#[test]
fn test_function_call_with_signature_creates_tool_use_with_signature() {
let mut mapper = GoogleEventMapper::new();
let response = GenerateContentResponse {
candidates: Some(vec![GenerateContentCandidate {
index: Some(0),
content: Content {
parts: vec![Part::FunctionCallPart(FunctionCallPart {
function_call: FunctionCall {
name: "test_function".to_string(),
args: json!({"arg": "value"}),
},
thought_signature: Some("test_signature_123".to_string()),
})],
role: GoogleRole::Model,
},
finish_reason: None,
finish_message: None,
safety_ratings: None,
citation_metadata: None,
}]),
prompt_feedback: None,
usage_metadata: None,
};
let events = mapper.map_event(response);
assert_eq!(events.len(), 2); // ToolUse event + Stop event
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
assert_eq!(tool_use.name.as_ref(), "test_function");
assert_eq!(
tool_use.thought_signature.as_deref(),
Some("test_signature_123")
);
} else {
panic!("Expected ToolUse event");
}
}
#[test]
fn test_function_call_without_signature_has_none() {
let mut mapper = GoogleEventMapper::new();
let response = GenerateContentResponse {
candidates: Some(vec![GenerateContentCandidate {
index: Some(0),
content: Content {
parts: vec![Part::FunctionCallPart(FunctionCallPart {
function_call: FunctionCall {
name: "test_function".to_string(),
args: json!({"arg": "value"}),
},
thought_signature: None,
})],
role: GoogleRole::Model,
},
finish_reason: None,
finish_message: None,
safety_ratings: None,
citation_metadata: None,
}]),
prompt_feedback: None,
usage_metadata: None,
};
let events = mapper.map_event(response);
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
assert_eq!(tool_use.thought_signature, None);
} else {
panic!("Expected ToolUse event");
}
}
#[test]
fn test_empty_string_signature_normalized_to_none() {
let mut mapper = GoogleEventMapper::new();
let response = GenerateContentResponse {
candidates: Some(vec![GenerateContentCandidate {
index: Some(0),
content: Content {
parts: vec![Part::FunctionCallPart(FunctionCallPart {
function_call: FunctionCall {
name: "test_function".to_string(),
args: json!({"arg": "value"}),
},
thought_signature: Some("".to_string()),
})],
role: GoogleRole::Model,
},
finish_reason: None,
finish_message: None,
safety_ratings: None,
citation_metadata: None,
}]),
prompt_feedback: None,
usage_metadata: None,
};
let events = mapper.map_event(response);
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
assert_eq!(tool_use.thought_signature, None);
} else {
panic!("Expected ToolUse event");
}
}
#[test]
fn test_parallel_function_calls_preserve_signatures() {
let mut mapper = GoogleEventMapper::new();
let response = GenerateContentResponse {
candidates: Some(vec![GenerateContentCandidate {
index: Some(0),
content: Content {
parts: vec![
Part::FunctionCallPart(FunctionCallPart {
function_call: FunctionCall {
name: "function_1".to_string(),
args: json!({"arg": "value1"}),
},
thought_signature: Some("signature_1".to_string()),
}),
Part::FunctionCallPart(FunctionCallPart {
function_call: FunctionCall {
name: "function_2".to_string(),
args: json!({"arg": "value2"}),
},
thought_signature: None,
}),
],
role: GoogleRole::Model,
},
finish_reason: None,
finish_message: None,
safety_ratings: None,
citation_metadata: None,
}]),
prompt_feedback: None,
usage_metadata: None,
};
let events = mapper.map_event(response);
assert_eq!(events.len(), 3); // 2 ToolUse events + Stop event
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
assert_eq!(tool_use.name.as_ref(), "function_1");
assert_eq!(tool_use.thought_signature.as_deref(), Some("signature_1"));
} else {
panic!("Expected ToolUse event for function_1");
}
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[1] {
assert_eq!(tool_use.name.as_ref(), "function_2");
assert_eq!(tool_use.thought_signature, None);
} else {
panic!("Expected ToolUse event for function_2");
}
}
#[test]
fn test_tool_use_with_signature_converts_to_function_call_part() {
let tool_use = language_model::LanguageModelToolUse {
id: LanguageModelToolUseId::from("test_id"),
name: "test_function".into(),
raw_input: json!({"arg": "value"}).to_string(),
input: json!({"arg": "value"}),
is_input_complete: true,
thought_signature: Some("test_signature_456".to_string()),
};
let request = super::into_google(
LanguageModelRequest {
messages: vec![language_model::LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)],
cache: false,
}],
..Default::default()
},
"gemini-2.5-flash".to_string(),
GoogleModelMode::Default,
);
assert_eq!(request.contents[0].parts.len(), 1);
if let Part::FunctionCallPart(fc_part) = &request.contents[0].parts[0] {
assert_eq!(fc_part.function_call.name, "test_function");
assert_eq!(
fc_part.thought_signature.as_deref(),
Some("test_signature_456")
);
} else {
panic!("Expected FunctionCallPart");
}
}
#[test]
fn test_tool_use_without_signature_omits_field() {
let tool_use = language_model::LanguageModelToolUse {
id: LanguageModelToolUseId::from("test_id"),
name: "test_function".into(),
raw_input: json!({"arg": "value"}).to_string(),
input: json!({"arg": "value"}),
is_input_complete: true,
thought_signature: None,
};
let request = super::into_google(
LanguageModelRequest {
messages: vec![language_model::LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)],
cache: false,
}],
..Default::default()
},
"gemini-2.5-flash".to_string(),
GoogleModelMode::Default,
);
assert_eq!(request.contents[0].parts.len(), 1);
if let Part::FunctionCallPart(fc_part) = &request.contents[0].parts[0] {
assert_eq!(fc_part.thought_signature, None);
} else {
panic!("Expected FunctionCallPart");
}
}
#[test]
fn test_empty_signature_in_tool_use_normalized_to_none() {
let tool_use = language_model::LanguageModelToolUse {
id: LanguageModelToolUseId::from("test_id"),
name: "test_function".into(),
raw_input: json!({"arg": "value"}).to_string(),
input: json!({"arg": "value"}),
is_input_complete: true,
thought_signature: Some("".to_string()),
};
let request = super::into_google(
LanguageModelRequest {
messages: vec![language_model::LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)],
cache: false,
}],
..Default::default()
},
"gemini-2.5-flash".to_string(),
GoogleModelMode::Default,
);
if let Part::FunctionCallPart(fc_part) = &request.contents[0].parts[0] {
assert_eq!(fc_part.thought_signature, None);
} else {
panic!("Expected FunctionCallPart");
}
}
#[test]
fn test_round_trip_preserves_signature() {
let mut mapper = GoogleEventMapper::new();
// Simulate receiving a response from Google with a signature
let response = GenerateContentResponse {
candidates: Some(vec![GenerateContentCandidate {
index: Some(0),
content: Content {
parts: vec![Part::FunctionCallPart(FunctionCallPart {
function_call: FunctionCall {
name: "test_function".to_string(),
args: json!({"arg": "value"}),
},
thought_signature: Some("round_trip_sig".to_string()),
})],
role: GoogleRole::Model,
},
finish_reason: None,
finish_message: None,
safety_ratings: None,
citation_metadata: None,
}]),
prompt_feedback: None,
usage_metadata: None,
};
let events = mapper.map_event(response);
let tool_use = if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
tool_use.clone()
} else {
panic!("Expected ToolUse event");
};
// Convert back to Google format
let request = super::into_google(
LanguageModelRequest {
messages: vec![language_model::LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)],
cache: false,
}],
..Default::default()
},
"gemini-2.5-flash".to_string(),
GoogleModelMode::Default,
);
// Verify signature is preserved
if let Part::FunctionCallPart(fc_part) = &request.contents[0].parts[0] {
assert_eq!(fc_part.thought_signature.as_deref(), Some("round_trip_sig"));
} else {
panic!("Expected FunctionCallPart");
}
}
#[test]
fn test_mixed_text_and_function_call_with_signature() {
let mut mapper = GoogleEventMapper::new();
let response = GenerateContentResponse {
candidates: Some(vec![GenerateContentCandidate {
index: Some(0),
content: Content {
parts: vec![
Part::TextPart(TextPart {
text: "I'll help with that.".to_string(),
}),
Part::FunctionCallPart(FunctionCallPart {
function_call: FunctionCall {
name: "helper_function".to_string(),
args: json!({"query": "help"}),
},
thought_signature: Some("mixed_sig".to_string()),
}),
],
role: GoogleRole::Model,
},
finish_reason: None,
finish_message: None,
safety_ratings: None,
citation_metadata: None,
}]),
prompt_feedback: None,
usage_metadata: None,
};
let events = mapper.map_event(response);
assert_eq!(events.len(), 3); // Text event + ToolUse event + Stop event
if let Ok(LanguageModelCompletionEvent::Text(text)) = &events[0] {
assert_eq!(text, "I'll help with that.");
} else {
panic!("Expected Text event");
}
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[1] {
assert_eq!(tool_use.name.as_ref(), "helper_function");
assert_eq!(tool_use.thought_signature.as_deref(), Some("mixed_sig"));
} else {
panic!("Expected ToolUse event");
}
}
#[test]
fn test_special_characters_in_signature_preserved() {
let mut mapper = GoogleEventMapper::new();
let signature_with_special_chars = "sig<>\"'&%$#@!{}[]".to_string();
let response = GenerateContentResponse {
candidates: Some(vec![GenerateContentCandidate {
index: Some(0),
content: Content {
parts: vec![Part::FunctionCallPart(FunctionCallPart {
function_call: FunctionCall {
name: "test_function".to_string(),
args: json!({"arg": "value"}),
},
thought_signature: Some(signature_with_special_chars.clone()),
})],
role: GoogleRole::Model,
},
finish_reason: None,
finish_message: None,
safety_ratings: None,
citation_metadata: None,
}]),
prompt_feedback: None,
usage_metadata: None,
};
let events = mapper.map_event(response);
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
assert_eq!(
tool_use.thought_signature.as_deref(),
Some(signature_with_special_chars.as_str())
);
} else {
panic!("Expected ToolUse event");
}
}
}

View File

@@ -569,7 +569,6 @@ impl LmStudioEventMapper {
is_input_complete: true,
input,
raw_input: tool_call.arguments,
thought_signature: None,
},
)),
Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {

View File

@@ -720,7 +720,6 @@ impl MistralEventMapper {
is_input_complete: true,
input,
raw_input: tool_call.arguments,
thought_signature: None,
},
))),
Err(error) => {

View File

@@ -592,7 +592,6 @@ fn map_to_language_model_completion_events(
raw_input: function.arguments.to_string(),
input: function.arguments,
is_input_complete: true,
thought_signature: None,
});
events.push(Ok(event));
state.used_tools = true;

View File

@@ -586,7 +586,6 @@ impl OpenAiEventMapper {
is_input_complete: true,
input,
raw_input: tool_call.arguments.clone(),
thought_signature: None,
},
)),
Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {

View File

@@ -635,7 +635,6 @@ impl OpenRouterEventMapper {
is_input_complete: true,
input,
raw_input: tool_call.arguments.clone(),
thought_signature: None,
},
)),
Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {

View File

@@ -812,11 +812,13 @@ impl SearchableItem for LspLogView {
&mut self,
index: usize,
matches: &[Self::Match],
collapse: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor
.update(cx, |e, cx| e.activate_match(index, matches, window, cx))
self.editor.update(cx, |e, cx| {
e.activate_match(index, matches, collapse, window, cx)
})
}
fn select_matches(

View File

@@ -98,7 +98,6 @@ text.workspace = true
theme = { workspace = true, features = ["test-support"] }
tree-sitter-bash.workspace = true
tree-sitter-c.workspace = true
tree-sitter-cpp.workspace = true
tree-sitter-css.workspace = true
tree-sitter-go.workspace = true
tree-sitter-python.workspace = true

View File

@@ -44,11 +44,7 @@ mod tests {
let expect_indents_to =
|buffer: &mut Buffer, cx: &mut Context<Buffer>, input: &str, expected: &str| {
buffer.edit(
[(0..buffer.len(), input)],
Some(AutoindentMode::EachLine),
cx,
);
buffer.edit( [(0..buffer.len(), input)], Some(AutoindentMode::EachLine), cx, );
assert_eq!(buffer.text(), expected);
};

View File

@@ -395,10 +395,10 @@ mod tests {
use language::{AutoindentMode, Buffer};
use settings::SettingsStore;
use std::num::NonZeroU32;
use unindent::Unindent;
#[gpui::test]
async fn test_c_autoindent_basic(cx: &mut TestAppContext) {
async fn test_c_autoindent(cx: &mut TestAppContext) {
// cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
cx.update(|cx| {
let test_settings = SettingsStore::test(cx);
cx.set_global(test_settings);
@@ -413,229 +413,23 @@ mod tests {
cx.new(|cx| {
let mut buffer = Buffer::local("", cx).with_language(language, cx);
// empty function
buffer.edit([(0..0, "int main() {}")], None, cx);
// indent inside braces
let ix = buffer.len() - 1;
buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
assert_eq!(
buffer.text(),
"int main() {\n \n}",
"content inside braces should be indented"
);
assert_eq!(buffer.text(), "int main() {\n \n}");
buffer
});
}
// indent body of single-statement if statement
let ix = buffer.len() - 2;
buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx);
assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}");
#[gpui::test]
async fn test_c_autoindent_if_else(cx: &mut TestAppContext) {
cx.update(|cx| {
let test_settings = SettingsStore::test(cx);
cx.set_global(test_settings);
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |s| {
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
});
});
});
let language = crate::language("c", tree_sitter_c::LANGUAGE.into());
cx.new(|cx| {
let mut buffer = Buffer::local("", cx).with_language(language, cx);
buffer.edit(
[(
0..0,
r#"
int main() {
if (a)
b;
}
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
int main() {
if (a)
b;
}
"#
.unindent(),
"body of if-statement without braces should be indented"
);
let ix = buffer.len() - 4;
// indent inside field expression
let ix = buffer.len() - 3;
buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
assert_eq!(
buffer.text(),
r#"
int main() {
if (a)
b
.c;
}
"#
.unindent(),
"field expression (.c) should be indented further than the statement body"
);
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
buffer.edit(
[(
0..0,
r#"
int main() {
if (a) a++;
else b++;
}
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
int main() {
if (a) a++;
else b++;
}
"#
.unindent(),
"single-line if/else without braces should align at the same level"
);
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
buffer.edit(
[(
0..0,
r#"
int main() {
if (a)
b++;
else
c++;
}
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
int main() {
if (a)
b++;
else
c++;
}
"#
.unindent(),
"multi-line if/else without braces should indent statement bodies"
);
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
buffer.edit(
[(
0..0,
r#"
int main() {
if (a)
if (b)
c++;
}
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
int main() {
if (a)
if (b)
c++;
}
"#
.unindent(),
"nested if statements without braces should indent properly"
);
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
buffer.edit(
[(
0..0,
r#"
int main() {
if (a)
b++;
else if (c)
d++;
else
f++;
}
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
int main() {
if (a)
b++;
else if (c)
d++;
else
f++;
}
"#
.unindent(),
"else-if chains should align all conditions at same level with indented bodies"
);
buffer.edit([(0..buffer.len(), "")], Some(AutoindentMode::EachLine), cx);
buffer.edit(
[(
0..0,
r#"
int main() {
if (a) {
b++;
} else
c++;
}
"#
.unindent(),
)],
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
r#"
int main() {
if (a) {
b++;
} else
c++;
}
"#
.unindent(),
"mixed braces should indent properly"
);
assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}");
buffer
});

View File

@@ -4,7 +4,7 @@ path_suffixes = ["c"]
line_comments = ["// "]
decrease_indent_patterns = [
{ pattern = "^\\s*\\{.*\\}?\\s*$", valid_after = ["if", "for", "while", "do", "switch", "else"] },
{ pattern = "^\\s*else\\b", valid_after = ["if"] }
{ pattern = "^\\s*else\\s*$", valid_after = ["if"] }
]
autoclose_before = ";:.,=}])>"
brackets = [

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