Compare commits
2 Commits
vim-surrou
...
fix-user-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd7552f826 | ||
|
|
e737c6d790 |
@@ -10,15 +10,3 @@
|
||||
# Here, we opted to use `[target.'cfg(all())']` instead of `[build]` because `[target.'**']` is guaranteed to be cumulative.
|
||||
[target.'cfg(all())']
|
||||
rustflags = ["-D", "warnings"]
|
||||
|
||||
# Use Mold on Linux, because it's faster than GNU ld and LLD.
|
||||
#
|
||||
# We no longer set this in the default `config.toml` so that developers can opt in to Wild, which
|
||||
# is faster than Mold, in their own ~/.cargo/config.toml.
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
@@ -4,9 +4,16 @@ rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
perf-test = ["test", "--profile", "release-fast", "--lib", "--bins", "--tests", "--all-features", "--config", "target.'cfg(true)'.runner='cargo run -p perf --release'", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]"]
|
||||
# Keep similar flags here to share some ccache
|
||||
perf-compare = ["run", "--profile", "release-fast", "-p", "perf", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]", "--", "compare"]
|
||||
perf-test = ["test", "--profile", "release-fast", "--lib", "--bins", "--tests", "--config", "target.'cfg(true)'.runner='cargo run -p perf --release'", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]"]
|
||||
perf-compare = ["run", "--release", "-p", "perf", "--", "compare"]
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = [
|
||||
|
||||
@@ -26,7 +26,7 @@ third-party = [
|
||||
# build of remote_server should not include scap / its x11 dependency
|
||||
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
|
||||
# build of remote_server should not need to include on libalsa through rodio
|
||||
{ name = "rodio", git = "https://github.com/RustAudio/rodio" },
|
||||
{ name = "rodio", git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"},
|
||||
]
|
||||
|
||||
[final-excludes]
|
||||
@@ -37,6 +37,8 @@ workspace-members = [
|
||||
"zed_glsl",
|
||||
"zed_html",
|
||||
"zed_proto",
|
||||
"zed_ruff",
|
||||
"slash_commands_example",
|
||||
"zed_snippets",
|
||||
"zed_test_extension",
|
||||
]
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
name: Community Champion Auto Labeler
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
label_community_champion:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check if author is a community champion and apply label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const communityChampionBody = `${{ secrets.COMMUNITY_CHAMPIONS }}`;
|
||||
|
||||
const communityChampions = communityChampionBody
|
||||
.split('\n')
|
||||
.map(handle => handle.trim().toLowerCase());
|
||||
|
||||
let author;
|
||||
if (context.eventName === 'issues') {
|
||||
author = context.payload.issue.user.login;
|
||||
} else if (context.eventName === 'pull_request_target') {
|
||||
author = context.payload.pull_request.user.login;
|
||||
}
|
||||
|
||||
if (!author || !communityChampions.includes(author.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
|
||||
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: ['community champion']
|
||||
});
|
||||
|
||||
console.log(`Applied 'community champion' label to #${issueNumber} by ${author}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to apply label: ${error.message}`);
|
||||
}
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
send_release_notes_email:
|
||||
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
|
||||
if: github.repository_owner == 'zed-industries' && !github.event.release.prerelease
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
33
.github/workflows/issue_response.yml
vendored
Normal file
33
.github/workflows/issue_response.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Issue Response
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * 2"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
issue-response:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "script/issue_response/pnpm-lock.yaml"
|
||||
|
||||
- run: pnpm install --dir script/issue_response
|
||||
|
||||
- name: Run Issue Response
|
||||
run: pnpm run --dir script/issue_response start
|
||||
env:
|
||||
ISSUE_RESPONSE_GITHUB_TOKEN: ${{ secrets.ISSUE_RESPONSE_GITHUB_TOKEN }}
|
||||
SLACK_ISSUE_RESPONSE_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_RESPONSE_WEBHOOK_URL }}
|
||||
@@ -63,7 +63,6 @@ Although there are few hard and fast rules, typically we don't merge:
|
||||
- New file icons. Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner, please don't submit PRs with off-the-shelf SVGs.
|
||||
- Giant refactorings.
|
||||
- Non-trivial changes with no tests.
|
||||
- Stylistic code changes that do not alter any app logic. Reducing allocations, removing `.unwrap()`s, fixing typos is great; making code "more readable" — maybe not so much.
|
||||
- Features where (in our subjective opinion) the extra complexity isn't worth it for the number of people who will benefit.
|
||||
- Anything that seems completely AI generated.
|
||||
|
||||
|
||||
208
Cargo.lock
generated
208
Cargo.lock
generated
@@ -195,9 +195,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.4.3"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3aaa2bd05a2401887945f8bfd70026e90bc3cf96c62ab9eba2779835bf21dc60"
|
||||
checksum = "00e33b9f4bd34d342b6f80b7156d3a37a04aeec16313f264001e52d6a9118600"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -419,6 +419,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"shlex",
|
||||
"smol",
|
||||
"streaming_diff",
|
||||
"task",
|
||||
@@ -485,6 +486,7 @@ dependencies = [
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"component",
|
||||
"feature_flags",
|
||||
"gpui",
|
||||
"language_model",
|
||||
"serde",
|
||||
@@ -515,7 +517,7 @@ dependencies = [
|
||||
"rustix-openpty",
|
||||
"serde",
|
||||
"signal-hook",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.0",
|
||||
"vte",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -890,7 +892,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -1404,7 +1405,6 @@ dependencies = [
|
||||
"async-tar",
|
||||
"collections",
|
||||
"crossbeam",
|
||||
"denoise",
|
||||
"gpui",
|
||||
"libwebrtc",
|
||||
"log",
|
||||
@@ -2306,15 +2306,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4deb8f595ce7f00dee3543ebf6fd9a20ea86fc421ab79600dac30876250bdae"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
"bitflags 2.9.0",
|
||||
"bytemuck",
|
||||
"codespan-reporting",
|
||||
"codespan-reporting 0.11.1",
|
||||
"glow",
|
||||
"gpu-alloc",
|
||||
"gpu-alloc-ash",
|
||||
@@ -2332,7 +2331,6 @@ dependencies = [
|
||||
"objc2-metal",
|
||||
"objc2-quartz-core",
|
||||
"objc2-ui-kit",
|
||||
"once_cell",
|
||||
"raw-window-handle",
|
||||
"slab",
|
||||
"wasm-bindgen",
|
||||
@@ -2342,8 +2340,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2352,9 +2349,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "blade-util"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a6be3a82c001ba7a17b6f8e413ede5d1004e6047213f8efaf0ffc15b5c4904c"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"blade-graphics",
|
||||
"bytemuck",
|
||||
@@ -3083,7 +3079,6 @@ name = "cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askpass",
|
||||
"clap",
|
||||
"collections",
|
||||
"core-foundation 0.10.0",
|
||||
@@ -3220,7 +3215,6 @@ dependencies = [
|
||||
"indoc",
|
||||
"ordered-float 2.10.1",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"strum 0.27.1",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -3300,6 +3294,16 @@ dependencies = [
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.12.0"
|
||||
@@ -3308,7 +3312,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3384,6 +3388,7 @@ dependencies = [
|
||||
"reqwest 0.11.27",
|
||||
"reqwest_client",
|
||||
"rpc",
|
||||
"rustc-demangle",
|
||||
"scrypt",
|
||||
"sea-orm",
|
||||
"semantic_version",
|
||||
@@ -3576,7 +3581,7 @@ dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -3690,7 +3695,6 @@ dependencies = [
|
||||
"paths",
|
||||
"project",
|
||||
"rpc",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -4105,9 +4109,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.3.0"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
|
||||
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
@@ -4366,7 +4370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b4400e26ea4b99417e4263b1ce2d8452404d750ba0809a7bd043072593d430d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
"codespan-reporting 0.12.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"scratch",
|
||||
@@ -4380,7 +4384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31860c98f69fc14da5742c5deaf78983e846c7b27804ca8c8319e32eef421bde"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"codespan-reporting",
|
||||
"codespan-reporting 0.12.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
@@ -5119,6 +5123,7 @@ dependencies = [
|
||||
"client",
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -5165,13 +5170,11 @@ dependencies = [
|
||||
"collections",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"hashbrown 0.15.3",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
"ordered-float 2.10.1",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"regex",
|
||||
@@ -6264,7 +6267,6 @@ dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"core-foundation 0.10.0",
|
||||
"fsevent-sys 3.1.0",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"tempfile",
|
||||
"workspace-hack",
|
||||
@@ -6937,9 +6939,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "glow"
|
||||
version = "0.16.0"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
|
||||
checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"slotmap",
|
||||
@@ -7084,7 +7086,6 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pathfinder_geometry",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"profiling",
|
||||
"rand 0.9.1",
|
||||
"raw-window-handle",
|
||||
@@ -8385,28 +8386,6 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json_schema_store"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dap",
|
||||
"extension",
|
||||
"gpui",
|
||||
"language",
|
||||
"paths",
|
||||
"project",
|
||||
"schemars 1.0.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"snippet_provider",
|
||||
"task",
|
||||
"theme",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "0.30.0"
|
||||
@@ -8494,7 +8473,6 @@ dependencies = [
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"json_schema_store",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
@@ -8702,6 +8680,7 @@ dependencies = [
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
@@ -8726,6 +8705,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smol",
|
||||
"strum 0.27.1",
|
||||
"theme",
|
||||
"thiserror 2.0.12",
|
||||
"tiktoken-rs",
|
||||
"tokio",
|
||||
@@ -8812,16 +8792,17 @@ dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"collections",
|
||||
"dap",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"itertools 0.14.0",
|
||||
"json_schema_store",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"pet",
|
||||
"pet-conda",
|
||||
"pet-core",
|
||||
@@ -8834,6 +8815,7 @@ dependencies = [
|
||||
"regex",
|
||||
"rope",
|
||||
"rust-embed",
|
||||
"schemars 1.0.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
@@ -8841,6 +8823,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"shlex",
|
||||
"smol",
|
||||
"snippet_provider",
|
||||
"task",
|
||||
"tempfile",
|
||||
"text",
|
||||
@@ -9691,7 +9674,6 @@ dependencies = [
|
||||
"streaming-iterator",
|
||||
"tree-sitter",
|
||||
"tree-sitter-json",
|
||||
"unindent",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -9922,7 +9904,7 @@ dependencies = [
|
||||
"bit-set 0.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"cfg_aliases 0.2.1",
|
||||
"codespan-reporting",
|
||||
"codespan-reporting 0.12.0",
|
||||
"half",
|
||||
"hashbrown 0.15.3",
|
||||
"hexf-parse",
|
||||
@@ -12082,8 +12064,6 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"askpass",
|
||||
"async-compression",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"buffer_diff",
|
||||
@@ -12096,7 +12076,6 @@ dependencies = [
|
||||
"dap_adapters",
|
||||
"extension",
|
||||
"fancy-regex 0.14.0",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -12115,6 +12094,7 @@ dependencies = [
|
||||
"markdown",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"pathdiff",
|
||||
"paths",
|
||||
"postage",
|
||||
"prettier",
|
||||
@@ -12161,18 +12141,17 @@ dependencies = [
|
||||
"client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"criterion",
|
||||
"db",
|
||||
"editor",
|
||||
"file_icons",
|
||||
"git",
|
||||
"git_ui",
|
||||
"gpui",
|
||||
"indexmap 2.9.0",
|
||||
"language",
|
||||
"menu",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rayon",
|
||||
"schemars 1.0.1",
|
||||
"search",
|
||||
"serde",
|
||||
@@ -13064,7 +13043,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
"http_client",
|
||||
"json_schema_store",
|
||||
"language",
|
||||
"language_extension",
|
||||
"language_model",
|
||||
@@ -13423,7 +13401,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rodio"
|
||||
version = "0.21.1"
|
||||
source = "git+https://github.com/RustAudio/rodio#e2074c6c2acf07b57cf717e076bdda7a9ac6e70b"
|
||||
source = "git+https://github.com/RustAudio/rodio?branch=better_wav_output#82514bd1f2c6cfd9a1a885019b26a8ffea75bc5c"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"dasp_sample",
|
||||
@@ -14485,7 +14463,6 @@ dependencies = [
|
||||
"serde_with",
|
||||
"settings_macros",
|
||||
"smallvec",
|
||||
"strum 0.27.1",
|
||||
"tree-sitter",
|
||||
"tree-sitter-json",
|
||||
"unindent",
|
||||
@@ -14525,37 +14502,6 @@ dependencies = [
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "settings_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"client",
|
||||
"command_palette_hooks",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"node_runtime",
|
||||
"paths",
|
||||
"project",
|
||||
"serde",
|
||||
"session",
|
||||
"settings",
|
||||
"strum 0.27.1",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
@@ -14575,9 +14521,9 @@ checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
@@ -14833,7 +14779,6 @@ dependencies = [
|
||||
"paths",
|
||||
"schemars 1.0.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"snippet",
|
||||
"util",
|
||||
@@ -16095,7 +16040,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"libc",
|
||||
"log",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
@@ -16904,9 +16848,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.25.10"
|
||||
version = "0.25.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87"
|
||||
checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -17079,9 +17023,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-python"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c"
|
||||
version = "0.23.6"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-python?rev=218fcbf3fda3d029225f3dec005cb497d111b35e#218fcbf3fda3d029225f3dec005cb497d111b35e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
@@ -17429,6 +17372,12 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
@@ -17548,7 +17497,6 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"nix 0.29.0",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
@@ -17703,11 +17651,9 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"menu",
|
||||
"multi_buffer",
|
||||
"nvim-rs",
|
||||
"parking_lot",
|
||||
"perf",
|
||||
"picker",
|
||||
"project",
|
||||
"project_panel",
|
||||
@@ -19681,7 +19627,7 @@ dependencies = [
|
||||
"cipher",
|
||||
"clap",
|
||||
"clap_builder",
|
||||
"codespan-reporting",
|
||||
"codespan-reporting 0.12.0",
|
||||
"concurrent-queue",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
@@ -19745,6 +19691,7 @@ dependencies = [
|
||||
"nix 0.29.0",
|
||||
"nix 0.30.1",
|
||||
"nom 7.1.3",
|
||||
"num",
|
||||
"num-bigint",
|
||||
"num-bigint-dig",
|
||||
"num-complex",
|
||||
@@ -20201,7 +20148,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.208.0"
|
||||
version = "0.207.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
@@ -20210,6 +20157,7 @@ dependencies = [
|
||||
"agent_ui",
|
||||
"anyhow",
|
||||
"ashpd 0.11.0",
|
||||
"askpass",
|
||||
"assets",
|
||||
"assistant_tools",
|
||||
"audio",
|
||||
@@ -20259,7 +20207,6 @@ dependencies = [
|
||||
"install_cli",
|
||||
"itertools 0.14.0",
|
||||
"journal",
|
||||
"json_schema_store",
|
||||
"keymap_editor",
|
||||
"language",
|
||||
"language_extension",
|
||||
@@ -20306,7 +20253,6 @@ dependencies = [
|
||||
"session",
|
||||
"settings",
|
||||
"settings_profile_selector",
|
||||
"settings_ui",
|
||||
"shellexpand 2.1.2",
|
||||
"smol",
|
||||
"snippet_provider",
|
||||
@@ -20393,17 +20339,6 @@ dependencies = [
|
||||
"wit-bindgen 0.41.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen 0.41.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_glsl"
|
||||
version = "0.1.0"
|
||||
@@ -20413,9 +20348,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_html"
|
||||
version = "0.2.3"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -20425,6 +20360,21 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_ruff"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_snippets"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_test_extension"
|
||||
version = "0.1.0"
|
||||
@@ -20631,23 +20581,18 @@ dependencies = [
|
||||
"arrayvec",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"cloud_zeta2_prompt",
|
||||
"edit_prediction",
|
||||
"edit_prediction_context",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
"language_model",
|
||||
"log",
|
||||
"lsp",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"release_channel",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -20663,7 +20608,6 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"edit_prediction_context",
|
||||
"editor",
|
||||
@@ -20694,7 +20638,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"cloud_zeta2_prompt",
|
||||
"debug_adapter_extension",
|
||||
"edit_prediction_context",
|
||||
@@ -20708,9 +20651,7 @@ dependencies = [
|
||||
"language_model",
|
||||
"language_models",
|
||||
"languages",
|
||||
"log",
|
||||
"node_runtime",
|
||||
"ordered-float 2.10.1",
|
||||
"paths",
|
||||
"project",
|
||||
"prompt_store",
|
||||
@@ -20727,7 +20668,6 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
"zeta",
|
||||
"zeta2",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
24
Cargo.toml
24
Cargo.toml
@@ -91,7 +91,6 @@ members = [
|
||||
"crates/inspector_ui",
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
"crates/json_schema_store",
|
||||
"crates/keymap_editor",
|
||||
"crates/language",
|
||||
"crates/language_extension",
|
||||
@@ -152,7 +151,6 @@ members = [
|
||||
"crates/settings",
|
||||
"crates/settings_macros",
|
||||
"crates/settings_profile_selector",
|
||||
"crates/settings_ui",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
"crates/snippets_ui",
|
||||
@@ -212,7 +210,9 @@ members = [
|
||||
"extensions/glsl",
|
||||
"extensions/html",
|
||||
"extensions/proto",
|
||||
"extensions/ruff",
|
||||
"extensions/slash-commands-example",
|
||||
"extensions/snippets",
|
||||
"extensions/test-extension",
|
||||
|
||||
#
|
||||
@@ -321,7 +321,6 @@ zeta2_tools = { path = "crates/zeta2_tools" }
|
||||
inspector_ui = { path = "crates/inspector_ui" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
journal = { path = "crates/journal" }
|
||||
json_schema_store = { path = "crates/json_schema_store" }
|
||||
keymap_editor = { path = "crates/keymap_editor" }
|
||||
language = { path = "crates/language" }
|
||||
language_extension = { path = "crates/language_extension" }
|
||||
@@ -376,7 +375,7 @@ remote_server = { path = "crates/remote_server" }
|
||||
repl = { path = "crates/repl" }
|
||||
reqwest_client = { path = "crates/reqwest_client" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rodio = { git = "https://github.com/RustAudio/rodio" }
|
||||
rodio = { git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"}
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
@@ -384,7 +383,6 @@ search = { path = "crates/search" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
session = { path = "crates/session" }
|
||||
settings = { path = "crates/settings" }
|
||||
settings_macros = { path = "crates/settings_macros" }
|
||||
settings_ui = { path = "crates/settings_ui" }
|
||||
snippet = { path = "crates/snippet" }
|
||||
snippet_provider = { path = "crates/snippet_provider" }
|
||||
@@ -441,7 +439,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "0.4.3", features = ["unstable"] }
|
||||
agent-client-protocol = { version = "0.4.2", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
@@ -472,9 +470,9 @@ backtrace = "0.3"
|
||||
base64 = "0.22"
|
||||
bincode = "1.2.1"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { version = "0.7.0" }
|
||||
blade-macros = { version = "0.3.0" }
|
||||
blade-util = { version = "0.3.0" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
@@ -512,7 +510,6 @@ futures-lite = "1.13"
|
||||
git2 = { version = "0.20.1", default-features = false }
|
||||
globset = "0.4"
|
||||
handlebars = "4.3"
|
||||
hashbrown = "0.15.3"
|
||||
heck = "0.5"
|
||||
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
@@ -618,6 +615,7 @@ runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
rustc-demangle = "0.1.23"
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = { version = "0.23.26" }
|
||||
rustls-platform-verifier = "0.5.0"
|
||||
@@ -666,7 +664,7 @@ tokio = { version = "1" }
|
||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
||||
toml = "0.8"
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.10", features = ["wasm"] }
|
||||
tree-sitter = { version = "0.25.6", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.25.0"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
|
||||
@@ -683,7 +681,7 @@ tree-sitter-html = "0.23"
|
||||
tree-sitter-jsdoc = "0.23"
|
||||
tree-sitter-json = "0.24"
|
||||
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
|
||||
tree-sitter-python = "0.25"
|
||||
tree-sitter-python = { git = "https://github.com/zed-industries/tree-sitter-python", rev = "218fcbf3fda3d029225f3dec005cb497d111b35e" }
|
||||
tree-sitter-regex = "0.24"
|
||||
tree-sitter-ruby = "0.23"
|
||||
tree-sitter-rust = "0.24"
|
||||
@@ -810,7 +808,6 @@ image_viewer = { codegen-units = 1 }
|
||||
edit_prediction_button = { codegen-units = 1 }
|
||||
install_cli = { codegen-units = 1 }
|
||||
journal = { codegen-units = 1 }
|
||||
json_schema_store = { codegen-units = 1 }
|
||||
lmstudio = { codegen-units = 1 }
|
||||
menu = { codegen-units = 1 }
|
||||
notifications = { codegen-units = 1 }
|
||||
@@ -862,7 +859,6 @@ todo = "deny"
|
||||
declare_interior_mutable_const = "deny"
|
||||
|
||||
redundant_clone = "deny"
|
||||
disallowed_methods = "deny"
|
||||
|
||||
# We currently do not restrict any style rules
|
||||
# as it slows down shipping code to Zed.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.1645 4.45825L5.20344 9.52074C4.98225 9.74193 4.85798 10.0419 4.85798 10.3548C4.85798 10.6676 4.98225 10.9676 5.20344 11.1888C5.42464 11.41 5.72464 11.5342 6.03746 11.5342C6.35028 11.5342 6.65028 11.41 6.87148 11.1888L11.8326 6.12629C12.2749 5.68397 12.5234 5.08407 12.5234 4.45854C12.5234 3.83302 12.2749 3.23311 11.8326 2.7908C11.3902 2.34849 10.7903 2.1 10.1648 2.1C9.53928 2.1 8.93938 2.34849 8.49707 2.7908L3.55663 7.83265C3.22373 8.16017 2.95897 8.55037 2.77762 8.98072C2.59628 9.41108 2.50193 9.87308 2.50003 10.3401C2.49813 10.8071 2.58871 11.2698 2.76654 11.7017C2.94438 12.1335 3.20595 12.5258 3.53618 12.856C3.8664 13.1863 4.25873 13.4478 4.69055 13.6257C5.12237 13.8035 5.58513 13.8941 6.05213 13.8922C6.51913 13.8903 6.98114 13.7959 7.41149 13.6146C7.84185 13.4332 8.23204 13.1685 8.55957 12.8356L13.5 7.79373" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -250,7 +250,7 @@
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"ctrl-y": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"ctrl-alt-z": "agent::RejectOnce"
|
||||
"ctrl-d": "agent::RejectOnce"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"cmd-y": "agent::AllowOnce",
|
||||
"cmd-alt-y": "agent::AllowAlways",
|
||||
"cmd-alt-z": "agent::RejectOnce"
|
||||
"cmd-d": "agent::RejectOnce"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -251,7 +251,7 @@
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"ctrl-y": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"ctrl-alt-z": "agent::RejectOnce"
|
||||
"ctrl-d": "agent::RejectOnce"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -345,7 +345,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor && !use_modifier_to_send",
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
@@ -355,17 +355,6 @@
|
||||
"shift-tab": "agent::CycleModeSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor && use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"use_key_equivalents": true,
|
||||
@@ -618,6 +607,8 @@
|
||||
"shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
|
||||
"shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
|
||||
"shift-alt-0": "workspace::ResetOpenDocksSize",
|
||||
"ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
|
||||
"ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
@@ -1121,7 +1112,6 @@
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
"ctrl-delete": ["terminal::SendText", "\u001bd"],
|
||||
"ctrl-n": "workspace::NewTerminal",
|
||||
// Overrides for conflicting keybindings
|
||||
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// from the command palette.
|
||||
[
|
||||
{
|
||||
"context": "!GitPanel",
|
||||
"bindings": {
|
||||
"ctrl-g": "menu::Cancel"
|
||||
}
|
||||
|
||||
@@ -240,7 +240,6 @@
|
||||
"delete": "vim::DeleteRight",
|
||||
"g shift-j": "vim::JoinLinesNoWhitespace",
|
||||
"y": "vim::PushYank",
|
||||
"shift-y": "vim::YankToEndOfLine",
|
||||
"x": "vim::DeleteRight",
|
||||
"shift-x": "vim::DeleteLeft",
|
||||
"ctrl-a": "vim::Increment",
|
||||
@@ -393,7 +392,7 @@
|
||||
"escape": "editor::Cancel",
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"shift-y": "vim::YankToEndOfLine",
|
||||
"shift-y": "vim::YankLine",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
"o": "vim::InsertLineBelow",
|
||||
@@ -580,18 +579,18 @@
|
||||
// "q": "vim::AnyQuotes",
|
||||
"q": "vim::MiniQuotes",
|
||||
"|": "vim::VerticalBars",
|
||||
"(": ["vim::Parentheses", { "opening": true }],
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
"b": "vim::Parentheses",
|
||||
// "b": "vim::AnyBrackets",
|
||||
// "b": "vim::MiniBrackets",
|
||||
"[": ["vim::SquareBrackets", { "opening": true }],
|
||||
"[": "vim::SquareBrackets",
|
||||
"]": "vim::SquareBrackets",
|
||||
"r": "vim::SquareBrackets",
|
||||
"{": ["vim::CurlyBrackets", { "opening": true }],
|
||||
"{": "vim::CurlyBrackets",
|
||||
"}": "vim::CurlyBrackets",
|
||||
"shift-b": "vim::CurlyBrackets",
|
||||
"<": ["vim::AngleBrackets", { "opening": true }],
|
||||
"<": "vim::AngleBrackets",
|
||||
">": "vim::AngleBrackets",
|
||||
"a": "vim::Argument",
|
||||
"i": "vim::IndentObj",
|
||||
@@ -884,12 +883,10 @@
|
||||
"/": "project_panel::NewSearchInDirectory",
|
||||
"d": "project_panel::NewDirectory",
|
||||
"enter": "project_panel::OpenPermanent",
|
||||
"escape": "vim::ToggleProjectPanelFocus",
|
||||
"escape": "project_panel::ToggleFocus",
|
||||
"h": "project_panel::CollapseSelectedEntry",
|
||||
"j": "vim::MenuSelectNext",
|
||||
"k": "vim::MenuSelectPrevious",
|
||||
"down": "vim::MenuSelectNext",
|
||||
"up": "vim::MenuSelectPrevious",
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrevious",
|
||||
"l": "project_panel::ExpandSelectedEntry",
|
||||
"shift-d": "project_panel::Delete",
|
||||
"shift-r": "project_panel::Rename",
|
||||
@@ -908,22 +905,7 @@
|
||||
"{": "project_panel::SelectPrevDirectory",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "project_panel::SelectParent",
|
||||
"ctrl-u": "project_panel::ScrollUp",
|
||||
"ctrl-d": "project_panel::ScrollDown",
|
||||
"z t": "project_panel::ScrollCursorTop",
|
||||
"z z": "project_panel::ScrollCursorCenter",
|
||||
"z b": "project_panel::ScrollCursorBottom",
|
||||
"0": ["vim::Number", 0],
|
||||
"1": ["vim::Number", 1],
|
||||
"2": ["vim::Number", 2],
|
||||
"3": ["vim::Number", 3],
|
||||
"4": ["vim::Number", 4],
|
||||
"5": ["vim::Number", 5],
|
||||
"6": ["vim::Number", 6],
|
||||
"7": ["vim::Number", 7],
|
||||
"8": ["vim::Number", 8],
|
||||
"9": ["vim::Number", 9]
|
||||
"-": "project_panel::SelectParent"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,9 +29,7 @@ Generate {{content_type}} based on the following prompt:
|
||||
|
||||
Match the indentation in the original file in the inserted {{content_type}}, don't include any indentation on blank lines.
|
||||
|
||||
Return ONLY the {{content_type}} to insert. Do NOT include any XML tags like <document>, <insert_here>, or any surrounding markup from the input.
|
||||
|
||||
Respond with a code block containing the {{content_type}} to insert. Replace \{{INSERTED_CODE}} with your actual {{content_type}}:
|
||||
Immediately start with the following format with no remarks:
|
||||
|
||||
```
|
||||
\{{INSERTED_CODE}}
|
||||
@@ -68,9 +66,7 @@ Only make changes that are necessary to fulfill the prompt, leave everything els
|
||||
|
||||
Start at the indentation level in the original file in the rewritten {{content_type}}. Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions.
|
||||
|
||||
Return ONLY the rewritten {{content_type}}. Do NOT include any XML tags like <document>, <rewrite_this>, or any surrounding markup from the input.
|
||||
|
||||
Respond with a code block containing the rewritten {{content_type}}. Replace \{{REWRITTEN_CODE}} with your actual rewritten {{content_type}}:
|
||||
Immediately start with the following format with no remarks:
|
||||
|
||||
```
|
||||
\{{REWRITTEN_CODE}}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
/// The displayed name of this project. If not set or empty, the root directory name
|
||||
/// will be displayed.
|
||||
"project_name": "",
|
||||
"project_name": null,
|
||||
// The name of the Zed theme to use for the UI.
|
||||
//
|
||||
// `mode` is one of:
|
||||
@@ -117,7 +115,6 @@
|
||||
// Whether to enable vim modes and key bindings.
|
||||
"vim_mode": false,
|
||||
// Whether to enable helix mode and key bindings.
|
||||
// Enabling this mode will automatically enable vim mode.
|
||||
"helix_mode": false,
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
@@ -394,6 +391,8 @@
|
||||
"use_system_window_tabs": false,
|
||||
// Titlebar related settings
|
||||
"title_bar": {
|
||||
// When to show the title bar: "always" | "never" | "hide_in_full_screen".
|
||||
"show": "always",
|
||||
// Whether to show the branch icon beside branch switcher in the titlebar.
|
||||
"show_branch_icon": false,
|
||||
// Whether to show the branch name button in the titlebar.
|
||||
@@ -414,33 +413,15 @@
|
||||
"experimental.rodio_audio": false,
|
||||
// Requires 'rodio_audio: true'
|
||||
//
|
||||
// Automatically increase or decrease you microphone's volume. This affects how
|
||||
// loud you sound to others.
|
||||
//
|
||||
// Recommended: off (default)
|
||||
// Microphones are too quite in zed, until everyone is on experimental
|
||||
// audio and has auto speaker volume on this will make you very loud
|
||||
// compared to other speakers.
|
||||
"experimental.auto_microphone_volume": false,
|
||||
// Use the new audio systems automatic gain control for your microphone.
|
||||
// This affects how loud you sound to others.
|
||||
"experimental.control_input_volume": false,
|
||||
// Requires 'rodio_audio: true'
|
||||
//
|
||||
// Automatically increate or decrease the volume of other call members.
|
||||
// This only affects how things sound for you.
|
||||
"experimental.auto_speaker_volume": true,
|
||||
// Requires 'rodio_audio: true'
|
||||
//
|
||||
// Remove background noises. Works great for typing, cars, dogs, AC. Does
|
||||
// not work well on music.
|
||||
"experimental.denoise": true,
|
||||
// Requires 'rodio_audio: true'
|
||||
//
|
||||
// Use audio parameters compatible with the previous versions of
|
||||
// experimental audio and non-experimental audio. When this is false you
|
||||
// will sound strange to anyone not on the latest experimental audio. In
|
||||
// the future we will migrate by setting this to false
|
||||
//
|
||||
// You need to rejoin a call for this setting to apply
|
||||
"experimental.legacy_audio_compatible": true
|
||||
// Use the new audio systems automatic gain control on everyone in the
|
||||
// call. This makes call members who are too quite louder and those who are
|
||||
// too loud quieter. This only affects how things sound for you.
|
||||
"experimental.control_output_volume": false
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
@@ -1244,9 +1225,6 @@
|
||||
// The minimum column number to show the inline blame information at
|
||||
"min_column": 0
|
||||
},
|
||||
"blame": {
|
||||
"show_avatar": true
|
||||
},
|
||||
// Control which information is shown in the branch picker.
|
||||
"branch_picker": {
|
||||
"show_author_name": true
|
||||
@@ -1436,7 +1414,7 @@
|
||||
// "line_height": {
|
||||
// "custom": 2
|
||||
// },
|
||||
"line_height": "standard",
|
||||
"line_height": "comfortable",
|
||||
// Activate the python virtual environment, if one is found, in the
|
||||
// terminal's working directory (as resolved by the working_directory
|
||||
// setting). Set this to "off" to disable this behavior.
|
||||
@@ -1456,7 +1434,7 @@
|
||||
//
|
||||
// The shell running in the terminal needs to be configured to emit the title.
|
||||
// Example: `echo -e "\e]2;New Title\007";`
|
||||
"breadcrumbs": false
|
||||
"breadcrumbs": true
|
||||
},
|
||||
// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
|
||||
Binary file not shown.
@@ -192,7 +192,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"comment": {
|
||||
"color": "#5c6773ff",
|
||||
"color": "#abb5be8c",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
@@ -239,7 +239,7 @@
|
||||
"hint": {
|
||||
"color": "#628b80ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#ff8f3fff",
|
||||
@@ -583,7 +583,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"comment": {
|
||||
"color": "#abb0b6ff",
|
||||
"color": "#787b8099",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
@@ -630,7 +630,7 @@
|
||||
"hint": {
|
||||
"color": "#8ca7c2ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#fa8d3eff",
|
||||
@@ -974,7 +974,7 @@
|
||||
"font_weight": null
|
||||
},
|
||||
"comment": {
|
||||
"color": "#5c6773ff",
|
||||
"color": "#b8cfe680",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
@@ -1021,7 +1021,7 @@
|
||||
"hint": {
|
||||
"color": "#7399a3ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#ffad65ff",
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
"hint": {
|
||||
"color": "#8c957dff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#fb4833ff",
|
||||
@@ -653,7 +653,7 @@
|
||||
"hint": {
|
||||
"color": "#8c957dff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#fb4833ff",
|
||||
@@ -1058,7 +1058,7 @@
|
||||
"hint": {
|
||||
"color": "#8c957dff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#fb4833ff",
|
||||
@@ -1463,7 +1463,7 @@
|
||||
"hint": {
|
||||
"color": "#677562ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#9d0006ff",
|
||||
@@ -1868,7 +1868,7 @@
|
||||
"hint": {
|
||||
"color": "#677562ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#9d0006ff",
|
||||
@@ -2273,7 +2273,7 @@
|
||||
"hint": {
|
||||
"color": "#677562ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#9d0006ff",
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
"hint": {
|
||||
"color": "#788ca6ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#b477cfff",
|
||||
@@ -643,7 +643,7 @@
|
||||
"hint": {
|
||||
"color": "#7274a7ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
"font_weight": 700
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#a449abff",
|
||||
|
||||
11
clippy.toml
11
clippy.toml
@@ -5,14 +5,3 @@ ignore-interior-mutability = [
|
||||
# and Hash impls do not use fields with interior mutability.
|
||||
"agent::context::AgentContextKey"
|
||||
]
|
||||
disallowed-methods = [
|
||||
{ path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" },
|
||||
{ path = "std::process::Command::output", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::output" },
|
||||
{ path = "std::process::Command::status", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::status" },
|
||||
]
|
||||
disallowed-types = [
|
||||
# { path = "std::collections::HashMap", replacement = "collections::HashMap" },
|
||||
# { path = "std::collections::HashSet", replacement = "collections::HashSet" },
|
||||
# { path = "indexmap::IndexSet", replacement = "collections::IndexSet" },
|
||||
# { path = "indexmap::IndexMap", replacement = "collections::IndexMap" },
|
||||
]
|
||||
|
||||
@@ -573,7 +573,7 @@ impl ToolCallContent {
|
||||
))),
|
||||
acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
|
||||
Diff::finalized(
|
||||
diff.path.to_string_lossy().into_owned(),
|
||||
diff.path,
|
||||
diff.old_text,
|
||||
diff.new_text,
|
||||
language_registry,
|
||||
@@ -1780,26 +1780,20 @@ impl AcpThread {
|
||||
limit: Option<u32>,
|
||||
reuse_shared_snapshot: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<String, acp::Error>> {
|
||||
) -> Task<Result<String>> {
|
||||
// Args are 1-based, move to 0-based
|
||||
let line = line.unwrap_or_default().saturating_sub(1);
|
||||
let limit = limit.unwrap_or(u32::MAX);
|
||||
let project = self.project.clone();
|
||||
let action_log = self.action_log.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let load = project
|
||||
.update(cx, |project, cx| {
|
||||
let path = project
|
||||
.project_path_for_absolute_path(&path, cx)
|
||||
.ok_or_else(|| {
|
||||
acp::Error::resource_not_found(Some(path.display().to_string()))
|
||||
})?;
|
||||
Ok(project.open_buffer(path, cx))
|
||||
})
|
||||
.map_err(|e| acp::Error::internal_error().with_data(e.to_string()))
|
||||
.flatten()?;
|
||||
|
||||
let buffer = load.await?;
|
||||
let load = project.update(cx, |project, cx| {
|
||||
let path = project
|
||||
.project_path_for_absolute_path(&path, cx)
|
||||
.context("invalid path")?;
|
||||
anyhow::Ok(project.open_buffer(path, cx))
|
||||
});
|
||||
let buffer = load??.await?;
|
||||
|
||||
let snapshot = if reuse_shared_snapshot {
|
||||
this.read_with(cx, |this, _| {
|
||||
@@ -1826,17 +1820,15 @@ impl AcpThread {
|
||||
};
|
||||
|
||||
let max_point = snapshot.max_point();
|
||||
let start_position = Point::new(line, 0);
|
||||
|
||||
if start_position > max_point {
|
||||
return Err(acp::Error::invalid_params().with_data(format!(
|
||||
if line >= max_point.row {
|
||||
anyhow::bail!(
|
||||
"Attempting to read beyond the end of the file, line {}:{}",
|
||||
max_point.row + 1,
|
||||
max_point.column
|
||||
)));
|
||||
);
|
||||
}
|
||||
|
||||
let start = snapshot.anchor_before(start_position);
|
||||
let start = snapshot.anchor_before(Point::new(line, 0));
|
||||
let end = snapshot.anchor_before(Point::new(line.saturating_add(limit), 0));
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
@@ -1968,8 +1960,9 @@ impl AcpThread {
|
||||
|
||||
let env = cx.spawn(async move |_, _| {
|
||||
let mut env = env.await.unwrap_or_default();
|
||||
// Disables paging for `git` and hopefully other commands
|
||||
env.insert("PAGER".into(), "".into());
|
||||
if cfg!(unix) {
|
||||
env.insert("PAGER".into(), "cat".into());
|
||||
}
|
||||
for var in extra_env {
|
||||
env.insert(var.name, var.value);
|
||||
}
|
||||
@@ -1984,7 +1977,7 @@ impl AcpThread {
|
||||
let terminal_id = terminal_id.clone();
|
||||
async move |_this, cx| {
|
||||
let env = env.await;
|
||||
let (task_command, task_args) = ShellBuilder::new(
|
||||
let (command, args) = ShellBuilder::new(
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project
|
||||
@@ -1995,13 +1988,13 @@ impl AcpThread {
|
||||
&Shell::Program(get_default_system_shell()),
|
||||
)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(command.clone()), &args);
|
||||
.build(Some(command), &args);
|
||||
let terminal = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal_task(
|
||||
task::SpawnInTerminal {
|
||||
command: Some(task_command),
|
||||
args: task_args,
|
||||
command: Some(command.clone()),
|
||||
args: args.clone(),
|
||||
cwd: cwd.clone(),
|
||||
env,
|
||||
..Default::default()
|
||||
@@ -2456,81 +2449,6 @@ mod tests {
|
||||
|
||||
assert_eq!(content, "two\nthree\n");
|
||||
|
||||
// Invalid
|
||||
let err = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/tmp/foo").into(), Some(6), Some(2), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Invalid params: \"Attempting to read beyond the end of the file, line 5:0\""
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reading_empty_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/tmp"), json!({"foo": ""})).await;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let connection = Rc::new(FakeAgentConnection::new());
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Whole file
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content, "");
|
||||
|
||||
// Only start line
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/tmp/foo").into(), Some(1), None, false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content, "");
|
||||
|
||||
// Only limit
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content, "");
|
||||
|
||||
// Range
|
||||
let content = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/tmp/foo").into(), Some(1), Some(1), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content, "");
|
||||
|
||||
// Invalid
|
||||
let err = thread
|
||||
.update(cx, |thread, cx| {
|
||||
@@ -2541,40 +2459,9 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Invalid params: \"Attempting to read beyond the end of the file, line 1:0\""
|
||||
"Attempting to read beyond the end of the file, line 5:0"
|
||||
);
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_reading_non_existing_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/tmp"), json!({})).await;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree(path!("/tmp"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let connection = Rc::new(FakeAgentConnection::new());
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Out of project file
|
||||
let err = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(path!("/foo").into(), None, None, false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.code, acp::ErrorCode::RESOURCE_NOT_FOUND.code);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -6,7 +6,12 @@ use itertools::Itertools;
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer,
|
||||
};
|
||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
pub enum Diff {
|
||||
@@ -16,7 +21,7 @@ pub enum Diff {
|
||||
|
||||
impl Diff {
|
||||
pub fn finalized(
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
old_text: Option<String>,
|
||||
new_text: String,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
@@ -31,7 +36,7 @@ impl Diff {
|
||||
let buffer = new_buffer.clone();
|
||||
async move |_, cx| {
|
||||
let language = language_registry
|
||||
.load_language_for_file_path(Path::new(&path))
|
||||
.language_for_file_path(&path)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
@@ -147,15 +152,12 @@ impl Diff {
|
||||
let path = match self {
|
||||
Diff::Pending(PendingDiff {
|
||||
new_buffer: buffer, ..
|
||||
}) => buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| file.path().display(file.path_style(cx))),
|
||||
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_str().into()),
|
||||
}) => buffer.read(cx).file().map(|file| file.path().as_ref()),
|
||||
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
|
||||
};
|
||||
format!(
|
||||
"Diff: {}\n```\n{}\n```\n",
|
||||
path.unwrap_or("untitled".into()),
|
||||
path.unwrap_or(Path::new("untitled")).display(),
|
||||
buffer_text
|
||||
)
|
||||
}
|
||||
@@ -242,8 +244,8 @@ impl PendingDiff {
|
||||
.new_buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| file.path().display(file.path_style(cx)))
|
||||
.unwrap_or("untitled".into())
|
||||
.map(|file| file.path().as_ref())
|
||||
.unwrap_or(Path::new("untitled"))
|
||||
.into();
|
||||
|
||||
// Replace the buffer in the multibuffer with the snapshot
|
||||
@@ -346,7 +348,7 @@ impl PendingDiff {
|
||||
}
|
||||
|
||||
pub struct FinalizedDiff {
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
base_text: Arc<String>,
|
||||
new_buffer: Entity<Buffer>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
|
||||
@@ -126,39 +126,6 @@ impl MentionUri {
|
||||
abs_path: None,
|
||||
line_range,
|
||||
})
|
||||
} else if let Some(name) = path.strip_prefix("/agent/symbol/") {
|
||||
let fragment = url
|
||||
.fragment()
|
||||
.context("Missing fragment for untitled buffer selection")?;
|
||||
let line_range = parse_line_range(fragment)?;
|
||||
let path =
|
||||
single_query_param(&url, "path")?.context("Missing path for symbol")?;
|
||||
Ok(Self::Symbol {
|
||||
name: name.to_string(),
|
||||
abs_path: path.into(),
|
||||
line_range,
|
||||
})
|
||||
} else if path.starts_with("/agent/file") {
|
||||
let path =
|
||||
single_query_param(&url, "path")?.context("Missing path for file")?;
|
||||
Ok(Self::File {
|
||||
abs_path: path.into(),
|
||||
})
|
||||
} else if path.starts_with("/agent/directory") {
|
||||
let path =
|
||||
single_query_param(&url, "path")?.context("Missing path for directory")?;
|
||||
Ok(Self::Directory {
|
||||
abs_path: path.into(),
|
||||
})
|
||||
} else if path.starts_with("/agent/selection") {
|
||||
let fragment = url.fragment().context("Missing fragment for selection")?;
|
||||
let line_range = parse_line_range(fragment)?;
|
||||
let path =
|
||||
single_query_param(&url, "path")?.context("Missing path for selection")?;
|
||||
Ok(Self::Selection {
|
||||
abs_path: Some(path.into()),
|
||||
line_range,
|
||||
})
|
||||
} else {
|
||||
bail!("invalid zed url: {:?}", input);
|
||||
}
|
||||
@@ -213,29 +180,20 @@ impl MentionUri {
|
||||
pub fn to_uri(&self) -> Url {
|
||||
match self {
|
||||
MentionUri::File { abs_path } => {
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path("/agent/file");
|
||||
url.query_pairs_mut()
|
||||
.append_pair("path", &abs_path.to_string_lossy());
|
||||
url
|
||||
Url::from_file_path(abs_path).expect("mention path should be absolute")
|
||||
}
|
||||
MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
|
||||
MentionUri::Directory { abs_path } => {
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path("/agent/directory");
|
||||
url.query_pairs_mut()
|
||||
.append_pair("path", &abs_path.to_string_lossy());
|
||||
url
|
||||
Url::from_directory_path(abs_path).expect("mention path should be absolute")
|
||||
}
|
||||
MentionUri::Symbol {
|
||||
abs_path,
|
||||
name,
|
||||
line_range,
|
||||
} => {
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path(&format!("/agent/symbol/{name}"));
|
||||
url.query_pairs_mut()
|
||||
.append_pair("path", &abs_path.to_string_lossy());
|
||||
let mut url =
|
||||
Url::from_file_path(abs_path).expect("mention path should be absolute");
|
||||
url.query_pairs_mut().append_pair("symbol", name);
|
||||
url.set_fragment(Some(&format!(
|
||||
"L{}:{}",
|
||||
line_range.start() + 1,
|
||||
@@ -244,16 +202,15 @@ impl MentionUri {
|
||||
url
|
||||
}
|
||||
MentionUri::Selection {
|
||||
abs_path,
|
||||
abs_path: path,
|
||||
line_range,
|
||||
} => {
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
if let Some(abs_path) = abs_path {
|
||||
url.set_path("/agent/selection");
|
||||
url.query_pairs_mut()
|
||||
.append_pair("path", &abs_path.to_string_lossy());
|
||||
let mut url = if let Some(path) = path {
|
||||
Url::from_file_path(path).expect("mention path should be absolute")
|
||||
} else {
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path("/agent/untitled-buffer");
|
||||
url
|
||||
};
|
||||
url.set_fragment(Some(&format!(
|
||||
"L{}:{}",
|
||||
@@ -338,32 +295,37 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_file_uri() {
|
||||
let old_uri = uri!("file:///path/to/file.rs");
|
||||
let parsed = MentionUri::parse(old_uri).unwrap();
|
||||
let file_uri = uri!("file:///path/to/file.rs");
|
||||
let parsed = MentionUri::parse(file_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::File { abs_path } => {
|
||||
assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/file.rs"));
|
||||
}
|
||||
_ => panic!("Expected File variant"),
|
||||
}
|
||||
let new_uri = parsed.to_uri().to_string();
|
||||
assert!(new_uri.starts_with("zed:///agent/file"));
|
||||
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
|
||||
assert_eq!(parsed.to_uri().to_string(), file_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_directory_uri() {
|
||||
let old_uri = uri!("file:///path/to/dir/");
|
||||
let parsed = MentionUri::parse(old_uri).unwrap();
|
||||
let file_uri = uri!("file:///path/to/dir/");
|
||||
let parsed = MentionUri::parse(file_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Directory { abs_path } => {
|
||||
assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/dir/"));
|
||||
}
|
||||
_ => panic!("Expected Directory variant"),
|
||||
}
|
||||
let new_uri = parsed.to_uri().to_string();
|
||||
assert!(new_uri.starts_with("zed:///agent/directory"));
|
||||
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
|
||||
assert_eq!(parsed.to_uri().to_string(), file_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_directory_uri_with_slash() {
|
||||
let uri = MentionUri::Directory {
|
||||
abs_path: PathBuf::from(path!("/path/to/dir/")),
|
||||
};
|
||||
let expected = uri!("file:///path/to/dir/");
|
||||
assert_eq!(uri.to_uri().to_string(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -371,15 +333,14 @@ mod tests {
|
||||
let uri = MentionUri::Directory {
|
||||
abs_path: PathBuf::from(path!("/path/to/dir")),
|
||||
};
|
||||
let uri_string = uri.to_uri().to_string();
|
||||
assert!(uri_string.starts_with("zed:///agent/directory"));
|
||||
assert_eq!(MentionUri::parse(&uri_string).unwrap(), uri);
|
||||
let expected = uri!("file:///path/to/dir/");
|
||||
assert_eq!(uri.to_uri().to_string(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_symbol_uri() {
|
||||
let old_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
|
||||
let parsed = MentionUri::parse(old_uri).unwrap();
|
||||
let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
|
||||
let parsed = MentionUri::parse(symbol_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Symbol {
|
||||
abs_path: path,
|
||||
@@ -393,15 +354,13 @@ mod tests {
|
||||
}
|
||||
_ => panic!("Expected Symbol variant"),
|
||||
}
|
||||
let new_uri = parsed.to_uri().to_string();
|
||||
assert!(new_uri.starts_with("zed:///agent/symbol/MySymbol"));
|
||||
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
|
||||
assert_eq!(parsed.to_uri().to_string(), symbol_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_selection_uri() {
|
||||
let old_uri = uri!("file:///path/to/file.rs#L5:15");
|
||||
let parsed = MentionUri::parse(old_uri).unwrap();
|
||||
let selection_uri = uri!("file:///path/to/file.rs#L5:15");
|
||||
let parsed = MentionUri::parse(selection_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Selection {
|
||||
abs_path: path,
|
||||
@@ -416,9 +375,7 @@ mod tests {
|
||||
}
|
||||
_ => panic!("Expected Selection variant"),
|
||||
}
|
||||
let new_uri = parsed.to_uri().to_string();
|
||||
assert!(new_uri.starts_with("zed:///agent/selection"));
|
||||
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
|
||||
assert_eq!(parsed.to_uri().to_string(), selection_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,7 +8,10 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
|
||||
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
|
||||
use std::{cmp, ops::Range, sync::Arc};
|
||||
use text::{Edit, Patch, Rope};
|
||||
use util::{RangeExt, ResultExt as _};
|
||||
use util::{
|
||||
RangeExt, ResultExt as _,
|
||||
paths::{PathStyle, RemotePathBuf},
|
||||
};
|
||||
|
||||
/// Tracks actions performed by tools in a thread
|
||||
pub struct ActionLog {
|
||||
@@ -59,13 +62,7 @@ impl ActionLog {
|
||||
let file_path = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| {
|
||||
let mut path = file.full_path(cx).to_string_lossy().into_owned();
|
||||
if file.path_style(cx).is_windows() {
|
||||
path = path.replace('\\', "/");
|
||||
}
|
||||
path
|
||||
})
|
||||
.map(|file| RemotePathBuf::new(file.full_path(cx), PathStyle::Posix).to_proto())
|
||||
.unwrap_or_else(|| format!("buffer_{}", buffer.entity_id()));
|
||||
|
||||
let mut result = String::new();
|
||||
@@ -2304,7 +2301,7 @@ mod tests {
|
||||
.await;
|
||||
fs.set_head_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[("file.txt", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
|
||||
&[("file.txt".into(), "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
|
||||
"0000000",
|
||||
);
|
||||
cx.run_until_parked();
|
||||
@@ -2387,7 +2384,7 @@ mod tests {
|
||||
// - Ignores the last line edit (j stays as j)
|
||||
fs.set_head_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[("file.txt", "A\nb\nc\nf\nG\nh\ni\nj".into())],
|
||||
&[("file.txt".into(), "A\nb\nc\nf\nG\nh\ni\nj".into())],
|
||||
"0000001",
|
||||
);
|
||||
cx.run_until_parked();
|
||||
@@ -2418,7 +2415,10 @@ mod tests {
|
||||
// Make another commit that accepts the NEW line but with different content
|
||||
fs.set_head_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[("file.txt", "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into())],
|
||||
&[(
|
||||
"file.txt".into(),
|
||||
"A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into(),
|
||||
)],
|
||||
"0000002",
|
||||
);
|
||||
cx.run_until_parked();
|
||||
@@ -2444,7 +2444,7 @@ mod tests {
|
||||
// Final commit that accepts all remaining edits
|
||||
fs.set_head_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[("file.txt", "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
|
||||
&[("file.txt".into(), "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
|
||||
"0000003",
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -9,14 +9,12 @@ pub mod tool_use;
|
||||
|
||||
pub use context::{AgentContext, ContextId, ContextLoadResult};
|
||||
pub use context_store::ContextStore;
|
||||
use fs::Fs;
|
||||
use std::sync::Arc;
|
||||
pub use thread::{
|
||||
LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
|
||||
ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio,
|
||||
};
|
||||
pub use thread_store::{SerializedThread, TextThreadStore, ThreadStore};
|
||||
|
||||
pub fn init(fs: Arc<dyn Fs>, cx: &mut gpui::App) {
|
||||
thread_store::init(fs, cx);
|
||||
pub fn init(cx: &mut gpui::App) {
|
||||
thread_store::init(cx);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ use std::path::PathBuf;
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
use text::{Anchor, OffsetRangeExt as _};
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
use util::rel_path::RelPath;
|
||||
use util::{ResultExt as _, post_inc};
|
||||
|
||||
pub const RULES_ICON: IconName = IconName::Reader;
|
||||
@@ -159,7 +158,7 @@ pub struct FileContextHandle {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileContext {
|
||||
pub handle: FileContextHandle,
|
||||
pub full_path: String,
|
||||
pub full_path: Arc<Path>,
|
||||
pub text: SharedString,
|
||||
pub is_outline: bool,
|
||||
}
|
||||
@@ -187,7 +186,7 @@ impl FileContextHandle {
|
||||
log::error!("file context missing path");
|
||||
return Task::ready(None);
|
||||
};
|
||||
let full_path = file.full_path(cx).to_string_lossy().into_owned();
|
||||
let full_path: Arc<Path> = file.full_path(cx).into();
|
||||
let rope = buffer_ref.as_rope().clone();
|
||||
let buffer = self.buffer.clone();
|
||||
|
||||
@@ -236,14 +235,14 @@ pub struct DirectoryContextHandle {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirectoryContext {
|
||||
pub handle: DirectoryContextHandle,
|
||||
pub full_path: String,
|
||||
pub full_path: Arc<Path>,
|
||||
pub descendants: Vec<DirectoryContextDescendant>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirectoryContextDescendant {
|
||||
/// Path within the directory.
|
||||
pub rel_path: Arc<RelPath>,
|
||||
pub rel_path: Arc<Path>,
|
||||
pub fenced_codeblock: SharedString,
|
||||
}
|
||||
|
||||
@@ -274,16 +273,13 @@ impl DirectoryContextHandle {
|
||||
}
|
||||
|
||||
let directory_path = entry.path.clone();
|
||||
let directory_full_path = worktree_ref
|
||||
.full_path(&directory_path)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let directory_full_path = worktree_ref.full_path(&directory_path).into();
|
||||
|
||||
let file_paths = collect_files_in_path(worktree_ref, &directory_path);
|
||||
let descendants_future = future::join_all(file_paths.into_iter().map(|path| {
|
||||
let worktree_ref = worktree.read(cx);
|
||||
let worktree_id = worktree_ref.id();
|
||||
let full_path = worktree_ref.full_path(&path).to_string_lossy().into_owned();
|
||||
let full_path = worktree_ref.full_path(&path);
|
||||
|
||||
let rel_path = path
|
||||
.strip_prefix(&directory_path)
|
||||
@@ -364,7 +360,7 @@ pub struct SymbolContextHandle {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SymbolContext {
|
||||
pub handle: SymbolContextHandle,
|
||||
pub full_path: String,
|
||||
pub full_path: Arc<Path>,
|
||||
pub line_range: Range<Point>,
|
||||
pub text: SharedString,
|
||||
}
|
||||
@@ -403,7 +399,7 @@ impl SymbolContextHandle {
|
||||
log::error!("symbol context's file has no path");
|
||||
return Task::ready(None);
|
||||
};
|
||||
let full_path = file.full_path(cx).to_string_lossy().into_owned();
|
||||
let full_path = file.full_path(cx).into();
|
||||
let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot());
|
||||
let text = self.text(cx);
|
||||
let buffer = self.buffer.clone();
|
||||
@@ -437,7 +433,7 @@ pub struct SelectionContextHandle {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SelectionContext {
|
||||
pub handle: SelectionContextHandle,
|
||||
pub full_path: String,
|
||||
pub full_path: Arc<Path>,
|
||||
pub line_range: Range<Point>,
|
||||
pub text: SharedString,
|
||||
}
|
||||
@@ -476,7 +472,7 @@ impl SelectionContextHandle {
|
||||
let text = self.text(cx);
|
||||
let buffer = self.buffer.clone();
|
||||
let context = AgentContext::Selection(SelectionContext {
|
||||
full_path: full_path.to_string_lossy().into_owned(),
|
||||
full_path: full_path.into(),
|
||||
line_range: self.line_range(cx),
|
||||
text,
|
||||
handle: self,
|
||||
@@ -706,7 +702,7 @@ impl Display for RulesContext {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImageContext {
|
||||
pub project_path: Option<ProjectPath>,
|
||||
pub full_path: Option<String>,
|
||||
pub full_path: Option<Arc<Path>>,
|
||||
pub original_image: Arc<gpui::Image>,
|
||||
// TODO: handle this elsewhere and remove `ignore-interior-mutability` opt-out in clippy.toml
|
||||
// needed due to a false positive of `clippy::mutable_key_type`.
|
||||
@@ -972,7 +968,7 @@ pub fn load_context(
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<Arc<RelPath>> {
|
||||
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
for entry in worktree.child_entries(path) {
|
||||
@@ -986,17 +982,14 @@ fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<Arc<RelPath
|
||||
files
|
||||
}
|
||||
|
||||
fn codeblock_tag(full_path: &str, line_range: Option<Range<Point>>) -> String {
|
||||
fn codeblock_tag(full_path: &Path, line_range: Option<Range<Point>>) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
if let Some(extension) = Path::new(full_path)
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
{
|
||||
if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
|
||||
let _ = write!(result, "{} ", extension);
|
||||
}
|
||||
|
||||
let _ = write!(result, "{}", full_path);
|
||||
let _ = write!(result, "{}", full_path.display());
|
||||
|
||||
if let Some(range) = line_range {
|
||||
if range.start.row == range.end.row {
|
||||
|
||||
@@ -14,10 +14,7 @@ use futures::{self, FutureExt};
|
||||
use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
|
||||
use language::{Buffer, File as _};
|
||||
use language_model::LanguageModelImage;
|
||||
use project::{
|
||||
Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file,
|
||||
lsp_store::SymbolLocation,
|
||||
};
|
||||
use project::{Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file};
|
||||
use prompt_store::UserPromptId;
|
||||
use ref_cast::RefCast as _;
|
||||
use std::{
|
||||
@@ -312,7 +309,7 @@ impl ContextStore {
|
||||
let item = image_item.read(cx);
|
||||
this.insert_image(
|
||||
Some(item.project_path(cx)),
|
||||
Some(item.file.full_path(cx).to_string_lossy().into_owned()),
|
||||
Some(item.file.full_path(cx).into()),
|
||||
item.image.clone(),
|
||||
remove_if_exists,
|
||||
cx,
|
||||
@@ -328,7 +325,7 @@ impl ContextStore {
|
||||
fn insert_image(
|
||||
&mut self,
|
||||
project_path: Option<ProjectPath>,
|
||||
full_path: Option<String>,
|
||||
full_path: Option<Arc<Path>>,
|
||||
image: Arc<Image>,
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<ContextStore>,
|
||||
@@ -503,7 +500,7 @@ impl ContextStore {
|
||||
let Some(context_path) = buffer.project_path(cx) else {
|
||||
return false;
|
||||
};
|
||||
if symbol.path != SymbolLocation::InProject(context_path) {
|
||||
if context_path != symbol.path {
|
||||
return false;
|
||||
}
|
||||
let context_range = context.range.to_point_utf16(&buffer.snapshot());
|
||||
|
||||
@@ -155,7 +155,7 @@ impl HistoryStore {
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
HistoryEntryId::Context(path) => path.file_name().map(|file| {
|
||||
SerializedRecentOpen::ContextName(file.to_string_lossy().into_owned())
|
||||
SerializedRecentOpen::ContextName(file.to_string_lossy().to_string())
|
||||
}),
|
||||
HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::Thread(id.to_string())),
|
||||
})
|
||||
|
||||
@@ -234,6 +234,7 @@ impl MessageSegment {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ProjectSnapshot {
|
||||
pub worktree_snapshots: Vec<WorktreeSnapshot>,
|
||||
pub unsaved_buffer_paths: Vec<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
@@ -2856,11 +2857,27 @@ impl Thread {
|
||||
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
|
||||
.collect();
|
||||
|
||||
cx.spawn(async move |_, _| {
|
||||
cx.spawn(async move |_, cx| {
|
||||
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
|
||||
|
||||
let mut unsaved_buffers = Vec::new();
|
||||
cx.update(|app_cx| {
|
||||
let buffer_store = project.read(app_cx).buffer_store();
|
||||
for buffer_handle in buffer_store.read(app_cx).buffers() {
|
||||
let buffer = buffer_handle.read(app_cx);
|
||||
if buffer.is_dirty()
|
||||
&& let Some(file) = buffer.file()
|
||||
{
|
||||
let path = file.path().to_string_lossy().to_string();
|
||||
unsaved_buffers.push(path);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
Arc::new(ProjectSnapshot {
|
||||
worktree_snapshots,
|
||||
unsaved_buffer_paths: unsaved_buffers,
|
||||
timestamp: Utc::now(),
|
||||
})
|
||||
})
|
||||
@@ -2875,7 +2892,7 @@ impl Thread {
|
||||
// Get worktree path and snapshot
|
||||
let worktree_info = cx.update(|app_cx| {
|
||||
let worktree = worktree.read(app_cx);
|
||||
let path = worktree.abs_path().to_string_lossy().into_owned();
|
||||
let path = worktree.abs_path().to_string_lossy().to_string();
|
||||
let snapshot = worktree.snapshot();
|
||||
(path, snapshot)
|
||||
});
|
||||
@@ -3258,7 +3275,6 @@ mod tests {
|
||||
use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use assistant_tool::ToolRegistry;
|
||||
use assistant_tools;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::stream::BoxStream;
|
||||
@@ -3282,10 +3298,9 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_message_with_context(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3360,10 +3375,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_only_include_new_contexts(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({
|
||||
"file1.rs": "fn function1() {}\n",
|
||||
@@ -3517,10 +3531,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_message_without_files(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3597,10 +3610,9 @@ fn main() {{
|
||||
#[gpui::test]
|
||||
#[ignore] // turn this test on when project_notifications tool is re-enabled
|
||||
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3726,10 +3738,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3749,10 +3760,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_serializing_thread_profile(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3793,10 +3803,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_temperature_setting(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
&fs,
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
@@ -3888,9 +3897,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
@@ -3973,9 +3982,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
@@ -3995,9 +4004,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
@@ -4149,9 +4158,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retry_on_overloaded_error(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4227,9 +4236,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retry_on_internal_server_error(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4309,9 +4318,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_exponential_backoff_on_retries(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4429,9 +4438,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_max_retries_exceeded(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4520,9 +4529,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retry_message_removed_on_retry(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4693,9 +4702,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_successful_completion_clears_retry_state(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -4859,9 +4868,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rate_limit_retry_single_attempt(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -5044,9 +5053,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ui_only_messages_not_sent_to_model(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Insert a regular user message
|
||||
@@ -5144,9 +5153,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_no_retry_without_burn_mode(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Ensure we're in Normal mode (not Burn mode)
|
||||
@@ -5217,9 +5226,9 @@ fn main() {{
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) {
|
||||
let fs = init_test_settings(cx);
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(&fs, cx, json!({})).await;
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
@@ -5325,8 +5334,7 @@ fn main() {{
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn init_test_settings(cx: &mut TestAppContext) -> Arc<dyn Fs> {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fn init_test_settings(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
@@ -5334,7 +5342,7 @@ fn main() {{
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(fs.clone(), cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
language_model::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
@@ -5348,17 +5356,16 @@ fn main() {{
|
||||
));
|
||||
assistant_tools::init(http_client, cx);
|
||||
});
|
||||
fs
|
||||
}
|
||||
|
||||
// Helper to create a test project with test files
|
||||
async fn create_test_project(
|
||||
fs: &Arc<dyn Fs>,
|
||||
cx: &mut TestAppContext,
|
||||
files: serde_json::Value,
|
||||
) -> Entity<Project> {
|
||||
fs.as_fake().insert_tree(path!("/test"), files).await;
|
||||
Project::test(fs.clone(), [path!("/test").as_ref()], cx).await
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/test"), files).await;
|
||||
Project::test(fs, [path!("/test").as_ref()], cx).await
|
||||
}
|
||||
|
||||
async fn setup_test_environment(
|
||||
|
||||
@@ -10,7 +10,6 @@ use assistant_tool::{Tool, ToolId, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{
|
||||
FutureExt as _, StreamExt as _,
|
||||
channel::{mpsc, oneshot},
|
||||
@@ -40,7 +39,7 @@ use std::{
|
||||
rc::Rc,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use util::{ResultExt as _, rel_path::RelPath};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use zed_env_vars::ZED_STATELESS;
|
||||
|
||||
@@ -86,8 +85,8 @@ const RULES_FILE_NAMES: [&str; 9] = [
|
||||
"GEMINI.md",
|
||||
];
|
||||
|
||||
pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
ThreadsDatabase::init(fs, cx);
|
||||
pub fn init(cx: &mut App) {
|
||||
ThreadsDatabase::init(cx);
|
||||
}
|
||||
|
||||
/// A system prompt shared by all threads created by this ThreadStore
|
||||
@@ -235,7 +234,7 @@ impl ThreadStore {
|
||||
if items.iter().any(|(path, _, _)| {
|
||||
RULES_FILE_NAMES
|
||||
.iter()
|
||||
.any(|name| path.as_ref() == RelPath::unix(name).unwrap())
|
||||
.any(|name| path.as_ref() == Path::new(name))
|
||||
}) {
|
||||
self.enqueue_system_prompt_reload();
|
||||
}
|
||||
@@ -328,7 +327,7 @@ impl ThreadStore {
|
||||
cx: &mut App,
|
||||
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
|
||||
let tree = worktree.read(cx);
|
||||
let root_name = tree.root_name_str().into();
|
||||
let root_name = tree.root_name().into();
|
||||
let abs_path = tree.abs_path();
|
||||
|
||||
let mut context = WorktreeContext {
|
||||
@@ -368,7 +367,7 @@ impl ThreadStore {
|
||||
.into_iter()
|
||||
.filter_map(|name| {
|
||||
worktree
|
||||
.entry_for_path(RelPath::unix(name).unwrap())
|
||||
.entry_for_path(name)
|
||||
.filter(|entry| entry.is_file())
|
||||
.map(|entry| entry.path.clone())
|
||||
})
|
||||
@@ -870,13 +869,13 @@ impl ThreadsDatabase {
|
||||
GlobalThreadsDatabase::global(cx).0.clone()
|
||||
}
|
||||
|
||||
fn init(fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
fn init(cx: &mut App) {
|
||||
let executor = cx.background_executor().clone();
|
||||
let database_future = executor
|
||||
.spawn({
|
||||
let executor = executor.clone();
|
||||
let threads_dir = paths::data_dir().join("threads");
|
||||
async move { ThreadsDatabase::new(fs, threads_dir, executor).await }
|
||||
async move { ThreadsDatabase::new(threads_dir, executor) }
|
||||
})
|
||||
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
|
||||
.boxed()
|
||||
@@ -885,17 +884,13 @@ impl ThreadsDatabase {
|
||||
cx.set_global(GlobalThreadsDatabase(database_future));
|
||||
}
|
||||
|
||||
pub async fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
threads_dir: PathBuf,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Result<Self> {
|
||||
fs.create_dir(&threads_dir).await?;
|
||||
pub fn new(threads_dir: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
|
||||
std::fs::create_dir_all(&threads_dir)?;
|
||||
|
||||
let sqlite_path = threads_dir.join("threads.db");
|
||||
let mdb_path = threads_dir.join("threads-db.1.mdb");
|
||||
|
||||
let needs_migration_from_heed = fs.is_file(&mdb_path).await;
|
||||
let needs_migration_from_heed = mdb_path.exists();
|
||||
|
||||
let connection = if *ZED_STATELESS {
|
||||
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
|
||||
@@ -937,14 +932,7 @@ impl ThreadsDatabase {
|
||||
.spawn(async move {
|
||||
log::info!("Starting threads.db migration");
|
||||
Self::migrate_from_heed(&mdb_path, db_connection, executor_clone)?;
|
||||
fs.remove_dir(
|
||||
&mdb_path,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
std::fs::remove_dir_all(mdb_path)?;
|
||||
log::info!("threads.db migrated to sqlite");
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
|
||||
@@ -27,7 +27,6 @@ use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
use util::rel_path::RelPath;
|
||||
|
||||
const RULES_FILE_NAMES: [&str; 9] = [
|
||||
".rules",
|
||||
@@ -435,7 +434,7 @@ impl NativeAgent {
|
||||
cx: &mut App,
|
||||
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
|
||||
let tree = worktree.read(cx);
|
||||
let root_name = tree.root_name_str().into();
|
||||
let root_name = tree.root_name().into();
|
||||
let abs_path = tree.abs_path();
|
||||
|
||||
let mut context = WorktreeContext {
|
||||
@@ -475,7 +474,7 @@ impl NativeAgent {
|
||||
.into_iter()
|
||||
.filter_map(|name| {
|
||||
worktree
|
||||
.entry_for_path(RelPath::unix(name).unwrap())
|
||||
.entry_for_path(name)
|
||||
.filter(|entry| entry.is_file())
|
||||
.map(|entry| entry.path.clone())
|
||||
})
|
||||
@@ -559,7 +558,7 @@ impl NativeAgent {
|
||||
if items.iter().any(|(path, _, _)| {
|
||||
RULES_FILE_NAMES
|
||||
.iter()
|
||||
.any(|name| path.as_ref() == RelPath::unix(name).unwrap())
|
||||
.any(|name| path.as_ref() == Path::new(name))
|
||||
}) {
|
||||
self.project_context_needs_refresh.send(()).ok();
|
||||
}
|
||||
@@ -1205,11 +1204,11 @@ mod tests {
|
||||
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri};
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use indoc::formatdoc;
|
||||
use indoc::indoc;
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::{path, rel_path::rel_path};
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_maintaining_project_context(cx: &mut TestAppContext) {
|
||||
@@ -1259,17 +1258,14 @@ mod tests {
|
||||
fs.insert_file("/a/.rules", Vec::new()).await;
|
||||
cx.run_until_parked();
|
||||
agent.read_with(cx, |agent, cx| {
|
||||
let rules_entry = worktree
|
||||
.read(cx)
|
||||
.entry_for_path(rel_path(".rules"))
|
||||
.unwrap();
|
||||
let rules_entry = worktree.read(cx).entry_for_path(".rules").unwrap();
|
||||
assert_eq!(
|
||||
agent.project_context.read(cx).worktrees,
|
||||
vec mean?
|
||||
What does [@b.md](file:///a/b.md) mean?
|
||||
|
||||
## Assistant
|
||||
|
||||
@@ -1548,10 +1540,10 @@ mod tests {
|
||||
acp_thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(cx),
|
||||
formatdoc! {"
|
||||
indoc! {"
|
||||
## User
|
||||
|
||||
What does [@b.md]({uri}) mean?
|
||||
What does [@b.md](file:///a/b.md) mean?
|
||||
|
||||
## Assistant
|
||||
|
||||
|
||||
@@ -422,15 +422,17 @@ mod tests {
|
||||
use agent::MessageSegment;
|
||||
use agent::context::LoadedContext;
|
||||
use client::Client;
|
||||
use fs::{FakeFs, Fs};
|
||||
use fs::FakeFs;
|
||||
use gpui::AppContext;
|
||||
use gpui::TestAppContext;
|
||||
use http_client::FakeHttpClient;
|
||||
use language_model::Role;
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::test::TempTree;
|
||||
|
||||
fn init_test(fs: Arc<dyn Fs>, cx: &mut TestAppContext) {
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
env_logger::try_init().ok();
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
@@ -441,7 +443,7 @@ mod tests {
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let clock = Arc::new(clock::FakeSystemClock::new());
|
||||
let client = Client::new(clock, http_client, cx);
|
||||
agent::init(fs, cx);
|
||||
agent::init(cx);
|
||||
agent_settings::init(cx);
|
||||
language_model::init(client, cx);
|
||||
});
|
||||
@@ -449,8 +451,10 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
|
||||
let tree = TempTree::new(json!({}));
|
||||
util::paths::set_home_dir(tree.path().into());
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
init_test(fs.clone(), cx);
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
|
||||
// Save a thread using the old agent.
|
||||
|
||||
@@ -262,7 +262,7 @@ impl HistoryStore {
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
HistoryEntryId::TextThread(path) => path.file_name().map(|file| {
|
||||
SerializedRecentOpen::TextThread(file.to_string_lossy().into_owned())
|
||||
SerializedRecentOpen::TextThread(file.to_string_lossy().to_string())
|
||||
}),
|
||||
HistoryEntryId::AcpThread(id) => {
|
||||
Some(SerializedRecentOpen::AcpThread(id.to_string()))
|
||||
|
||||
@@ -879,11 +879,27 @@ impl Thread {
|
||||
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
|
||||
.collect();
|
||||
|
||||
cx.spawn(async move |_, _| {
|
||||
cx.spawn(async move |_, cx| {
|
||||
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
|
||||
|
||||
let mut unsaved_buffers = Vec::new();
|
||||
cx.update(|app_cx| {
|
||||
let buffer_store = project.read(app_cx).buffer_store();
|
||||
for buffer_handle in buffer_store.read(app_cx).buffers() {
|
||||
let buffer = buffer_handle.read(app_cx);
|
||||
if buffer.is_dirty()
|
||||
&& let Some(file) = buffer.file()
|
||||
{
|
||||
let path = file.path().to_string_lossy().to_string();
|
||||
unsaved_buffers.push(path);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
Arc::new(ProjectSnapshot {
|
||||
worktree_snapshots,
|
||||
unsaved_buffer_paths: unsaved_buffers,
|
||||
timestamp: Utc::now(),
|
||||
})
|
||||
})
|
||||
@@ -898,7 +914,7 @@ impl Thread {
|
||||
// Get worktree path and snapshot
|
||||
let worktree_info = cx.update(|app_cx| {
|
||||
let worktree = worktree.read(app_cx);
|
||||
let path = worktree.abs_path().to_string_lossy().into_owned();
|
||||
let path = worktree.abs_path().to_string_lossy().to_string();
|
||||
let snapshot = worktree.snapshot();
|
||||
(path, snapshot)
|
||||
});
|
||||
|
||||
@@ -84,7 +84,9 @@ impl AgentTool for CopyPathTool {
|
||||
.and_then(|project_path| project.entry_for_path(&project_path, cx))
|
||||
{
|
||||
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
|
||||
Some(project_path) => project.copy_entry(entity.id, project_path, cx),
|
||||
Some(project_path) => {
|
||||
project.copy_entry(entity.id, None, project_path.path, cx)
|
||||
}
|
||||
None => Task::ready(Err(anyhow!(
|
||||
"Destination path {} was outside the project.",
|
||||
input.destination_path
|
||||
|
||||
@@ -6,7 +6,7 @@ use language::{DiagnosticSeverity, OffsetRangeExt};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write, sync::Arc};
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::SharedString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
@@ -147,7 +147,9 @@ impl AgentTool for DiagnosticsTool {
|
||||
has_diagnostics = true;
|
||||
output.push_str(&format!(
|
||||
"{}: {} error(s), {} warning(s)\n",
|
||||
worktree.read(cx).absolutize(&project_path.path).display(),
|
||||
Path::new(worktree.read(cx).root_name())
|
||||
.join(project_path.path)
|
||||
.display(),
|
||||
summary.error_count,
|
||||
summary.warning_count
|
||||
));
|
||||
|
||||
@@ -17,12 +17,10 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use smol::stream::StreamExt as _;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use ui::SharedString;
|
||||
use util::ResultExt;
|
||||
use util::rel_path::RelPath;
|
||||
|
||||
const DEFAULT_UI_TEXT: &str = "Editing file";
|
||||
|
||||
@@ -150,11 +148,12 @@ impl EditFileTool {
|
||||
|
||||
// If any path component matches the local settings folder, then this could affect
|
||||
// the editor in ways beyond the project source, so prompt.
|
||||
let local_settings_folder = paths::local_settings_folder_name();
|
||||
let local_settings_folder = paths::local_settings_folder_relative_path();
|
||||
let path = Path::new(&input.path);
|
||||
if path.components().any(|component| {
|
||||
component.as_os_str() == <_ as AsRef<OsStr>>::as_ref(&local_settings_folder)
|
||||
}) {
|
||||
if path
|
||||
.components()
|
||||
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
|
||||
{
|
||||
return event_stream.authorize(
|
||||
format!("{} (local settings)", input.display_description),
|
||||
cx,
|
||||
@@ -163,7 +162,6 @@ impl EditFileTool {
|
||||
|
||||
// It's also possible that the global config dir is configured to be inside the project,
|
||||
// so check for that edge case too.
|
||||
// TODO this is broken when remoting
|
||||
if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
|
||||
&& canonical_path.starts_with(paths::config_dir())
|
||||
{
|
||||
@@ -218,7 +216,9 @@ impl AgentTool for EditFileTool {
|
||||
.read(cx)
|
||||
.short_full_path_for_project_path(&project_path, cx)
|
||||
})
|
||||
.unwrap_or(input.path.to_string_lossy().into_owned())
|
||||
.unwrap_or(Path::new(&input.path).into())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into(),
|
||||
Err(raw_input) => {
|
||||
if let Some(input) =
|
||||
@@ -235,7 +235,9 @@ impl AgentTool for EditFileTool {
|
||||
.read(cx)
|
||||
.short_full_path_for_project_path(&project_path, cx)
|
||||
})
|
||||
.unwrap_or(input.path)
|
||||
.unwrap_or(Path::new(&input.path).into())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into();
|
||||
}
|
||||
|
||||
@@ -476,7 +478,7 @@ impl AgentTool for EditFileTool {
|
||||
) -> Result<()> {
|
||||
event_stream.update_diff(cx.new(|cx| {
|
||||
Diff::finalized(
|
||||
output.input_path.to_string_lossy().into_owned(),
|
||||
output.input_path,
|
||||
Some(output.old_text.to_string()),
|
||||
output.new_text,
|
||||
self.language_registry.clone(),
|
||||
@@ -540,12 +542,10 @@ fn resolve_path(
|
||||
let file_name = input
|
||||
.path
|
||||
.file_name()
|
||||
.and_then(|file_name| file_name.to_str())
|
||||
.and_then(|file_name| RelPath::unix(file_name).ok())
|
||||
.context("Can't create file: invalid filename")?;
|
||||
|
||||
let new_file_path = parent_project_path.map(|parent| ProjectPath {
|
||||
path: parent.path.join(file_name),
|
||||
path: Arc::from(parent.path.join(file_name)),
|
||||
..parent
|
||||
});
|
||||
|
||||
@@ -565,7 +565,7 @@ mod tests {
|
||||
use prompt_store::ProjectContext;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::{path, rel_path::rel_path};
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
|
||||
@@ -614,13 +614,13 @@ mod tests {
|
||||
let mode = &EditFileMode::Create;
|
||||
|
||||
let result = test_resolve_path(mode, "root/new.txt", cx);
|
||||
assert_resolved_path_eq(result.await, rel_path("new.txt"));
|
||||
assert_resolved_path_eq(result.await, "new.txt");
|
||||
|
||||
let result = test_resolve_path(mode, "new.txt", cx);
|
||||
assert_resolved_path_eq(result.await, rel_path("new.txt"));
|
||||
assert_resolved_path_eq(result.await, "new.txt");
|
||||
|
||||
let result = test_resolve_path(mode, "dir/new.txt", cx);
|
||||
assert_resolved_path_eq(result.await, rel_path("dir/new.txt"));
|
||||
assert_resolved_path_eq(result.await, "dir/new.txt");
|
||||
|
||||
let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
|
||||
assert_eq!(
|
||||
@@ -642,10 +642,10 @@ mod tests {
|
||||
let path_with_root = "root/dir/subdir/existing.txt";
|
||||
let path_without_root = "dir/subdir/existing.txt";
|
||||
let result = test_resolve_path(mode, path_with_root, cx);
|
||||
assert_resolved_path_eq(result.await, rel_path(path_without_root));
|
||||
assert_resolved_path_eq(result.await, path_without_root);
|
||||
|
||||
let result = test_resolve_path(mode, path_without_root, cx);
|
||||
assert_resolved_path_eq(result.await, rel_path(path_without_root));
|
||||
assert_resolved_path_eq(result.await, path_without_root);
|
||||
|
||||
let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
|
||||
assert_eq!(
|
||||
@@ -690,10 +690,14 @@ mod tests {
|
||||
cx.update(|cx| resolve_path(&input, project, cx))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &RelPath) {
|
||||
let actual = path.expect("Should return valid path").path;
|
||||
assert_eq!(actual.as_ref(), expected);
|
||||
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
|
||||
let actual = path
|
||||
.expect("Should return valid path")
|
||||
.path
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace("\\", "/"); // Naive Windows paths normalization
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1404,8 +1408,8 @@ mod tests {
|
||||
// Parent directory references - find_project_path resolves these
|
||||
(
|
||||
"project/../other",
|
||||
true,
|
||||
"Path with .. that goes outside of root directory",
|
||||
false,
|
||||
"Path with .. is resolved by find_project_path",
|
||||
),
|
||||
(
|
||||
"project/./src/file.rs",
|
||||
@@ -1433,18 +1437,16 @@ mod tests {
|
||||
)
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
if should_confirm {
|
||||
stream_rx.expect_authorization().await;
|
||||
} else {
|
||||
auth.await.unwrap();
|
||||
assert!(
|
||||
stream_rx.try_next().is_err(),
|
||||
"Failed for case: {} - path: {} - expected no confirmation but got one",
|
||||
description,
|
||||
path
|
||||
);
|
||||
auth.await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,14 +156,10 @@ impl AgentTool for FindPathTool {
|
||||
}
|
||||
|
||||
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let path_matcher = match PathMatcher::new(
|
||||
[
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { glob },
|
||||
],
|
||||
path_style,
|
||||
) {
|
||||
let path_matcher = match PathMatcher::new([
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { glob },
|
||||
]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
|
||||
};
|
||||
@@ -177,8 +173,9 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
|
||||
let mut results = Vec::new();
|
||||
for snapshot in snapshots {
|
||||
for entry in snapshot.entries(false, 0) {
|
||||
if path_matcher.is_match(snapshot.root_name().join(&entry.path).as_std_path()) {
|
||||
results.push(snapshot.absolutize(&entry.path));
|
||||
let root_name = PathBuf::from(snapshot.root_name());
|
||||
if path_matcher.is_match(root_name.join(&entry.path)) {
|
||||
results.push(snapshot.abs_path().join(entry.path.as_ref()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,15 +110,12 @@ impl AgentTool for GrepTool {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
const MAX_ANCESTOR_LINES: u32 = 10;
|
||||
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
|
||||
let include_matcher = match PathMatcher::new(
|
||||
input
|
||||
.include_pattern
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
path_style,
|
||||
) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(error) => {
|
||||
@@ -135,7 +132,7 @@ impl AgentTool for GrepTool {
|
||||
.iter()
|
||||
.chain(global_settings.private_files.sources().iter());
|
||||
|
||||
match PathMatcher::new(exclude_patterns, path_style) {
|
||||
match PathMatcher::new(exclude_patterns) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(error) => {
|
||||
return Task::ready(Err(anyhow!("invalid exclude pattern: {error}")));
|
||||
|
||||
@@ -2,12 +2,12 @@ use crate::{AgentTool, ToolCallEventStream};
|
||||
use agent_client_protocol::ToolKind;
|
||||
use anyhow::{Result, anyhow};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::{Project, ProjectPath, WorktreeSettings};
|
||||
use project::{Project, WorktreeSettings};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
|
||||
@@ -86,13 +86,13 @@ impl AgentTool for ListDirectoryTool {
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.filter_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let root_entry = worktree.root_entry()?;
|
||||
if root_entry.is_dir() {
|
||||
Some(root_entry.path.display(worktree.path_style()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
worktree.read(cx).root_entry().and_then(|entry| {
|
||||
if entry.is_dir() {
|
||||
entry.path.to_str()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
@@ -143,7 +143,7 @@ impl AgentTool for ListDirectoryTool {
|
||||
}
|
||||
|
||||
let worktree_snapshot = worktree.read(cx).snapshot();
|
||||
let worktree_root_name = worktree.read(cx).root_name();
|
||||
let worktree_root_name = worktree.read(cx).root_name().to_string();
|
||||
|
||||
let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
|
||||
return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
|
||||
@@ -165,17 +165,25 @@ impl AgentTool for ListDirectoryTool {
|
||||
continue;
|
||||
}
|
||||
|
||||
let project_path: ProjectPath = (worktree_snapshot.id(), entry.path.clone()).into();
|
||||
if worktree_settings.is_path_excluded(&project_path.path)
|
||||
|| worktree_settings.is_path_private(&project_path.path)
|
||||
if self
|
||||
.project
|
||||
.read(cx)
|
||||
.find_project_path(&entry.path, cx)
|
||||
.map(|project_path| {
|
||||
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
|
||||
|
||||
worktree_settings.is_path_excluded(&project_path.path)
|
||||
|| worktree_settings.is_path_private(&project_path.path)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let full_path = worktree_root_name
|
||||
let full_path = Path::new(&worktree_root_name)
|
||||
.join(&entry.path)
|
||||
.display(worktree_snapshot.path_style())
|
||||
.into_owned();
|
||||
.display()
|
||||
.to_string();
|
||||
if entry.is_dir() {
|
||||
folders.push(full_path);
|
||||
} else {
|
||||
|
||||
@@ -98,7 +98,7 @@ impl AgentTool for MovePathTool {
|
||||
.and_then(|project_path| project.entry_for_path(&project_path, cx))
|
||||
{
|
||||
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
|
||||
Some(project_path) => project.rename_entry(entity.id, project_path, cx),
|
||||
Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
|
||||
None => Task::ready(Err(anyhow!(
|
||||
"Destination path {} was outside the project.",
|
||||
input.destination_path
|
||||
|
||||
@@ -104,7 +104,7 @@ mod tests {
|
||||
async fn test_to_absolute_path(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let temp_path = temp_dir.path().to_string_lossy().into_owned();
|
||||
let temp_path = temp_dir.path().to_string_lossy().to_string();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
|
||||
@@ -82,12 +82,12 @@ impl AgentTool for ReadFileTool {
|
||||
{
|
||||
match (input.start_line, input.end_line) {
|
||||
(Some(start), Some(end)) => {
|
||||
format!("Read file `{path}` (lines {}-{})", start, end,)
|
||||
format!("Read file `{}` (lines {}-{})", path.display(), start, end,)
|
||||
}
|
||||
(Some(start), None) => {
|
||||
format!("Read file `{path}` (from line {})", start)
|
||||
format!("Read file `{}` (from line {})", path.display(), start)
|
||||
}
|
||||
_ => format!("Read file `{path}`"),
|
||||
_ => format!("Read file `{}`", path.display()),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
@@ -225,12 +225,9 @@ impl AgentTool for ReadFileTool {
|
||||
Ok(result.into())
|
||||
} else {
|
||||
// No line ranges specified, so check file size to see if it's too big.
|
||||
let buffer_content = outline::get_buffer_content_or_outline(
|
||||
buffer.clone(),
|
||||
Some(&abs_path.to_string_lossy()),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
let buffer_content =
|
||||
outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), cx)
|
||||
.await?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx);
|
||||
|
||||
@@ -380,10 +380,6 @@ impl AgentConnection for AcpConnection {
|
||||
match result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) => {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
return Err(anyhow!(acp::Error::auth_required()));
|
||||
}
|
||||
|
||||
if err.code != ErrorCode::INTERNAL_ERROR.code {
|
||||
anyhow::bail!(err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
mod acp;
|
||||
mod claude;
|
||||
mod codex;
|
||||
mod custom;
|
||||
mod gemini;
|
||||
|
||||
@@ -9,7 +8,6 @@ pub mod e2e_tests;
|
||||
|
||||
pub use claude::*;
|
||||
use client::ProxySettings;
|
||||
pub use codex::*;
|
||||
use collections::HashMap;
|
||||
pub use custom::*;
|
||||
use fs::Fs;
|
||||
@@ -101,9 +99,6 @@ pub fn load_proxy_env(cx: &mut App) -> HashMap<String, String> {
|
||||
|
||||
if let Some(no_proxy) = read_no_proxy_from_env() {
|
||||
env.insert("NO_PROXY".to_owned(), no_proxy);
|
||||
} else if proxy_url.is_some() {
|
||||
// We sometimes need local MCP servers that we don't want to proxy
|
||||
env.insert("NO_PROXY".to_owned(), "localhost,127.0.0.1".to_owned());
|
||||
}
|
||||
|
||||
env
|
||||
|
||||
@@ -62,7 +62,7 @@ impl AgentServer for ClaudeCode {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
let extra_env = load_proxy_env(cx);
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, path::Path};
|
||||
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, SharedString, Task};
|
||||
use project::agent_server_store::CODEX_NAME;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Codex;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
crate::common_e2e_tests!(async |_, _, _| Codex, allow_option_id = "proceed_once");
|
||||
}
|
||||
|
||||
impl AgentServer for Codex {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"codex"
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Codex".into()
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
ui::IconName::AiOpenAi
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
let extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
.update(cx, |store, cx| {
|
||||
let agent = store
|
||||
.get_external_agent(&CODEX_NAME.into())
|
||||
.context("Codex is not registered")?;
|
||||
anyhow::Ok(agent.get_command(
|
||||
root_dir.as_deref(),
|
||||
extra_env,
|
||||
delegate.status_tx,
|
||||
// For now, report that there are no updates.
|
||||
// (A future PR will use the GitHub Releases API to fetch them.)
|
||||
delegate.new_version_available,
|
||||
&mut cx.to_async(),
|
||||
))
|
||||
})??
|
||||
.await?;
|
||||
|
||||
let connection = crate::acp::connect(
|
||||
name,
|
||||
command,
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
Ok((connection, login))
|
||||
})
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ impl crate::AgentServer for CustomAgentServer {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let default_mode = self.default_mode(cx);
|
||||
let store = delegate.store.downgrade();
|
||||
|
||||
@@ -483,13 +483,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
default_mode: None,
|
||||
}),
|
||||
gemini: Some(crate::gemini::tests::local_command().into()),
|
||||
codex: Some(BuiltinAgentServerSettings {
|
||||
path: Some("codex-acp".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
}),
|
||||
custom: collections::HashMap::default(),
|
||||
},
|
||||
cx,
|
||||
|
||||
@@ -31,7 +31,7 @@ impl AgentServer for Gemini {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
let mut extra_env = load_proxy_env(cx);
|
||||
|
||||
@@ -80,6 +80,7 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
shlex.workspace = true
|
||||
smol.workspace = true
|
||||
streaming_diff.workspace = true
|
||||
task.workspace = true
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@@ -14,7 +13,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::lsp_store::{CompletionDocumentation, SymbolLocation};
|
||||
use project::lsp_store::CompletionDocumentation;
|
||||
use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
|
||||
ProjectPath, Symbol, WorktreeId,
|
||||
@@ -23,7 +22,6 @@ use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
use text::{Anchor, ToPoint as _};
|
||||
use ui::prelude::*;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::AgentPanel;
|
||||
@@ -189,7 +187,7 @@ impl ContextPickerCompletionProvider {
|
||||
|
||||
pub(crate) fn completion_for_path(
|
||||
project_path: ProjectPath,
|
||||
path_prefix: &RelPath,
|
||||
path_prefix: &str,
|
||||
is_recent: bool,
|
||||
is_directory: bool,
|
||||
source_range: Range<Anchor>,
|
||||
@@ -197,12 +195,10 @@ impl ContextPickerCompletionProvider {
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Option<Completion> {
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let (file_name, directory) =
|
||||
crate::context_picker::file_context_picker::extract_file_name_and_directory(
|
||||
&project_path.path,
|
||||
path_prefix,
|
||||
path_style,
|
||||
);
|
||||
|
||||
let label =
|
||||
@@ -254,15 +250,7 @@ impl ContextPickerCompletionProvider {
|
||||
|
||||
let label = CodeLabel::plain(symbol.name.clone(), None);
|
||||
|
||||
let abs_path = match &symbol.path {
|
||||
SymbolLocation::InProject(project_path) => {
|
||||
project.read(cx).absolute_path(&project_path, cx)?
|
||||
}
|
||||
SymbolLocation::OutsideProject {
|
||||
abs_path,
|
||||
signature: _,
|
||||
} => PathBuf::from(abs_path.as_ref()),
|
||||
};
|
||||
let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
|
||||
let uri = MentionUri::Symbol {
|
||||
abs_path,
|
||||
name: symbol.name.clone(),
|
||||
|
||||
@@ -48,7 +48,7 @@ use std::{
|
||||
use text::OffsetRangeExt;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{ButtonLike, TintColor, Toggleable, prelude::*};
|
||||
use util::{ResultExt, debug_panic, rel_path::RelPath};
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt as _};
|
||||
use zed_actions::agent::Chat;
|
||||
|
||||
@@ -76,7 +76,7 @@ pub enum MessageEditorEvent {
|
||||
|
||||
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||
|
||||
const COMMAND_HINT_INLAY_ID: u32 = 0;
|
||||
const COMMAND_HINT_INLAY_ID: usize = 0;
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
@@ -452,12 +452,9 @@ impl MessageEditor {
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
cx.spawn(async move |_, cx| {
|
||||
let buffer = buffer.await?;
|
||||
let buffer_content = outline::get_buffer_content_or_outline(
|
||||
buffer.clone(),
|
||||
Some(&abs_path.to_string_lossy()),
|
||||
&cx,
|
||||
)
|
||||
.await?;
|
||||
let buffer_content =
|
||||
outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), &cx)
|
||||
.await?;
|
||||
|
||||
Ok(Mention::Text {
|
||||
content: buffer_content.text,
|
||||
@@ -950,7 +947,6 @@ impl MessageEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
let buffer = self.editor.read(cx).buffer().clone();
|
||||
let Some(buffer) = buffer.read(cx).as_singleton() else {
|
||||
return;
|
||||
@@ -960,15 +956,18 @@ impl MessageEditor {
|
||||
let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
|
||||
continue;
|
||||
};
|
||||
let Some(worktree) = self.project.read(cx).worktree_for_id(path.worktree_id, cx) else {
|
||||
let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
|
||||
continue;
|
||||
};
|
||||
let abs_path = worktree.read(cx).absolutize(&path.path);
|
||||
let path_prefix = abs_path
|
||||
.file_name()
|
||||
.unwrap_or(path.path.as_os_str())
|
||||
.display()
|
||||
.to_string();
|
||||
let (file_name, _) =
|
||||
crate::context_picker::file_context_picker::extract_file_name_and_directory(
|
||||
&path.path,
|
||||
worktree.read(cx).root_name(),
|
||||
path_style,
|
||||
&path_prefix,
|
||||
);
|
||||
|
||||
let uri = if entry.is_dir() {
|
||||
@@ -1177,20 +1176,14 @@ fn full_mention_for_directory(
|
||||
abs_path: &Path,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Mention>> {
|
||||
fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
|
||||
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
for entry in worktree.child_entries(path) {
|
||||
if entry.is_dir() {
|
||||
files.extend(collect_files_in_path(worktree, &entry.path));
|
||||
} else if entry.is_file() {
|
||||
files.push((
|
||||
entry.path.clone(),
|
||||
worktree
|
||||
.full_path(&entry.path)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
));
|
||||
files.push((entry.path.clone(), worktree.full_path(&entry.path)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1268,7 +1261,7 @@ fn full_mention_for_directory(
|
||||
})
|
||||
}
|
||||
|
||||
fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
|
||||
fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
|
||||
let mut output = String::new();
|
||||
for (_relative_path, full_path, content) in entries {
|
||||
let fence = codeblock_fence_for_path(Some(&full_path), None);
|
||||
@@ -1602,7 +1595,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use text::Point;
|
||||
use ui::{App, Context, IntoElement, Render, SharedString, Window};
|
||||
use util::{path, paths::PathStyle, rel_path::rel_path};
|
||||
use util::{path, uri};
|
||||
use workspace::{AppState, Item, Workspace};
|
||||
|
||||
use crate::acp::{
|
||||
@@ -2112,18 +2105,16 @@ mod tests {
|
||||
let mut cx = VisualTestContext::from_window(*window, cx);
|
||||
|
||||
let paths = vec![
|
||||
rel_path("a/one.txt"),
|
||||
rel_path("a/two.txt"),
|
||||
rel_path("a/three.txt"),
|
||||
rel_path("a/four.txt"),
|
||||
rel_path("b/five.txt"),
|
||||
rel_path("b/six.txt"),
|
||||
rel_path("b/seven.txt"),
|
||||
rel_path("b/eight.txt"),
|
||||
path!("a/one.txt"),
|
||||
path!("a/two.txt"),
|
||||
path!("a/three.txt"),
|
||||
path!("a/four.txt"),
|
||||
path!("b/five.txt"),
|
||||
path!("b/six.txt"),
|
||||
path!("b/seven.txt"),
|
||||
path!("b/eight.txt"),
|
||||
];
|
||||
|
||||
let slash = PathStyle::local().separator();
|
||||
|
||||
let mut opened_editors = Vec::new();
|
||||
for path in paths {
|
||||
let buffer = workspace
|
||||
@@ -2131,7 +2122,7 @@ mod tests {
|
||||
workspace.open_path(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: path.into(),
|
||||
path: Path::new(path).into(),
|
||||
},
|
||||
None,
|
||||
false,
|
||||
@@ -2192,10 +2183,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
&[
|
||||
format!("eight.txt dir{slash}b{slash}"),
|
||||
format!("seven.txt dir{slash}b{slash}"),
|
||||
format!("six.txt dir{slash}b{slash}"),
|
||||
format!("five.txt dir{slash}b{slash}"),
|
||||
"eight.txt dir/b/",
|
||||
"seven.txt dir/b/",
|
||||
"six.txt dir/b/",
|
||||
"five.txt dir/b/",
|
||||
]
|
||||
);
|
||||
editor.set_text("", window, cx);
|
||||
@@ -2223,14 +2214,14 @@ mod tests {
|
||||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
&[
|
||||
format!("eight.txt dir{slash}b{slash}"),
|
||||
format!("seven.txt dir{slash}b{slash}"),
|
||||
format!("six.txt dir{slash}b{slash}"),
|
||||
format!("five.txt dir{slash}b{slash}"),
|
||||
"Files & Directories".into(),
|
||||
"Symbols".into(),
|
||||
"Threads".into(),
|
||||
"Fetch".into()
|
||||
"eight.txt dir/b/",
|
||||
"seven.txt dir/b/",
|
||||
"six.txt dir/b/",
|
||||
"five.txt dir/b/",
|
||||
"Files & Directories",
|
||||
"Symbols",
|
||||
"Threads",
|
||||
"Fetch"
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -2257,10 +2248,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem @file one");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
vec![format!("one.txt dir{slash}a{slash}")]
|
||||
);
|
||||
assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
|
||||
});
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
@@ -2268,11 +2256,7 @@ mod tests {
|
||||
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
|
||||
});
|
||||
|
||||
let url_one = MentionUri::File {
|
||||
abs_path: path!("/dir/a/one.txt").into(),
|
||||
}
|
||||
.to_uri()
|
||||
.to_string();
|
||||
let url_one = uri!("file:///dir/a/one.txt");
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
|
||||
@@ -2377,11 +2361,7 @@ mod tests {
|
||||
.into_values()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let url_eight = MentionUri::File {
|
||||
abs_path: path!("/dir/b/eight.txt").into(),
|
||||
}
|
||||
.to_uri()
|
||||
.to_string();
|
||||
let url_eight = uri!("file:///dir/b/eight.txt");
|
||||
|
||||
{
|
||||
let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
|
||||
@@ -2480,12 +2460,6 @@ mod tests {
|
||||
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
|
||||
});
|
||||
|
||||
let symbol = MentionUri::Symbol {
|
||||
abs_path: path!("/dir/a/one.txt").into(),
|
||||
name: "MySymbol".into(),
|
||||
line_range: 0..=0,
|
||||
};
|
||||
|
||||
let contents = message_editor
|
||||
.update(&mut cx, |message_editor, cx| {
|
||||
message_editor.mention_set().contents(
|
||||
@@ -2505,7 +2479,12 @@ mod tests {
|
||||
panic!("Unexpected mentions");
|
||||
};
|
||||
pretty_assertions::assert_eq!(content, "1");
|
||||
pretty_assertions::assert_eq!(uri, &symbol);
|
||||
pretty_assertions::assert_eq!(
|
||||
uri,
|
||||
&format!("{url_one}?symbol=MySymbol#L1:1")
|
||||
.parse::<MentionUri>()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
cx.run_until_parked();
|
||||
@@ -2513,10 +2492,7 @@ mod tests {
|
||||
editor.read_with(&cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!(
|
||||
"Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
|
||||
symbol.to_uri(),
|
||||
)
|
||||
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2526,10 +2502,10 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
|
||||
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
|
||||
assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
|
||||
});
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
@@ -2555,10 +2531,7 @@ mod tests {
|
||||
editor.read_with(&cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!(
|
||||
"Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
|
||||
symbol.to_uri()
|
||||
)
|
||||
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2568,10 +2541,10 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
|
||||
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
|
||||
assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
|
||||
});
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
@@ -2583,14 +2556,11 @@ mod tests {
|
||||
|
||||
// Mention was removed
|
||||
editor.read_with(&cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!(
|
||||
"Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
|
||||
symbol.to_uri()
|
||||
)
|
||||
);
|
||||
});
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
|
||||
);
|
||||
});
|
||||
|
||||
// Now getting the contents succeeds, because the invalid mention was removed
|
||||
let contents = message_editor
|
||||
|
||||
@@ -9,7 +9,7 @@ use agent_client_protocol::{self as acp, PromptCapabilities};
|
||||
use agent_servers::{AgentServer, AgentServerDelegate};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
|
||||
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use arrayvec::ArrayVec;
|
||||
use audio::{Audio, Sound};
|
||||
use buffer_diff::BufferDiff;
|
||||
@@ -577,31 +577,6 @@ impl AcpThreadView {
|
||||
|
||||
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
||||
|
||||
// Proactively surface Authentication Required if the agent advertises auth methods.
|
||||
if let Some(acp_conn) = thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.clone()
|
||||
.downcast::<agent_servers::AcpConnection>()
|
||||
{
|
||||
let methods = acp_conn.auth_methods();
|
||||
if !methods.is_empty() {
|
||||
// Immediately transition to auth-required UI, but defer to avoid re-entrant update.
|
||||
let err = AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
};
|
||||
let this_weak = cx.weak_entity();
|
||||
let agent = agent.clone();
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this_weak, err, agent, connection, window, cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.model_selector = thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
@@ -1037,13 +1012,11 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let auth_methods = connection.auth_methods();
|
||||
let has_supported_auth = auth_methods.iter().any(|method| {
|
||||
let id = method.id.0.as_ref();
|
||||
id == "claude-login" || id == "spawn-gemini-cli"
|
||||
});
|
||||
let can_login = has_supported_auth || auth_methods.is_empty() || self.login.is_some();
|
||||
if !can_login {
|
||||
if !connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.any(|method| method.id.0.as_ref() == "claude-login")
|
||||
{
|
||||
return;
|
||||
};
|
||||
let this = cx.weak_entity();
|
||||
@@ -1606,20 +1579,31 @@ impl AcpThreadView {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let cwd = project.read(cx).first_project_directory(cx);
|
||||
let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
let mut task = login.clone();
|
||||
task.shell = task::Shell::WithArguments {
|
||||
program: task.command.take().expect("login command should be set"),
|
||||
args: std::mem::take(&mut task.args),
|
||||
title_override: None
|
||||
};
|
||||
task.command = task
|
||||
.command
|
||||
.map(|command| anyhow::Ok(shlex::try_quote(&command)?.to_string()))
|
||||
.transpose()?;
|
||||
task.args = task
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
Ok(shlex::try_quote(arg)
|
||||
.context("Failed to quote argument")?
|
||||
.to_string())
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
task.full_label = task.label.clone();
|
||||
task.id = task::TaskId(format!("external-agent-{}-login", task.label));
|
||||
task.command_label = task.label.clone();
|
||||
task.use_new_terminal = true;
|
||||
task.allow_concurrent_runs = true;
|
||||
task.hide = task::HideStrategy::Always;
|
||||
task.shell = shell;
|
||||
|
||||
let terminal = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
|
||||
terminal_panel.spawn_task(&task, window, cx)
|
||||
@@ -3720,15 +3704,15 @@ impl AcpThreadView {
|
||||
|(index, (buffer, _diff))| {
|
||||
let file = buffer.read(cx).file()?;
|
||||
let path = file.path();
|
||||
let path_style = file.path_style(cx);
|
||||
let separator = file.path_style(cx).separator();
|
||||
|
||||
let file_path = path.parent().and_then(|parent| {
|
||||
if parent.is_empty() {
|
||||
let parent_str = parent.to_string_lossy();
|
||||
|
||||
if parent_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(format!("{}{separator}", parent.display(path_style)))
|
||||
Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx),
|
||||
@@ -3737,12 +3721,12 @@ impl AcpThreadView {
|
||||
});
|
||||
|
||||
let file_name = path.file_name().map(|name| {
|
||||
Label::new(name.to_string())
|
||||
Label::new(name.to_string_lossy().to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx)
|
||||
});
|
||||
|
||||
let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
|
||||
let file_icon = FileIcons::get_icon(path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
|
||||
.unwrap_or_else(|| {
|
||||
@@ -4585,7 +4569,7 @@ impl AcpThreadView {
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.map(|worktree| worktree.read(cx).root_name_str().to_string())
|
||||
.map(|worktree| worktree.read(cx).root_name().to_string())
|
||||
});
|
||||
|
||||
if let Some(screen_window) = cx
|
||||
|
||||
@@ -15,7 +15,6 @@ use context_server::ContextServerId;
|
||||
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
|
||||
use extension::ExtensionManifest;
|
||||
use extension_host::ExtensionStore;
|
||||
use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
@@ -27,7 +26,7 @@ use language_model::{
|
||||
};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
|
||||
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, GEMINI_NAME},
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
@@ -1015,9 +1014,7 @@ impl AgentConfiguration {
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.external_agents()
|
||||
.filter(|name| {
|
||||
name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
|
||||
})
|
||||
.filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1080,23 +1077,15 @@ impl AgentConfiguration {
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiClaude,
|
||||
"Claude Code",
|
||||
))
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
.when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
|
||||
this
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiOpenAi,
|
||||
"Codex",
|
||||
))
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
})
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiGemini,
|
||||
"Gemini CLI",
|
||||
))
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiClaude,
|
||||
"Claude Code",
|
||||
))
|
||||
.map(|mut parent| {
|
||||
for agent in user_defined_agents {
|
||||
parent = parent.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
|
||||
@@ -317,8 +317,6 @@ impl ManageProfilesModal {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement + use<> {
|
||||
let is_focused = profile.navigation.focus_handle.contains_focused(window, cx);
|
||||
|
||||
div()
|
||||
.id(SharedString::from(format!("profile-{}", profile.id)))
|
||||
.track_focus(&profile.navigation.focus_handle)
|
||||
@@ -330,27 +328,25 @@ impl ManageProfilesModal {
|
||||
})
|
||||
.child(
|
||||
ListItem::new(SharedString::from(format!("profile-{}", profile.id)))
|
||||
.toggle_state(is_focused)
|
||||
.toggle_state(profile.navigation.focus_handle.contains_focused(window, cx))
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.child(Label::new(profile.name.clone()))
|
||||
.when(is_focused, |this| {
|
||||
this.end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Customize")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Customize")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.on_click({
|
||||
let profile_id = profile.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
|
||||
@@ -7,7 +7,7 @@ use acp_thread::AcpThread;
|
||||
use agent2::{DbThreadMetadata, HistoryEntry};
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use project::agent_server_store::{
|
||||
AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME,
|
||||
AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{
|
||||
@@ -75,7 +75,6 @@ use zed_actions::{
|
||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||
};
|
||||
|
||||
use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
|
||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -217,7 +216,6 @@ pub enum AgentType {
|
||||
TextThread,
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
NativeAgent,
|
||||
Custom {
|
||||
name: SharedString,
|
||||
@@ -232,7 +230,6 @@ impl AgentType {
|
||||
Self::NativeAgent => "Agent 2".into(),
|
||||
Self::Gemini => "Gemini CLI".into(),
|
||||
Self::ClaudeCode => "Claude Code".into(),
|
||||
Self::Codex => "Codex".into(),
|
||||
Self::Custom { name, .. } => name.into(),
|
||||
}
|
||||
}
|
||||
@@ -242,7 +239,6 @@ impl AgentType {
|
||||
Self::Zed | Self::NativeAgent | Self::TextThread => None,
|
||||
Self::Gemini => Some(IconName::AiGemini),
|
||||
Self::ClaudeCode => Some(IconName::AiClaude),
|
||||
Self::Codex => Some(IconName::AiOpenAi),
|
||||
Self::Custom { .. } => Some(IconName::Terminal),
|
||||
}
|
||||
}
|
||||
@@ -253,7 +249,6 @@ impl From<ExternalAgent> for AgentType {
|
||||
match value {
|
||||
ExternalAgent::Gemini => Self::Gemini,
|
||||
ExternalAgent::ClaudeCode => Self::ClaudeCode,
|
||||
ExternalAgent::Codex => Self::Codex,
|
||||
ExternalAgent::Custom { name, command } => Self::Custom { name, command },
|
||||
ExternalAgent::NativeAgent => Self::NativeAgent,
|
||||
}
|
||||
@@ -1432,11 +1427,6 @@ impl AgentPanel {
|
||||
cx,
|
||||
)
|
||||
}
|
||||
AgentType::Codex => {
|
||||
self.selected_agent = AgentType::Codex;
|
||||
self.serialize(cx);
|
||||
self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
|
||||
}
|
||||
AgentType::Custom { name, command } => self.external_thread(
|
||||
Some(crate::ExternalAgent::Custom { name, command }),
|
||||
None,
|
||||
@@ -1949,60 +1939,6 @@ impl AgentPanel {
|
||||
)
|
||||
.separator()
|
||||
.header("External Agents")
|
||||
.item(
|
||||
ContextMenuEntry::new("New Claude Code Thread")
|
||||
.icon(IconName::AiClaude)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) =
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
AgentType::ClaudeCode,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
|
||||
this.item(
|
||||
ContextMenuEntry::new("New Codex Thread")
|
||||
.icon(IconName::AiOpenAi)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) =
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
AgentType::Codex,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.item(
|
||||
ContextMenuEntry::new("New Gemini CLI Thread")
|
||||
.icon(IconName::AiGemini)
|
||||
@@ -2029,12 +1965,38 @@ impl AgentPanel {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Claude Code Thread")
|
||||
.icon(IconName::AiClaude)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) =
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
AgentType::ClaudeCode,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.map(|mut menu| {
|
||||
let agent_names = agent_server_store
|
||||
.read(cx)
|
||||
.external_agents()
|
||||
.filter(|name| {
|
||||
name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
|
||||
name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -167,7 +167,6 @@ enum ExternalAgent {
|
||||
#[default]
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
NativeAgent,
|
||||
Custom {
|
||||
name: SharedString,
|
||||
@@ -189,7 +188,6 @@ impl ExternalAgent {
|
||||
Self::NativeAgent => "zed",
|
||||
Self::Gemini => "gemini-cli",
|
||||
Self::ClaudeCode => "claude-code",
|
||||
Self::Codex => "codex",
|
||||
Self::Custom { .. } => "custom",
|
||||
}
|
||||
}
|
||||
@@ -202,7 +200,6 @@ impl ExternalAgent {
|
||||
match self {
|
||||
Self::Gemini => Rc::new(agent_servers::Gemini),
|
||||
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||
Self::Codex => Rc::new(agent_servers::Codex),
|
||||
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
|
||||
Self::Custom { name, command: _ } => {
|
||||
Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
|
||||
@@ -267,7 +264,7 @@ pub fn init(
|
||||
init_language_model_settings(cx);
|
||||
}
|
||||
assistant_slash_command::init(cx);
|
||||
agent::init(fs.clone(), cx);
|
||||
agent::init(cx);
|
||||
agent_panel::init(cx);
|
||||
context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
|
||||
TextThreadEditor::init(cx);
|
||||
|
||||
@@ -33,8 +33,6 @@ use thread_context_picker::{
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
||||
};
|
||||
use util::paths::PathStyle;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use agent::{
|
||||
@@ -230,19 +228,12 @@ impl ContextPicker {
|
||||
let context_picker = cx.entity();
|
||||
|
||||
let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return menu;
|
||||
};
|
||||
let path_style = workspace.read(cx).path_style(cx);
|
||||
let recent = self.recent_entries(cx);
|
||||
let has_recent = !recent.is_empty();
|
||||
let recent_entries = recent
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, entry)| {
|
||||
self.recent_menu_item(context_picker.clone(), ix, entry, path_style)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
|
||||
|
||||
let entries = self
|
||||
.workspace
|
||||
@@ -404,7 +395,6 @@ impl ContextPicker {
|
||||
context_picker: Entity<ContextPicker>,
|
||||
ix: usize,
|
||||
entry: RecentEntry,
|
||||
path_style: PathStyle,
|
||||
) -> ContextMenuItem {
|
||||
match entry {
|
||||
RecentEntry::File {
|
||||
@@ -423,7 +413,6 @@ impl ContextPicker {
|
||||
&path,
|
||||
&path_prefix,
|
||||
false,
|
||||
path_style,
|
||||
context_store.clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -597,7 +586,7 @@ impl Render for ContextPicker {
|
||||
pub(crate) enum RecentEntry {
|
||||
File {
|
||||
project_path: ProjectPath,
|
||||
path_prefix: Arc<RelPath>,
|
||||
path_prefix: Arc<str>,
|
||||
},
|
||||
Thread(ThreadContextEntry),
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ use http_client::HttpClientWithUrl;
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::lsp_store::SymbolLocation;
|
||||
use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
|
||||
Symbol, WorktreeId,
|
||||
@@ -23,8 +22,6 @@ use rope::Point;
|
||||
use text::{Anchor, OffsetRangeExt, ToPoint};
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt as _;
|
||||
use util::paths::PathStyle;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::Workspace;
|
||||
|
||||
use agent::{
|
||||
@@ -577,12 +574,11 @@ impl ContextPickerCompletionProvider {
|
||||
|
||||
fn completion_for_path(
|
||||
project_path: ProjectPath,
|
||||
path_prefix: &RelPath,
|
||||
path_prefix: &str,
|
||||
is_recent: bool,
|
||||
is_directory: bool,
|
||||
excerpt_id: ExcerptId,
|
||||
source_range: Range<Anchor>,
|
||||
path_style: PathStyle,
|
||||
editor: Entity<Editor>,
|
||||
context_store: Entity<ContextStore>,
|
||||
cx: &App,
|
||||
@@ -590,7 +586,6 @@ impl ContextPickerCompletionProvider {
|
||||
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
|
||||
&project_path.path,
|
||||
path_prefix,
|
||||
path_style,
|
||||
);
|
||||
|
||||
let label =
|
||||
@@ -662,22 +657,17 @@ impl ContextPickerCompletionProvider {
|
||||
workspace: Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Option<Completion> {
|
||||
let path_style = workspace.read(cx).path_style(cx);
|
||||
let SymbolLocation::InProject(symbol_path) = &symbol.path else {
|
||||
return None;
|
||||
};
|
||||
let path_prefix = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.worktree_for_id(symbol_path.worktree_id, cx)?
|
||||
.worktree_for_id(symbol.path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.root_name();
|
||||
|
||||
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
|
||||
&symbol_path.path,
|
||||
&symbol.path.path,
|
||||
path_prefix,
|
||||
path_style,
|
||||
);
|
||||
let full_path = if let Some(directory) = directory {
|
||||
format!("{}{}", directory, file_name)
|
||||
@@ -778,7 +768,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
let text_thread_store = self.text_thread_store.clone();
|
||||
let editor = self.editor.clone();
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let path_style = workspace.read(cx).path_style(cx);
|
||||
|
||||
let MentionCompletion { mode, argument, .. } = state;
|
||||
let query = argument.unwrap_or_else(|| "".to_string());
|
||||
@@ -845,7 +834,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
mat.is_dir,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
path_style,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
cx,
|
||||
@@ -1076,7 +1064,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{ops::Deref, rc::Rc};
|
||||
use util::{path, rel_path::rel_path};
|
||||
use util::path;
|
||||
use workspace::{AppState, Item};
|
||||
|
||||
#[test]
|
||||
@@ -1227,18 +1215,16 @@ mod tests {
|
||||
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
|
||||
|
||||
let paths = vec![
|
||||
rel_path("a/one.txt"),
|
||||
rel_path("a/two.txt"),
|
||||
rel_path("a/three.txt"),
|
||||
rel_path("a/four.txt"),
|
||||
rel_path("b/five.txt"),
|
||||
rel_path("b/six.txt"),
|
||||
rel_path("b/seven.txt"),
|
||||
rel_path("b/eight.txt"),
|
||||
path!("a/one.txt"),
|
||||
path!("a/two.txt"),
|
||||
path!("a/three.txt"),
|
||||
path!("a/four.txt"),
|
||||
path!("b/five.txt"),
|
||||
path!("b/six.txt"),
|
||||
path!("b/seven.txt"),
|
||||
path!("b/eight.txt"),
|
||||
];
|
||||
|
||||
let slash = PathStyle::local().separator();
|
||||
|
||||
let mut opened_editors = Vec::new();
|
||||
for path in paths {
|
||||
let buffer = workspace
|
||||
@@ -1246,7 +1232,7 @@ mod tests {
|
||||
workspace.open_path(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: path.into(),
|
||||
path: Path::new(path).into(),
|
||||
},
|
||||
None,
|
||||
false,
|
||||
@@ -1322,13 +1308,13 @@ mod tests {
|
||||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
&[
|
||||
format!("seven.txt dir{slash}b{slash}"),
|
||||
format!("six.txt dir{slash}b{slash}"),
|
||||
format!("five.txt dir{slash}b{slash}"),
|
||||
format!("four.txt dir{slash}a{slash}"),
|
||||
"Files & Directories".into(),
|
||||
"Symbols".into(),
|
||||
"Fetch".into()
|
||||
"seven.txt dir/b/",
|
||||
"six.txt dir/b/",
|
||||
"five.txt dir/b/",
|
||||
"four.txt dir/a/",
|
||||
"Files & Directories",
|
||||
"Symbols",
|
||||
"Fetch"
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -1355,10 +1341,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem @file one");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
vec![format!("one.txt dir{slash}a{slash}")]
|
||||
);
|
||||
assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
|
||||
});
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
@@ -1367,10 +1350,7 @@ mod tests {
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) ")
|
||||
);
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
@@ -1381,10 +1361,7 @@ mod tests {
|
||||
cx.simulate_input(" ");
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) ")
|
||||
);
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
@@ -1397,7 +1374,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum "),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1411,7 +1388,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum @file "),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1429,7 +1406,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) ")
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) "
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1446,7 +1423,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) \n@")
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) \n@"
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1467,7 +1444,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) \n[@six.txt](@file:dir{slash}b{slash}six.txt) ")
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) \n[@six.txt](@file:dir/b/six.txt) "
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
@@ -9,7 +10,7 @@ use gpui::{
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
||||
use ui::{ListItem, Tooltip, prelude::*};
|
||||
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::ContextPicker;
|
||||
@@ -160,8 +161,6 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let FileMatch { mat, .. } = &self.matches.get(ix)?;
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let path_style = workspace.read(cx).path_style(cx);
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
@@ -173,7 +172,6 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
&mat.path,
|
||||
&mat.path_prefix,
|
||||
mat.is_dir,
|
||||
path_style,
|
||||
self.context_store.clone(),
|
||||
cx,
|
||||
)),
|
||||
@@ -216,13 +214,14 @@ pub(crate) fn search_files(
|
||||
|
||||
let file_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||
worktree.entries(false, 0).map(move |entry| FileMatch {
|
||||
mat: PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: worktree.root_name().into(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: entry.is_dir(),
|
||||
},
|
||||
@@ -270,31 +269,51 @@ pub(crate) fn search_files(
|
||||
}
|
||||
|
||||
pub fn extract_file_name_and_directory(
|
||||
path: &RelPath,
|
||||
path_prefix: &RelPath,
|
||||
path_style: PathStyle,
|
||||
path: &Path,
|
||||
path_prefix: &str,
|
||||
) -> (SharedString, Option<SharedString>) {
|
||||
let full_path = path_prefix.join(path);
|
||||
let file_name = full_path.file_name().unwrap_or_default();
|
||||
let display_path = full_path.display(path_style);
|
||||
let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
|
||||
(
|
||||
file_name.to_string().into(),
|
||||
Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
|
||||
)
|
||||
if path == Path::new("") {
|
||||
(
|
||||
SharedString::from(
|
||||
path_prefix
|
||||
.trim_end_matches(std::path::MAIN_SEPARATOR)
|
||||
.to_string(),
|
||||
),
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into();
|
||||
|
||||
let mut directory = path_prefix
|
||||
.trim_end_matches(std::path::MAIN_SEPARATOR)
|
||||
.to_string();
|
||||
if !directory.ends_with('/') {
|
||||
directory.push('/');
|
||||
}
|
||||
if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
|
||||
directory.push_str(&parent.to_string_lossy());
|
||||
directory.push('/');
|
||||
}
|
||||
|
||||
(file_name, Some(directory.into()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_file_context_entry(
|
||||
id: ElementId,
|
||||
worktree_id: WorktreeId,
|
||||
path: &Arc<RelPath>,
|
||||
path_prefix: &Arc<RelPath>,
|
||||
path: &Arc<Path>,
|
||||
path_prefix: &Arc<str>,
|
||||
is_directory: bool,
|
||||
path_style: PathStyle,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
cx: &App,
|
||||
) -> Stateful<Div> {
|
||||
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix, path_style);
|
||||
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
|
||||
|
||||
let added = context_store.upgrade().and_then(|context_store| {
|
||||
let project_path = ProjectPath {
|
||||
@@ -311,9 +330,9 @@ pub fn render_file_context_entry(
|
||||
});
|
||||
|
||||
let file_icon = if is_directory {
|
||||
FileIcons::get_folder_icon(false, path.as_std_path(), cx)
|
||||
FileIcons::get_folder_icon(false, path, cx)
|
||||
} else {
|
||||
FileIcons::get_icon(path.as_std_path(), cx)
|
||||
FileIcons::get_icon(path, cx)
|
||||
}
|
||||
.map(Icon::from_path)
|
||||
.unwrap_or_else(|| Icon::new(IconName::File));
|
||||
|
||||
@@ -2,14 +2,13 @@ use std::cmp::Reverse;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::Result;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::lsp_store::SymbolLocation;
|
||||
use project::{DocumentSymbol, Symbol};
|
||||
use ui::{ListItem, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
@@ -192,10 +191,7 @@ pub(crate) fn add_symbol(
|
||||
) -> Task<Result<(Option<AgentContextHandle>, bool)>> {
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let open_buffer_task = project.update(cx, |project, cx| {
|
||||
let SymbolLocation::InProject(symbol_path) = &symbol.path else {
|
||||
return Task::ready(Err(anyhow!("can't add symbol from outside of project")));
|
||||
};
|
||||
project.open_buffer(symbol_path.clone(), cx)
|
||||
project.open_buffer(symbol.path.clone(), cx)
|
||||
});
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = open_buffer_task.await?;
|
||||
@@ -295,11 +291,10 @@ pub(crate) fn search_symbols(
|
||||
.map(|(id, symbol)| {
|
||||
StringMatchCandidate::new(id, symbol.label.filter_text())
|
||||
})
|
||||
.partition(|candidate| match &symbols[candidate.id].path {
|
||||
SymbolLocation::InProject(project_path) => project
|
||||
.entry_for_path(project_path, cx)
|
||||
.is_some_and(|e| !e.is_ignored),
|
||||
SymbolLocation::OutsideProject { .. } => false,
|
||||
.partition(|candidate| {
|
||||
project
|
||||
.entry_for_path(&symbols[candidate.id].path, cx)
|
||||
.is_some_and(|e| !e.is_ignored)
|
||||
})
|
||||
})
|
||||
.log_err()
|
||||
@@ -365,18 +360,13 @@ fn compute_symbol_entries(
|
||||
}
|
||||
|
||||
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
|
||||
let path = match &entry.symbol.path {
|
||||
SymbolLocation::InProject(project_path) => {
|
||||
project_path.path.file_name().unwrap_or_default().into()
|
||||
}
|
||||
SymbolLocation::OutsideProject {
|
||||
abs_path,
|
||||
signature: _,
|
||||
} => abs_path
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy())
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
let path = entry
|
||||
.symbol
|
||||
.path
|
||||
.path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy())
|
||||
.unwrap_or_default();
|
||||
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
|
||||
|
||||
h_flex()
|
||||
|
||||
@@ -3,19 +3,12 @@ use agent_settings::{
|
||||
AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, builtin_profiles,
|
||||
};
|
||||
use fs::Fs;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, Context, DismissEvent, Entity, FocusHandle,
|
||||
Focusable, SharedString, Subscription, Task, Window,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};
|
||||
use settings::{Settings as _, SettingsStore, update_settings_file};
|
||||
use std::{
|
||||
sync::atomic::Ordering,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*};
|
||||
use settings::{DockPosition, Settings as _, SettingsStore, update_settings_file};
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
|
||||
ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, PopoverMenu,
|
||||
PopoverMenuHandle, TintColor, Tooltip, prelude::*,
|
||||
};
|
||||
|
||||
/// Trait for types that can provide and manage agent profiles
|
||||
@@ -32,11 +25,9 @@ pub trait ProfileProvider {
|
||||
|
||||
pub struct ProfileSelector {
|
||||
profiles: AvailableProfiles,
|
||||
pending_refresh: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
provider: Arc<dyn ProfileProvider>,
|
||||
picker: Option<Entity<Picker<ProfilePickerDelegate>>>,
|
||||
picker_handle: PopoverMenuHandle<Picker<ProfilePickerDelegate>>,
|
||||
menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
focus_handle: FocusHandle,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -49,91 +40,125 @@ impl ProfileSelector {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
this.pending_refresh = true;
|
||||
cx.notify();
|
||||
this.refresh_profiles(cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
profiles: AgentProfile::available_profiles(cx),
|
||||
pending_refresh: false,
|
||||
fs,
|
||||
provider,
|
||||
picker: None,
|
||||
picker_handle: PopoverMenuHandle::default(),
|
||||
menu_handle: PopoverMenuHandle::default(),
|
||||
focus_handle,
|
||||
_subscriptions: vec![settings_subscription],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn menu_handle(&self) -> PopoverMenuHandle<Picker<ProfilePickerDelegate>> {
|
||||
self.picker_handle.clone()
|
||||
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
|
||||
self.menu_handle.clone()
|
||||
}
|
||||
|
||||
fn ensure_picker(
|
||||
&mut self,
|
||||
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
|
||||
self.profiles = AgentProfile::available_profiles(cx);
|
||||
}
|
||||
|
||||
fn build_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<Picker<ProfilePickerDelegate>> {
|
||||
if self.picker.is_none() {
|
||||
let delegate = ProfilePickerDelegate::new(
|
||||
self.fs.clone(),
|
||||
self.provider.clone(),
|
||||
self.profiles.clone(),
|
||||
cx.background_executor().clone(),
|
||||
cx,
|
||||
);
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
let picker = cx.new(|cx| {
|
||||
Picker::list(delegate, window, cx)
|
||||
.show_scrollbar(true)
|
||||
.width(rems(20.))
|
||||
.max_height(Some(rems(16.).into()))
|
||||
});
|
||||
|
||||
self.picker = Some(picker);
|
||||
}
|
||||
|
||||
if self.pending_refresh {
|
||||
if let Some(picker) = &self.picker {
|
||||
let profiles = AgentProfile::available_profiles(cx);
|
||||
self.profiles = profiles.clone();
|
||||
picker.update(cx, |picker, cx| {
|
||||
let query = picker.query(cx);
|
||||
picker
|
||||
.delegate
|
||||
.refresh_profiles(profiles.clone(), query, cx);
|
||||
});
|
||||
let mut found_non_builtin = false;
|
||||
for (profile_id, profile_name) in self.profiles.iter() {
|
||||
if !builtin_profiles::is_builtin(profile_id) {
|
||||
found_non_builtin = true;
|
||||
continue;
|
||||
}
|
||||
menu = menu.item(self.menu_entry_for_profile(
|
||||
profile_id.clone(),
|
||||
profile_name,
|
||||
settings,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
self.pending_refresh = false;
|
||||
}
|
||||
|
||||
self.picker.as_ref().unwrap().clone()
|
||||
if found_non_builtin {
|
||||
menu = menu.separator().header("Custom Profiles");
|
||||
for (profile_id, profile_name) in self.profiles.iter() {
|
||||
if builtin_profiles::is_builtin(profile_id) {
|
||||
continue;
|
||||
}
|
||||
menu = menu.item(self.menu_entry_for_profile(
|
||||
profile_id.clone(),
|
||||
profile_name,
|
||||
settings,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
|
||||
move |window, cx| {
|
||||
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
|
||||
},
|
||||
));
|
||||
|
||||
menu
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ProfileSelector {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
if let Some(picker) = &self.picker {
|
||||
picker.focus_handle(cx)
|
||||
fn menu_entry_for_profile(
|
||||
&self,
|
||||
profile_id: AgentProfileId,
|
||||
profile_name: &SharedString,
|
||||
settings: &AgentSettings,
|
||||
cx: &App,
|
||||
) -> ContextMenuEntry {
|
||||
let documentation = match profile_name.to_lowercase().as_str() {
|
||||
builtin_profiles::WRITE => Some("Get help to write anything."),
|
||||
builtin_profiles::ASK => Some("Chat about your codebase."),
|
||||
builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
|
||||
_ => None,
|
||||
};
|
||||
let thread_profile_id = self.provider.profile_id(cx);
|
||||
|
||||
let entry = ContextMenuEntry::new(profile_name.clone())
|
||||
.toggleable(IconPosition::End, profile_id == thread_profile_id);
|
||||
|
||||
let entry = if let Some(doc_text) = documentation {
|
||||
entry.documentation_aside(
|
||||
documentation_side(settings.dock),
|
||||
DocumentationEdge::Top,
|
||||
move |_| Label::new(doc_text).into_any_element(),
|
||||
)
|
||||
} else {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
entry
|
||||
};
|
||||
|
||||
entry.handler({
|
||||
let fs = self.fs.clone();
|
||||
let provider = self.provider.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_profile(profile_id.0);
|
||||
}
|
||||
});
|
||||
|
||||
provider.set_profile(profile_id.clone(), cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProfileSelector {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if !self.provider.profiles_supported(cx) {
|
||||
return Button::new("tools-not-supported-button", "Tools Unsupported")
|
||||
.disabled(true)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.tooltip(Tooltip::text("This model does not support tools."))
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
let picker = self.ensure_picker(window, cx);
|
||||
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let profile_id = self.provider.profile_id(cx);
|
||||
let profile = settings.profiles.get(&profile_id);
|
||||
@@ -141,572 +166,62 @@ impl Render for ProfileSelector {
|
||||
let selected_profile = profile
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let trigger_button = Button::new("profile-selector", selected_profile)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ChevronDown)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent));
|
||||
if self.provider.profiles_supported(cx) {
|
||||
let this = cx.entity();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let trigger_button = Button::new("profile-selector-model", selected_profile)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ChevronDown)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent));
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
picker,
|
||||
trigger_button,
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Profile Menu",
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::BottomRight,
|
||||
cx,
|
||||
)
|
||||
.with_handle(self.picker_handle.clone())
|
||||
.render(window, cx)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ProfileCandidate {
|
||||
id: AgentProfileId,
|
||||
name: SharedString,
|
||||
is_builtin: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ProfileMatchEntry {
|
||||
candidate_index: usize,
|
||||
positions: Vec<usize>,
|
||||
}
|
||||
|
||||
enum ProfilePickerEntry {
|
||||
Header(SharedString),
|
||||
Profile(ProfileMatchEntry),
|
||||
}
|
||||
|
||||
pub(crate) struct ProfilePickerDelegate {
|
||||
fs: Arc<dyn Fs>,
|
||||
provider: Arc<dyn ProfileProvider>,
|
||||
background: BackgroundExecutor,
|
||||
candidates: Vec<ProfileCandidate>,
|
||||
string_candidates: Arc<Vec<StringMatchCandidate>>,
|
||||
filtered_entries: Vec<ProfilePickerEntry>,
|
||||
selected_index: usize,
|
||||
query: String,
|
||||
cancel: Option<Arc<AtomicBool>>,
|
||||
}
|
||||
|
||||
impl ProfilePickerDelegate {
|
||||
fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
provider: Arc<dyn ProfileProvider>,
|
||||
profiles: AvailableProfiles,
|
||||
background: BackgroundExecutor,
|
||||
cx: &mut Context<ProfileSelector>,
|
||||
) -> Self {
|
||||
let candidates = Self::candidates_from(profiles);
|
||||
let string_candidates = Arc::new(Self::string_candidates(&candidates));
|
||||
let filtered_entries = Self::entries_from_candidates(&candidates);
|
||||
|
||||
let mut this = Self {
|
||||
fs,
|
||||
provider,
|
||||
background,
|
||||
candidates,
|
||||
string_candidates,
|
||||
filtered_entries,
|
||||
selected_index: 0,
|
||||
query: String::new(),
|
||||
cancel: None,
|
||||
};
|
||||
|
||||
this.selected_index = this
|
||||
.index_of_profile(&this.provider.profile_id(cx))
|
||||
.unwrap_or_else(|| this.first_selectable_index().unwrap_or(0));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn refresh_profiles(
|
||||
&mut self,
|
||||
profiles: AvailableProfiles,
|
||||
query: String,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.candidates = Self::candidates_from(profiles);
|
||||
self.string_candidates = Arc::new(Self::string_candidates(&self.candidates));
|
||||
self.query = query;
|
||||
|
||||
if self.query.is_empty() {
|
||||
self.filtered_entries = Self::entries_from_candidates(&self.candidates);
|
||||
} else {
|
||||
let matches = self.search_blocking(&self.query);
|
||||
self.filtered_entries = self.entries_from_matches(matches);
|
||||
}
|
||||
|
||||
self.selected_index = self
|
||||
.index_of_profile(&self.provider.profile_id(cx))
|
||||
.unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn candidates_from(profiles: AvailableProfiles) -> Vec<ProfileCandidate> {
|
||||
profiles
|
||||
.into_iter()
|
||||
.map(|(id, name)| ProfileCandidate {
|
||||
is_builtin: builtin_profiles::is_builtin(&id),
|
||||
id,
|
||||
name,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn string_candidates(candidates: &[ProfileCandidate]) -> Vec<StringMatchCandidate> {
|
||||
candidates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatchCandidate::new(index, candidate.name.as_ref()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn documentation(candidate: &ProfileCandidate) -> Option<&'static str> {
|
||||
match candidate.id.as_str() {
|
||||
builtin_profiles::WRITE => Some("Get help to write anything."),
|
||||
builtin_profiles::ASK => Some("Chat about your codebase."),
|
||||
builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn entries_from_candidates(candidates: &[ProfileCandidate]) -> Vec<ProfilePickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
let mut inserted_custom_header = false;
|
||||
|
||||
for (idx, candidate) in candidates.iter().enumerate() {
|
||||
if !candidate.is_builtin && !inserted_custom_header {
|
||||
if !entries.is_empty() {
|
||||
entries.push(ProfilePickerEntry::Header("Custom Profiles".into()));
|
||||
}
|
||||
inserted_custom_header = true;
|
||||
}
|
||||
|
||||
entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {
|
||||
candidate_index: idx,
|
||||
positions: Vec::new(),
|
||||
}));
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
fn entries_from_matches(&self, matches: Vec<StringMatch>) -> Vec<ProfilePickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
for mat in matches {
|
||||
if self.candidates.get(mat.candidate_id).is_some() {
|
||||
entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {
|
||||
candidate_index: mat.candidate_id,
|
||||
positions: mat.positions,
|
||||
}));
|
||||
}
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
fn first_selectable_index(&self) -> Option<usize> {
|
||||
self.filtered_entries
|
||||
.iter()
|
||||
.position(|entry| matches!(entry, ProfilePickerEntry::Profile(_)))
|
||||
}
|
||||
|
||||
fn index_of_profile(&self, profile_id: &AgentProfileId) -> Option<usize> {
|
||||
self.filtered_entries.iter().position(|entry| {
|
||||
matches!(entry, ProfilePickerEntry::Profile(profile) if self
|
||||
.candidates
|
||||
.get(profile.candidate_index)
|
||||
.map(|candidate| &candidate.id == profile_id)
|
||||
.unwrap_or(false))
|
||||
})
|
||||
}
|
||||
|
||||
fn search_blocking(&self, query: &str) -> Vec<StringMatch> {
|
||||
if query.is_empty() {
|
||||
return self
|
||||
.string_candidates
|
||||
.iter()
|
||||
.map(|candidate| StringMatch {
|
||||
candidate_id: candidate.id,
|
||||
score: 0.0,
|
||||
positions: Vec::new(),
|
||||
string: candidate.string.clone(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
let cancel_flag = AtomicBool::new(false);
|
||||
|
||||
self.background.block(match_strings(
|
||||
self.string_candidates.as_ref(),
|
||||
query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
&cancel_flag,
|
||||
self.background.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ProfilePickerDelegate {
|
||||
type ListItem = AnyElement;
|
||||
|
||||
fn placeholder_text(&self, _: &mut Window, _: &mut App) -> Arc<str> {
|
||||
"Search profiles…".into()
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
let text = if self.candidates.is_empty() {
|
||||
"No profiles.".into()
|
||||
} else {
|
||||
"No profiles match your search.".into()
|
||||
};
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.filtered_entries.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn can_select(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> bool {
|
||||
match self.filtered_entries.get(ix) {
|
||||
Some(ProfilePickerEntry::Profile(_)) => true,
|
||||
Some(ProfilePickerEntry::Header(_)) | None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
if query.is_empty() {
|
||||
self.query.clear();
|
||||
self.filtered_entries = Self::entries_from_candidates(&self.candidates);
|
||||
self.selected_index = self
|
||||
.index_of_profile(&self.provider.profile_id(cx))
|
||||
.unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));
|
||||
cx.notify();
|
||||
return Task::ready(());
|
||||
}
|
||||
|
||||
if let Some(prev) = &self.cancel {
|
||||
prev.store(true, Ordering::Relaxed);
|
||||
}
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
self.cancel = Some(cancel.clone());
|
||||
|
||||
let string_candidates = self.string_candidates.clone();
|
||||
let background = self.background.clone();
|
||||
let provider = self.provider.clone();
|
||||
self.query = query.clone();
|
||||
|
||||
let cancel_for_future = cancel;
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = match_strings(
|
||||
string_candidates.as_ref(),
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
cancel_for_future.as_ref(),
|
||||
background,
|
||||
)
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, _, cx| {
|
||||
if this.delegate.query != query {
|
||||
return;
|
||||
}
|
||||
|
||||
this.delegate.filtered_entries = this.delegate.entries_from_matches(matches);
|
||||
this.delegate.selected_index = this
|
||||
.delegate
|
||||
.index_of_profile(&provider.profile_id(cx))
|
||||
.unwrap_or_else(|| this.delegate.first_selectable_index().unwrap_or(0));
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
match self.filtered_entries.get(self.selected_index) {
|
||||
Some(ProfilePickerEntry::Profile(entry)) => {
|
||||
if let Some(candidate) = self.candidates.get(entry.candidate_index) {
|
||||
let profile_id = candidate.id.clone();
|
||||
let fs = self.fs.clone();
|
||||
let provider = self.provider.clone();
|
||||
|
||||
update_settings_file(fs, cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_profile(profile_id.0);
|
||||
}
|
||||
});
|
||||
|
||||
provider.set_profile(profile_id.clone(), cx);
|
||||
|
||||
telemetry::event!(
|
||||
"agent_profile_switched",
|
||||
profile_id = profile_id.as_str(),
|
||||
source = "picker"
|
||||
);
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
cx.defer_in(window, |picker, window, cx| {
|
||||
picker.set_query("", window, cx);
|
||||
});
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
match self.filtered_entries.get(ix)? {
|
||||
ProfilePickerEntry::Header(label) => Some(
|
||||
div()
|
||||
.px_2p5()
|
||||
.pb_0p5()
|
||||
.when(ix > 0, |this| {
|
||||
this.mt_1p5()
|
||||
.pt_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
})
|
||||
.child(
|
||||
Label::new(label.clone())
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
ProfilePickerEntry::Profile(entry) => {
|
||||
let candidate = self.candidates.get(entry.candidate_index)?;
|
||||
let active_id = self.provider.profile_id(cx);
|
||||
let is_active = active_id == candidate.id;
|
||||
|
||||
Some(
|
||||
ListItem::new(SharedString::from(candidate.id.0.clone()))
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(
|
||||
v_flex()
|
||||
.child(HighlightedLabel::new(
|
||||
candidate.name.clone(),
|
||||
entry.positions.clone(),
|
||||
))
|
||||
.when_some(Self::documentation(candidate), |this, doc| {
|
||||
this.child(
|
||||
Label::new(doc).size(LabelSize::Small).color(Color::Muted),
|
||||
)
|
||||
}),
|
||||
PopoverMenu::new("profile-selector")
|
||||
.trigger_with_tooltip(trigger_button, {
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Profile Menu",
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.when(is_active, |this| {
|
||||
this.end_slot(
|
||||
div()
|
||||
.pr_2()
|
||||
.child(Icon::new(IconName::Check).color(Color::Accent)),
|
||||
)
|
||||
})
|
||||
.into_any_element(),
|
||||
}
|
||||
})
|
||||
.anchor(
|
||||
if documentation_side(settings.dock) == DocumentationSide::Left {
|
||||
gpui::Corner::BottomRight
|
||||
} else {
|
||||
gpui::Corner::BottomLeft
|
||||
},
|
||||
)
|
||||
}
|
||||
.with_handle(self.menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
Button::new("tools-not-supported-button", "Tools Unsupported")
|
||||
.disabled(true)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.tooltip(Tooltip::text("This model does not support tools."))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_footer(
|
||||
&self,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<gpui::AnyElement> {
|
||||
Some(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.p_1()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.child(
|
||||
Button::new("configure", "Configure")
|
||||
.icon(IconName::Settings)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
fn entries_include_custom_profiles(_cx: &mut TestAppContext) {
|
||||
let candidates = vec![
|
||||
ProfileCandidate {
|
||||
id: AgentProfileId("write".into()),
|
||||
name: SharedString::from("Write"),
|
||||
is_builtin: true,
|
||||
},
|
||||
ProfileCandidate {
|
||||
id: AgentProfileId("my-custom".into()),
|
||||
name: SharedString::from("My Custom"),
|
||||
is_builtin: false,
|
||||
},
|
||||
];
|
||||
|
||||
let entries = ProfilePickerDelegate::entries_from_candidates(&candidates);
|
||||
|
||||
assert!(entries.iter().any(|entry| matches!(
|
||||
entry,
|
||||
ProfilePickerEntry::Profile(profile)
|
||||
if candidates[profile.candidate_index].id.as_str() == "my-custom"
|
||||
)));
|
||||
assert!(entries.iter().any(|entry| matches!(
|
||||
entry,
|
||||
ProfilePickerEntry::Header(label) if label.as_ref() == "Custom Profiles"
|
||||
)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn fuzzy_filter_returns_no_results_and_keeps_configure(cx: &mut TestAppContext) {
|
||||
let candidates = vec![ProfileCandidate {
|
||||
id: AgentProfileId("write".into()),
|
||||
name: SharedString::from("Write"),
|
||||
is_builtin: true,
|
||||
}];
|
||||
|
||||
let delegate = ProfilePickerDelegate {
|
||||
fs: FakeFs::new(cx.executor()),
|
||||
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
|
||||
background: cx.executor(),
|
||||
candidates,
|
||||
string_candidates: Arc::new(Vec::new()),
|
||||
filtered_entries: Vec::new(),
|
||||
selected_index: 0,
|
||||
query: String::new(),
|
||||
cancel: None,
|
||||
};
|
||||
|
||||
let matches = Vec::new(); // No matches
|
||||
let _entries = delegate.entries_from_matches(matches);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn active_profile_selection_logic_works(cx: &mut TestAppContext) {
|
||||
let candidates = vec![
|
||||
ProfileCandidate {
|
||||
id: AgentProfileId("write".into()),
|
||||
name: SharedString::from("Write"),
|
||||
is_builtin: true,
|
||||
},
|
||||
ProfileCandidate {
|
||||
id: AgentProfileId("ask".into()),
|
||||
name: SharedString::from("Ask"),
|
||||
is_builtin: true,
|
||||
},
|
||||
];
|
||||
|
||||
let delegate = ProfilePickerDelegate {
|
||||
fs: FakeFs::new(cx.executor()),
|
||||
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
|
||||
background: cx.executor(),
|
||||
candidates,
|
||||
string_candidates: Arc::new(Vec::new()),
|
||||
filtered_entries: vec![
|
||||
ProfilePickerEntry::Profile(ProfileMatchEntry {
|
||||
candidate_index: 0,
|
||||
positions: Vec::new(),
|
||||
}),
|
||||
ProfilePickerEntry::Profile(ProfileMatchEntry {
|
||||
candidate_index: 1,
|
||||
positions: Vec::new(),
|
||||
}),
|
||||
],
|
||||
selected_index: 0,
|
||||
query: String::new(),
|
||||
cancel: None,
|
||||
};
|
||||
|
||||
// Active profile should be found at index 0
|
||||
let active_index = delegate.index_of_profile(&AgentProfileId("write".into()));
|
||||
assert_eq!(active_index, Some(0));
|
||||
}
|
||||
|
||||
struct TestProfileProvider {
|
||||
profile_id: AgentProfileId,
|
||||
}
|
||||
|
||||
impl TestProfileProvider {
|
||||
fn new(profile_id: AgentProfileId) -> Self {
|
||||
Self { profile_id }
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfileProvider for TestProfileProvider {
|
||||
fn profile_id(&self, _cx: &App) -> AgentProfileId {
|
||||
self.profile_id.clone()
|
||||
}
|
||||
|
||||
fn set_profile(&self, _profile_id: AgentProfileId, _cx: &mut App) {}
|
||||
|
||||
fn profiles_supported(&self, _cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
fn documentation_side(position: DockPosition) -> DocumentationSide {
|
||||
match position {
|
||||
DockPosition::Left => DocumentationSide::Right,
|
||||
DockPosition::Bottom => DocumentationSide::Left,
|
||||
DockPosition::Right => DocumentationSide::Left,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ impl TerminalInlineAssistant {
|
||||
let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES);
|
||||
let working_directory = terminal
|
||||
.working_directory()
|
||||
.map(|path| path.to_string_lossy().into_owned());
|
||||
.map(|path| path.to_string_lossy().to_string());
|
||||
(latest_output, working_directory)
|
||||
})
|
||||
.ok()
|
||||
|
||||
@@ -1431,14 +1431,10 @@ impl TextThreadEditor {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let path_style = worktree.read(cx).path_style();
|
||||
let full_path = worktree
|
||||
.read(cx)
|
||||
.root_name()
|
||||
.join(&project_path.path)
|
||||
.display(path_style)
|
||||
.into_owned();
|
||||
file_slash_command_args.push(full_path);
|
||||
let worktree_root_name = worktree.read(cx).root_name().to_string();
|
||||
let mut full_path = PathBuf::from(worktree_root_name.clone());
|
||||
full_path.push(&project_path.path);
|
||||
file_slash_command_args.push(full_path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
let cmd_name = FileSlashCommand.name();
|
||||
|
||||
@@ -48,7 +48,7 @@ impl Render for BurnModeTooltip {
|
||||
let keybinding = KeyBinding::for_action(&ToggleBurnMode, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.)));
|
||||
|
||||
tooltip_container(cx, |this, _| {
|
||||
tooltip_container(window, cx, |this, _, _| {
|
||||
this
|
||||
.child(
|
||||
h_flex()
|
||||
|
||||
@@ -17,7 +17,6 @@ use agent::context::{
|
||||
FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
|
||||
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
|
||||
};
|
||||
use util::paths::PathStyle;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub enum ContextPill {
|
||||
@@ -304,54 +303,33 @@ impl AddedContext {
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
match handle {
|
||||
AgentContextHandle::File(handle) => {
|
||||
Self::pending_file(handle, project.path_style(cx), cx)
|
||||
}
|
||||
AgentContextHandle::File(handle) => Self::pending_file(handle, cx),
|
||||
AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
|
||||
AgentContextHandle::Symbol(handle) => {
|
||||
Self::pending_symbol(handle, project.path_style(cx), cx)
|
||||
}
|
||||
AgentContextHandle::Selection(handle) => {
|
||||
Self::pending_selection(handle, project.path_style(cx), cx)
|
||||
}
|
||||
AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx),
|
||||
AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
|
||||
AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
|
||||
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
|
||||
AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
|
||||
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
|
||||
AgentContextHandle::Image(handle) => {
|
||||
Some(Self::image(handle, model, project.path_style(cx), cx))
|
||||
}
|
||||
AgentContextHandle::Image(handle) => Some(Self::image(handle, model, cx)),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_file(
|
||||
handle: FileContextHandle,
|
||||
path_style: PathStyle,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
let full_path = handle
|
||||
.buffer
|
||||
.read(cx)
|
||||
.file()?
|
||||
.full_path(cx)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
Some(Self::file(handle, &full_path, path_style, cx))
|
||||
fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let full_path = handle.buffer.read(cx).file()?.full_path(cx);
|
||||
Some(Self::file(handle, &full_path, cx))
|
||||
}
|
||||
|
||||
fn file(
|
||||
handle: FileContextHandle,
|
||||
full_path: &str,
|
||||
path_style: PathStyle,
|
||||
cx: &App,
|
||||
) -> AddedContext {
|
||||
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
|
||||
fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
|
||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||
let (name, parent) =
|
||||
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
|
||||
AddedContext {
|
||||
kind: ContextKind::File,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(SharedString::new(full_path)),
|
||||
icon_path: FileIcons::get_icon(Path::new(full_path), cx),
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: FileIcons::get_icon(full_path, cx),
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::File(handle),
|
||||
@@ -365,24 +343,19 @@ impl AddedContext {
|
||||
) -> Option<AddedContext> {
|
||||
let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
|
||||
let entry = worktree.entry_for_id(handle.entry_id)?;
|
||||
let full_path = worktree
|
||||
.full_path(&entry.path)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
Some(Self::directory(handle, &full_path, project.path_style(cx)))
|
||||
let full_path = worktree.full_path(&entry.path);
|
||||
Some(Self::directory(handle, &full_path))
|
||||
}
|
||||
|
||||
fn directory(
|
||||
handle: DirectoryContextHandle,
|
||||
full_path: &str,
|
||||
path_style: PathStyle,
|
||||
) -> AddedContext {
|
||||
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
|
||||
fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
|
||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||
let (name, parent) =
|
||||
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
|
||||
AddedContext {
|
||||
kind: ContextKind::Directory,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(SharedString::new(full_path)),
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
@@ -390,17 +363,9 @@ impl AddedContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_symbol(
|
||||
handle: SymbolContextHandle,
|
||||
path_style: PathStyle,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
let excerpt = ContextFileExcerpt::new(
|
||||
&handle.full_path(cx)?.to_string_lossy(),
|
||||
handle.enclosing_line_range(cx),
|
||||
path_style,
|
||||
cx,
|
||||
);
|
||||
fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let excerpt =
|
||||
ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Symbol,
|
||||
name: handle.symbol.clone(),
|
||||
@@ -418,17 +383,8 @@ impl AddedContext {
|
||||
})
|
||||
}
|
||||
|
||||
fn pending_selection(
|
||||
handle: SelectionContextHandle,
|
||||
path_style: PathStyle,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
let excerpt = ContextFileExcerpt::new(
|
||||
&handle.full_path(cx)?.to_string_lossy(),
|
||||
handle.line_range(cx),
|
||||
path_style,
|
||||
cx,
|
||||
);
|
||||
fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Selection,
|
||||
name: excerpt.file_name_and_range.clone(),
|
||||
@@ -529,13 +485,13 @@ impl AddedContext {
|
||||
fn image(
|
||||
context: ImageContext,
|
||||
model: Option<&Arc<dyn language_model::LanguageModel>>,
|
||||
path_style: PathStyle,
|
||||
cx: &App,
|
||||
) -> AddedContext {
|
||||
let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
|
||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||
let (name, parent) =
|
||||
extract_file_name_and_directory_from_full_path(full_path, path_style);
|
||||
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
|
||||
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
|
||||
let icon_path = FileIcons::get_icon(full_path, cx);
|
||||
(name, parent, icon_path)
|
||||
} else {
|
||||
("Image".into(), None, None)
|
||||
@@ -584,20 +540,19 @@ impl AddedContext {
|
||||
}
|
||||
|
||||
fn extract_file_name_and_directory_from_full_path(
|
||||
path: &str,
|
||||
path_style: PathStyle,
|
||||
path: &Path,
|
||||
name_fallback: &SharedString,
|
||||
) -> (SharedString, Option<SharedString>) {
|
||||
let (parent, file_name) = path_style.split(path);
|
||||
let parent = parent.and_then(|parent| {
|
||||
let parent = parent.trim_end_matches(path_style.separator());
|
||||
let (_, parent) = path_style.split(parent);
|
||||
if parent.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SharedString::new(parent))
|
||||
}
|
||||
});
|
||||
(SharedString::new(file_name), parent)
|
||||
let name = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| name_fallback.clone());
|
||||
let parent = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
|
||||
(name, parent)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -609,25 +564,25 @@ struct ContextFileExcerpt {
|
||||
}
|
||||
|
||||
impl ContextFileExcerpt {
|
||||
pub fn new(full_path: &str, line_range: Range<Point>, path_style: PathStyle, cx: &App) -> Self {
|
||||
let (parent, file_name) = path_style.split(full_path);
|
||||
pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
|
||||
let full_path_string = full_path.to_string_lossy().into_owned();
|
||||
let file_name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
|
||||
let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
|
||||
let mut full_path_and_range = full_path.to_owned();
|
||||
let mut full_path_and_range = full_path_string;
|
||||
full_path_and_range.push_str(&line_range_text);
|
||||
let mut file_name_and_range = file_name.to_owned();
|
||||
let mut file_name_and_range = file_name;
|
||||
file_name_and_range.push_str(&line_range_text);
|
||||
|
||||
let parent_name = parent.and_then(|parent| {
|
||||
let parent = parent.trim_end_matches(path_style.separator());
|
||||
let (_, parent) = path_style.split(parent);
|
||||
if parent.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SharedString::new(parent))
|
||||
}
|
||||
});
|
||||
let parent_name = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
|
||||
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
|
||||
let icon_path = FileIcons::get_icon(full_path, cx);
|
||||
|
||||
ContextFileExcerpt {
|
||||
file_name_and_range: file_name_and_range.into(),
|
||||
@@ -704,7 +659,7 @@ impl ContextPillHover {
|
||||
|
||||
impl Render for ContextPillHover {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
tooltip_container(cx, move |this, cx| {
|
||||
tooltip_container(window, cx, move |this, window, cx| {
|
||||
this.occlude()
|
||||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
@@ -735,7 +690,6 @@ impl Component for AddedContext {
|
||||
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
||||
},
|
||||
None,
|
||||
PathStyle::local(),
|
||||
cx,
|
||||
),
|
||||
);
|
||||
@@ -756,7 +710,6 @@ impl Component for AddedContext {
|
||||
.shared(),
|
||||
},
|
||||
None,
|
||||
PathStyle::local(),
|
||||
cx,
|
||||
),
|
||||
);
|
||||
@@ -772,7 +725,6 @@ impl Component for AddedContext {
|
||||
image_task: Task::ready(None).shared(),
|
||||
},
|
||||
None,
|
||||
PathStyle::local(),
|
||||
cx,
|
||||
),
|
||||
);
|
||||
@@ -815,8 +767,7 @@ mod tests {
|
||||
full_path: None,
|
||||
};
|
||||
|
||||
let added_context =
|
||||
AddedContext::image(image_context, Some(&model), PathStyle::local(), cx);
|
||||
let added_context = AddedContext::image(image_context, Some(&model), cx);
|
||||
|
||||
assert!(matches!(
|
||||
added_context.status,
|
||||
@@ -839,7 +790,7 @@ mod tests {
|
||||
full_path: None,
|
||||
};
|
||||
|
||||
let added_context = AddedContext::image(image_context, None, PathStyle::local(), cx);
|
||||
let added_context = AddedContext::image(image_context, None, cx);
|
||||
|
||||
assert!(
|
||||
matches!(added_context.status, ContextStatus::Ready),
|
||||
|
||||
@@ -40,7 +40,7 @@ impl AgentOnboardingModal {
|
||||
}
|
||||
|
||||
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url("https://zed.dev/blog/fastest-ai-code-editor");
|
||||
cx.open_url("http://zed.dev/blog/fastest-ai-code-editor");
|
||||
cx.notify();
|
||||
|
||||
agent_onboarding_event!("Blog Link Clicked");
|
||||
|
||||
@@ -12,8 +12,8 @@ impl UnavailableEditingTooltip {
|
||||
}
|
||||
|
||||
impl Render for UnavailableEditingTooltip {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
tooltip_container(cx, |this, _| {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
tooltip_container(window, cx, |this, _, _| {
|
||||
this.child(Label::new("Unavailable Editing")).child(
|
||||
div().max_w_64().child(
|
||||
Label::new(format!(
|
||||
|
||||
@@ -18,6 +18,7 @@ default = []
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
component.workspace = true
|
||||
feature_flags.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -18,6 +18,7 @@ pub use young_account_banner::YoungAccountBanner;
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt as _};
|
||||
use gpui::{AnyElement, Entity, IntoElement, ParentElement};
|
||||
use ui::{Divider, RegisterComponent, Tooltip, prelude::*};
|
||||
|
||||
@@ -84,7 +85,7 @@ impl ZedAiOnboarding {
|
||||
self
|
||||
}
|
||||
|
||||
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
|
||||
fn render_sign_in_disclaimer(&self, cx: &mut App) -> AnyElement {
|
||||
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
|
||||
|
||||
v_flex()
|
||||
@@ -95,7 +96,7 @@ impl ZedAiOnboarding {
|
||||
.color(Color::Muted)
|
||||
.mb_2(),
|
||||
)
|
||||
.child(PlanDefinitions.pro_plan(true, false))
|
||||
.child(PlanDefinitions.pro_plan(cx.has_flag::<BillingV2FeatureFlag>(), false))
|
||||
.child(
|
||||
Button::new("sign_in", "Try Zed Pro for Free")
|
||||
.disabled(signing_in)
|
||||
@@ -119,7 +120,7 @@ impl ZedAiOnboarding {
|
||||
.max_w_full()
|
||||
.gap_1()
|
||||
.child(Headline::new("Welcome to Zed AI"))
|
||||
.child(YoungAccountBanner)
|
||||
.child(YoungAccountBanner::new(is_v2))
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
@@ -306,7 +307,7 @@ impl RenderOnce for ZedAiOnboarding {
|
||||
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
|
||||
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
|
||||
match self.plan {
|
||||
None => self.render_free_plan_state(true, cx),
|
||||
None => self.render_free_plan_state(cx.has_flag::<BillingV2FeatureFlag>(), cx),
|
||||
Some(plan @ (Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))) => {
|
||||
self.render_free_plan_state(plan.is_v2(), cx)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use cloud_llm_client::{Plan, PlanV1, PlanV2};
|
||||
use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt};
|
||||
use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window};
|
||||
use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*};
|
||||
|
||||
@@ -49,7 +50,9 @@ impl AiUpsellCard {
|
||||
|
||||
impl RenderOnce for AiUpsellCard {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let is_v2_plan = self.user_plan.map_or(true, |plan| plan.is_v2());
|
||||
let is_v2_plan = self
|
||||
.user_plan
|
||||
.map_or(cx.has_flag::<BillingV2FeatureFlag>(), |plan| plan.is_v2());
|
||||
|
||||
let pro_section = v_flex()
|
||||
.flex_grow()
|
||||
@@ -172,7 +175,7 @@ impl RenderOnce for AiUpsellCard {
|
||||
.child(Label::new("Try Zed AI").size(LabelSize::Large))
|
||||
.map(|this| {
|
||||
if self.account_too_young {
|
||||
this.child(YoungAccountBanner).child(
|
||||
this.child(YoungAccountBanner::new(is_v2_plan)).child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
|
||||
@@ -2,17 +2,30 @@ use gpui::{IntoElement, ParentElement};
|
||||
use ui::{Banner, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct YoungAccountBanner;
|
||||
pub struct YoungAccountBanner {
|
||||
is_v2: bool,
|
||||
}
|
||||
|
||||
impl YoungAccountBanner {
|
||||
pub fn new(is_v2: bool) -> Self {
|
||||
Self { is_v2 }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for YoungAccountBanner {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for the Pro trial. You can request an exception by reaching out to billing-support@zed.dev";
|
||||
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for free plan usage or Pro plan free trial. You can request an exception by reaching out to billing-support@zed.dev";
|
||||
const YOUNG_ACCOUNT_DISCLAIMER_V2: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for the Pro trial. You can request an exception by reaching out to billing-support@zed.dev";
|
||||
|
||||
let label = div()
|
||||
.w_full()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(YOUNG_ACCOUNT_DISCLAIMER);
|
||||
.child(if self.is_v2 {
|
||||
YOUNG_ACCOUNT_DISCLAIMER_V2
|
||||
} else {
|
||||
YOUNG_ACCOUNT_DISCLAIMER
|
||||
});
|
||||
|
||||
div()
|
||||
.max_w_full()
|
||||
|
||||
@@ -67,6 +67,7 @@ pub enum Model {
|
||||
alias = "claude-opus-4-1-thinking-latest"
|
||||
)]
|
||||
ClaudeOpus4_1Thinking,
|
||||
#[default]
|
||||
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
|
||||
ClaudeSonnet4,
|
||||
#[serde(
|
||||
@@ -74,14 +75,6 @@ pub enum Model {
|
||||
alias = "claude-sonnet-4-thinking-latest"
|
||||
)]
|
||||
ClaudeSonnet4Thinking,
|
||||
#[default]
|
||||
#[serde(rename = "claude-sonnet-4-5", alias = "claude-sonnet-4-5-latest")]
|
||||
ClaudeSonnet4_5,
|
||||
#[serde(
|
||||
rename = "claude-sonnet-4-5-thinking",
|
||||
alias = "claude-sonnet-4-5-thinking-latest"
|
||||
)]
|
||||
ClaudeSonnet4_5Thinking,
|
||||
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
||||
Claude3_7Sonnet,
|
||||
#[serde(
|
||||
@@ -140,14 +133,6 @@ impl Model {
|
||||
return Ok(Self::ClaudeOpus4);
|
||||
}
|
||||
|
||||
if id.starts_with("claude-sonnet-4-5-thinking") {
|
||||
return Ok(Self::ClaudeSonnet4_5Thinking);
|
||||
}
|
||||
|
||||
if id.starts_with("claude-sonnet-4-5") {
|
||||
return Ok(Self::ClaudeSonnet4_5);
|
||||
}
|
||||
|
||||
if id.starts_with("claude-sonnet-4-thinking") {
|
||||
return Ok(Self::ClaudeSonnet4Thinking);
|
||||
}
|
||||
@@ -195,8 +180,6 @@ impl Model {
|
||||
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
|
||||
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
|
||||
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
|
||||
Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest",
|
||||
Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-thinking-latest",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
Self::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
|
||||
Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
|
||||
@@ -214,7 +197,6 @@ impl Model {
|
||||
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
|
||||
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
|
||||
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
|
||||
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-20250929",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
|
||||
Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
|
||||
@@ -233,8 +215,6 @@ impl Model {
|
||||
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
||||
Self::ClaudeSonnet4 => "Claude Sonnet 4",
|
||||
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
||||
Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
|
||||
Self::ClaudeSonnet4_5Thinking => "Claude Sonnet 4.5 Thinking",
|
||||
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
|
||||
@@ -256,8 +236,6 @@ impl Model {
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5
|
||||
| Self::ClaudeSonnet4_5Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet
|
||||
@@ -283,8 +261,6 @@ impl Model {
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5
|
||||
| Self::ClaudeSonnet4_5Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet
|
||||
@@ -304,8 +280,6 @@ impl Model {
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5
|
||||
| Self::ClaudeSonnet4_5Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
@@ -325,8 +299,6 @@ impl Model {
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5
|
||||
| Self::ClaudeSonnet4_5Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
@@ -346,7 +318,6 @@ impl Model {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4_5
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
@@ -356,7 +327,6 @@ impl Model {
|
||||
Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5Thinking
|
||||
| Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
|
||||
budget_tokens: Some(4_096),
|
||||
},
|
||||
|
||||
@@ -85,8 +85,11 @@ impl AskPassSession {
|
||||
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
|
||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
|
||||
let zed_cli_path =
|
||||
util::get_shell_safe_zed_cli_path().context("getting zed-cli path for askpass")?;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let zed_path = util::get_shell_safe_zed_path()?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let zed_path = std::env::current_exe()
|
||||
.context("finding current executable path for use in askpass")?;
|
||||
|
||||
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
|
||||
let mut kill_tx = Some(askpass_kill_master_tx);
|
||||
@@ -134,7 +137,7 @@ impl AskPassSession {
|
||||
});
|
||||
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = generate_askpass_script(&zed_cli_path, &askpass_socket);
|
||||
let askpass_script = generate_askpass_script(&zed_path, &askpass_socket);
|
||||
fs::write(&askpass_script_path, askpass_script)
|
||||
.await
|
||||
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
|
||||
@@ -251,10 +254,10 @@ pub fn main(socket: &str) {
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path) -> String {
|
||||
fn generate_askpass_script(zed_path: &str, askpass_socket: &std::path::Path) -> String {
|
||||
format!(
|
||||
"{shebang}\n{print_args} | {zed_cli} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
zed_cli = zed_cli_path,
|
||||
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
zed_exe = zed_path,
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
@@ -263,13 +266,13 @@ fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path)
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path) -> String {
|
||||
fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::path::Path) -> String {
|
||||
format!(
|
||||
r#"
|
||||
$ErrorActionPreference = 'Stop';
|
||||
($args -join [char]0) | & "{zed_cli}" --askpass={askpass_socket} 2> $null
|
||||
($args -join [char]0) | & "{zed_exe}" --askpass={askpass_socket} 2> $null
|
||||
"#,
|
||||
zed_cli = zed_cli_path,
|
||||
zed_exe = zed_path.display(),
|
||||
askpass_socket = askpass_socket.display(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,14 +63,12 @@ impl TryFrom<&str> for EncryptedPassword {
|
||||
if padded_length != len {
|
||||
value.resize(padded_length as usize, 0);
|
||||
}
|
||||
if len != 0 {
|
||||
unsafe {
|
||||
CryptProtectMemory(
|
||||
value.as_mut_ptr() as _,
|
||||
padded_length,
|
||||
CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
)?;
|
||||
}
|
||||
unsafe {
|
||||
CryptProtectMemory(
|
||||
value.as_mut_ptr() as _,
|
||||
len,
|
||||
CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
)?;
|
||||
}
|
||||
Ok(Self(value, len))
|
||||
}
|
||||
@@ -93,22 +91,19 @@ pub(crate) fn decrypt(mut password: EncryptedPassword) -> Result<String> {
|
||||
password.0.len(),
|
||||
CRYPTPROTECTMEMORY_BLOCK_SIZE
|
||||
);
|
||||
if password.1 != 0 {
|
||||
unsafe {
|
||||
CryptUnprotectMemory(
|
||||
password.0.as_mut_ptr() as _,
|
||||
password.0.len().try_into()?,
|
||||
CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
)
|
||||
.context("while decrypting a SSH password")?
|
||||
};
|
||||
unsafe {
|
||||
CryptUnprotectMemory(
|
||||
password.0.as_mut_ptr() as _,
|
||||
password.1,
|
||||
CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
)
|
||||
.context("while decrypting a SSH password")?
|
||||
};
|
||||
|
||||
{
|
||||
// Remove padding
|
||||
_ = password.0.drain(password.1 as usize..);
|
||||
}
|
||||
{
|
||||
// Remove padding
|
||||
_ = password.0.drain(password.1 as usize..);
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(std::mem::take(&mut password.0))?)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
|
||||
@@ -2669,7 +2669,7 @@ impl AssistantContext {
|
||||
}
|
||||
|
||||
pub fn summarize(&mut self, mut replace_old: bool, cx: &mut Context<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -1329,12 +1329,13 @@ fn setup_context_editor_with_fake_model(
|
||||
cx.update(|cx| {
|
||||
init_test(cx);
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
let configured_model = ConfiguredModel {
|
||||
provider: fake_provider.clone(),
|
||||
model: fake_model.clone(),
|
||||
};
|
||||
registry.set_default_model(Some(configured_model.clone()), cx);
|
||||
registry.set_thread_summary_model(Some(configured_model), cx);
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: fake_provider.clone(),
|
||||
model: fake_model.clone(),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ parking_lot.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use ui::prelude::*;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
@@ -50,10 +51,10 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
|
||||
}
|
||||
|
||||
fn root_path(&self) -> String {
|
||||
self.0.worktree_root_path().to_string_lossy().into_owned()
|
||||
self.0.worktree_root_path().to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
async fn read_text_file(&self, path: &RelPath) -> Result<String> {
|
||||
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
|
||||
self.0.read_text_file(path).await
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
|
||||
self.0
|
||||
.which(binary_name.as_ref())
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().into_owned())
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
async fn shell_env(&self) -> Vec<(String, String)> {
|
||||
|
||||
@@ -41,9 +41,6 @@ worktree.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
settings.workspace = true
|
||||
zlog.workspace = true
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
SlashCommandResult,
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use project::{Project, ProjectPath};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::Path,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct CargoWorkspaceSlashCommand;
|
||||
|
||||
impl CargoWorkspaceSlashCommand {
|
||||
async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
|
||||
let buffer = fs.load(path_to_cargo_toml).await?;
|
||||
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
|
||||
|
||||
let mut message = String::new();
|
||||
writeln!(message, "You are in a Rust project.")?;
|
||||
|
||||
if let Some(workspace) = cargo_toml.workspace {
|
||||
writeln!(
|
||||
message,
|
||||
"The project is a Cargo workspace with the following members:"
|
||||
)?;
|
||||
for member in workspace.members {
|
||||
writeln!(message, "- {member}")?;
|
||||
}
|
||||
|
||||
if !workspace.default_members.is_empty() {
|
||||
writeln!(message, "The default members are:")?;
|
||||
for member in workspace.default_members {
|
||||
writeln!(message, "- {member}")?;
|
||||
}
|
||||
}
|
||||
|
||||
if !workspace.dependencies.is_empty() {
|
||||
writeln!(
|
||||
message,
|
||||
"The following workspace dependencies are installed:"
|
||||
)?;
|
||||
for dependency in workspace.dependencies.keys() {
|
||||
writeln!(message, "- {dependency}")?;
|
||||
}
|
||||
}
|
||||
} else if let Some(package) = cargo_toml.package {
|
||||
writeln!(
|
||||
message,
|
||||
"The project name is \"{name}\".",
|
||||
name = package.name
|
||||
)?;
|
||||
|
||||
let description = package
|
||||
.description
|
||||
.as_ref()
|
||||
.and_then(|description| description.get().ok().cloned());
|
||||
if let Some(description) = description.as_ref() {
|
||||
writeln!(message, "It describes itself as \"{description}\".")?;
|
||||
}
|
||||
|
||||
if !cargo_toml.dependencies.is_empty() {
|
||||
writeln!(message, "The following dependencies are installed:")?;
|
||||
for dependency in cargo_toml.dependencies.keys() {
|
||||
writeln!(message, "- {dependency}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
fn path_to_cargo_toml(project: Entity<Project>, cx: &mut App) -> Option<Arc<Path>> {
|
||||
let worktree = project.read(cx).worktrees(cx).next()?;
|
||||
let worktree = worktree.read(cx);
|
||||
let entry = worktree.entry_for_path(RelPath::new("Cargo.toml").unwrap())?;
|
||||
let path = ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: entry.path.clone(),
|
||||
};
|
||||
Some(Arc::from(
|
||||
project.read(cx).absolute_path(&path, cx)?.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for CargoWorkspaceSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"cargo-workspace".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert project workspace metadata".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Project Workspace Metadata".into()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakEntity<Workspace>>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let fs = workspace.project().read(cx).fs().clone();
|
||||
let path = Self::path_to_cargo_toml(project, cx);
|
||||
let output = cx.background_spawn(async move {
|
||||
let path = path.with_context(|| "Cargo.toml not found")?;
|
||||
Self::build_message(fs, &path).await
|
||||
});
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = output.await?;
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::FileTree,
|
||||
label: "Project".into(),
|
||||
metadata: None,
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
.into_event_stream())
|
||||
})
|
||||
});
|
||||
output.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
}
|
||||
}
|
||||
@@ -6,19 +6,19 @@ use assistant_slash_command::{
|
||||
use fuzzy::{PathMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{
|
||||
Anchor, BufferSnapshot, DiagnosticEntryRef, DiagnosticSeverity, LspAdapterDelegate,
|
||||
Anchor, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, LspAdapterDelegate,
|
||||
OffsetRangeExt, ToOffset,
|
||||
};
|
||||
use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
|
||||
use rope::Point;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::paths::{PathMatcher, PathStyle};
|
||||
use util::{ResultExt, rel_path::RelPath};
|
||||
use util::ResultExt;
|
||||
use util::paths::PathMatcher;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::create_label_for_command;
|
||||
@@ -36,7 +36,7 @@ impl DiagnosticsSlashCommand {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
let path_prefix: Arc<RelPath> = RelPath::empty().into();
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
@@ -125,7 +125,6 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
let path_style = workspace.read(cx).project().read(cx).path_style(cx);
|
||||
let query = arguments.last().cloned().unwrap_or_default();
|
||||
|
||||
let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
@@ -135,11 +134,11 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|path_match| {
|
||||
path_match
|
||||
.path_prefix
|
||||
.join(&path_match.path)
|
||||
.display(path_style)
|
||||
.to_string()
|
||||
format!(
|
||||
"{}{}",
|
||||
path_match.path_prefix,
|
||||
path_match.path.to_string_lossy()
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -184,11 +183,9 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project();
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let options = Options::parse(arguments, path_style);
|
||||
let options = Options::parse(arguments);
|
||||
|
||||
let task = collect_diagnostics(project.clone(), options, cx);
|
||||
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
||||
|
||||
window.spawn(cx, async move |_| {
|
||||
task.await?
|
||||
@@ -207,14 +204,14 @@ struct Options {
|
||||
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
|
||||
|
||||
impl Options {
|
||||
fn parse(arguments: &[String], path_style: PathStyle) -> Self {
|
||||
fn parse(arguments: &[String]) -> Self {
|
||||
let mut include_warnings = false;
|
||||
let mut path_matcher = None;
|
||||
for arg in arguments {
|
||||
if arg == INCLUDE_WARNINGS_ARGUMENT {
|
||||
include_warnings = true;
|
||||
} else {
|
||||
path_matcher = PathMatcher::new(&[arg.to_owned()], path_style).log_err();
|
||||
path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
|
||||
}
|
||||
}
|
||||
Self {
|
||||
@@ -240,15 +237,21 @@ fn collect_diagnostics(
|
||||
None
|
||||
};
|
||||
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let glob_is_exact_file_match = if let Some(path) = options
|
||||
.path_matcher
|
||||
.as_ref()
|
||||
.and_then(|pm| pm.sources().first())
|
||||
{
|
||||
project
|
||||
.read(cx)
|
||||
.find_project_path(Path::new(path), cx)
|
||||
PathBuf::try_from(path)
|
||||
.ok()
|
||||
.and_then(|path| {
|
||||
project.read(cx).worktrees(cx).find_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree_root_path = Path::new(worktree.root_name());
|
||||
let relative_path = path.strip_prefix(worktree_root_path).ok()?;
|
||||
worktree.absolutize(relative_path).ok()
|
||||
})
|
||||
})
|
||||
.is_some()
|
||||
} else {
|
||||
false
|
||||
@@ -260,8 +263,9 @@ fn collect_diagnostics(
|
||||
.diagnostic_summaries(false, cx)
|
||||
.flat_map(|(path, _, summary)| {
|
||||
let worktree = project.read(cx).worktree_for_id(path.worktree_id, cx)?;
|
||||
let full_path = worktree.read(cx).root_name().join(&path.path);
|
||||
Some((path, full_path, summary))
|
||||
let mut path_buf = PathBuf::from(worktree.read(cx).root_name());
|
||||
path_buf.push(&path.path);
|
||||
Some((path, path_buf, summary))
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -277,7 +281,7 @@ fn collect_diagnostics(
|
||||
let mut project_summary = DiagnosticSummary::default();
|
||||
for (project_path, path, summary) in diagnostic_summaries {
|
||||
if let Some(path_matcher) = &options.path_matcher
|
||||
&& !path_matcher.is_match(&path.as_std_path())
|
||||
&& !path_matcher.is_match(&path)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -290,7 +294,7 @@ fn collect_diagnostics(
|
||||
}
|
||||
|
||||
let last_end = output.text.len();
|
||||
let file_path = path.display(path_style).to_string();
|
||||
let file_path = path.to_string_lossy().to_string();
|
||||
if !glob_is_exact_file_match {
|
||||
writeln!(&mut output.text, "{file_path}").unwrap();
|
||||
}
|
||||
@@ -367,7 +371,7 @@ pub fn collect_buffer_diagnostics(
|
||||
|
||||
fn collect_diagnostic(
|
||||
output: &mut SlashCommandOutput,
|
||||
entry: &DiagnosticEntryRef<'_, Anchor>,
|
||||
entry: &DiagnosticEntry<Anchor>,
|
||||
snapshot: &BufferSnapshot,
|
||||
include_warnings: bool,
|
||||
) {
|
||||
|
||||
@@ -14,11 +14,11 @@ use smol::stream::StreamExt;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
ops::{Range, RangeInclusive},
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt, rel_path::RelPath};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
use worktree::ChildEntriesOptions;
|
||||
|
||||
@@ -48,7 +48,7 @@ impl FileSlashCommand {
|
||||
include_dirs: true,
|
||||
include_ignored: false,
|
||||
};
|
||||
let entries = worktree.child_entries_with_options(RelPath::empty(), options);
|
||||
let entries = worktree.child_entries_with_options(Path::new(""), options);
|
||||
entries.map(move |entry| {
|
||||
(
|
||||
project::ProjectPath {
|
||||
@@ -61,18 +61,19 @@ impl FileSlashCommand {
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let path_prefix: Arc<RelPath> = RelPath::empty().into();
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
.filter_map(|(entry, is_dir)| {
|
||||
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
|
||||
let full_path = worktree.read(cx).root_name().join(&entry.path);
|
||||
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
|
||||
full_path.push(&entry.path);
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: entry.worktree_id.to_usize(),
|
||||
path: full_path,
|
||||
path: full_path.into(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir,
|
||||
@@ -148,8 +149,6 @@ impl SlashCommand for FileSlashCommand {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let path_style = workspace.read(cx).path_style(cx);
|
||||
|
||||
let paths = self.search_paths(
|
||||
arguments.last().cloned().unwrap_or_default(),
|
||||
cancellation_flag,
|
||||
@@ -162,14 +161,14 @@ impl SlashCommand for FileSlashCommand {
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|path_match| {
|
||||
let text = path_match
|
||||
.path_prefix
|
||||
.join(&path_match.path)
|
||||
.display(path_style)
|
||||
.to_string();
|
||||
let text = format!(
|
||||
"{}{}",
|
||||
path_match.path_prefix,
|
||||
path_match.path.to_string_lossy()
|
||||
);
|
||||
|
||||
let mut label = CodeLabel::default();
|
||||
let file_name = path_match.path.file_name()?;
|
||||
let file_name = path_match.path.file_name()?.to_string_lossy();
|
||||
let label_text = if path_match.is_dir {
|
||||
format!("{}/ ", file_name)
|
||||
} else {
|
||||
@@ -248,13 +247,14 @@ fn collect_files(
|
||||
cx.spawn(async move |cx| {
|
||||
for snapshot in snapshots {
|
||||
let worktree_id = snapshot.id();
|
||||
let path_style = snapshot.path_style();
|
||||
let mut directory_stack: Vec<Arc<RelPath>> = Vec::new();
|
||||
let mut folded_directory_names: Arc<RelPath> = RelPath::empty().into();
|
||||
let mut directory_stack: Vec<Arc<Path>> = Vec::new();
|
||||
let mut folded_directory_names_stack = Vec::new();
|
||||
let mut is_top_level_directory = true;
|
||||
|
||||
for entry in snapshot.entries(false, 0) {
|
||||
let path_including_worktree_name = snapshot.root_name().join(&entry.path);
|
||||
let mut path_including_worktree_name = PathBuf::new();
|
||||
path_including_worktree_name.push(snapshot.root_name());
|
||||
path_including_worktree_name.push(&entry.path);
|
||||
|
||||
if !matchers
|
||||
.iter()
|
||||
@@ -277,7 +277,13 @@ fn collect_files(
|
||||
)))?;
|
||||
}
|
||||
|
||||
let filename = entry.path.file_name().unwrap_or_default().to_string();
|
||||
let filename = entry
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
if entry.is_dir() {
|
||||
// Auto-fold directories that contain no files
|
||||
@@ -286,23 +292,24 @@ fn collect_files(
|
||||
if child_entries.next().is_none() && child.kind.is_dir() {
|
||||
if is_top_level_directory {
|
||||
is_top_level_directory = false;
|
||||
folded_directory_names =
|
||||
folded_directory_names.join(&path_including_worktree_name);
|
||||
folded_directory_names_stack.push(
|
||||
path_including_worktree_name.to_string_lossy().to_string(),
|
||||
);
|
||||
} else {
|
||||
folded_directory_names =
|
||||
folded_directory_names.join(RelPath::unix(&filename).unwrap());
|
||||
folded_directory_names_stack.push(filename.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Skip empty directories
|
||||
folded_directory_names = RelPath::empty().into();
|
||||
folded_directory_names_stack.clear();
|
||||
continue;
|
||||
}
|
||||
if folded_directory_names.is_empty() {
|
||||
let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
|
||||
if prefix_paths.is_empty() {
|
||||
let label = if is_top_level_directory {
|
||||
is_top_level_directory = false;
|
||||
path_including_worktree_name.display(path_style).to_string()
|
||||
path_including_worktree_name.to_string_lossy().to_string()
|
||||
} else {
|
||||
filename
|
||||
};
|
||||
@@ -313,23 +320,28 @@ fn collect_files(
|
||||
}))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: label.to_string(),
|
||||
text: label,
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
directory_stack.push(entry.path.clone());
|
||||
} else {
|
||||
let entry_name =
|
||||
folded_directory_names.join(RelPath::unix(&filename).unwrap());
|
||||
let entry_name = entry_name.display(path_style);
|
||||
// todo(windows)
|
||||
// Potential bug: this assumes that the path separator is always `\` on Windows
|
||||
let entry_name = format!(
|
||||
"{}{}{}",
|
||||
prefix_paths,
|
||||
std::path::MAIN_SEPARATOR_STR,
|
||||
&filename
|
||||
);
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::Folder,
|
||||
label: entry_name.to_string().into(),
|
||||
label: entry_name.clone().into(),
|
||||
metadata: None,
|
||||
}))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: entry_name.to_string(),
|
||||
text: entry_name,
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
@@ -344,7 +356,7 @@ fn collect_files(
|
||||
} else if entry.is_file() {
|
||||
let Some(open_buffer_task) = project_handle
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, entry.path.clone()), cx)
|
||||
project.open_buffer((worktree_id, &entry.path), cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
@@ -355,7 +367,7 @@ fn collect_files(
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
||||
append_buffer_to_output(
|
||||
&snapshot,
|
||||
Some(path_including_worktree_name.display(path_style).as_ref()),
|
||||
Some(&path_including_worktree_name),
|
||||
&mut output,
|
||||
)
|
||||
.log_err();
|
||||
@@ -380,18 +392,18 @@ fn collect_files(
|
||||
}
|
||||
|
||||
pub fn codeblock_fence_for_path(
|
||||
path: Option<&str>,
|
||||
path: Option<&Path>,
|
||||
row_range: Option<RangeInclusive<u32>>,
|
||||
) -> String {
|
||||
let mut text = String::new();
|
||||
write!(text, "```").unwrap();
|
||||
|
||||
if let Some(path) = path {
|
||||
if let Some(extension) = Path::new(path).extension().and_then(|ext| ext.to_str()) {
|
||||
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
|
||||
write!(text, "{} ", extension).unwrap();
|
||||
}
|
||||
|
||||
write!(text, "{path}").unwrap();
|
||||
write!(text, "{}", path.display()).unwrap();
|
||||
} else {
|
||||
write!(text, "untitled").unwrap();
|
||||
}
|
||||
@@ -411,12 +423,12 @@ pub struct FileCommandMetadata {
|
||||
|
||||
pub fn build_entry_output_section(
|
||||
range: Range<usize>,
|
||||
path: Option<&str>,
|
||||
path: Option<&Path>,
|
||||
is_directory: bool,
|
||||
line_range: Option<Range<u32>>,
|
||||
) -> SlashCommandOutputSection<usize> {
|
||||
let mut label = if let Some(path) = path {
|
||||
path.to_string()
|
||||
path.to_string_lossy().to_string()
|
||||
} else {
|
||||
"untitled".to_string()
|
||||
};
|
||||
@@ -439,7 +451,7 @@ pub fn build_entry_output_section(
|
||||
} else {
|
||||
path.and_then(|path| {
|
||||
serde_json::to_value(FileCommandMetadata {
|
||||
path: path.to_string(),
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
@@ -450,9 +462,10 @@ pub fn build_entry_output_section(
|
||||
/// This contains a small fork of the util::paths::PathMatcher, that is stricter about the prefix
|
||||
/// check. Only subpaths pass the prefix check, rather than any prefix.
|
||||
mod custom_path_matcher {
|
||||
use std::{fmt::Debug as _, path::Path};
|
||||
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use std::fmt::Debug as _;
|
||||
use util::{paths::SanitizedPath, rel_path::RelPath};
|
||||
use util::paths::SanitizedPath;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PathMatcher {
|
||||
@@ -479,12 +492,12 @@ mod custom_path_matcher {
|
||||
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
|
||||
let globs = globs
|
||||
.iter()
|
||||
.map(|glob| Glob::new(&SanitizedPath::new(glob).to_string()))
|
||||
.map(|glob| Glob::new(&SanitizedPath::new(glob).to_glob_string()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
|
||||
let sources_with_trailing_slash = globs
|
||||
.iter()
|
||||
.map(|glob| glob.glob().to_string() + "/")
|
||||
.map(|glob| glob.glob().to_string() + std::path::MAIN_SEPARATOR_STR)
|
||||
.collect();
|
||||
let mut glob_builder = GlobSetBuilder::new();
|
||||
for single_glob in globs {
|
||||
@@ -498,13 +511,16 @@ mod custom_path_matcher {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_match(&self, other: &RelPath) -> bool {
|
||||
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
|
||||
let other_path = other.as_ref();
|
||||
self.sources
|
||||
.iter()
|
||||
.zip(self.sources_with_trailing_slash.iter())
|
||||
.any(|(source, with_slash)| {
|
||||
let as_bytes = other.as_unix_str().as_bytes();
|
||||
let with_slash = if source.ends_with('/') {
|
||||
let as_bytes = other_path.as_os_str().as_encoded_bytes();
|
||||
// todo(windows)
|
||||
// Potential bug: this assumes that the path separator is always `\` on Windows
|
||||
let with_slash = if source.ends_with(std::path::MAIN_SEPARATOR_STR) {
|
||||
source.as_bytes()
|
||||
} else {
|
||||
with_slash.as_bytes()
|
||||
@@ -512,13 +528,13 @@ mod custom_path_matcher {
|
||||
|
||||
as_bytes.starts_with(with_slash) || as_bytes.ends_with(source.as_bytes())
|
||||
})
|
||||
|| self.glob.is_match(other.as_std_path())
|
||||
|| self.check_with_end_separator(other)
|
||||
|| self.glob.is_match(other_path)
|
||||
|| self.check_with_end_separator(other_path)
|
||||
}
|
||||
|
||||
fn check_with_end_separator(&self, path: &RelPath) -> bool {
|
||||
let path_str = path.as_unix_str();
|
||||
let separator = "/";
|
||||
fn check_with_end_separator(&self, path: &Path) -> bool {
|
||||
let path_str = path.to_string_lossy();
|
||||
let separator = std::path::MAIN_SEPARATOR_STR;
|
||||
if path_str.ends_with(separator) {
|
||||
false
|
||||
} else {
|
||||
@@ -530,7 +546,7 @@ mod custom_path_matcher {
|
||||
|
||||
pub fn append_buffer_to_output(
|
||||
buffer: &BufferSnapshot,
|
||||
path: Option<&str>,
|
||||
path: Option<&Path>,
|
||||
output: &mut SlashCommandOutput,
|
||||
) -> Result<()> {
|
||||
let prev_len = output.text.len();
|
||||
|
||||
@@ -137,9 +137,7 @@ pub fn selections_creases(
|
||||
None
|
||||
};
|
||||
let language_name = language_name.as_deref().unwrap_or("");
|
||||
let filename = snapshot
|
||||
.file_at(range.start)
|
||||
.map(|file| file.full_path(cx).to_string_lossy().into_owned());
|
||||
let filename = snapshot.file_at(range.start).map(|file| file.full_path(cx));
|
||||
let text = if language_name == "markdown" {
|
||||
selected_text
|
||||
.lines()
|
||||
@@ -189,9 +187,9 @@ pub fn selections_creases(
|
||||
let start_line = range.start.row + 1;
|
||||
let end_line = range.end.row + 1;
|
||||
if start_line == end_line {
|
||||
format!("{path}, Line {start_line}")
|
||||
format!("{}, Line {}", path.display(), start_line)
|
||||
} else {
|
||||
format!("{path}, Lines {start_line} to {end_line}")
|
||||
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
|
||||
}
|
||||
} else {
|
||||
"Quoted selection".to_string()
|
||||
|
||||
@@ -7,8 +7,8 @@ use editor::Editor;
|
||||
use gpui::{AppContext as _, Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use ui::{App, IconName, SharedString, Window};
|
||||
use std::{path::Path, sync::atomic::AtomicBool};
|
||||
use ui::{App, IconName, Window};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct OutlineSlashCommand;
|
||||
@@ -67,13 +67,13 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let path = snapshot.resolve_file_path(true, cx);
|
||||
let path = snapshot.resolve_file_path(cx, true);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let outline = snapshot.outline(None);
|
||||
|
||||
let path = path.as_deref().unwrap_or("untitled");
|
||||
let mut outline_text = format!("Symbols for {path}:\n");
|
||||
let path = path.as_deref().unwrap_or(Path::new("untitled"));
|
||||
let mut outline_text = format!("Symbols for {}:\n", path.display());
|
||||
for item in &outline.path_candidates {
|
||||
outline_text.push_str("- ");
|
||||
outline_text.push_str(&item.string);
|
||||
@@ -84,7 +84,7 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..outline_text.len(),
|
||||
icon: IconName::ListTree,
|
||||
label: SharedString::new(path),
|
||||
label: path.to_string_lossy().to_string().into(),
|
||||
metadata: None,
|
||||
}],
|
||||
text: outline_text,
|
||||
|
||||
@@ -8,9 +8,12 @@ use editor::Editor;
|
||||
use futures::future::join_all;
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::{ActiveTheme, App, Window, prelude::*};
|
||||
use util::{ResultExt, paths::PathStyle};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::file_command::append_buffer_to_output;
|
||||
@@ -69,42 +72,35 @@ impl SlashCommand for TabSlashCommand {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow::anyhow!("no workspace")));
|
||||
};
|
||||
|
||||
let active_item_path = workspace.update(cx, |workspace, cx| {
|
||||
let snapshot = active_item_buffer(workspace, cx).ok()?;
|
||||
snapshot.resolve_file_path(true, cx)
|
||||
let active_item_path = workspace.as_ref().and_then(|workspace| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let snapshot = active_item_buffer(workspace, cx).ok()?;
|
||||
snapshot.resolve_file_path(cx, true)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
let path_style = workspace.read(cx).path_style(cx);
|
||||
|
||||
let current_query = arguments.last().cloned().unwrap_or_default();
|
||||
let tab_items_search = tab_items_for_queries(
|
||||
Some(workspace.downgrade()),
|
||||
&[current_query],
|
||||
cancel,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let tab_items_search =
|
||||
tab_items_for_queries(workspace, &[current_query], cancel, false, window, cx);
|
||||
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
window.spawn(cx, async move |_| {
|
||||
let tab_items = tab_items_search.await?;
|
||||
let run_command = tab_items.len() == 1;
|
||||
let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
|
||||
let path = path?;
|
||||
if argument_set.contains(&path) {
|
||||
let path_string = path.as_deref()?.to_string_lossy().to_string();
|
||||
if argument_set.contains(&path_string) {
|
||||
return None;
|
||||
}
|
||||
if active_item_path.as_ref() == Some(&path) {
|
||||
if active_item_path.is_some() && active_item_path == path {
|
||||
return None;
|
||||
}
|
||||
let label = create_tab_completion_label(&path, path_style, comment_id);
|
||||
let label = create_tab_completion_label(path.as_ref()?, comment_id);
|
||||
Some(ArgumentCompletion {
|
||||
label,
|
||||
new_text: path,
|
||||
new_text: path_string,
|
||||
replace_previous_arguments: false,
|
||||
after_completion: run_command.into(),
|
||||
})
|
||||
@@ -113,9 +109,8 @@ impl SlashCommand for TabSlashCommand {
|
||||
let active_item_completion = active_item_path
|
||||
.as_deref()
|
||||
.map(|active_item_path| {
|
||||
let path_string = active_item_path.to_string();
|
||||
let label =
|
||||
create_tab_completion_label(active_item_path, path_style, comment_id);
|
||||
let path_string = active_item_path.to_string_lossy().to_string();
|
||||
let label = create_tab_completion_label(active_item_path, comment_id);
|
||||
ArgumentCompletion {
|
||||
label,
|
||||
new_text: path_string,
|
||||
@@ -174,7 +169,7 @@ fn tab_items_for_queries(
|
||||
strict_match: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<anyhow::Result<Vec<(Option<String>, BufferSnapshot, usize)>>> {
|
||||
) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
|
||||
let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
|
||||
let queries = queries.to_owned();
|
||||
window.spawn(cx, async move |cx| {
|
||||
@@ -184,7 +179,7 @@ fn tab_items_for_queries(
|
||||
.update(cx, |workspace, cx| {
|
||||
if strict_match && empty_query {
|
||||
let snapshot = active_item_buffer(workspace, cx)?;
|
||||
let full_path = snapshot.resolve_file_path(true, cx);
|
||||
let full_path = snapshot.resolve_file_path(cx, true);
|
||||
return anyhow::Ok(vec![(full_path, snapshot, 0)]);
|
||||
}
|
||||
|
||||
@@ -206,7 +201,7 @@ fn tab_items_for_queries(
|
||||
&& visited_buffers.insert(buffer.read(cx).remote_id())
|
||||
{
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let full_path = snapshot.resolve_file_path(true, cx);
|
||||
let full_path = snapshot.resolve_file_path(cx, true);
|
||||
open_buffers.push((full_path, snapshot, *timestamp));
|
||||
}
|
||||
}
|
||||
@@ -229,7 +224,10 @@ fn tab_items_for_queries(
|
||||
let match_candidates = open_buffers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(id, (full_path, ..))| Some((id, full_path.clone()?)))
|
||||
.filter_map(|(id, (full_path, ..))| {
|
||||
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
|
||||
Some((id, path_string))
|
||||
})
|
||||
.fold(HashMap::default(), |mut candidates, (id, path_string)| {
|
||||
candidates
|
||||
.entry(path_string)
|
||||
@@ -251,7 +249,8 @@ fn tab_items_for_queries(
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(id, (full_path, ..))| {
|
||||
Some(fuzzy::StringMatchCandidate::new(id, full_path.as_ref()?))
|
||||
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
|
||||
Some(fuzzy::StringMatchCandidate::new(id, &path_string))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut processed_matches = HashSet::default();
|
||||
@@ -303,15 +302,21 @@ fn active_item_buffer(
|
||||
}
|
||||
|
||||
fn create_tab_completion_label(
|
||||
path: &str,
|
||||
path_style: PathStyle,
|
||||
path: &std::path::Path,
|
||||
comment_id: Option<HighlightId>,
|
||||
) -> CodeLabel {
|
||||
let (parent_path, file_name) = path_style.split(path);
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy())
|
||||
.unwrap_or_default();
|
||||
let parent_path = path
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy())
|
||||
.unwrap_or_default();
|
||||
let mut label = CodeLabel::default();
|
||||
label.push_str(file_name, None);
|
||||
label.push_str(&file_name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(parent_path.unwrap_or_default(), comment_id);
|
||||
label.push_str(&parent_path, comment_id);
|
||||
label.filter_range = 0..file_name.len();
|
||||
label
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use language::{Buffer, OutlineItem, ParseStatus};
|
||||
use project::Project;
|
||||
use regex::Regex;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
use text::Point;
|
||||
|
||||
/// For files over this size, instead of reading them (or including them in context),
|
||||
@@ -142,7 +143,7 @@ pub struct BufferContent {
|
||||
/// For smaller files, returns the full content.
|
||||
pub async fn get_buffer_content_or_outline(
|
||||
buffer: Entity<Buffer>,
|
||||
path: Option<&str>,
|
||||
path: Option<&Path>,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<BufferContent> {
|
||||
let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?;
|
||||
@@ -169,10 +170,15 @@ pub async fn get_buffer_content_or_outline(
|
||||
|
||||
let text = if let Some(path) = path {
|
||||
format!(
|
||||
"# File outline for {path} (file too large to show full content)\n\n{outline_text}",
|
||||
"# File outline for {} (file too large to show full content)\n\n{}",
|
||||
path.display(),
|
||||
outline_text
|
||||
)
|
||||
} else {
|
||||
format!("# File outline (file too large to show full content)\n\n{outline_text}",)
|
||||
format!(
|
||||
"# File outline (file too large to show full content)\n\n{}",
|
||||
outline_text
|
||||
)
|
||||
};
|
||||
Ok(BufferContent {
|
||||
text,
|
||||
|
||||
@@ -96,7 +96,9 @@ impl Tool for CopyPathTool {
|
||||
.and_then(|project_path| project.entry_for_path(&project_path, cx))
|
||||
{
|
||||
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
|
||||
Some(project_path) => project.copy_entry(entity.id, project_path, cx),
|
||||
Some(project_path) => {
|
||||
project.copy_entry(entity.id, None, project_path.path, cx)
|
||||
}
|
||||
None => Task::ready(Err(anyhow!(
|
||||
"Destination path {} was outside the project.",
|
||||
input.destination_path
|
||||
|
||||
@@ -8,7 +8,7 @@ use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchem
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write, sync::Arc};
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
@@ -150,7 +150,9 @@ impl Tool for DiagnosticsTool {
|
||||
has_diagnostics = true;
|
||||
output.push_str(&format!(
|
||||
"{}: {} error(s), {} warning(s)\n",
|
||||
worktree.read(cx).absolutize(&project_path.path).display(),
|
||||
Path::new(worktree.read(cx).root_name())
|
||||
.join(project_path.path)
|
||||
.display(),
|
||||
summary.error_count,
|
||||
summary.warning_count
|
||||
));
|
||||
|
||||
@@ -26,13 +26,13 @@ use language_model::{
|
||||
use project::{AgentLocation, Project};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp, iter, mem, ops::Range, pin::Pin, sync::Arc, task::Poll};
|
||||
use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::Poll};
|
||||
use streaming_diff::{CharOperation, StreamingDiff};
|
||||
use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CreateFilePromptTemplate {
|
||||
path: Option<String>,
|
||||
path: Option<PathBuf>,
|
||||
edit_description: String,
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Template for CreateFilePromptTemplate {
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EditFileXmlPromptTemplate {
|
||||
path: Option<String>,
|
||||
path: Option<PathBuf>,
|
||||
edit_description: String,
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ impl Template for EditFileXmlPromptTemplate {
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EditFileDiffFencedPromptTemplate {
|
||||
path: Option<String>,
|
||||
path: Option<PathBuf>,
|
||||
edit_description: String,
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ impl EditAgent {
|
||||
let conversation = conversation.clone();
|
||||
let output = cx.spawn(async move |cx| {
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
||||
let path = cx.update(|cx| snapshot.resolve_file_path(true, cx))?;
|
||||
let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
|
||||
let prompt = CreateFilePromptTemplate {
|
||||
path,
|
||||
edit_description,
|
||||
@@ -229,7 +229,7 @@ impl EditAgent {
|
||||
let edit_format = self.edit_format;
|
||||
let output = cx.spawn(async move |cx| {
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
||||
let path = cx.update(|cx| snapshot.resolve_file_path(true, cx))?;
|
||||
let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
|
||||
let prompt = match edit_format {
|
||||
EditFormat::XmlTags => EditFileXmlPromptTemplate {
|
||||
path,
|
||||
|
||||
@@ -38,7 +38,6 @@ use settings::Settings;
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::HashSet,
|
||||
ffi::OsStr,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
@@ -46,7 +45,7 @@ use std::{
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
|
||||
use util::{ResultExt, rel_path::RelPath};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct EditFileTool;
|
||||
@@ -147,11 +146,11 @@ impl Tool for EditFileTool {
|
||||
|
||||
// If any path component matches the local settings folder, then this could affect
|
||||
// the editor in ways beyond the project source, so prompt.
|
||||
let local_settings_folder = paths::local_settings_folder_name();
|
||||
let local_settings_folder = paths::local_settings_folder_relative_path();
|
||||
let path = Path::new(&input.path);
|
||||
if path
|
||||
.components()
|
||||
.any(|c| c.as_os_str() == <str as AsRef<OsStr>>::as_ref(local_settings_folder))
|
||||
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -196,10 +195,10 @@ impl Tool for EditFileTool {
|
||||
let mut description = input.display_description.clone();
|
||||
|
||||
// Add context about why confirmation may be needed
|
||||
let local_settings_folder = paths::local_settings_folder_name();
|
||||
let local_settings_folder = paths::local_settings_folder_relative_path();
|
||||
if path
|
||||
.components()
|
||||
.any(|c| c.as_os_str() == <str as AsRef<OsStr>>::as_ref(local_settings_folder))
|
||||
.any(|c| c.as_os_str() == local_settings_folder.as_os_str())
|
||||
{
|
||||
description.push_str(" (local settings)");
|
||||
} else if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
|
||||
@@ -378,7 +377,7 @@ impl Tool for EditFileTool {
|
||||
.await;
|
||||
|
||||
let output = EditFileToolOutput {
|
||||
original_path: project_path.path.as_std_path().to_owned(),
|
||||
original_path: project_path.path.to_path_buf(),
|
||||
new_text,
|
||||
old_text,
|
||||
raw_output: Some(agent_output),
|
||||
@@ -550,11 +549,10 @@ fn resolve_path(
|
||||
let file_name = input
|
||||
.path
|
||||
.file_name()
|
||||
.and_then(|file_name| file_name.to_str())
|
||||
.context("Can't create file: invalid filename")?;
|
||||
|
||||
let new_file_path = parent_project_path.map(|parent| ProjectPath {
|
||||
path: parent.path.join(RelPath::unix(file_name).unwrap()),
|
||||
path: Arc::from(parent.path.join(file_name)),
|
||||
..parent
|
||||
});
|
||||
|
||||
@@ -1161,7 +1159,7 @@ async fn build_buffer(
|
||||
LineEnding::normalize(&mut text);
|
||||
let text = Rope::from(text);
|
||||
let language = cx
|
||||
.update(|_cx| language_registry.load_language_for_file_path(&path))?
|
||||
.update(|_cx| language_registry.language_for_file_path(&path))?
|
||||
.await
|
||||
.ok();
|
||||
let buffer = cx.new(|cx| {
|
||||
@@ -1238,7 +1236,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::fs;
|
||||
use util::{path, rel_path::rel_path};
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
|
||||
@@ -1357,10 +1355,14 @@ mod tests {
|
||||
cx.update(|cx| resolve_path(&input, project, cx))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
|
||||
let actual = path.expect("Should return valid path").path;
|
||||
assert_eq!(actual.as_ref(), rel_path(expected));
|
||||
let actual = path
|
||||
.expect("Should return valid path")
|
||||
.path
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace("\\", "/"); // Naive Windows paths normalization
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1974,22 +1976,25 @@ mod tests {
|
||||
let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
|
||||
|
||||
// Get the actual local settings folder name
|
||||
let local_settings_folder = paths::local_settings_folder_name();
|
||||
let local_settings_folder = paths::local_settings_folder_relative_path();
|
||||
|
||||
// Test various config path patterns
|
||||
let test_cases = vec![
|
||||
(
|
||||
format!("{local_settings_folder}/settings.json"),
|
||||
format!("{}/settings.json", local_settings_folder.display()),
|
||||
true,
|
||||
"Top-level local settings file".to_string(),
|
||||
),
|
||||
(
|
||||
format!("myproject/{local_settings_folder}/settings.json"),
|
||||
format!(
|
||||
"myproject/{}/settings.json",
|
||||
local_settings_folder.display()
|
||||
),
|
||||
true,
|
||||
"Local settings in project path".to_string(),
|
||||
),
|
||||
(
|
||||
format!("src/{local_settings_folder}/config.toml"),
|
||||
format!("src/{}/config.toml", local_settings_folder.display()),
|
||||
true,
|
||||
"Local settings in subdirectory".to_string(),
|
||||
),
|
||||
@@ -2200,7 +2205,12 @@ mod tests {
|
||||
("", false, "Empty path is treated as project root"),
|
||||
// Root directory
|
||||
("/", true, "Root directory should be outside project"),
|
||||
("project/../other", true, "Path with .. is outside project"),
|
||||
// Parent directory references - find_project_path resolves these
|
||||
(
|
||||
"project/../other",
|
||||
false,
|
||||
"Path with .. is resolved by find_project_path",
|
||||
),
|
||||
(
|
||||
"project/./src/file.rs",
|
||||
false,
|
||||
|
||||
@@ -161,13 +161,10 @@ impl Tool for FindPathTool {
|
||||
}
|
||||
|
||||
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
|
||||
let path_matcher = match PathMatcher::new(
|
||||
[
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { glob },
|
||||
],
|
||||
project.read(cx).path_style(cx),
|
||||
) {
|
||||
let path_matcher = match PathMatcher::new([
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { glob },
|
||||
]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
|
||||
};
|
||||
@@ -181,15 +178,10 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
|
||||
Ok(snapshots
|
||||
.iter()
|
||||
.flat_map(|snapshot| {
|
||||
let root_name = PathBuf::from(snapshot.root_name());
|
||||
snapshot
|
||||
.entries(false, 0)
|
||||
.map(move |entry| {
|
||||
snapshot
|
||||
.root_name()
|
||||
.join(&entry.path)
|
||||
.as_std_path()
|
||||
.to_path_buf()
|
||||
})
|
||||
.map(move |entry| root_name.join(&entry.path))
|
||||
.filter(|path| path_matcher.is_match(&path))
|
||||
})
|
||||
.collect())
|
||||
@@ -262,7 +254,7 @@ impl ToolCard for FindPathToolCard {
|
||||
.children(self.paths.iter().enumerate().map(|(index, path)| {
|
||||
let path_clone = path.clone();
|
||||
let workspace_clone = workspace.clone();
|
||||
let button_label = path.to_string_lossy().into_owned();
|
||||
let button_label = path.to_string_lossy().to_string();
|
||||
|
||||
Button::new(("path", index), button_label)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
|
||||
@@ -125,7 +125,6 @@ impl Tool for GrepTool {
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
project.read(cx).path_style(cx),
|
||||
) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(error) => {
|
||||
@@ -142,7 +141,7 @@ impl Tool for GrepTool {
|
||||
.iter()
|
||||
.chain(global_settings.private_files.sources().iter());
|
||||
|
||||
match PathMatcher::new(exclude_patterns, project.read(cx).path_style(cx)) {
|
||||
match PathMatcher::new(exclude_patterns) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(error) => {
|
||||
return Task::ready(Err(anyhow!("invalid exclude pattern: {error}"))).into();
|
||||
|
||||
@@ -4,11 +4,11 @@ use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use project::{Project, ProjectPath, WorktreeSettings};
|
||||
use project::{Project, WorktreeSettings};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::{fmt::Write, sync::Arc};
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
@@ -86,7 +86,6 @@ impl Tool for ListDirectoryTool {
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
@@ -101,7 +100,7 @@ impl Tool for ListDirectoryTool {
|
||||
.filter_map(|worktree| {
|
||||
worktree.read(cx).root_entry().and_then(|entry| {
|
||||
if entry.is_dir() {
|
||||
Some(entry.path.display(path_style))
|
||||
entry.path.to_str()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -159,6 +158,7 @@ impl Tool for ListDirectoryTool {
|
||||
}
|
||||
|
||||
let worktree_snapshot = worktree.read(cx).snapshot();
|
||||
let worktree_root_name = worktree.read(cx).root_name().to_string();
|
||||
|
||||
let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
|
||||
return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
|
||||
@@ -180,22 +180,23 @@ impl Tool for ListDirectoryTool {
|
||||
continue;
|
||||
}
|
||||
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree_snapshot.id(),
|
||||
path: entry.path.clone(),
|
||||
};
|
||||
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
|
||||
if project
|
||||
.read(cx)
|
||||
.find_project_path(&entry.path, cx)
|
||||
.map(|project_path| {
|
||||
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
|
||||
|
||||
if worktree_settings.is_path_excluded(&project_path.path)
|
||||
|| worktree_settings.is_path_private(&project_path.path)
|
||||
worktree_settings.is_path_excluded(&project_path.path)
|
||||
|| worktree_settings.is_path_private(&project_path.path)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let full_path = worktree_snapshot
|
||||
.root_name()
|
||||
let full_path = Path::new(&worktree_root_name)
|
||||
.join(&entry.path)
|
||||
.display(worktree_snapshot.path_style())
|
||||
.display()
|
||||
.to_string();
|
||||
if entry.is_dir() {
|
||||
folders.push(full_path);
|
||||
|
||||
@@ -108,7 +108,7 @@ impl Tool for MovePathTool {
|
||||
.and_then(|project_path| project.entry_for_path(&project_path, cx))
|
||||
{
|
||||
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
|
||||
Some(project_path) => project.rename_entry(entity.id, project_path, cx),
|
||||
Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
|
||||
None => Task::ready(Err(anyhow!(
|
||||
"Destination path {} was outside the project.",
|
||||
input.destination_path
|
||||
|
||||
@@ -104,7 +104,7 @@ mod tests {
|
||||
async fn test_to_absolute_path(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let temp_path = temp_dir.path().to_string_lossy().into_owned();
|
||||
let temp_path = temp_dir.path().to_string_lossy().to_string();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user