Compare commits
162 Commits
fix-git-wo
...
rodio-audi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48914ae8c1 | ||
|
|
8542c5f2f8 | ||
|
|
0b7932d38e | ||
|
|
e874f979ce | ||
|
|
28ca0edb06 | ||
|
|
d35f1c0b63 | ||
|
|
3720891480 | ||
|
|
2d13d4fe3d | ||
|
|
219c07c8e0 | ||
|
|
4f1634f95c | ||
|
|
40eec32cb8 | ||
|
|
17499453f6 | ||
|
|
80a4746a46 | ||
|
|
01f5b73e3b | ||
|
|
a0081dd693 | ||
|
|
f522823988 | ||
|
|
5a8603bebb | ||
|
|
abac87c2f8 | ||
|
|
c3d065cecc | ||
|
|
e1a5d29972 | ||
|
|
d342da4e9a | ||
|
|
7ae8f81d74 | ||
|
|
36364b16a0 | ||
|
|
b35959f4c2 | ||
|
|
9450bcad25 | ||
|
|
69bdef38ec | ||
|
|
0e33a3afe0 | ||
|
|
76aaf6a8fe | ||
|
|
0ef7ee172f | ||
|
|
29def012a1 | ||
|
|
5c30578c49 | ||
|
|
1552afd8bf | ||
|
|
e04473dd26 | ||
|
|
84f166fc85 | ||
|
|
065518577e | ||
|
|
1d828b6ac6 | ||
|
|
777ce7cc97 | ||
|
|
1f37fbd051 | ||
|
|
8c9442ad11 | ||
|
|
47a475681f | ||
|
|
23dc1f5ea4 | ||
|
|
a6a111cadd | ||
|
|
6a7b84eb87 | ||
|
|
59bdbf5a5d | ||
|
|
64b6e8ba0f | ||
|
|
236b3e546e | ||
|
|
ea363466aa | ||
|
|
c45177e296 | ||
|
|
45fa034107 | ||
|
|
1c5c8552f2 | ||
|
|
5d374193bb | ||
|
|
b65fb06264 | ||
|
|
b3405c3bd1 | ||
|
|
638320b21e | ||
|
|
91ab0636ec | ||
|
|
fb6cc8794f | ||
|
|
3d37611b6f | ||
|
|
360e372b57 | ||
|
|
74e8afe9a8 | ||
|
|
e30f45cf64 | ||
|
|
16c4fd4fc5 | ||
|
|
ec58adca13 | ||
|
|
bed358718b | ||
|
|
4124bedab7 | ||
|
|
57c6dbd71e | ||
|
|
fded3fbcdb | ||
|
|
a660527036 | ||
|
|
0cb8a8983c | ||
|
|
c7902478c1 | ||
|
|
3c0183fa5e | ||
|
|
e982cb824a | ||
|
|
1b865a60f8 | ||
|
|
4c32d5bf13 | ||
|
|
ccae033d85 | ||
|
|
c2fa9d7981 | ||
|
|
5f03202b5c | ||
|
|
223fda2fe2 | ||
|
|
a85946eba8 | ||
|
|
9d94358971 | ||
|
|
9e11105483 | ||
|
|
caebd0cc4d | ||
|
|
6e2922367c | ||
|
|
25ee9b1013 | ||
|
|
0870a1fe80 | ||
|
|
e37efc1e9b | ||
|
|
1ae326432e | ||
|
|
a05f86f97b | ||
|
|
473bbd78cc | ||
|
|
28c78d2d85 | ||
|
|
fca44f89c1 | ||
|
|
b7ad20773c | ||
|
|
aa1629b544 | ||
|
|
69a5c45672 | ||
|
|
d0aaf04673 | ||
|
|
d677c98f43 | ||
|
|
ce362864db | ||
|
|
3c021d0890 | ||
|
|
f36a545a86 | ||
|
|
9eeeda1330 | ||
|
|
da2d791127 | ||
|
|
d6f0811dab | ||
|
|
be0bb4a56b | ||
|
|
bf1ae1d196 | ||
|
|
3b7dbb87b0 | ||
|
|
bb13228ad5 | ||
|
|
ec1528b890 | ||
|
|
2aa0114b40 | ||
|
|
bb2d833373 | ||
|
|
eedfc5be5a | ||
|
|
0e76cc8036 | ||
|
|
6bd5251882 | ||
|
|
13de400a2a | ||
|
|
c3480c3d6f | ||
|
|
0cbacb8500 | ||
|
|
7327ef662b | ||
|
|
c1ca7303a8 | ||
|
|
92283285ae | ||
|
|
d80f9dda75 | ||
|
|
ebc22c290b | ||
|
|
7633bbf55a | ||
|
|
91cbb2ec25 | ||
|
|
40199266b6 | ||
|
|
9a8c5053c2 | ||
|
|
c446662862 | ||
|
|
6feae92616 | ||
|
|
ae840c6ef3 | ||
|
|
d7fd5910d7 | ||
|
|
8d5861322b | ||
|
|
5a9e18603d | ||
|
|
2a7761fe17 | ||
|
|
f23096034b | ||
|
|
1ed17fdd94 | ||
|
|
7ea7f4e767 | ||
|
|
035d7ddcf8 | ||
|
|
9d67276090 | ||
|
|
161d128d45 | ||
|
|
e1b0a98c34 | ||
|
|
ae0ee70abd | ||
|
|
893eb92f91 | ||
|
|
45fa6d81ac | ||
|
|
60ad82cc94 | ||
|
|
564ded71c1 | ||
|
|
63b3839a83 | ||
|
|
9f749881b3 | ||
|
|
946efb03df | ||
|
|
4b96ad3fba | ||
|
|
4368c1b56b | ||
|
|
e5a968b709 | ||
|
|
7aecab8e14 | ||
|
|
e4df866664 | ||
|
|
8770fcc841 | ||
|
|
6dcae2711d | ||
|
|
5e01fb8f1c | ||
|
|
88a79750cc | ||
|
|
4c411b9fc8 | ||
|
|
5ac6ae501f | ||
|
|
c01f12b15d | ||
|
|
dfa066dfe8 | ||
|
|
ac8c653ae6 | ||
|
|
d2318be8d9 | ||
|
|
a026163746 | ||
|
|
ad3ddd381d |
@@ -19,8 +19,6 @@ rustflags = [
|
||||
"windows_slim_errors", # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
|
||||
"-C",
|
||||
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
|
||||
"-C",
|
||||
"link-arg=-fuse-ld=lld",
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
@@ -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" },
|
||||
{ name = "rodio", git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"},
|
||||
]
|
||||
|
||||
[final-excludes]
|
||||
@@ -41,5 +41,4 @@ workspace-members = [
|
||||
"slash_commands_example",
|
||||
"zed_snippets",
|
||||
"zed_test_extension",
|
||||
"zed_toml",
|
||||
]
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -2,4 +2,4 @@
|
||||
*.json linguist-language=JSON-with-Comments
|
||||
|
||||
# Ensure the WSL script always has LF line endings, even on Windows
|
||||
crates/zed/resources/windows/zed-wsl text eol=lf
|
||||
crates/zed/resources/windows/zed.sh text eol=lf
|
||||
|
||||
159
.github/actions/run_tests_windows/action.yml
vendored
159
.github/actions/run_tests_windows/action.yml
vendored
@@ -20,167 +20,8 @@ runs:
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Configure crash dumps
|
||||
shell: powershell
|
||||
run: |
|
||||
# Record the start time for this CI run
|
||||
$runStartTime = Get-Date
|
||||
$runStartTimeStr = $runStartTime.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
Write-Host "CI run started at: $runStartTimeStr"
|
||||
|
||||
# Save the timestamp for later use
|
||||
echo "CI_RUN_START_TIME=$($runStartTime.Ticks)" >> $env:GITHUB_ENV
|
||||
|
||||
# Create crash dump directory in workspace (non-persistent)
|
||||
$dumpPath = "$env:GITHUB_WORKSPACE\crash_dumps"
|
||||
New-Item -ItemType Directory -Force -Path $dumpPath | Out-Null
|
||||
|
||||
Write-Host "Setting up crash dump detection..."
|
||||
Write-Host "Workspace dump path: $dumpPath"
|
||||
|
||||
# Note: We're NOT modifying registry on stateful runners
|
||||
# Instead, we'll check default Windows crash locations after tests
|
||||
|
||||
- name: Run tests
|
||||
shell: powershell
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
$env:RUST_BACKTRACE = "full"
|
||||
|
||||
# Enable Windows debugging features
|
||||
$env:_NT_SYMBOL_PATH = "srv*https://msdl.microsoft.com/download/symbols"
|
||||
|
||||
# .NET crash dump environment variables (ephemeral)
|
||||
$env:COMPlus_DbgEnableMiniDump = "1"
|
||||
$env:COMPlus_DbgMiniDumpType = "4"
|
||||
$env:COMPlus_CreateDumpDiagnostics = "1"
|
||||
|
||||
cargo nextest run --workspace --no-fail-fast
|
||||
|
||||
- name: Analyze crash dumps
|
||||
if: always()
|
||||
shell: powershell
|
||||
run: |
|
||||
Write-Host "Checking for crash dumps..."
|
||||
|
||||
# Get the CI run start time from the environment
|
||||
$runStartTime = [DateTime]::new([long]$env:CI_RUN_START_TIME)
|
||||
Write-Host "Only analyzing dumps created after: $($runStartTime.ToString('yyyy-MM-dd HH:mm:ss'))"
|
||||
|
||||
# Check all possible crash dump locations
|
||||
$searchPaths = @(
|
||||
"$env:GITHUB_WORKSPACE\crash_dumps",
|
||||
"$env:LOCALAPPDATA\CrashDumps",
|
||||
"$env:TEMP",
|
||||
"$env:GITHUB_WORKSPACE",
|
||||
"$env:USERPROFILE\AppData\Local\CrashDumps",
|
||||
"C:\Windows\System32\config\systemprofile\AppData\Local\CrashDumps"
|
||||
)
|
||||
|
||||
$dumps = @()
|
||||
foreach ($path in $searchPaths) {
|
||||
if (Test-Path $path) {
|
||||
Write-Host "Searching in: $path"
|
||||
$found = Get-ChildItem "$path\*.dmp" -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.CreationTime -gt $runStartTime
|
||||
}
|
||||
if ($found) {
|
||||
$dumps += $found
|
||||
Write-Host " Found $($found.Count) dump(s) from this CI run"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($dumps) {
|
||||
Write-Host "Found $($dumps.Count) crash dump(s)"
|
||||
|
||||
# Install debugging tools if not present
|
||||
$cdbPath = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe"
|
||||
if (-not (Test-Path $cdbPath)) {
|
||||
Write-Host "Installing Windows Debugging Tools..."
|
||||
$url = "https://go.microsoft.com/fwlink/?linkid=2237387"
|
||||
Invoke-WebRequest -Uri $url -OutFile winsdksetup.exe
|
||||
Start-Process -Wait winsdksetup.exe -ArgumentList "/features OptionId.WindowsDesktopDebuggers /quiet"
|
||||
}
|
||||
|
||||
foreach ($dump in $dumps) {
|
||||
Write-Host "`n=================================="
|
||||
Write-Host "Analyzing crash dump: $($dump.Name)"
|
||||
Write-Host "Size: $([math]::Round($dump.Length / 1MB, 2)) MB"
|
||||
Write-Host "Time: $($dump.CreationTime)"
|
||||
Write-Host "=================================="
|
||||
|
||||
# Set symbol path
|
||||
$env:_NT_SYMBOL_PATH = "srv*C:\symbols*https://msdl.microsoft.com/download/symbols"
|
||||
|
||||
# Run analysis
|
||||
$analysisOutput = & $cdbPath -z $dump.FullName -c "!analyze -v; ~*k; lm; q" 2>&1 | Out-String
|
||||
|
||||
# Extract key information
|
||||
if ($analysisOutput -match "ExceptionCode:\s*([\w]+)") {
|
||||
Write-Host "Exception Code: $($Matches[1])"
|
||||
if ($Matches[1] -eq "c0000005") {
|
||||
Write-Host "Exception Type: ACCESS VIOLATION"
|
||||
}
|
||||
}
|
||||
|
||||
if ($analysisOutput -match "EXCEPTION_RECORD:\s*(.+)") {
|
||||
Write-Host "Exception Record: $($Matches[1])"
|
||||
}
|
||||
|
||||
if ($analysisOutput -match "FAULTING_IP:\s*\n(.+)") {
|
||||
Write-Host "Faulting Instruction: $($Matches[1])"
|
||||
}
|
||||
|
||||
# Save full analysis
|
||||
$analysisFile = "$($dump.FullName).analysis.txt"
|
||||
$analysisOutput | Out-File -FilePath $analysisFile
|
||||
Write-Host "`nFull analysis saved to: $analysisFile"
|
||||
|
||||
# Print stack trace section
|
||||
Write-Host "`n--- Stack Trace Preview ---"
|
||||
$stackSection = $analysisOutput -split "STACK_TEXT:" | Select-Object -Last 1
|
||||
$stackLines = $stackSection -split "`n" | Select-Object -First 20
|
||||
$stackLines | ForEach-Object { Write-Host $_ }
|
||||
Write-Host "--- End Stack Trace Preview ---"
|
||||
}
|
||||
|
||||
Write-Host "`n⚠️ Crash dumps detected! Download the 'crash-dumps' artifact for detailed analysis."
|
||||
|
||||
# Copy dumps to workspace for artifact upload
|
||||
$artifactPath = "$env:GITHUB_WORKSPACE\crash_dumps_collected"
|
||||
New-Item -ItemType Directory -Force -Path $artifactPath | Out-Null
|
||||
|
||||
foreach ($dump in $dumps) {
|
||||
$destName = "$($dump.Directory.Name)_$($dump.Name)"
|
||||
Copy-Item $dump.FullName -Destination "$artifactPath\$destName"
|
||||
if (Test-Path "$($dump.FullName).analysis.txt") {
|
||||
Copy-Item "$($dump.FullName).analysis.txt" -Destination "$artifactPath\$destName.analysis.txt"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Copied $($dumps.Count) dump(s) to artifact directory"
|
||||
} else {
|
||||
Write-Host "No crash dumps from this CI run found"
|
||||
}
|
||||
|
||||
- name: Upload crash dumps
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: crash-dumps-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
crash_dumps_collected/*.dmp
|
||||
crash_dumps_collected/*.txt
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
- name: Check test results
|
||||
shell: powershell
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
# Re-check test results to fail the job if tests failed
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Tests failed with exit code: $LASTEXITCODE"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -81,6 +81,7 @@ jobs:
|
||||
echo "run_license=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "$CHANGED_FILES" | grep -qP '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' && \
|
||||
echo "$GITHUB_REF_NAME" | grep -qvP '^v[0-9]+\.[0-9]+\.[0-9x](-pre)?$' && \
|
||||
echo "run_nix=true" >> "$GITHUB_OUTPUT" || \
|
||||
echo "run_nix=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
|
||||
13
.rules
13
.rules
@@ -12,6 +12,19 @@
|
||||
- Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead
|
||||
* When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
|
||||
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
|
||||
* When creating new crates, prefer specifying the library root path in `Cargo.toml` using `[lib] path = "...rs"` instead of the default `lib.rs`, to maintain consistent and descriptive naming (e.g., `gpui.rs` or `main.rs`).
|
||||
* Avoid creative additions unless explicitly requested
|
||||
* Use full words for variable names (no abbreviations like "q" for "queue")
|
||||
* Use variable shadowing to scope clones in async contexts for clarity, minimizing the lifetime of borrowed references.
|
||||
Example:
|
||||
```rust
|
||||
executor.spawn({
|
||||
let task_ran = task_ran.clone();
|
||||
async move {
|
||||
*task_ran.borrow_mut() = true;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
# GPUI
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ If you would like to add a new icon to the Zed icon theme, [open a Discussion](h
|
||||
|
||||
## Bird's-eye view of Zed
|
||||
|
||||
We suggest you keep the [zed glossary](docs/src/development/GLOSSARY.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
|
||||
We suggest you keep the [zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
|
||||
|
||||
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
|
||||
|
||||
|
||||
345
Cargo.lock
generated
345
Cargo.lock
generated
@@ -26,7 +26,7 @@ dependencies = [
|
||||
"portable-pty",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -79,7 +79,7 @@ dependencies = [
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"text",
|
||||
@@ -172,7 +172,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"ref-cast",
|
||||
"rope",
|
||||
"schemars",
|
||||
@@ -190,14 +190,15 @@ dependencies = [
|
||||
"uuid",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_env_vars",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.2.0-alpha.4"
|
||||
version = "0.2.0-alpha.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "603941db1d130ee275840c465b73a2312727d4acef97449550ccf033de71301f"
|
||||
checksum = "6d02292efd75080932b6466471d428c70e2ac06908ae24792fc7c36ecbaf67ca"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -278,6 +279,7 @@ dependencies = [
|
||||
"web_search",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_env_vars",
|
||||
"zlog",
|
||||
"zstd",
|
||||
]
|
||||
@@ -406,7 +408,7 @@ dependencies = [
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"release_channel",
|
||||
"rope",
|
||||
"rules_library",
|
||||
@@ -832,7 +834,7 @@ dependencies = [
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rpc",
|
||||
"serde",
|
||||
@@ -848,6 +850,7 @@ dependencies = [
|
||||
"uuid",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_env_vars",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -930,7 +933,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -982,7 +985,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"reqwest_client",
|
||||
"rust-embed",
|
||||
@@ -1382,12 +1385,19 @@ name = "audio"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-tar",
|
||||
"collections",
|
||||
"crossbeam",
|
||||
"gpui",
|
||||
"libwebrtc",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"rodio",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"smol",
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -2288,7 +2298,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
@@ -2321,7 +2331,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2331,7 +2341,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-util"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"blade-graphics",
|
||||
"bytemuck",
|
||||
@@ -2348,19 +2358,6 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
@@ -2475,7 +2472,7 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rope",
|
||||
"serde_json",
|
||||
"sum_tree",
|
||||
@@ -2621,6 +2618,7 @@ dependencies = [
|
||||
"audio",
|
||||
"client",
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -2896,7 +2894,7 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"settings",
|
||||
@@ -3067,7 +3065,6 @@ dependencies = [
|
||||
"clock",
|
||||
"cloud_api_client",
|
||||
"cloud_llm_client",
|
||||
"cocoa 0.26.0",
|
||||
"collections",
|
||||
"credentials_provider",
|
||||
"derive_more",
|
||||
@@ -3080,10 +3077,11 @@ dependencies = [
|
||||
"http_client_tls",
|
||||
"httparse",
|
||||
"log",
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
@@ -3332,7 +3330,7 @@ dependencies = [
|
||||
"prometheus",
|
||||
"prompt_store",
|
||||
"prost 0.9.0",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"recent_projects",
|
||||
"release_channel",
|
||||
"remote",
|
||||
@@ -3512,6 +3510,7 @@ name = "component"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"documented",
|
||||
"gpui",
|
||||
"inventory",
|
||||
"parking_lot",
|
||||
@@ -3574,12 +3573,6 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "context_server"
|
||||
version = "0.1.0"
|
||||
@@ -4159,6 +4152,19 @@ dependencies = [
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
@@ -4489,6 +4495,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed_env_vars",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4693,7 +4700,7 @@ dependencies = [
|
||||
"markdown",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -5064,7 +5071,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
@@ -5559,7 +5566,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"release_channel",
|
||||
"remote",
|
||||
"reqwest_client",
|
||||
@@ -6159,17 +6166,6 @@ dependencies = [
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-batch"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f444c45a1cb86f2a7e301469fd50a82084a60dadc25d94529a8312276ecb71a"
|
||||
dependencies = [
|
||||
"futures 0.3.31",
|
||||
"futures-timer",
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
@@ -6265,12 +6261,6 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
@@ -6408,7 +6398,7 @@ dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rope",
|
||||
"schemars",
|
||||
@@ -7461,7 +7451,7 @@ dependencies = [
|
||||
"pathfinder_geometry",
|
||||
"postage",
|
||||
"profiling",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"raw-window-handle",
|
||||
"refineable",
|
||||
"reqwest_client",
|
||||
@@ -9074,7 +9064,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rpc",
|
||||
"schemars",
|
||||
@@ -9147,6 +9137,7 @@ dependencies = [
|
||||
"icons",
|
||||
"image",
|
||||
"log",
|
||||
"open_router",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
"schemars",
|
||||
@@ -9242,6 +9233,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"copilot",
|
||||
"editor",
|
||||
"futures 0.3.31",
|
||||
@@ -9512,6 +9504,21 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "line_ending_selector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"gpui",
|
||||
"language",
|
||||
"picker",
|
||||
"project",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.10"
|
||||
@@ -9666,6 +9673,7 @@ dependencies = [
|
||||
"scap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"settings",
|
||||
"sha2",
|
||||
"simplelog",
|
||||
@@ -10387,7 +10395,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rope",
|
||||
"serde",
|
||||
"settings",
|
||||
@@ -11222,6 +11230,8 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"thiserror 2.0.12",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -12611,7 +12621,7 @@ dependencies = [
|
||||
"postage",
|
||||
"prettier",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"remote",
|
||||
@@ -13745,7 +13755,6 @@ dependencies = [
|
||||
"regex",
|
||||
"reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)",
|
||||
"serde",
|
||||
"smol",
|
||||
"tokio",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -13866,15 +13875,15 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rodio"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183"
|
||||
source = "git+https://github.com/RustAudio/rodio?branch=better_wav_output#82514bd1f2c6cfd9a1a885019b26a8ffea75bc5c"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"dasp_sample",
|
||||
"hound",
|
||||
"num-rational",
|
||||
"rtrb",
|
||||
"symphonia",
|
||||
"tracing",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13886,7 +13895,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"gpui",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rayon",
|
||||
"smallvec",
|
||||
"sum_tree",
|
||||
@@ -13915,7 +13924,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -13948,6 +13957,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtrb"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba"
|
||||
|
||||
[[package]]
|
||||
name = "rules_library"
|
||||
version = "0.1.0"
|
||||
@@ -14350,6 +14365,19 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scheduler"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"chrono",
|
||||
"futures 0.3.31",
|
||||
"parking",
|
||||
"parking_lot",
|
||||
"rand 0.9.1",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schema_generator"
|
||||
version = "0.1.0"
|
||||
@@ -14652,49 +14680,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
|
||||
|
||||
[[package]]
|
||||
name = "semantic_index"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"blake3",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"futures-batch",
|
||||
"gpui",
|
||||
"heed",
|
||||
"http_client",
|
||||
"language",
|
||||
"language_model",
|
||||
"languages",
|
||||
"log",
|
||||
"open_ai",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"reqwest_client",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sha2",
|
||||
"smol",
|
||||
"streaming-iterator",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semantic_version"
|
||||
version = "0.1.0"
|
||||
@@ -14904,6 +14889,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command_palette_hooks",
|
||||
"debugger_ui",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"gpui",
|
||||
@@ -15311,6 +15297,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"indoc",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"smol",
|
||||
"sqlformat",
|
||||
@@ -15648,7 +15635,7 @@ name = "streaming_diff"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ordered-float 2.10.1",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rope",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
@@ -15762,7 +15749,7 @@ dependencies = [
|
||||
"arrayvec",
|
||||
"ctor",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rayon",
|
||||
"workspace-hack",
|
||||
"zlog",
|
||||
@@ -15937,12 +15924,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"symphonia-bundle-flac",
|
||||
"symphonia-bundle-mp3",
|
||||
"symphonia-codec-aac",
|
||||
"symphonia-codec-pcm",
|
||||
"symphonia-codec-vorbis",
|
||||
"symphonia-core",
|
||||
"symphonia-format-isomp4",
|
||||
"symphonia-format-ogg",
|
||||
"symphonia-format-riff",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-bundle-flac"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
"symphonia-utils-xiph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-bundle-mp3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-codec-aac"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-codec-pcm"
|
||||
version = "0.5.4"
|
||||
@@ -15953,6 +15981,17 @@ dependencies = [
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-codec-vorbis"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-utils-xiph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-core"
|
||||
version = "0.5.4"
|
||||
@@ -15966,6 +16005,31 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-isomp4"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
"symphonia-utils-xiph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-ogg"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
"symphonia-utils-xiph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-riff"
|
||||
version = "0.5.4"
|
||||
@@ -15990,6 +16054,16 @@ dependencies = [
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-utils-xiph"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe"
|
||||
dependencies = [
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -16353,7 +16427,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"libc",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
@@ -16401,7 +16475,7 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"schemars",
|
||||
"search",
|
||||
@@ -16433,7 +16507,7 @@ dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rope",
|
||||
"smallvec",
|
||||
@@ -16964,10 +17038,15 @@ checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
|
||||
name = "toolchain_selector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.8.0",
|
||||
"editor",
|
||||
"file_finder",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"picker",
|
||||
"project",
|
||||
"ui",
|
||||
@@ -17790,7 +17869,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"nix 0.29.0",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
@@ -17975,6 +18054,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -18581,7 +18662,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"workspace-hack",
|
||||
"zlog",
|
||||
]
|
||||
@@ -19873,6 +19954,7 @@ dependencies = [
|
||||
"core-foundation-sys",
|
||||
"cranelift-codegen",
|
||||
"crc32fast",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"crypto-common",
|
||||
@@ -19916,6 +19998,7 @@ dependencies = [
|
||||
"libsqlite3-sys",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"linux-raw-sys 0.9.4",
|
||||
"livekit-runtime",
|
||||
"log",
|
||||
"lyon",
|
||||
"lyon_path",
|
||||
@@ -20040,7 +20123,7 @@ dependencies = [
|
||||
"paths",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rpc",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -20396,7 +20479,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.203.0"
|
||||
version = "0.204.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
@@ -20469,7 +20552,7 @@ dependencies = [
|
||||
"language_tools",
|
||||
"languages",
|
||||
"libc",
|
||||
"livekit_client",
|
||||
"line_ending_selector",
|
||||
"log",
|
||||
"markdown",
|
||||
"markdown_preview",
|
||||
@@ -20546,6 +20629,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_env_vars",
|
||||
"zeta",
|
||||
"zlog",
|
||||
"zlog_settings",
|
||||
@@ -20562,6 +20646,13 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_env_vars"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.1.0"
|
||||
@@ -20612,7 +20703,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_snippets"
|
||||
version = "0.0.5"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"zed_extension_api 0.1.0",
|
||||
@@ -20625,13 +20716,6 @@ dependencies = [
|
||||
"zed_extension_api 0.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_toml"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.3.2"
|
||||
@@ -20795,9 +20879,10 @@ dependencies = [
|
||||
"language_model",
|
||||
"log",
|
||||
"menu",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
@@ -20867,7 +20952,7 @@ dependencies = [
|
||||
"aes",
|
||||
"byteorder",
|
||||
"bzip2",
|
||||
"constant_time_eq 0.1.5",
|
||||
"constant_time_eq",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
|
||||
56
Cargo.toml
56
Cargo.toml
@@ -97,6 +97,7 @@ members = [
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
"crates/languages",
|
||||
"crates/line_ending_selector",
|
||||
"crates/livekit_api",
|
||||
"crates/livekit_client",
|
||||
"crates/lmstudio",
|
||||
@@ -131,6 +132,7 @@ members = [
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/release_channel",
|
||||
"crates/scheduler",
|
||||
"crates/remote",
|
||||
"crates/remote_server",
|
||||
"crates/repl",
|
||||
@@ -141,7 +143,6 @@ members = [
|
||||
"crates/rules_library",
|
||||
"crates/schema_generator",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_version",
|
||||
"crates/session",
|
||||
"crates/settings",
|
||||
@@ -193,6 +194,7 @@ members = [
|
||||
"crates/x_ai",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zed_env_vars",
|
||||
"crates/zeta",
|
||||
"crates/zeta_cli",
|
||||
"crates/zlog",
|
||||
@@ -209,7 +211,6 @@ members = [
|
||||
"extensions/slash-commands-example",
|
||||
"extensions/snippets",
|
||||
"extensions/test-extension",
|
||||
"extensions/toml",
|
||||
|
||||
#
|
||||
# Tooling
|
||||
@@ -275,6 +276,7 @@ context_server = { path = "crates/context_server" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
crashes = { path = "crates/crashes" }
|
||||
credentials_provider = { path = "crates/credentials_provider" }
|
||||
crossbeam = "0.8.4"
|
||||
dap = { path = "crates/dap" }
|
||||
dap_adapters = { path = "crates/dap_adapters" }
|
||||
db = { path = "crates/db" }
|
||||
@@ -299,9 +301,7 @@ git_hosting_providers = { path = "crates/git_hosting_providers" }
|
||||
git_ui = { path = "crates/git_ui" }
|
||||
go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui", default-features = false, features = [
|
||||
"http_client",
|
||||
] }
|
||||
gpui = { path = "crates/gpui", default-features = false }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
gpui_tokio = { path = "crates/gpui_tokio" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
@@ -324,6 +324,7 @@ language_models = { path = "crates/language_models" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
language_tools = { path = "crates/language_tools" }
|
||||
languages = { path = "crates/languages" }
|
||||
line_ending_selector = { path = "crates/line_ending_selector" }
|
||||
livekit_api = { path = "crates/livekit_api" }
|
||||
livekit_client = { path = "crates/livekit_client" }
|
||||
lmstudio = { path = "crates/lmstudio" }
|
||||
@@ -361,17 +362,17 @@ proto = { path = "crates/proto" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
scheduler = { path = "crates/scheduler" }
|
||||
remote = { path = "crates/remote" }
|
||||
remote_server = { path = "crates/remote_server" }
|
||||
repl = { path = "crates/repl" }
|
||||
reqwest_client = { path = "crates/reqwest_client" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rodio = { version = "0.21.1", default-features = false }
|
||||
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" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
session = { path = "crates/session" }
|
||||
settings = { path = "crates/settings" }
|
||||
@@ -422,6 +423,7 @@ worktree = { path = "crates/worktree" }
|
||||
x_ai = { path = "crates/x_ai" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
zed_env_vars = { path = "crates/zed_env_vars" }
|
||||
zeta = { path = "crates/zeta" }
|
||||
zlog = { path = "crates/zlog" }
|
||||
zlog_settings = { path = "crates/zlog_settings" }
|
||||
@@ -430,7 +432,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "0.2.0-alpha.4", features = ["unstable"]}
|
||||
agent-client-protocol = { version = "0.2.0-alpha.6", features = ["unstable"]}
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
@@ -444,6 +446,7 @@ async-fs = "2.1"
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.5.0"
|
||||
async-task = "4.7"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = "0.29.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
@@ -459,9 +462,9 @@ aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
base64 = "0.22"
|
||||
bincode = "1.2.1"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
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"
|
||||
@@ -535,9 +538,35 @@ nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c80421
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
objc = "0.2"
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = [
|
||||
"NSArray",
|
||||
"NSAttributedString",
|
||||
"NSBundle",
|
||||
"NSCoder",
|
||||
"NSData",
|
||||
"NSDate",
|
||||
"NSDictionary",
|
||||
"NSEnumerator",
|
||||
"NSError",
|
||||
"NSGeometry",
|
||||
"NSNotification",
|
||||
"NSNull",
|
||||
"NSObjCRuntime",
|
||||
"NSObject",
|
||||
"NSProcessInfo",
|
||||
"NSRange",
|
||||
"NSRunLoop",
|
||||
"NSString",
|
||||
"NSURL",
|
||||
"NSUndoManager",
|
||||
"NSValue",
|
||||
"objc2-core-foundation",
|
||||
"std"
|
||||
] }
|
||||
open = "5.0.0"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking = "2.0"
|
||||
parking_lot = "0.12.1"
|
||||
partial-json-fixer = "0.5.3"
|
||||
parse_int = "0.9"
|
||||
@@ -560,7 +589,7 @@ prost-build = "0.9"
|
||||
prost-types = "0.9"
|
||||
pulldown-cmark = { version = "0.12.0", default-features = false }
|
||||
quote = "1.0.9"
|
||||
rand = "0.8.5"
|
||||
rand = "0.9"
|
||||
rayon = "1.8"
|
||||
ref-cast = "1.0.24"
|
||||
regex = "1.5"
|
||||
@@ -848,6 +877,9 @@ too_many_arguments = "allow"
|
||||
# We often have large enum variants yet we rarely actually bother with splitting them up.
|
||||
large_enum_variant = "allow"
|
||||
|
||||
# Boolean expressions can be hard to read, requiring only the minimal form gets in the way
|
||||
nonminimal_bool = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = [
|
||||
"bindgen",
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 29 KiB |
@@ -16,6 +16,7 @@
|
||||
"up": "menu::SelectPrevious",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
@@ -63,8 +64,8 @@
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-k ctrl-q": "editor::Rewrap",
|
||||
"ctrl-k q": "editor::Rewrap",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"cut": "editor::Cut",
|
||||
"shift-delete": "editor::Cut",
|
||||
"ctrl-x": "editor::Cut",
|
||||
@@ -582,7 +583,7 @@
|
||||
"ctrl-n": "workspace::NewFile",
|
||||
"shift-new": "workspace::NewWindow",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"ctrl-`": "terminal_panel::Toggle",
|
||||
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
|
||||
"alt-1": ["workspace::ActivatePane", 0],
|
||||
"alt-2": ["workspace::ActivatePane", 1],
|
||||
@@ -627,6 +628,7 @@
|
||||
"alt-save": "workspace::SaveAll",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
"ctrl-k m": "language_selector::Toggle",
|
||||
"ctrl-k ctrl-m": "toolchain::AddToolchain",
|
||||
"escape": "workspace::Unfollow",
|
||||
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
|
||||
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
|
||||
@@ -1027,6 +1029,13 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ToolchainSelector",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-a": "toolchain::AddToolchain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor)",
|
||||
"bindings": {
|
||||
|
||||
@@ -70,9 +70,9 @@
|
||||
"cmd-k q": "editor::Rewrap",
|
||||
"cmd-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"cmd-delete": "editor::DeleteToEndOfLine",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"cmd-x": "editor::Cut",
|
||||
"cmd-c": "editor::Copy",
|
||||
"cmd-v": "editor::Paste",
|
||||
@@ -649,7 +649,7 @@
|
||||
"alt-shift-enter": "toast::RunAction",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"ctrl-`": "terminal_panel::Toggle",
|
||||
"cmd-1": ["workspace::ActivatePane", 0],
|
||||
"cmd-2": ["workspace::ActivatePane", 1],
|
||||
"cmd-3": ["workspace::ActivatePane", 2],
|
||||
@@ -690,6 +690,7 @@
|
||||
"cmd-?": "agent::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
"cmd-k m": "language_selector::Toggle",
|
||||
"cmd-k cmd-m": "toolchain::AddToolchain",
|
||||
"escape": "workspace::Unfollow",
|
||||
"cmd-k cmd-left": "workspace::ActivatePaneLeft",
|
||||
"cmd-k cmd-right": "workspace::ActivatePaneRight",
|
||||
@@ -1094,6 +1095,13 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ToolchainSelector",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-a": "toolchain::AddToolchain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"open": "workspace::Open",
|
||||
"ctrl-o": "workspace::Open",
|
||||
"ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
"ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
@@ -66,20 +65,15 @@
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-k ctrl-q": "editor::Rewrap",
|
||||
"ctrl-k q": "editor::Rewrap",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"cut": "editor::Cut",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"shift-delete": "editor::Cut",
|
||||
"ctrl-x": "editor::Cut",
|
||||
"copy": "editor::Copy",
|
||||
"ctrl-insert": "editor::Copy",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"paste": "editor::Paste",
|
||||
"shift-insert": "editor::Paste",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"undo": "editor::Undo",
|
||||
"ctrl-z": "editor::Undo",
|
||||
"redo": "editor::Redo",
|
||||
"ctrl-y": "editor::Redo",
|
||||
"ctrl-shift-z": "editor::Redo",
|
||||
"up": "editor::MoveUp",
|
||||
@@ -138,7 +132,6 @@
|
||||
"ctrl-shift-enter": "editor::NewlineAbove",
|
||||
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-k z": "editor::ToggleSoftWrap",
|
||||
"find": "buffer_search::Deploy",
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
"ctrl-h": "buffer_search::DeployReplace",
|
||||
"ctrl-shift-.": "assistant::QuoteSelection",
|
||||
@@ -177,7 +170,6 @@
|
||||
"context": "Markdown",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"copy": "markdown::Copy",
|
||||
"ctrl-c": "markdown::Copy"
|
||||
}
|
||||
},
|
||||
@@ -225,7 +217,6 @@
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"save": "workspace::Save",
|
||||
"ctrl-shift-,": "assistant::InsertIntoEditor",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
@@ -272,7 +263,6 @@
|
||||
"context": "AgentPanel > Markdown",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown"
|
||||
}
|
||||
},
|
||||
@@ -367,7 +357,6 @@
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"new": "rules_library::NewRule",
|
||||
"ctrl-n": "rules_library::NewRule",
|
||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
|
||||
}
|
||||
@@ -381,7 +370,6 @@
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPreviousMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"find": "search::FocusSearch",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"ctrl-h": "search::ToggleReplace",
|
||||
"ctrl-l": "search::ToggleSelection"
|
||||
@@ -408,7 +396,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"shift-find": "search::FocusSearch",
|
||||
"ctrl-shift-f": "search::FocusSearch",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"alt-r": "search::ToggleRegex" // vscode
|
||||
@@ -472,14 +459,12 @@
|
||||
"forward": "pane::GoForward",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPreviousMatch",
|
||||
"shift-find": "project_search::ToggleFocus",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus",
|
||||
"shift-alt-h": "search::ToggleReplace",
|
||||
"alt-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-w": "search::ToggleWholeWord",
|
||||
"alt-find": "project_search::ToggleFilters",
|
||||
"alt-f": "project_search::ToggleFilters",
|
||||
"alt-r": "search::ToggleRegex",
|
||||
// "ctrl-shift-alt-x": "search::ToggleRegex",
|
||||
@@ -579,27 +564,21 @@
|
||||
"context": "Workspace",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-open": ["projects::OpenRecent", { "create_new_window": false }],
|
||||
// Change the default action on `menu::Confirm` by setting the parameter
|
||||
// "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }],
|
||||
"ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
|
||||
"shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
|
||||
// Change to open path modal for existing remote connection by setting the parameter
|
||||
// "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
|
||||
"ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
|
||||
"shift-alt-b": "branches::OpenRecent",
|
||||
"shift-alt-enter": "toast::RunAction",
|
||||
"ctrl-shift-`": "workspace::NewTerminal",
|
||||
"save": "workspace::Save",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat",
|
||||
"shift-save": "workspace::SaveAs",
|
||||
"ctrl-shift-s": "workspace::SaveAs",
|
||||
"new": "workspace::NewFile",
|
||||
"ctrl-n": "workspace::NewFile",
|
||||
"shift-new": "workspace::NewWindow",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"ctrl-`": "terminal_panel::Toggle",
|
||||
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
|
||||
"alt-1": ["workspace::ActivatePane", 0],
|
||||
"alt-2": ["workspace::ActivatePane", 1],
|
||||
@@ -621,7 +600,6 @@
|
||||
"shift-alt-0": "workspace::ResetOpenDocksSize",
|
||||
"ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
|
||||
"ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
|
||||
"shift-find": "pane::DeploySearch",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
@@ -641,9 +619,9 @@
|
||||
"ctrl-shift-g": "git_panel::ToggleFocus",
|
||||
"ctrl-shift-d": "debug_panel::ToggleFocus",
|
||||
"ctrl-shift-/": "agent::ToggleFocus",
|
||||
"alt-save": "workspace::SaveAll",
|
||||
"ctrl-k s": "workspace::SaveAll",
|
||||
"ctrl-k m": "language_selector::Toggle",
|
||||
"ctrl-m ctrl-m": "toolchain::AddToolchain",
|
||||
"escape": "workspace::Unfollow",
|
||||
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
|
||||
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
|
||||
@@ -848,9 +826,7 @@
|
||||
"bindings": {
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
"right": "outline_panel::ExpandSelectedEntry",
|
||||
"alt-copy": "outline_panel::CopyPath",
|
||||
"shift-alt-c": "outline_panel::CopyPath",
|
||||
"shift-alt-copy": "workspace::CopyRelativePath",
|
||||
"ctrl-shift-alt-c": "workspace::CopyRelativePath",
|
||||
"ctrl-alt-r": "outline_panel::RevealInFileManager",
|
||||
"space": "outline_panel::OpenSelectedEntry",
|
||||
@@ -866,21 +842,14 @@
|
||||
"bindings": {
|
||||
"left": "project_panel::CollapseSelectedEntry",
|
||||
"right": "project_panel::ExpandSelectedEntry",
|
||||
"new": "project_panel::NewFile",
|
||||
"ctrl-n": "project_panel::NewFile",
|
||||
"alt-new": "project_panel::NewDirectory",
|
||||
"alt-n": "project_panel::NewDirectory",
|
||||
"cut": "project_panel::Cut",
|
||||
"ctrl-x": "project_panel::Cut",
|
||||
"copy": "project_panel::Copy",
|
||||
"ctrl-insert": "project_panel::Copy",
|
||||
"ctrl-c": "project_panel::Copy",
|
||||
"paste": "project_panel::Paste",
|
||||
"shift-insert": "project_panel::Paste",
|
||||
"ctrl-v": "project_panel::Paste",
|
||||
"alt-copy": "project_panel::CopyPath",
|
||||
"shift-alt-c": "project_panel::CopyPath",
|
||||
"shift-alt-copy": "workspace::CopyRelativePath",
|
||||
"ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
|
||||
"enter": "project_panel::Rename",
|
||||
"f2": "project_panel::Rename",
|
||||
@@ -892,7 +861,6 @@
|
||||
"ctrl-alt-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"alt-d": "project_panel::CompareMarkedFiles",
|
||||
"shift-find": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
@@ -1075,6 +1043,13 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ToolchainSelector",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-a": "toolchain::AddToolchain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
@@ -1110,10 +1085,8 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-alt-space": "terminal::ShowCharacterPalette",
|
||||
"copy": "terminal::Copy",
|
||||
"ctrl-insert": "terminal::Copy",
|
||||
"ctrl-shift-c": "terminal::Copy",
|
||||
"paste": "terminal::Paste",
|
||||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
@@ -1129,7 +1102,6 @@
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-shift-a": "editor::SelectAll",
|
||||
"find": "buffer_search::Deploy",
|
||||
"ctrl-shift-f": "buffer_search::Deploy",
|
||||
"ctrl-shift-l": "terminal::Clear",
|
||||
"ctrl-shift-w": "pane::CloseActiveItem",
|
||||
@@ -1210,7 +1182,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"alt-find": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-c": "keymap_editor::ToggleConflictFilter",
|
||||
"enter": "keymap_editor::EditBinding",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
|
||||
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
|
||||
"ctrl-k": "editor::KillRingCut", // kill-line
|
||||
"ctrl-w": "editor::Cut", // kill-region
|
||||
"alt-w": "editor::Copy", // kill-ring-save
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
"alt-f12": "terminal_panel::ToggleFocus",
|
||||
"alt-f12": "terminal_panel::Toggle",
|
||||
"ctrl-shift-k": "git::Push"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-right": "editor::MoveToNextSubwordEnd",
|
||||
"alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
"alt-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
|
||||
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
|
||||
"ctrl-k": "editor::KillRingCut", // kill-line
|
||||
"ctrl-w": "editor::Cut", // kill-region
|
||||
"alt-w": "editor::Copy", // kill-ring-save
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
"alt-f12": "terminal_panel::ToggleFocus",
|
||||
"alt-f12": "terminal_panel::Toggle",
|
||||
"cmd-shift-k": "git::Push"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
"cmd-shift-j": "editor::JoinLines",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-right": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"alt-shift-backspace": "editor::DeleteToNextWordEnd",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-shift-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-shift-backspace": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-shift-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"alt-left": ["editor::MoveToPreviousWordStart", { "stop_at_soft_wraps": true }],
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
"ctrl-x ctrl-z": "editor::Cancel",
|
||||
"ctrl-x ctrl-e": "vim::LineDown",
|
||||
"ctrl-x ctrl-y": "vim::LineUp",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-t": "vim::Indent",
|
||||
"ctrl-d": "vim::Outdent",
|
||||
|
||||
@@ -962,7 +962,7 @@
|
||||
// Show git status colors in the editor tabs.
|
||||
"git_status": false,
|
||||
// Position of the close button on the editor tabs.
|
||||
// One of: ["right", "left", "hidden"]
|
||||
// One of: ["right", "left"]
|
||||
"close_position": "right",
|
||||
// Whether to show the file icon for a tab.
|
||||
"file_icons": false,
|
||||
|
||||
@@ -785,7 +785,6 @@ pub struct AcpThread {
|
||||
session_id: acp::SessionId,
|
||||
token_usage: Option<TokenUsage>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
available_commands: Vec<acp::AvailableCommand>,
|
||||
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
|
||||
determine_shell: Shared<Task<String>>,
|
||||
terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
|
||||
@@ -804,6 +803,8 @@ pub enum AcpThreadEvent {
|
||||
Error,
|
||||
LoadError(LoadError),
|
||||
PromptCapabilitiesUpdated,
|
||||
Refusal,
|
||||
AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
|
||||
}
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
@@ -859,7 +860,6 @@ impl AcpThread {
|
||||
action_log: Entity<ActionLog>,
|
||||
session_id: acp::SessionId,
|
||||
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||
available_commands: Vec<acp::AvailableCommand>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let prompt_capabilities = *prompt_capabilities_rx.borrow();
|
||||
@@ -899,7 +899,6 @@ impl AcpThread {
|
||||
session_id,
|
||||
token_usage: None,
|
||||
prompt_capabilities,
|
||||
available_commands,
|
||||
_observe_prompt_capabilities: task,
|
||||
terminals: HashMap::default(),
|
||||
determine_shell,
|
||||
@@ -910,10 +909,6 @@ impl AcpThread {
|
||||
self.prompt_capabilities
|
||||
}
|
||||
|
||||
pub fn available_commands(&self) -> Vec<acp::AvailableCommand> {
|
||||
self.available_commands.clone()
|
||||
}
|
||||
|
||||
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
||||
&self.connection
|
||||
}
|
||||
@@ -1009,6 +1004,9 @@ impl AcpThread {
|
||||
acp::SessionUpdate::Plan(plan) => {
|
||||
self.update_plan(plan, cx);
|
||||
}
|
||||
acp::SessionUpdate::AvailableCommandsUpdate { available_commands } => {
|
||||
cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands))
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1569,15 +1567,42 @@ impl AcpThread {
|
||||
this.send_task.take();
|
||||
}
|
||||
|
||||
// Truncate entries if the last prompt was refused.
|
||||
// Handle refusal - distinguish between user prompt and tool call refusals
|
||||
if let Ok(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
})) = result
|
||||
&& let Some((ix, _)) = this.last_user_message()
|
||||
{
|
||||
let range = ix..this.entries.len();
|
||||
this.entries.truncate(ix);
|
||||
cx.emit(AcpThreadEvent::EntriesRemoved(range));
|
||||
if let Some((user_msg_ix, _)) = this.last_user_message() {
|
||||
// Check if there's a completed tool call with results after the last user message
|
||||
// This indicates the refusal is in response to tool output, not the user's prompt
|
||||
let has_completed_tool_call_after_user_msg =
|
||||
this.entries.iter().skip(user_msg_ix + 1).any(|entry| {
|
||||
if let AgentThreadEntry::ToolCall(tool_call) = entry {
|
||||
// Check if the tool call has completed and has output
|
||||
matches!(tool_call.status, ToolCallStatus::Completed)
|
||||
&& tool_call.raw_output.is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if has_completed_tool_call_after_user_msg {
|
||||
// Refusal is due to tool output - don't truncate, just notify
|
||||
// The model refused based on what the tool returned
|
||||
cx.emit(AcpThreadEvent::Refusal);
|
||||
} else {
|
||||
// User prompt was refused - truncate back to before the user message
|
||||
let range = user_msg_ix..this.entries.len();
|
||||
if range.start < range.end {
|
||||
this.entries.truncate(user_msg_ix);
|
||||
cx.emit(AcpThreadEvent::EntriesRemoved(range));
|
||||
}
|
||||
cx.emit(AcpThreadEvent::Refusal);
|
||||
}
|
||||
} else {
|
||||
// No user message found, treat as general refusal
|
||||
cx.emit(AcpThreadEvent::Refusal);
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(AcpThreadEvent::Stopped);
|
||||
@@ -2089,7 +2114,7 @@ mod tests {
|
||||
use gpui::{App, AsyncApp, TestAppContext, WeakEntity};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Fs};
|
||||
use rand::Rng as _;
|
||||
use rand::{distr, prelude::*};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt as _;
|
||||
@@ -2681,6 +2706,187 @@ mod tests {
|
||||
assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tool_result_refusal(cx: &mut TestAppContext) {
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
|
||||
// Create a connection that simulates refusal after tool result
|
||||
let prompt_count = Arc::new(AtomicUsize::new(0));
|
||||
let connection = Rc::new(FakeAgentConnection::new().on_user_message({
|
||||
let prompt_count = prompt_count.clone();
|
||||
move |_request, thread, mut cx| {
|
||||
let count = prompt_count.fetch_add(1, SeqCst);
|
||||
async move {
|
||||
if count == 0 {
|
||||
// First prompt: Generate a tool call with result
|
||||
thread.update(&mut cx, |thread, cx| {
|
||||
thread
|
||||
.handle_session_update(
|
||||
acp::SessionUpdate::ToolCall(acp::ToolCall {
|
||||
id: acp::ToolCallId("tool1".into()),
|
||||
title: "Test Tool".into(),
|
||||
kind: acp::ToolKind::Fetch,
|
||||
status: acp::ToolCallStatus::Completed,
|
||||
content: vec![],
|
||||
locations: vec![],
|
||||
raw_input: Some(serde_json::json!({"query": "test"})),
|
||||
raw_output: Some(
|
||||
serde_json::json!({"result": "inappropriate content"}),
|
||||
),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
})?;
|
||||
|
||||
// Now return refusal because of the tool result
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
})
|
||||
} else {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
})
|
||||
}
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}));
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new("/test"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Track if we see a Refusal event
|
||||
let saw_refusal_event = Arc::new(std::sync::Mutex::new(false));
|
||||
let saw_refusal_event_captured = saw_refusal_event.clone();
|
||||
thread.update(cx, |_thread, cx| {
|
||||
cx.subscribe(
|
||||
&thread,
|
||||
move |_thread, _event_thread, event: &AcpThreadEvent, _cx| {
|
||||
if matches!(event, AcpThreadEvent::Refusal) {
|
||||
*saw_refusal_event_captured.lock().unwrap() = true;
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
|
||||
// Send a user message - this will trigger tool call and then refusal
|
||||
let send_task = thread.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
vec![acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Hello".into(),
|
||||
annotations: None,
|
||||
})],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.background_executor.spawn(send_task).detach();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify that:
|
||||
// 1. A Refusal event WAS emitted (because it's a tool result refusal, not user prompt)
|
||||
// 2. The user message was NOT truncated
|
||||
assert!(
|
||||
*saw_refusal_event.lock().unwrap(),
|
||||
"Refusal event should be emitted for tool result refusals"
|
||||
);
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
let entries = thread.entries();
|
||||
assert!(entries.len() >= 2, "Should have user message and tool call");
|
||||
|
||||
// Verify user message is still there
|
||||
assert!(
|
||||
matches!(entries[0], AgentThreadEntry::UserMessage(_)),
|
||||
"User message should not be truncated"
|
||||
);
|
||||
|
||||
// Verify tool call is there with result
|
||||
if let AgentThreadEntry::ToolCall(tool_call) = &entries[1] {
|
||||
assert!(
|
||||
tool_call.raw_output.is_some(),
|
||||
"Tool call should have output"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected tool call at index 1");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_user_prompt_refusal_emits_event(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
|
||||
let refuse_next = Arc::new(AtomicBool::new(false));
|
||||
let connection = Rc::new(FakeAgentConnection::new().on_user_message({
|
||||
let refuse_next = refuse_next.clone();
|
||||
move |_request, _thread, _cx| {
|
||||
if refuse_next.load(SeqCst) {
|
||||
async move {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
} else {
|
||||
async move {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Track if we see a Refusal event
|
||||
let saw_refusal_event = Arc::new(std::sync::Mutex::new(false));
|
||||
let saw_refusal_event_captured = saw_refusal_event.clone();
|
||||
thread.update(cx, |_thread, cx| {
|
||||
cx.subscribe(
|
||||
&thread,
|
||||
move |_thread, _event_thread, event: &AcpThreadEvent, _cx| {
|
||||
if matches!(event, AcpThreadEvent::Refusal) {
|
||||
*saw_refusal_event_captured.lock().unwrap() = true;
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
|
||||
// Send a message that will be refused
|
||||
refuse_next.store(true, SeqCst);
|
||||
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that a Refusal event WAS emitted for user prompt refusal
|
||||
assert!(
|
||||
*saw_refusal_event.lock().unwrap(),
|
||||
"Refusal event should be emitted for user prompt refusals"
|
||||
);
|
||||
|
||||
// Verify the message was truncated (user prompt refusal)
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(thread.to_markdown(cx), "");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_refusal(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -2744,8 +2950,8 @@ mod tests {
|
||||
);
|
||||
});
|
||||
|
||||
// Simulate refusing the second message, ensuring the conversation gets
|
||||
// truncated to before sending it.
|
||||
// Simulate refusing the second message. The message should be truncated
|
||||
// when a user prompt is refused.
|
||||
refuse_next.store(true, SeqCst);
|
||||
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx)))
|
||||
.await
|
||||
@@ -2851,8 +3057,8 @@ mod tests {
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||
let session_id = acp::SessionId(
|
||||
rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
rand::rng()
|
||||
.sample_iter(&distr::Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
@@ -2871,7 +3077,6 @@ mod tests {
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -338,7 +338,6 @@ mod test_support {
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -2218,7 +2218,7 @@ mod tests {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
|
||||
for _ in 0..operations {
|
||||
match rng.gen_range(0..100) {
|
||||
match rng.random_range(0..100) {
|
||||
0..25 => {
|
||||
action_log.update(cx, |log, cx| {
|
||||
let range = buffer.read(cx).random_byte_range(0, &mut rng);
|
||||
@@ -2237,7 +2237,7 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
_ => {
|
||||
let is_agent_edit = rng.gen_bool(0.5);
|
||||
let is_agent_edit = rng.random_bool(0.5);
|
||||
if is_agent_edit {
|
||||
log::info!("agent edit");
|
||||
} else {
|
||||
@@ -2252,7 +2252,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
if rng.gen_bool(0.2) {
|
||||
if rng.random_bool(0.2) {
|
||||
quiesce(&action_log, &buffer, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ impl ActivityIndicator {
|
||||
) -> Entity<ActivityIndicator> {
|
||||
let project = workspace.project().clone();
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let workspace_handle = cx.entity();
|
||||
let this = cx.new(|cx| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -102,20 +101,6 @@ impl ActivityIndicator {
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe_in(
|
||||
&workspace_handle,
|
||||
window,
|
||||
|activity_indicator, _, event, window, cx| {
|
||||
if let workspace::Event::ClearActivityIndicator = event
|
||||
&& activity_indicator.statuses.pop().is_some()
|
||||
{
|
||||
activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.subscribe(
|
||||
&project.read(cx).lsp_store(),
|
||||
|activity_indicator, _, event, cx| {
|
||||
@@ -227,7 +212,8 @@ impl ActivityIndicator {
|
||||
server_name,
|
||||
status,
|
||||
} => {
|
||||
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
|
||||
let create_buffer =
|
||||
project.update(cx, |project, cx| project.create_buffer(false, cx));
|
||||
let status = status.clone();
|
||||
let server_name = server_name.clone();
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
|
||||
@@ -63,6 +63,7 @@ time.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_env_vars.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -41,8 +41,7 @@ use std::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
|
||||
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
|
||||
use zed_env_vars::ZED_STATELESS;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DataType {
|
||||
|
||||
@@ -68,6 +68,7 @@ uuid.workspace = true
|
||||
watch.workspace = true
|
||||
web_search.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_env_vars.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -292,7 +292,6 @@ impl NativeAgent {
|
||||
action_log.clone(),
|
||||
session_id.clone(),
|
||||
prompt_capabilities_rx,
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ use sqlez::{
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use ui::{App, SharedString};
|
||||
use zed_env_vars::ZED_STATELESS;
|
||||
|
||||
pub type DbMessage = crate::Message;
|
||||
pub type DbSummary = DetailedSummaryState;
|
||||
@@ -201,9 +202,6 @@ impl DbThread {
|
||||
}
|
||||
}
|
||||
|
||||
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
|
||||
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DataType {
|
||||
#[serde(rename = "json")]
|
||||
|
||||
@@ -72,7 +72,6 @@ async fn test_echo(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_thinking(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
@@ -1349,7 +1348,6 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
@@ -1688,7 +1686,6 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_title_generation(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
@@ -2353,15 +2350,20 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
|
||||
settings::init(cx);
|
||||
Project::init_settings(cx);
|
||||
agent_settings::init(cx);
|
||||
gpui_tokio::init(cx);
|
||||
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
|
||||
client::init_settings(cx);
|
||||
let client = Client::production(cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store, client.clone(), cx);
|
||||
match model {
|
||||
TestModel::Fake => {}
|
||||
TestModel::Sonnet4 => {
|
||||
gpui_tokio::init(cx);
|
||||
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
client::init_settings(cx);
|
||||
let client = Client::production(cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store, client.clone(), cx);
|
||||
}
|
||||
};
|
||||
|
||||
watch_settings(fs.clone(), cx);
|
||||
});
|
||||
@@ -2475,6 +2477,7 @@ fn setup_context_server(
|
||||
path: "somebinary".into(),
|
||||
args: Vec::new(),
|
||||
env: None,
|
||||
timeout: None,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -83,6 +83,7 @@ struct EditFileToolPartialInput {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[schemars(inline)]
|
||||
pub enum EditFileMode {
|
||||
Edit,
|
||||
Create,
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[schemars(inline)]
|
||||
pub enum Timezone {
|
||||
/// Use UTC for the datetime.
|
||||
Utc,
|
||||
|
||||
@@ -224,7 +224,6 @@ impl AgentConnection for AcpConnection {
|
||||
session_id.clone(),
|
||||
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
|
||||
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
|
||||
response.available_commands,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -45,11 +45,20 @@ pub fn init(cx: &mut App) {
|
||||
pub struct AgentServerDelegate {
|
||||
project: Entity<Project>,
|
||||
status_tx: Option<watch::Sender<SharedString>>,
|
||||
new_version_available: Option<watch::Sender<Option<String>>>,
|
||||
}
|
||||
|
||||
impl AgentServerDelegate {
|
||||
pub fn new(project: Entity<Project>, status_tx: Option<watch::Sender<SharedString>>) -> Self {
|
||||
Self { project, status_tx }
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
status_tx: Option<watch::Sender<SharedString>>,
|
||||
new_version_tx: Option<watch::Sender<Option<String>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project,
|
||||
status_tx,
|
||||
new_version_available: new_version_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project(&self) -> &Entity<Project> {
|
||||
@@ -73,6 +82,7 @@ impl AgentServerDelegate {
|
||||
)));
|
||||
};
|
||||
let status_tx = self.status_tx;
|
||||
let new_version_available = self.new_version_available;
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
if !ignore_system_version {
|
||||
@@ -101,9 +111,11 @@ impl AgentServerDelegate {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(version) = file_name
|
||||
.to_str()
|
||||
.and_then(|name| semver::Version::from_str(&name).ok())
|
||||
if let Some(name) = file_name.to_str()
|
||||
&& let Some(version) = semver::Version::from_str(name).ok()
|
||||
&& fs
|
||||
.is_file(&dir.join(file_name).join(&entrypoint_path))
|
||||
.await
|
||||
{
|
||||
versions.push((version, file_name.to_owned()));
|
||||
} else {
|
||||
@@ -146,6 +158,7 @@ impl AgentServerDelegate {
|
||||
cx.background_spawn({
|
||||
let file_name = file_name.clone();
|
||||
let dir = dir.clone();
|
||||
let fs = fs.clone();
|
||||
async move {
|
||||
let latest_version =
|
||||
node_runtime.npm_package_latest_version(&package_name).await;
|
||||
@@ -160,6 +173,9 @@ impl AgentServerDelegate {
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
if let Some(mut new_version_available) = new_version_available {
|
||||
new_version_available.send(Some(latest_version)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -171,7 +187,7 @@ impl AgentServerDelegate {
|
||||
}
|
||||
let dir = dir.clone();
|
||||
cx.background_spawn(Self::download_latest_version(
|
||||
fs,
|
||||
fs.clone(),
|
||||
dir.clone(),
|
||||
node_runtime,
|
||||
package_name,
|
||||
@@ -179,14 +195,18 @@ impl AgentServerDelegate {
|
||||
.await?
|
||||
.into()
|
||||
};
|
||||
|
||||
let agent_server_path = dir.join(version).join(entrypoint_path);
|
||||
let agent_server_path_exists = fs.is_file(&agent_server_path).await;
|
||||
anyhow::ensure!(
|
||||
agent_server_path_exists,
|
||||
"Missing entrypoint path {} after installation",
|
||||
agent_server_path.to_string_lossy()
|
||||
);
|
||||
|
||||
anyhow::Ok(AgentServerCommand {
|
||||
path: node_path,
|
||||
args: vec![
|
||||
dir.join(version)
|
||||
.join(entrypoint_path)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
],
|
||||
args: vec![agent_server_path.to_string_lossy().to_string()],
|
||||
env: Default::default(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
|
||||
use settings::SettingsStore;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
@@ -40,7 +39,7 @@ impl ClaudeCode {
|
||||
Self::PACKAGE_NAME.into(),
|
||||
"node_modules/@anthropic-ai/claude-code/cli.js".into(),
|
||||
true,
|
||||
None,
|
||||
Some("0.2.5".parse().unwrap()),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
@@ -76,12 +75,20 @@ impl AgentServer for ClaudeCode {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let fs = delegate.project().read(cx).fs().clone();
|
||||
let server_name = self.name();
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
let project = delegate.project().clone();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut project_env = project
|
||||
.update(cx, |project, cx| {
|
||||
project.directory_environment(root_dir.as_path().into(), cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let mut command = if let Some(settings) = settings {
|
||||
settings.command
|
||||
} else {
|
||||
@@ -97,17 +104,20 @@ impl AgentServer for ClaudeCode {
|
||||
})?
|
||||
.await?
|
||||
};
|
||||
project_env.extend(command.env.take().unwrap_or_default());
|
||||
command.env = Some(project_env);
|
||||
|
||||
if let Some(api_key) = cx
|
||||
.update(AnthropicLanguageModelProvider::api_key)?
|
||||
.await
|
||||
.ok()
|
||||
{
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
.insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
|
||||
}
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
.insert("ANTHROPIC_API_KEY".to_owned(), "".to_owned());
|
||||
|
||||
let root_dir_exists = fs.is_dir(&root_dir).await;
|
||||
anyhow::ensure!(
|
||||
root_dir_exists,
|
||||
"Session root {} does not exist or is not a directory",
|
||||
root_dir.to_string_lossy()
|
||||
);
|
||||
|
||||
crate::acp::connect(server_name, command.clone(), &root_dir, cx).await
|
||||
})
|
||||
|
||||
@@ -498,7 +498,7 @@ pub async fn new_test_thread(
|
||||
current_dir: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<AcpThread> {
|
||||
let delegate = AgentServerDelegate::new(project.clone(), None);
|
||||
let delegate = AgentServerDelegate::new(project.clone(), None, None);
|
||||
|
||||
let connection = cx
|
||||
.update(|cx| server.connect(current_dir.as_ref(), delegate, cx))
|
||||
|
||||
@@ -36,16 +36,24 @@ impl AgentServer for Gemini {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let fs = delegate.project().read(cx).fs().clone();
|
||||
let server_name = self.name();
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).gemini.clone()
|
||||
});
|
||||
let project = delegate.project().clone();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let ignore_system_version = settings
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.ignore_system_version)
|
||||
.unwrap_or(true);
|
||||
let mut project_env = project
|
||||
.update(cx, |project, cx| {
|
||||
project.directory_environment(root_dir.as_path().into(), cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let mut command = if let Some(settings) = settings
|
||||
&& let Some(command) = settings.custom_command()
|
||||
{
|
||||
@@ -66,13 +74,19 @@ impl AgentServer for Gemini {
|
||||
if !command.args.contains(&ACP_ARG.into()) {
|
||||
command.args.push(ACP_ARG.into());
|
||||
}
|
||||
|
||||
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
project_env
|
||||
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
|
||||
}
|
||||
project_env.extend(command.env.take().unwrap_or_default());
|
||||
command.env = Some(project_env);
|
||||
|
||||
let root_dir_exists = fs.is_dir(&root_dir).await;
|
||||
anyhow::ensure!(
|
||||
root_dir_exists,
|
||||
"Session root {} does not exist or is not a directory",
|
||||
root_dir.to_string_lossy()
|
||||
);
|
||||
|
||||
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
|
||||
match &result {
|
||||
@@ -92,7 +106,7 @@ impl AgentServer for Gemini {
|
||||
log::error!("connected to gemini, but missing prompt_capabilities.image (version is {current_version})");
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: command.path.to_string_lossy().to_string().into(),
|
||||
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
|
||||
minimum_version: Self::MINIMUM_VERSION.into(),
|
||||
}
|
||||
.into());
|
||||
@@ -129,7 +143,7 @@ impl AgentServer for Gemini {
|
||||
if !supported {
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: command.path.to_string_lossy().to_string().into(),
|
||||
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
|
||||
minimum_version: Self::MINIMUM_VERSION.into(),
|
||||
}
|
||||
.into());
|
||||
|
||||
@@ -6,13 +6,14 @@ use collections::HashMap;
|
||||
use gpui::{App, SharedString};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AllAgentServersSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi)]
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "agent_servers")]
|
||||
pub struct AllAgentServersSettings {
|
||||
pub gemini: Option<BuiltinAgentServerSettings>,
|
||||
pub claude: Option<CustomAgentServerSettings>,
|
||||
@@ -75,8 +76,6 @@ pub struct CustomAgentServerSettings {
|
||||
}
|
||||
|
||||
impl settings::Settings for AllAgentServersSettings {
|
||||
const KEY: Option<&'static str> = Some("agent_servers");
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
|
||||
@@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString};
|
||||
use language_model::LanguageModel;
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
@@ -223,7 +223,8 @@ impl AgentSettingsContent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi)]
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "agent", fallback_key = "assistant")]
|
||||
pub struct AgentSettingsContent {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
@@ -399,10 +400,6 @@ pub struct ContextServerPresetContent {
|
||||
}
|
||||
|
||||
impl Settings for AgentSettings {
|
||||
const KEY: Option<&'static str> = Some("agent");
|
||||
|
||||
const FALLBACK_KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AgentSettingsContent;
|
||||
|
||||
@@ -15,7 +15,8 @@ use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::lsp_store::CompletionDocumentation;
|
||||
use project::{
|
||||
Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
|
||||
ProjectPath, Symbol, WorktreeId,
|
||||
};
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
@@ -732,7 +733,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
replace_range: source_range.clone(),
|
||||
new_text,
|
||||
label: CodeLabel::plain(command.name.to_string(), None),
|
||||
documentation: Some(CompletionDocumentation::SingleLine(
|
||||
documentation: Some(CompletionDocumentation::MultiLinePlainText(
|
||||
command.description.into(),
|
||||
)),
|
||||
source: project::CompletionSource::Custom,
|
||||
@@ -771,6 +772,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
|
||||
Ok(vec![CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions {
|
||||
dynamic_width: true,
|
||||
},
|
||||
// Since this does its own filtering (see `filter_completions()` returns false),
|
||||
// there is no benefit to computing whether this set of completions is incomplete.
|
||||
is_incomplete: true,
|
||||
@@ -862,6 +866,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
|
||||
Ok(vec![CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions {
|
||||
dynamic_width: true,
|
||||
},
|
||||
// Since this does its own filtering (see `filter_completions()` returns false),
|
||||
// there is no benefit to computing whether this set of completions is incomplete.
|
||||
is_incomplete: true,
|
||||
@@ -1005,56 +1012,44 @@ impl ContextCompletion {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
struct SlashCommandCompletion {
|
||||
source_range: Range<usize>,
|
||||
command: Option<String>,
|
||||
argument: Option<String>,
|
||||
pub struct SlashCommandCompletion {
|
||||
pub source_range: Range<usize>,
|
||||
pub command: Option<String>,
|
||||
pub argument: Option<String>,
|
||||
}
|
||||
|
||||
impl SlashCommandCompletion {
|
||||
fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
|
||||
pub fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
|
||||
// If we decide to support commands that are not at the beginning of the prompt, we can remove this check
|
||||
if !line.starts_with('/') || offset_to_line != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let last_command_start = line.rfind('/')?;
|
||||
if last_command_start >= line.len() {
|
||||
return Some(Self::default());
|
||||
}
|
||||
if last_command_start > 0
|
||||
&& line
|
||||
.chars()
|
||||
.nth(last_command_start - 1)
|
||||
.is_some_and(|c| !c.is_whitespace())
|
||||
let (prefix, last_command) = line.rsplit_once('/')?;
|
||||
if prefix.chars().last().is_some_and(|c| !c.is_whitespace())
|
||||
|| last_command.starts_with(char::is_whitespace)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let rest_of_line = &line[last_command_start + 1..];
|
||||
|
||||
let mut command = None;
|
||||
let mut argument = None;
|
||||
let mut end = last_command_start + 1;
|
||||
|
||||
if let Some(command_text) = rest_of_line.split_whitespace().next() {
|
||||
command = Some(command_text.to_string());
|
||||
end += command_text.len();
|
||||
|
||||
// Find the start of arguments after the command
|
||||
if let Some(args_start) =
|
||||
rest_of_line[command_text.len()..].find(|c: char| !c.is_whitespace())
|
||||
{
|
||||
let args = &rest_of_line[command_text.len() + args_start..].trim_end();
|
||||
if !args.is_empty() {
|
||||
argument = Some(args.to_string());
|
||||
end += args.len() + 1;
|
||||
}
|
||||
let mut command = None;
|
||||
if let Some((command_text, args)) = last_command.split_once(char::is_whitespace) {
|
||||
if !args.is_empty() {
|
||||
argument = Some(args.trim_end().to_string());
|
||||
}
|
||||
}
|
||||
command = Some(command_text.to_string());
|
||||
} else if !last_command.is_empty() {
|
||||
command = Some(last_command.to_string());
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
source_range: last_command_start + offset_to_line..end + offset_to_line,
|
||||
source_range: prefix.len() + offset_to_line
|
||||
..line
|
||||
.rfind(|c: char| !c.is_whitespace())
|
||||
.unwrap_or_else(|| line.len())
|
||||
+ 1
|
||||
+ offset_to_line,
|
||||
command,
|
||||
argument,
|
||||
})
|
||||
@@ -1173,6 +1168,15 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SlashCommandCompletion::try_parse("/拿不到命令 拿不到命令 ", 0),
|
||||
Some(SlashCommandCompletion {
|
||||
source_range: 0..30,
|
||||
command: Some("拿不到命令".to_string()),
|
||||
argument: Some("拿不到命令".to_string()),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None);
|
||||
@@ -1180,6 +1184,8 @@ mod tests {
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("/ ", 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -11,7 +11,7 @@ use collections::HashMap;
|
||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||
use gpui::{
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||
ScrollHandle, TextStyleRefinement, WeakEntity, Window,
|
||||
ScrollHandle, SharedString, TextStyleRefinement, WeakEntity, Window,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
use project::Project;
|
||||
@@ -32,6 +32,7 @@ pub struct EntryViewState {
|
||||
entries: Vec<Entry>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
}
|
||||
|
||||
impl EntryViewState {
|
||||
@@ -42,6 +43,7 @@ impl EntryViewState {
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
@@ -51,6 +53,7 @@ impl EntryViewState {
|
||||
entries: Vec::new(),
|
||||
prompt_capabilities,
|
||||
available_commands,
|
||||
agent_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +93,7 @@ impl EntryViewState {
|
||||
self.prompt_store.clone(),
|
||||
self.prompt_capabilities.clone(),
|
||||
self.available_commands.clone(),
|
||||
self.agent_name.clone(),
|
||||
"Edit message - @ to include context",
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
@@ -203,7 +207,7 @@ impl EntryViewState {
|
||||
self.entries.drain(range);
|
||||
}
|
||||
|
||||
pub fn settings_changed(&mut self, cx: &mut App) {
|
||||
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||
@@ -476,6 +480,7 @@ mod tests {
|
||||
None,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
acp::completion_provider::ContextPickerCompletionProvider,
|
||||
acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion},
|
||||
context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
|
||||
};
|
||||
use acp_thread::{MentionUri, selection_name};
|
||||
@@ -11,10 +11,10 @@ use assistant_slash_commands::codeblock_fence_for_path;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
|
||||
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer,
|
||||
ToOffset,
|
||||
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
|
||||
MultiBuffer, ToOffset,
|
||||
actions::Paste,
|
||||
display_map::{Crease, CreaseId, FoldId},
|
||||
display_map::{Crease, CreaseId, FoldId, Inlay},
|
||||
};
|
||||
use futures::{
|
||||
FutureExt as _,
|
||||
@@ -22,13 +22,15 @@ use futures::{
|
||||
};
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
|
||||
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, Subscription, Task,
|
||||
TextStyle, WeakEntity, pulsating_between,
|
||||
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
|
||||
Subscription, Task, TextStyle, WeakEntity, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language::{Buffer, Language, language_settings::InlayHintKind};
|
||||
use language_model::LanguageModelImage;
|
||||
use postage::stream::Stream as _;
|
||||
use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
|
||||
use project::{
|
||||
CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
|
||||
};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
@@ -47,8 +49,8 @@ use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _,
|
||||
FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label,
|
||||
LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled,
|
||||
TextSize, TintColor, Toggleable, Window, div, h_flex,
|
||||
LabelCommon, LabelSize, ParentElement, Render, SelectableButton, Styled, TextSize, TintColor,
|
||||
Toggleable, Window, div, h_flex,
|
||||
};
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt as _};
|
||||
@@ -62,6 +64,8 @@ pub struct MessageEditor {
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
_parse_slash_command_task: Task<()>,
|
||||
}
|
||||
@@ -76,6 +80,8 @@ pub enum MessageEditorEvent {
|
||||
|
||||
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||
|
||||
const COMMAND_HINT_INLAY_ID: usize = 0;
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -84,6 +90,7 @@ impl MessageEditor {
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
placeholder: impl Into<Arc<str>>,
|
||||
mode: EditorMode,
|
||||
window: &mut Window,
|
||||
@@ -102,7 +109,7 @@ impl MessageEditor {
|
||||
history_store.clone(),
|
||||
prompt_store.clone(),
|
||||
prompt_capabilities.clone(),
|
||||
available_commands,
|
||||
available_commands.clone(),
|
||||
));
|
||||
let mention_set = MentionSet::default();
|
||||
let editor = cx.new(|cx| {
|
||||
@@ -133,12 +140,33 @@ impl MessageEditor {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut has_hint = false;
|
||||
let mut subscriptions = Vec::new();
|
||||
|
||||
subscriptions.push(cx.subscribe_in(&editor, window, {
|
||||
move |this, editor, event, window, cx| {
|
||||
if let EditorEvent::Edited { .. } = event {
|
||||
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
let snapshot = editor.update(cx, |editor, cx| {
|
||||
let new_hints = this
|
||||
.command_hint(editor.buffer(), cx)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let has_new_hint = !new_hints.is_empty();
|
||||
editor.splice_inlays(
|
||||
if has_hint {
|
||||
&[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
new_hints,
|
||||
cx,
|
||||
);
|
||||
has_hint = has_new_hint;
|
||||
|
||||
editor.snapshot(window, cx)
|
||||
});
|
||||
this.mention_set.remove_invalid(snapshot);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -152,11 +180,56 @@ impl MessageEditor {
|
||||
history_store,
|
||||
prompt_store,
|
||||
prompt_capabilities,
|
||||
available_commands,
|
||||
agent_name,
|
||||
_subscriptions: subscriptions,
|
||||
_parse_slash_command_task: Task::ready(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn command_hint(&self, buffer: &Entity<MultiBuffer>, cx: &App) -> Option<Inlay> {
|
||||
let available_commands = self.available_commands.borrow();
|
||||
if available_commands.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
|
||||
if parsed_command.argument.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let command_name = parsed_command.command?;
|
||||
let available_command = available_commands
|
||||
.iter()
|
||||
.find(|command| command.name == command_name)?;
|
||||
|
||||
let acp::AvailableCommandInput::Unstructured { mut hint } =
|
||||
available_command.input.clone()?;
|
||||
|
||||
let mut hint_pos = parsed_command.source_range.end + 1;
|
||||
if hint_pos > snapshot.len() {
|
||||
hint_pos = snapshot.len();
|
||||
hint.insert(0, ' ');
|
||||
}
|
||||
|
||||
let hint_pos = snapshot.anchor_after(hint_pos);
|
||||
|
||||
Some(Inlay::hint(
|
||||
COMMAND_HINT_INLAY_ID,
|
||||
hint_pos,
|
||||
&InlayHint {
|
||||
position: hint_pos.text_anchor,
|
||||
label: InlayHintLabel::String(hint),
|
||||
kind: Some(InlayHintKind::Parameter),
|
||||
padding_left: false,
|
||||
padding_right: false,
|
||||
tooltip: None,
|
||||
resolve_state: project::ResolveState::Resolved,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn insert_thread_summary(
|
||||
&mut self,
|
||||
thread: agent2::DbThreadMetadata,
|
||||
@@ -420,14 +493,13 @@ impl MessageEditor {
|
||||
let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
|
||||
return Task::ready(Err(anyhow!("project entry not found")));
|
||||
};
|
||||
let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else {
|
||||
let directory_path = entry.path.clone();
|
||||
let worktree_id = project_path.worktree_id;
|
||||
let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("worktree not found")));
|
||||
};
|
||||
let project = self.project.clone();
|
||||
cx.spawn(async move |_, cx| {
|
||||
let directory_path = entry.path.clone();
|
||||
|
||||
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
|
||||
let file_paths = worktree.read_with(cx, |worktree, _cx| {
|
||||
collect_files_in_path(worktree, &directory_path)
|
||||
})?;
|
||||
@@ -627,7 +699,7 @@ impl MessageEditor {
|
||||
self.project.read(cx).fs().clone(),
|
||||
self.history_store.clone(),
|
||||
));
|
||||
let delegate = AgentServerDelegate::new(self.project.clone(), None);
|
||||
let delegate = AgentServerDelegate::new(self.project.clone(), None, None);
|
||||
let connection = server.connect(Path::new(""), delegate, cx);
|
||||
cx.spawn(async move |_, cx| {
|
||||
let agent = connection.await?;
|
||||
@@ -661,10 +733,52 @@ impl MessageEditor {
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_slash_commands(
|
||||
text: &str,
|
||||
available_commands: &[acp::AvailableCommand],
|
||||
agent_name: &str,
|
||||
) -> Result<()> {
|
||||
if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
|
||||
if let Some(command_name) = parsed_command.command {
|
||||
// Check if this command is in the list of available commands from the server
|
||||
let is_supported = available_commands
|
||||
.iter()
|
||||
.any(|cmd| cmd.name == command_name);
|
||||
|
||||
if !is_supported {
|
||||
return Err(anyhow!(
|
||||
"The /{} command is not supported by {}.\n\nAvailable commands: {}",
|
||||
command_name,
|
||||
agent_name,
|
||||
if available_commands.is_empty() {
|
||||
"none".to_string()
|
||||
} else {
|
||||
available_commands
|
||||
.iter()
|
||||
.map(|cmd| format!("/{}", cmd.name))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn contents(
|
||||
&self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
|
||||
// Check for unsupported slash commands before spawning async task
|
||||
let text = self.editor.read(cx).text(cx);
|
||||
let available_commands = self.available_commands.borrow().clone();
|
||||
if let Err(err) =
|
||||
Self::validate_slash_commands(&text, &available_commands, &self.agent_name)
|
||||
{
|
||||
return Task::ready(Err(err));
|
||||
}
|
||||
|
||||
let contents = self
|
||||
.mention_set
|
||||
.contents(&self.prompt_capabilities.get(), cx);
|
||||
@@ -674,7 +788,7 @@ impl MessageEditor {
|
||||
let contents = contents.await?;
|
||||
let mut all_tracked_buffers = Vec::new();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let result = editor.update(cx, |editor, cx| {
|
||||
let mut ix = 0;
|
||||
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
|
||||
let text = editor.text(cx);
|
||||
@@ -767,9 +881,9 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(chunks, all_tracked_buffers)
|
||||
})
|
||||
Ok((chunks, all_tracked_buffers))
|
||||
})?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1184,6 +1298,7 @@ impl Render for MessageEditor {
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
inlay_hints_style: editor::make_inlay_hints_style(cx),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -1502,6 +1617,7 @@ mod tests {
|
||||
None,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
@@ -1579,6 +1695,140 @@ mod tests {
|
||||
pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
".zed": {
|
||||
"tasks.json": r#"[{"label": "test", "command": "echo"}]"#
|
||||
},
|
||||
"src": {
|
||||
"main.rs": "fn main() {}",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
|
||||
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
|
||||
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
|
||||
// Start with no available commands - simulating Claude which doesn't support slash commands
|
||||
let available_commands = Rc::new(RefCell::new(vec![]));
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let workspace_handle = workspace.downgrade();
|
||||
let message_editor = workspace.update_in(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace_handle.clone(),
|
||||
project.clone(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
prompt_capabilities.clone(),
|
||||
available_commands.clone(),
|
||||
"Claude Code".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
|
||||
|
||||
// Test that slash commands fail when no available_commands are set (empty list means no commands supported)
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("/file test.txt", window, cx);
|
||||
});
|
||||
|
||||
let contents_result = message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx))
|
||||
.await;
|
||||
|
||||
// Should fail because available_commands is empty (no commands supported)
|
||||
assert!(contents_result.is_err());
|
||||
let error_message = contents_result.unwrap_err().to_string();
|
||||
assert!(error_message.contains("not supported by Claude Code"));
|
||||
assert!(error_message.contains("Available commands: none"));
|
||||
|
||||
// Now simulate Claude providing its list of available commands (which doesn't include file)
|
||||
available_commands.replace(vec![acp::AvailableCommand {
|
||||
name: "help".to_string(),
|
||||
description: "Get help".to_string(),
|
||||
input: None,
|
||||
}]);
|
||||
|
||||
// Test that unsupported slash commands trigger an error when we have a list of available commands
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("/file test.txt", window, cx);
|
||||
});
|
||||
|
||||
let contents_result = message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx))
|
||||
.await;
|
||||
|
||||
assert!(contents_result.is_err());
|
||||
let error_message = contents_result.unwrap_err().to_string();
|
||||
assert!(error_message.contains("not supported by Claude Code"));
|
||||
assert!(error_message.contains("/file"));
|
||||
assert!(error_message.contains("Available commands: /help"));
|
||||
|
||||
// Test that supported commands work fine
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("/help", window, cx);
|
||||
});
|
||||
|
||||
let contents_result = message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx))
|
||||
.await;
|
||||
|
||||
// Should succeed because /help is in available_commands
|
||||
assert!(contents_result.is_ok());
|
||||
|
||||
// Test that regular text works fine
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Hello Claude!", window, cx);
|
||||
});
|
||||
|
||||
let (content, _) = message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content.len(), 1);
|
||||
if let acp::ContentBlock::Text(text) = &content[0] {
|
||||
assert_eq!(text.text, "Hello Claude!");
|
||||
} else {
|
||||
panic!("Expected ContentBlock::Text");
|
||||
}
|
||||
|
||||
// Test that @ mentions still work
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Check this @", window, cx);
|
||||
});
|
||||
|
||||
// The @ mention functionality should not be affected
|
||||
let (content, _) = message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content.len(), 1);
|
||||
if let acp::ContentBlock::Text(text) = &content[0] {
|
||||
assert_eq!(text.text, "Check this @");
|
||||
} else {
|
||||
panic!("Expected ContentBlock::Text");
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageEditorItem(Entity<MessageEditor>);
|
||||
|
||||
impl Item for MessageEditorItem {
|
||||
@@ -1639,7 +1889,7 @@ mod tests {
|
||||
name: "say-hello".to_string(),
|
||||
description: "Say hello to whoever you want".to_string(),
|
||||
input: Some(acp::AvailableCommandInput::Unstructured {
|
||||
hint: "Who do you want to say hello to?".to_string(),
|
||||
hint: "<name>".to_string(),
|
||||
}),
|
||||
},
|
||||
]));
|
||||
@@ -1654,6 +1904,7 @@ mod tests {
|
||||
None,
|
||||
prompt_capabilities.clone(),
|
||||
available_commands.clone(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: None,
|
||||
@@ -1714,7 +1965,7 @@ mod tests {
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/quick-math ");
|
||||
assert_eq!(editor.display_text(cx), "/quick-math ");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
editor.set_text("", window, cx);
|
||||
});
|
||||
@@ -1722,7 +1973,7 @@ mod tests {
|
||||
cx.simulate_input("/say");
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say");
|
||||
assert_eq!(editor.display_text(cx), "/say");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
|
||||
assert_eq!(
|
||||
@@ -1740,6 +1991,7 @@ mod tests {
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say-hello ");
|
||||
assert_eq!(editor.display_text(cx), "/say-hello <name>");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
|
||||
assert_eq!(
|
||||
@@ -1757,8 +2009,35 @@ mod tests {
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say-hello GPT5");
|
||||
assert_eq!(editor.display_text(cx), "/say-hello GPT5");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
|
||||
// Delete argument
|
||||
for _ in 0..4 {
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say-hello ");
|
||||
// Hint is visible because argument was deleted
|
||||
assert_eq!(editor.display_text(cx), "/say-hello <name>");
|
||||
|
||||
// Delete last command letter
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
// Hint goes away once command no longer matches an available one
|
||||
assert_eq!(editor.text(cx), "/say-hell");
|
||||
assert_eq!(editor.display_text(cx), "/say-hell");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
});
|
||||
}
|
||||
@@ -1858,6 +2137,7 @@ mod tests {
|
||||
None,
|
||||
prompt_capabilities.clone(),
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: None,
|
||||
|
||||
@@ -192,8 +192,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
|
||||
@@ -36,6 +36,14 @@ impl AcpModelSelectorPopover {
|
||||
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
pub fn active_model_name(&self, cx: &App) -> Option<SharedString> {
|
||||
self.selector
|
||||
.read(cx)
|
||||
.delegate
|
||||
.active_model()
|
||||
.map(|model| model.name.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AcpModelSelectorPopover {
|
||||
|
||||
@@ -8,7 +8,7 @@ use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, PromptCapabilities};
|
||||
use agent_servers::{AgentServer, AgentServerDelegate, ClaudeCode};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
|
||||
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore};
|
||||
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use audio::{Audio, Sound};
|
||||
use buffer_diff::BufferDiff;
|
||||
@@ -43,10 +43,10 @@ use std::{collections::BTreeMap, rc::Rc, time::Duration};
|
||||
use task::SpawnInTerminal;
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::Anchor;
|
||||
use theme::ThemeSettings;
|
||||
use theme::{AgentFontSize, ThemeSettings};
|
||||
use ui::{
|
||||
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
|
||||
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*,
|
||||
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
@@ -78,10 +78,12 @@ enum ThreadFeedback {
|
||||
Negative,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ThreadError {
|
||||
PaymentRequired,
|
||||
ModelRequestLimitReached(cloud_llm_client::Plan),
|
||||
ToolUseLimitReached,
|
||||
Refusal,
|
||||
AuthenticationRequired(SharedString),
|
||||
Other(SharedString),
|
||||
}
|
||||
@@ -286,8 +288,9 @@ pub struct AcpThreadView {
|
||||
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
is_loading_contents: bool,
|
||||
new_server_version_available: Option<SharedString>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 3],
|
||||
_subscriptions: [Subscription; 4],
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -330,6 +333,11 @@ impl AcpThreadView {
|
||||
|
||||
let placeholder = if agent.name() == "Zed Agent" {
|
||||
format!("Message the {} — @ to include context", agent.name())
|
||||
} else if agent.name() == "Claude Code" || !available_commands.borrow().is_empty() {
|
||||
format!(
|
||||
"Message {} — @ to include context, / for commands",
|
||||
agent.name()
|
||||
)
|
||||
} else {
|
||||
format!("Message {} — @ to include context", agent.name())
|
||||
};
|
||||
@@ -342,6 +350,7 @@ impl AcpThreadView {
|
||||
prompt_store.clone(),
|
||||
prompt_capabilities.clone(),
|
||||
available_commands.clone(),
|
||||
agent.name(),
|
||||
placeholder,
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: MIN_EDITOR_LINES,
|
||||
@@ -366,11 +375,13 @@ impl AcpThreadView {
|
||||
prompt_store.clone(),
|
||||
prompt_capabilities.clone(),
|
||||
available_commands.clone(),
|
||||
agent.name(),
|
||||
)
|
||||
});
|
||||
|
||||
let subscriptions = [
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_font_size_changed),
|
||||
cx.observe_global_in::<AgentFontSize>(window, Self::agent_font_size_changed),
|
||||
cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
|
||||
cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
|
||||
];
|
||||
@@ -407,9 +418,24 @@ impl AcpThreadView {
|
||||
_subscriptions: subscriptions,
|
||||
_cancel_task: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
new_server_version_available: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread_state = Self::initial_state(
|
||||
self.agent.clone(),
|
||||
None,
|
||||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
self.available_commands.replace(vec![]);
|
||||
self.new_server_version_available.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn initial_state(
|
||||
agent: Rc<dyn AgentServer>,
|
||||
resume_thread: Option<DbThreadMetadata>,
|
||||
@@ -418,14 +444,37 @@ impl AcpThreadView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ThreadState {
|
||||
let root_dir = project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
if !project.read(cx).is_local() && agent.clone().downcast::<NativeAgentServer>().is_none() {
|
||||
return ThreadState::LoadError(LoadError::Other(
|
||||
"External agents are not yet supported for remote projects.".into(),
|
||||
));
|
||||
}
|
||||
let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
// Pick the first non-single-file worktree for the root directory if there are any,
|
||||
// and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees.
|
||||
worktrees.sort_by(|l, r| {
|
||||
l.read(cx)
|
||||
.is_single_file()
|
||||
.cmp(&r.read(cx).is_single_file())
|
||||
});
|
||||
let root_dir = worktrees
|
||||
.into_iter()
|
||||
.filter_map(|worktree| {
|
||||
if worktree.read(cx).is_single_file() {
|
||||
Some(worktree.read(cx).abs_path().parent()?.into())
|
||||
} else {
|
||||
Some(worktree.read(cx).abs_path())
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
.unwrap_or_else(|| paths::home_dir().as_path().into());
|
||||
let (tx, mut rx) = watch::channel("Loading…".into());
|
||||
let delegate = AgentServerDelegate::new(project.clone(), Some(tx));
|
||||
let (status_tx, mut status_rx) = watch::channel("Loading…".into());
|
||||
let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
|
||||
let delegate = AgentServerDelegate::new(
|
||||
project.clone(),
|
||||
Some(status_tx),
|
||||
Some(new_version_available_tx),
|
||||
);
|
||||
|
||||
let connect_task = agent.connect(&root_dir, delegate, cx);
|
||||
let load_task = cx.spawn_in(window, async move |this, cx| {
|
||||
@@ -488,9 +537,6 @@ impl AcpThreadView {
|
||||
Ok(thread) => {
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
|
||||
this.available_commands
|
||||
.replace(thread.read(cx).available_commands());
|
||||
|
||||
this.prompt_capabilities
|
||||
.set(thread.read(cx).prompt_capabilities());
|
||||
|
||||
@@ -583,10 +629,23 @@ impl AcpThreadView {
|
||||
.log_err();
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Ok(new_version) = new_version_available_rx.recv().await {
|
||||
if let Some(new_version) = new_version {
|
||||
this.update(cx, |this, cx| {
|
||||
this.new_server_version_available = Some(new_version.into());
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let loading_view = cx.new(|cx| {
|
||||
let update_title_task = cx.spawn(async move |this, cx| {
|
||||
loop {
|
||||
let status = rx.recv().await?;
|
||||
let status = status_rx.recv().await?;
|
||||
this.update(cx, |this: &mut LoadingView, cx| {
|
||||
this.title = status;
|
||||
cx.notify();
|
||||
@@ -622,17 +681,13 @@ impl AcpThreadView {
|
||||
move |_, ev, window, cx| {
|
||||
if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
|
||||
&& &provider_id == updated_provider_id
|
||||
&& LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.provider(&provider_id)
|
||||
.map_or(false, |provider| provider.is_authenticated(cx))
|
||||
{
|
||||
this.update(cx, |this, cx| {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent.clone(),
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
this.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
this.reset(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -907,6 +962,40 @@ impl AcpThreadView {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = self.message_editor.read(cx).text(cx);
|
||||
let text = text.trim();
|
||||
if text == "/login" || text == "/logout" {
|
||||
let ThreadState::Ready { thread, .. } = &self.thread_state else {
|
||||
return;
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
if !connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.any(|method| method.id.0.as_ref() == "claude-login")
|
||||
{
|
||||
return;
|
||||
};
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
let contents = self
|
||||
.message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx));
|
||||
@@ -1197,6 +1286,14 @@ impl AcpThreadView {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AcpThreadEvent::Refusal => {
|
||||
self.thread_retry_status.take();
|
||||
self.thread_error = Some(ThreadError::Refusal);
|
||||
let model_or_agent_name = self.get_current_model_name(cx);
|
||||
let notification_message =
|
||||
format!("{} refused to respond to this request", model_or_agent_name);
|
||||
self.notify_with_sound(¬ification_message, IconName::Warning, window, cx);
|
||||
}
|
||||
AcpThreadEvent::Error => {
|
||||
self.thread_retry_status.take();
|
||||
self.notify_with_sound(
|
||||
@@ -1228,6 +1325,30 @@ impl AcpThreadView {
|
||||
.set(thread.read(cx).prompt_capabilities());
|
||||
}
|
||||
AcpThreadEvent::TokenUsageUpdated => {}
|
||||
AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
|
||||
let mut available_commands = available_commands.clone();
|
||||
|
||||
if thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.any(|method| method.id.0.as_ref() == "claude-login")
|
||||
{
|
||||
available_commands.push(acp::AvailableCommand {
|
||||
name: "login".to_owned(),
|
||||
description: "Authenticate".to_owned(),
|
||||
input: None,
|
||||
});
|
||||
available_commands.push(acp::AvailableCommand {
|
||||
name: "logout".to_owned(),
|
||||
description: "Authenticate".to_owned(),
|
||||
input: None,
|
||||
});
|
||||
}
|
||||
|
||||
self.available_commands.replace(available_commands);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1279,11 +1400,11 @@ impl AcpThreadView {
|
||||
.read(cx)
|
||||
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
|
||||
.unwrap();
|
||||
if !provider.is_authenticated(cx) {
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
window.defer(cx, |window, cx| {
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
if !provider.is_authenticated(cx) {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
@@ -1295,9 +1416,21 @@ impl AcpThreadView {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
this.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else if method.0.as_ref() == "vertex-ai"
|
||||
&& std::env::var("GOOGLE_API_KEY").is_err()
|
||||
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
|
||||
@@ -1341,7 +1474,6 @@ impl AcpThreadView {
|
||||
cx.notify();
|
||||
self.auth_task =
|
||||
Some(cx.spawn_in(window, {
|
||||
let project = self.project.clone();
|
||||
let agent = self.agent.clone();
|
||||
async move |this, cx| {
|
||||
let result = authenticate.await;
|
||||
@@ -1370,14 +1502,7 @@ impl AcpThreadView {
|
||||
}
|
||||
this.handle_thread_error(err, cx);
|
||||
} else {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
this.reset(window, cx);
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
@@ -1399,7 +1524,7 @@ impl AcpThreadView {
|
||||
let cwd = project.first_project_directory(cx);
|
||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||
|
||||
let delegate = AgentServerDelegate::new(project_entity.clone(), None);
|
||||
let delegate = AgentServerDelegate::new(project_entity.clone(), None, None);
|
||||
let command = ClaudeCode::login_command(delegate, cx);
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
@@ -2518,28 +2643,6 @@ impl AcpThreadView {
|
||||
.with_rotate_animation(2)
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Disclosure::new(
|
||||
SharedString::from(format!(
|
||||
"terminal-tool-disclosure-{}",
|
||||
terminal.entity_id()
|
||||
)),
|
||||
is_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.visible_on_hover(&header_group)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
if is_expanded {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
.when(truncated_output, |header| {
|
||||
let tooltip = if let Some(output) = output {
|
||||
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
|
||||
@@ -2598,7 +2701,29 @@ impl AcpThreadView {
|
||||
)))
|
||||
}),
|
||||
)
|
||||
});
|
||||
})
|
||||
.child(
|
||||
Disclosure::new(
|
||||
SharedString::from(format!(
|
||||
"terminal-tool-disclosure-{}",
|
||||
terminal.entity_id()
|
||||
)),
|
||||
is_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.visible_on_hover(&header_group)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
if is_expanded {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
}
|
||||
})),
|
||||
);
|
||||
|
||||
let terminal_view = self
|
||||
.entry_view_state
|
||||
@@ -2886,6 +3011,8 @@ impl AcpThreadView {
|
||||
let show_description =
|
||||
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
|
||||
|
||||
let auth_methods = connection.auth_methods();
|
||||
|
||||
v_flex().flex_1().size_full().justify_end().child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
@@ -2916,21 +3043,23 @@ impl AcpThreadView {
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.when(
|
||||
show_description,
|
||||
|el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}. Please choose one of the following options:",
|
||||
self.agent.name()
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when(show_description, |el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}.{}",
|
||||
self.agent.name(),
|
||||
if auth_methods.len() > 1 {
|
||||
" Please choose one of the following options:"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
})
|
||||
.when_some(pending_auth_method, |el, _| {
|
||||
el.child(
|
||||
h_flex()
|
||||
@@ -2942,12 +3071,12 @@ impl AcpThreadView {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2)
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
.child(Label::new("Authenticating…").size(LabelSize::Small)),
|
||||
)
|
||||
})
|
||||
.when(!connection.auth_methods().is_empty(), |this| {
|
||||
.when(!auth_methods.is_empty(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
@@ -2959,38 +3088,32 @@ impl AcpThreadView {
|
||||
.pt_2()
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
})
|
||||
.children(
|
||||
connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.map(|(ix, method)| {
|
||||
Button::new(
|
||||
SharedString::from(method.id.0.clone()),
|
||||
method.name.clone(),
|
||||
)
|
||||
.when(ix == 0, |el| {
|
||||
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||
})
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let method_id = method.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = this.agent.telemetry_id(),
|
||||
method = method_id
|
||||
);
|
||||
.children(connection.auth_methods().iter().enumerate().rev().map(
|
||||
|(ix, method)| {
|
||||
Button::new(
|
||||
SharedString::from(method.id.0.clone()),
|
||||
method.name.clone(),
|
||||
)
|
||||
.when(ix == 0, |el| {
|
||||
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||
})
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let method_id = method.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = this.agent.telemetry_id(),
|
||||
method = method_id
|
||||
);
|
||||
|
||||
this.authenticate(method_id.clone(), window, cx)
|
||||
})
|
||||
this.authenticate(method_id.clone(), window, cx)
|
||||
})
|
||||
}),
|
||||
),
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3947,15 +4070,15 @@ impl AcpThreadView {
|
||||
MentionUri::PastedImage => {}
|
||||
MentionUri::Directory { abs_path } => {
|
||||
let project = workspace.project();
|
||||
let Some(entry) = project.update(cx, |project, cx| {
|
||||
let Some(entry_id) = project.update(cx, |project, cx| {
|
||||
let path = project.find_project_path(abs_path, cx)?;
|
||||
project.entry_for_path(&path, cx)
|
||||
project.entry_for_path(&path, cx).map(|entry| entry.id)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry_id));
|
||||
});
|
||||
}
|
||||
MentionUri::Symbol {
|
||||
@@ -3968,11 +4091,9 @@ impl AcpThreadView {
|
||||
line_range,
|
||||
} => {
|
||||
let project = workspace.project();
|
||||
let Some((path, _)) = project.update(cx, |project, cx| {
|
||||
let path = project.find_project_path(path, cx)?;
|
||||
let entry = project.entry_for_path(&path, cx)?;
|
||||
Some((path, entry))
|
||||
}) else {
|
||||
let Some(path) =
|
||||
project.update(cx, |project, cx| project.find_project_path(path, cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -4134,7 +4255,7 @@ impl AcpThreadView {
|
||||
}
|
||||
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer(&markdown, Some(markdown_language), cx)
|
||||
project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
|
||||
});
|
||||
let buffer = cx.new(|cx| {
|
||||
MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
|
||||
@@ -4613,9 +4734,9 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn agent_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.entry_view_state.update(cx, |entry_view_state, cx| {
|
||||
entry_view_state.settings_changed(cx);
|
||||
entry_view_state.agent_font_size_changed(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4682,6 +4803,7 @@ impl AcpThreadView {
|
||||
fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
let content = match self.thread_error.as_ref()? {
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
|
||||
ThreadError::Refusal => self.render_refusal_error(cx),
|
||||
ThreadError::AuthenticationRequired(error) => {
|
||||
self.render_authentication_required_error(error.clone(), cx)
|
||||
}
|
||||
@@ -4697,6 +4819,75 @@ impl AcpThreadView {
|
||||
Some(div().child(content))
|
||||
}
|
||||
|
||||
fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
|
||||
v_flex().w_full().justify_end().child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.pr_3()
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Download)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(Label::new("New version available").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
Button::new("update-button", format!("Update to v{}", version))
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.reset(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_current_model_name(&self, cx: &App) -> SharedString {
|
||||
// For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
|
||||
// For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
|
||||
// This provides better clarity about what refused the request
|
||||
if self
|
||||
.agent
|
||||
.clone()
|
||||
.downcast::<agent2::NativeAgentServer>()
|
||||
.is_some()
|
||||
{
|
||||
// Native agent - use the model name
|
||||
self.model_selector
|
||||
.as_ref()
|
||||
.and_then(|selector| selector.read(cx).active_model_name(cx))
|
||||
.unwrap_or_else(|| SharedString::from("The model"))
|
||||
} else {
|
||||
// ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
|
||||
self.agent.name()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
|
||||
let model_or_agent_name = self.get_current_model_name(cx);
|
||||
let refusal_message = format!(
|
||||
"{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.",
|
||||
model_or_agent_name
|
||||
);
|
||||
|
||||
Callout::new()
|
||||
.severity(Severity::Error)
|
||||
.title("Request Refused")
|
||||
.icon(IconName::XCircle)
|
||||
.description(refusal_message.clone())
|
||||
.actions_slot(self.create_copy_button(&refusal_message))
|
||||
.dismiss_action(self.dismiss_error_button(cx))
|
||||
}
|
||||
|
||||
fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
|
||||
let can_resume = self
|
||||
.thread()
|
||||
@@ -5070,6 +5261,12 @@ impl Render for AcpThreadView {
|
||||
})
|
||||
.children(self.render_thread_retry_status_callout(window, cx))
|
||||
.children(self.render_thread_error(window, cx))
|
||||
.when_some(
|
||||
self.new_server_version_available.as_ref().filter(|_| {
|
||||
!has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
|
||||
}),
|
||||
|this, version| this.child(self.render_new_version_callout(&version, cx)),
|
||||
)
|
||||
.children(
|
||||
if let Some(usage_callout) = self.render_usage_callout(line_height, cx) {
|
||||
Some(usage_callout.into_any_element())
|
||||
@@ -5324,6 +5521,33 @@ pub(crate) mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_refusal_handling(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let (thread_view, cx) =
|
||||
setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await;
|
||||
|
||||
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||
message_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Do something harmful", window, cx);
|
||||
});
|
||||
|
||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||
thread_view.send(window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Check that the refusal error is set
|
||||
thread_view.read_with(cx, |thread_view, _cx| {
|
||||
assert!(
|
||||
matches!(thread_view.thread_error, Some(ThreadError::Refusal)),
|
||||
"Expected refusal error to be set"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -5523,7 +5747,6 @@ pub(crate) mod tests {
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
@@ -5559,6 +5782,67 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulates a model which always returns a refusal response
|
||||
#[derive(Clone)]
|
||||
struct RefusalAgentConnection;
|
||||
|
||||
impl AgentConnection for RefusalAgentConnection {
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
_cwd: &Path,
|
||||
cx: &mut gpui::App,
|
||||
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||
Task::ready(Ok(cx.new(|cx| {
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
AcpThread::new(
|
||||
"RefusalAgentConnection",
|
||||
self,
|
||||
project,
|
||||
action_log,
|
||||
SessionId("test".into()),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn authenticate(
|
||||
&self,
|
||||
_method_id: acp::AuthMethodId,
|
||||
_cx: &mut App,
|
||||
) -> Task<gpui::Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<acp_thread::UserMessageId>,
|
||||
_params: acp::PromptRequest,
|
||||
_cx: &mut App,
|
||||
) -> Task<gpui::Result<acp::PromptResponse>> {
|
||||
Task::ready(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
}))
|
||||
}
|
||||
|
||||
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
||||
@@ -1001,8 +1001,22 @@ impl ActiveThread {
|
||||
// Don't notify for intermediate tool use
|
||||
}
|
||||
Ok(StopReason::Refusal) => {
|
||||
let model_name = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map(|configured| configured.model.name().0.to_string())
|
||||
.unwrap_or_else(|| "The model".to_string());
|
||||
let refusal_message = format!(
|
||||
"{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.",
|
||||
model_name
|
||||
);
|
||||
self.last_error = Some(ThreadError::Message {
|
||||
header: SharedString::from("Request Refused"),
|
||||
message: SharedString::from(refusal_message),
|
||||
});
|
||||
self.notify_with_sound(
|
||||
"Language model refused to respond",
|
||||
format!("{} refused to respond", model_name),
|
||||
IconName::Warning,
|
||||
window,
|
||||
cx,
|
||||
@@ -3571,7 +3585,7 @@ pub(crate) fn open_active_thread_as_markdown(
|
||||
}
|
||||
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer(&markdown, Some(markdown_language), cx)
|
||||
project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
|
||||
});
|
||||
let buffer =
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone()));
|
||||
|
||||
@@ -251,6 +251,7 @@ pub struct ConfigureContextServerModal {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
source: ConfigurationSource,
|
||||
state: State,
|
||||
original_server_id: Option<ContextServerId>,
|
||||
}
|
||||
|
||||
impl ConfigureContextServerModal {
|
||||
@@ -348,6 +349,11 @@ impl ConfigureContextServerModal {
|
||||
context_server_store,
|
||||
workspace: workspace_handle,
|
||||
state: State::Idle,
|
||||
original_server_id: match &target {
|
||||
ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
|
||||
ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
|
||||
ConfigurationTarget::New => None,
|
||||
},
|
||||
source: ConfigurationSource::from_target(
|
||||
target,
|
||||
language_registry,
|
||||
@@ -415,9 +421,19 @@ impl ConfigureContextServerModal {
|
||||
// When we write the settings to the file, the context server will be restarted.
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
|
||||
project_settings.context_servers.insert(id.0, settings);
|
||||
});
|
||||
let original_server_id = self.original_server_id.clone();
|
||||
update_settings_file::<ProjectSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |project_settings, _| {
|
||||
if let Some(original_id) = original_server_id {
|
||||
if original_id != id {
|
||||
project_settings.context_servers.remove(&original_id.0);
|
||||
}
|
||||
}
|
||||
project_settings.context_servers.insert(id.0, settings);
|
||||
},
|
||||
);
|
||||
});
|
||||
} else if let Some(existing_server) = existing_server {
|
||||
self.context_server_store
|
||||
|
||||
@@ -1517,7 +1517,10 @@ impl AgentDiff {
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) => {
|
||||
AcpThreadEvent::Stopped
|
||||
| AcpThreadEvent::Error
|
||||
| AcpThreadEvent::LoadError(_)
|
||||
| AcpThreadEvent::Refusal => {
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
AcpThreadEvent::TitleUpdated
|
||||
@@ -1525,6 +1528,7 @@ impl AgentDiff {
|
||||
| AcpThreadEvent::EntriesRemoved(_)
|
||||
| AcpThreadEvent::ToolAuthorizationRequired
|
||||
| AcpThreadEvent::PromptCapabilitiesUpdated
|
||||
| AcpThreadEvent::AvailableCommandsUpdated(_)
|
||||
| AcpThreadEvent::Retry(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ use agent2::{DbThreadMetadata, HistoryEntry};
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_actions::agent::ReauthenticateAgent;
|
||||
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
||||
|
||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::ui::AcpOnboardingModal;
|
||||
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||
@@ -207,6 +207,9 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
|
||||
AcpOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|workspace, _: &OpenClaudeCodeOnboardingModal, window, cx| {
|
||||
ClaudeCodeOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
||||
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
||||
window.refresh();
|
||||
@@ -1091,6 +1094,7 @@ impl AgentPanel {
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
let fs = self.fs.clone();
|
||||
let is_not_local = !self.project.read(cx).is_local();
|
||||
|
||||
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
|
||||
|
||||
@@ -1122,17 +1126,21 @@ impl AgentPanel {
|
||||
agent
|
||||
}
|
||||
None => {
|
||||
cx.background_spawn(async move {
|
||||
KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
.and_then(|value| {
|
||||
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.agent
|
||||
if is_not_local {
|
||||
ExternalAgent::NativeAgent
|
||||
} else {
|
||||
cx.background_spawn(async move {
|
||||
KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
.and_then(|value| {
|
||||
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.agent
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1911,13 +1919,17 @@ impl AgentPanel {
|
||||
AgentType::Gemini => {
|
||||
self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
|
||||
}
|
||||
AgentType::ClaudeCode => self.external_thread(
|
||||
Some(crate::ExternalAgent::ClaudeCode),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
AgentType::ClaudeCode => {
|
||||
self.selected_agent = AgentType::ClaudeCode;
|
||||
self.serialize(cx);
|
||||
self.external_thread(
|
||||
Some(crate::ExternalAgent::ClaudeCode),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
AgentType::Custom { name, command } => self.external_thread(
|
||||
Some(crate::ExternalAgent::Custom { name, command }),
|
||||
None,
|
||||
@@ -2523,6 +2535,9 @@ impl AgentPanel {
|
||||
.with_handle(self.new_thread_menu_handle.clone())
|
||||
.menu({
|
||||
let workspace = self.workspace.clone();
|
||||
let is_not_local = workspace
|
||||
.update(cx, |workspace, cx| !workspace.project().read(cx).is_local())
|
||||
.unwrap_or_default();
|
||||
|
||||
move |window, cx| {
|
||||
telemetry::event!("New Thread Clicked");
|
||||
@@ -2613,6 +2628,7 @@ impl AgentPanel {
|
||||
ContextMenuEntry::new("New Gemini CLI Thread")
|
||||
.icon(IconName::AiGemini)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_not_local)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
@@ -2639,6 +2655,7 @@ impl AgentPanel {
|
||||
menu.item(
|
||||
ContextMenuEntry::new("New Claude Code Thread")
|
||||
.icon(IconName::AiClaude)
|
||||
.disabled(is_not_local)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
@@ -2671,6 +2688,7 @@ impl AgentPanel {
|
||||
ContextMenuEntry::new(format!("New {} Thread", agent_name))
|
||||
.icon(IconName::Terminal)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_not_local)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
let agent_name = agent_name.clone();
|
||||
@@ -2949,6 +2967,20 @@ impl AgentPanel {
|
||||
return false;
|
||||
}
|
||||
|
||||
let user_store = self.user_store.read(cx);
|
||||
|
||||
if user_store
|
||||
.plan()
|
||||
.is_some_and(|plan| matches!(plan, Plan::ZedPro))
|
||||
&& user_store
|
||||
.subscription_period()
|
||||
.and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
|
||||
.is_some_and(|date| date < chrono::Utc::now())
|
||||
{
|
||||
OnboardingUpsell::set_dismissed(true, cx);
|
||||
return false;
|
||||
}
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::History | ActiveView::Configuration => false,
|
||||
ActiveView::ExternalAgentThread { thread_view, .. }
|
||||
@@ -3517,6 +3549,11 @@ impl AgentPanel {
|
||||
) -> AnyElement {
|
||||
let message_with_header = format!("{}\n{}", header, message);
|
||||
|
||||
// Don't show Retry button for refusals
|
||||
let is_refusal = header == "Request Refused";
|
||||
let retry_button = self.render_retry_button(thread);
|
||||
let copy_button = self.create_copy_button(message_with_header);
|
||||
|
||||
Callout::new()
|
||||
.severity(Severity::Error)
|
||||
.icon(IconName::XCircle)
|
||||
@@ -3525,8 +3562,8 @@ impl AgentPanel {
|
||||
.actions_slot(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(self.render_retry_button(thread))
|
||||
.child(self.create_copy_button(message_with_header)),
|
||||
.when(!is_refusal, |this| this.child(retry_button))
|
||||
.child(copy_button),
|
||||
)
|
||||
.dismiss_action(self.dismiss_error_button(thread, cx))
|
||||
.into_any_element()
|
||||
|
||||
@@ -337,8 +337,7 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
];
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
|
||||
filter
|
||||
.show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
|
||||
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1139,7 +1139,7 @@ mod tests {
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let len = rng.random_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
@@ -1208,7 +1208,7 @@ mod tests {
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let len = rng.random_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
@@ -1277,7 +1277,7 @@ mod tests {
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let len = rng.random_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
|
||||
@@ -987,7 +987,8 @@ impl MentionLink {
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.entry_for_path(&project_path, cx)?;
|
||||
.entry_for_path(&project_path, cx)?
|
||||
.clone();
|
||||
Some(MentionLink::File(project_path, entry))
|
||||
}
|
||||
Self::SYMBOL => {
|
||||
|
||||
@@ -13,7 +13,10 @@ use http_client::HttpClientWithUrl;
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, Symbol, WorktreeId};
|
||||
use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
|
||||
Symbol, WorktreeId,
|
||||
};
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
use text::{Anchor, OffsetRangeExt, ToPoint};
|
||||
@@ -897,6 +900,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
|
||||
Ok(vec![CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
// Since this does its own filtering (see `filter_completions()` returns false),
|
||||
// there is no benefit to computing whether this set of completions is incomplete.
|
||||
is_incomplete: true,
|
||||
|
||||
@@ -144,7 +144,8 @@ impl InlineAssistant {
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
let enabled = AgentSettings::get_global(cx).enabled;
|
||||
let enabled = !DisableAiSettings::get_global(cx).disable_ai
|
||||
&& AgentSettings::get_global(cx).enabled;
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
terminal_panel.set_assistant_enabled(enabled, cx)
|
||||
});
|
||||
|
||||
@@ -93,8 +93,8 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
};
|
||||
|
||||
let bottom_padding = match &self.mode {
|
||||
PromptEditorMode::Buffer { .. } => Pixels::from(0.),
|
||||
PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
|
||||
PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
|
||||
PromptEditorMode::Terminal { .. } => rems_from_px(8.0),
|
||||
};
|
||||
|
||||
buttons.extend(self.render_buttons(window, cx));
|
||||
@@ -762,20 +762,22 @@ impl<T: 'static> PromptEditor<T> {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(window.rem_size()) * 1.3;
|
||||
fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
div()
|
||||
.key_context("InlineAssistEditor")
|
||||
.size_full()
|
||||
.p_2()
|
||||
.pl_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.bg(colors.editor_background)
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = settings.buffer_font_size(cx);
|
||||
let line_height = font_size * 1.2;
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
color: colors.editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
@@ -786,7 +788,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
background: colors.editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
|
||||
@@ -125,6 +125,7 @@ pub(crate) fn create_editor(
|
||||
cx,
|
||||
);
|
||||
editor.set_placeholder_text("Message the agent – @ to include context", cx);
|
||||
editor.disable_word_completions();
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_soft_wrap();
|
||||
editor.set_use_modal_editing(true);
|
||||
|
||||
@@ -7,7 +7,10 @@ use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
|
||||
use language::{Anchor, Buffer, ToPoint};
|
||||
use parking_lot::Mutex;
|
||||
use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
|
||||
use project::{
|
||||
CompletionDisplayOptions, CompletionIntent, CompletionSource,
|
||||
lsp_store::CompletionDocumentation,
|
||||
};
|
||||
use rope::Point;
|
||||
use std::{
|
||||
ops::Range,
|
||||
@@ -133,6 +136,7 @@ impl SlashCommandCompletionProvider {
|
||||
|
||||
vec![project::CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]
|
||||
})
|
||||
@@ -237,6 +241,7 @@ impl SlashCommandCompletionProvider {
|
||||
|
||||
Ok(vec![project::CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
// TODO: Could have slash commands indicate whether their completions are incomplete.
|
||||
is_incomplete: true,
|
||||
}])
|
||||
@@ -244,6 +249,7 @@ impl SlashCommandCompletionProvider {
|
||||
} else {
|
||||
Task::ready(Ok(vec![project::CompletionResponse {
|
||||
completions: Vec::new(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: true,
|
||||
}]))
|
||||
}
|
||||
@@ -305,6 +311,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
else {
|
||||
return Task::ready(Ok(vec![project::CompletionResponse {
|
||||
completions: Vec::new(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]));
|
||||
};
|
||||
|
||||
@@ -2,10 +2,11 @@ use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
/// Settings for slash commands.
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)]
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "slash_commands")]
|
||||
pub struct SlashCommandSettings {
|
||||
/// Settings for the `/cargo-workspace` slash command.
|
||||
#[serde(default)]
|
||||
@@ -21,8 +22,6 @@ pub struct CargoWorkspaceCommandSettings {
|
||||
}
|
||||
|
||||
impl Settings for SlashCommandSettings {
|
||||
const KEY: Option<&'static str> = Some("slash_commands");
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod acp_onboarding_modal;
|
||||
mod agent_notification;
|
||||
mod burn_mode_tooltip;
|
||||
mod claude_code_onboarding_modal;
|
||||
mod context_pill;
|
||||
mod end_trial_upsell;
|
||||
mod onboarding_modal;
|
||||
@@ -10,6 +11,7 @@ mod unavailable_editing_tooltip;
|
||||
pub use acp_onboarding_modal::*;
|
||||
pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use claude_code_onboarding_modal::*;
|
||||
pub use context_pill::*;
|
||||
pub use end_trial_upsell::*;
|
||||
pub use onboarding_modal::*;
|
||||
|
||||
@@ -141,20 +141,12 @@ impl Render for AcpOnboardingModal {
|
||||
.bg(gpui::black().opacity(0.15)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
)
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AcpLogoSerif,
|
||||
rems_from_px(111.),
|
||||
rems_from_px(41.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
),
|
||||
Vector::new(
|
||||
VectorName::AcpLogoSerif,
|
||||
rems_from_px(257.),
|
||||
rems_from_px(47.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
|
||||
254
crates/agent_ui/src/ui/claude_code_onboarding_modal.rs
Normal file
254
crates/agent_ui/src/ui/claude_code_onboarding_modal.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use client::zed_urls;
|
||||
use gpui::{
|
||||
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
|
||||
linear_color_stop, linear_gradient,
|
||||
};
|
||||
use ui::{TintColor, Vector, VectorName, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::agent_panel::{AgentPanel, AgentType};
|
||||
|
||||
macro_rules! claude_code_onboarding_event {
|
||||
($name:expr) => {
|
||||
telemetry::event!($name, source = "ACP Claude Code Onboarding");
|
||||
};
|
||||
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
|
||||
telemetry::event!($name, source = "ACP Claude Code Onboarding", $($key $(= $value)?),+);
|
||||
};
|
||||
}
|
||||
|
||||
pub struct ClaudeCodeOnboardingModal {
|
||||
focus_handle: FocusHandle,
|
||||
workspace: Entity<Workspace>,
|
||||
}
|
||||
|
||||
impl ClaudeCodeOnboardingModal {
|
||||
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
let workspace_entity = cx.entity();
|
||||
workspace.toggle_modal(window, cx, |_window, cx| Self {
|
||||
workspace: workspace_entity,
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
}
|
||||
|
||||
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.workspace.update(cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(AgentType::ClaudeCode, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
claude_code_onboarding_event!("Open Panel Clicked");
|
||||
}
|
||||
|
||||
fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url(&zed_urls::external_agents_docs(cx));
|
||||
cx.notify();
|
||||
|
||||
claude_code_onboarding_event!("Documentation Link Clicked");
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ClaudeCodeOnboardingModal {}
|
||||
|
||||
impl Focusable for ClaudeCodeOnboardingModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for ClaudeCodeOnboardingModal {}
|
||||
|
||||
impl Render for ClaudeCodeOnboardingModal {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let illustration_element = |icon: IconName, label: Option<SharedString>, opacity: f32| {
|
||||
h_flex()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().element_active.opacity(0.05))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_dashed()
|
||||
.child(
|
||||
Icon::new(icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
|
||||
)
|
||||
.map(|this| {
|
||||
if let Some(label_text) = label {
|
||||
this.child(
|
||||
Label::new(label_text)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
div().w_16().h_1().rounded_full().bg(cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_active
|
||||
.opacity(0.6)),
|
||||
)
|
||||
}
|
||||
})
|
||||
.opacity(opacity)
|
||||
};
|
||||
|
||||
let illustration = h_flex()
|
||||
.relative()
|
||||
.h(rems_from_px(126.))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_center()
|
||||
.gap_8()
|
||||
.rounded_t_md()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
|
||||
Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
|
||||
),
|
||||
)
|
||||
.child(div().absolute().inset_0().size_full().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.1),
|
||||
0.9,
|
||||
),
|
||||
linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.),
|
||||
0.,
|
||||
),
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
.bg(gpui::black().opacity(0.15)),
|
||||
)
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AcpLogoSerif,
|
||||
rems_from_px(257.),
|
||||
rems_from_px(47.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(illustration_element(IconName::Stop, None, 0.15))
|
||||
.child(illustration_element(
|
||||
IconName::AiGemini,
|
||||
Some("New Gemini CLI Thread".into()),
|
||||
0.3,
|
||||
))
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().element_active.opacity(0.2))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Icon::new(IconName::AiClaude)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("New Claude Code Thread").size(LabelSize::Small)),
|
||||
)
|
||||
.child(illustration_element(
|
||||
IconName::Stop,
|
||||
Some("Your Agent Here".into()),
|
||||
0.3,
|
||||
))
|
||||
.child(illustration_element(IconName::Stop, None, 0.15)),
|
||||
);
|
||||
|
||||
let heading = v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Beta Release")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Headline::new("Claude Code: Natively in Zed").size(HeadlineSize::Large));
|
||||
|
||||
let copy = "Powered by the Agent Client Protocol, you can now run Claude Code as\na first-class citizen in Zed's agent panel.";
|
||||
|
||||
let open_panel_button = Button::new("open-panel", "Start with Claude Code")
|
||||
.icon_size(IconSize::Indicator)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::open_panel));
|
||||
|
||||
let docs_button = Button::new("add-other-agents", "Add Other Agents")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::view_docs));
|
||||
|
||||
let close_button = h_flex().absolute().top_2().right_2().child(
|
||||
IconButton::new("cancel", IconName::Close).on_click(cx.listener(
|
||||
|_, _: &ClickEvent, _window, cx| {
|
||||
claude_code_onboarding_event!("Canceled", trigger = "X click");
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.id("acp-onboarding")
|
||||
.key_context("AcpOnboardingModal")
|
||||
.relative()
|
||||
.w(rems(34.))
|
||||
.h_full()
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.overflow_hidden()
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
claude_code_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(illustration)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.child(heading)
|
||||
.child(Label::new(copy).color(Color::Muted))
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.child(open_panel_button)
|
||||
.child(docs_button),
|
||||
),
|
||||
)
|
||||
.child(close_button)
|
||||
}
|
||||
}
|
||||
@@ -86,10 +86,16 @@ impl RenderOnce for AiUpsellCard {
|
||||
)
|
||||
.child(plan_definitions.free_plan());
|
||||
|
||||
let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
|
||||
Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
|
||||
.color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
|
||||
);
|
||||
let grid_bg = h_flex()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.w_full()
|
||||
.h(px(240.))
|
||||
.bg(gpui::pattern_slash(
|
||||
cx.theme().colors().border.opacity(0.1),
|
||||
2.,
|
||||
25.,
|
||||
));
|
||||
|
||||
let gradient_bg = div()
|
||||
.absolute()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use cloud_llm_client::Plan;
|
||||
use gpui::{Entity, IntoElement, ParentElement};
|
||||
use ui::prelude::*;
|
||||
|
||||
@@ -35,6 +36,8 @@ impl EditPredictionOnboarding {
|
||||
|
||||
impl Render for EditPredictionOnboarding {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_free_plan = self.user_store.read(cx).plan() == Some(Plan::ZedFree);
|
||||
|
||||
let github_copilot = v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(if self.copilot_is_configured {
|
||||
@@ -67,7 +70,8 @@ impl Render for EditPredictionOnboarding {
|
||||
self.continue_with_zed_ai.clone(),
|
||||
cx,
|
||||
))
|
||||
.child(ui::Divider::horizontal())
|
||||
.child(github_copilot)
|
||||
.when(is_free_plan, |this| {
|
||||
this.child(ui::Divider::horizontal()).child(github_copilot)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ pub struct YoungAccountBanner;
|
||||
|
||||
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, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach 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. To request an exception, reach out to billing-support@zed.dev.";
|
||||
|
||||
let label = div()
|
||||
.w_full()
|
||||
|
||||
@@ -363,11 +363,9 @@ pub async fn complete(
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
beta_headers: String,
|
||||
) -> Result<Response, AnthropicError> {
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
let beta_headers = Model::from_id(&request.model)
|
||||
.map(|model| model.beta_headers())
|
||||
.unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
@@ -409,8 +407,9 @@ pub async fn stream_completion(
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
beta_headers: String,
|
||||
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
|
||||
stream_completion_with_rate_limit_info(client, api_url, api_key, request)
|
||||
stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
|
||||
.await
|
||||
.map(|output| output.0)
|
||||
}
|
||||
@@ -506,6 +505,7 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
beta_headers: String,
|
||||
) -> Result<
|
||||
(
|
||||
BoxStream<'static, Result<Event, AnthropicError>>,
|
||||
@@ -518,9 +518,7 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
stream: true,
|
||||
};
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
let beta_headers = Model::from_id(&request.base.model)
|
||||
.map(|model| model.beta_headers())
|
||||
.unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
|
||||
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
|
||||
@@ -50,8 +50,9 @@ text.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_env_vars.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -764,7 +764,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
let network = Arc::new(Mutex::new(Network::new(rng.clone())));
|
||||
let mut contexts = Vec::new();
|
||||
|
||||
let num_peers = rng.gen_range(min_peers..=max_peers);
|
||||
let num_peers = rng.random_range(min_peers..=max_peers);
|
||||
let context_id = ContextId::new();
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
for i in 0..num_peers {
|
||||
@@ -806,10 +806,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
|| !network.lock().is_idle()
|
||||
|| network.lock().contains_disconnected_peers()
|
||||
{
|
||||
let context_index = rng.gen_range(0..contexts.len());
|
||||
let context_index = rng.random_range(0..contexts.len());
|
||||
let context = &contexts[context_index];
|
||||
|
||||
match rng.gen_range(0..100) {
|
||||
match rng.random_range(0..100) {
|
||||
0..=29 if mutation_count > 0 => {
|
||||
log::info!("Context {}: edit buffer", context_index);
|
||||
context.update(cx, |context, cx| {
|
||||
@@ -874,10 +874,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
merge_same_roles: true,
|
||||
})];
|
||||
|
||||
let num_sections = rng.gen_range(0..=3);
|
||||
let num_sections = rng.random_range(0..=3);
|
||||
let mut section_start = 0;
|
||||
for _ in 0..num_sections {
|
||||
let mut section_end = rng.gen_range(section_start..=output_text.len());
|
||||
let mut section_end = rng.random_range(section_start..=output_text.len());
|
||||
while !output_text.is_char_boundary(section_end) {
|
||||
section_end += 1;
|
||||
}
|
||||
@@ -924,7 +924,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
75..=84 if mutation_count > 0 => {
|
||||
context.update(cx, |context, cx| {
|
||||
if let Some(message) = context.messages(cx).choose(&mut rng) {
|
||||
let new_status = match rng.gen_range(0..3) {
|
||||
let new_status = match rng.random_range(0..3) {
|
||||
0 => MessageStatus::Done,
|
||||
1 => MessageStatus::Pending,
|
||||
_ => MessageStatus::Error(SharedString::from("Random error")),
|
||||
@@ -971,7 +971,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
|
||||
network.lock().broadcast(replica_id, ops_to_send);
|
||||
context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx));
|
||||
} else if rng.gen_bool(0.1) && replica_id != 0 {
|
||||
} else if rng.random_bool(0.1) && replica_id != 0 {
|
||||
log::info!("Context {}: disconnecting", context_index);
|
||||
network.lock().disconnect_peer(replica_id);
|
||||
} else if network.lock().has_unreceived(replica_id) {
|
||||
|
||||
@@ -24,6 +24,7 @@ use rpc::AnyProtoClient;
|
||||
use std::sync::LazyLock;
|
||||
use std::{cmp::Reverse, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use zed_env_vars::ZED_STATELESS;
|
||||
|
||||
pub(crate) fn init(client: &AnyProtoClient) {
|
||||
client.add_entity_message_handler(ContextStore::handle_advertise_contexts);
|
||||
@@ -788,8 +789,6 @@ impl ContextStore {
|
||||
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
pub static ZED_STATELESS: LazyLock<bool> =
|
||||
LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
|
||||
if *ZED_STATELESS {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1315,17 +1315,17 @@ mod tests {
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_indents(mut rng: StdRng) {
|
||||
let len = rng.gen_range(1..=100);
|
||||
let len = rng.random_range(1..=100);
|
||||
let new_text = util::RandomCharIter::new(&mut rng)
|
||||
.with_simple_text()
|
||||
.take(len)
|
||||
.collect::<String>();
|
||||
let new_text = new_text
|
||||
.split('\n')
|
||||
.map(|line| format!("{}{}", " ".repeat(rng.gen_range(0..=8)), line))
|
||||
.map(|line| format!("{}{}", " ".repeat(rng.random_range(0..=8)), line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let delta = IndentDelta::Spaces(rng.gen_range(-4..=4));
|
||||
let delta = IndentDelta::Spaces(rng.random_range(-4i8..=4i8) as isize);
|
||||
|
||||
let chunks = to_random_chunks(&mut rng, &new_text);
|
||||
let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| {
|
||||
@@ -1357,7 +1357,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -204,7 +204,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn parse_random_chunks(input: &str, parser: &mut CreateFileParser, rng: &mut StdRng) -> String {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -996,7 +996,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -1399,7 +1399,7 @@ fn eval(
|
||||
}
|
||||
|
||||
fn run_eval(eval: EvalInput, tx: mpsc::Sender<Result<EvalOutput>>) {
|
||||
let dispatcher = gpui::TestDispatcher::new(StdRng::from_entropy());
|
||||
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
|
||||
let mut cx = TestAppContext::build(dispatcher, None);
|
||||
let output = cx.executor().block_test(async {
|
||||
let test = EditAgentTest::new(&mut cx).await;
|
||||
@@ -1707,7 +1707,7 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
|
||||
};
|
||||
|
||||
if let Some(retry_after) = retry_delay {
|
||||
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
|
||||
let jitter = retry_after.mul_f64(rand::rng().random_range(0.0..1.0));
|
||||
eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}");
|
||||
Timer::after(retry_after + jitter).await;
|
||||
} else {
|
||||
|
||||
@@ -771,7 +771,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -14,11 +14,20 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-tar.workspace = true
|
||||
collections.workspace = true
|
||||
crossbeam.workspace = true
|
||||
gpui.workspace = true
|
||||
settings.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] }
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] }
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
|
||||
libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" }
|
||||
|
||||
@@ -1,19 +1,56 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, BorrowAppContext, Global};
|
||||
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered};
|
||||
use gpui::{App, BackgroundExecutor, BorrowAppContext, Global};
|
||||
|
||||
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
|
||||
mod non_windows_and_freebsd_deps {
|
||||
pub(super) use gpui::AsyncApp;
|
||||
pub(super) use libwebrtc::native::apm;
|
||||
pub(super) use log::info;
|
||||
pub(super) use parking_lot::Mutex;
|
||||
pub(super) use rodio::cpal::Sample;
|
||||
pub(super) use rodio::source::{LimitSettings, UniformSourceIterator};
|
||||
pub(super) use std::sync::Arc;
|
||||
}
|
||||
|
||||
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
|
||||
use non_windows_and_freebsd_deps::*;
|
||||
|
||||
use rodio::{
|
||||
Decoder, OutputStream, OutputStreamBuilder, Source, mixer::Mixer, nz, source::Buffered,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::io::Cursor;
|
||||
use std::{io::Cursor, num::NonZero, path::PathBuf, sync::atomic::Ordering, time::Duration};
|
||||
use util::ResultExt;
|
||||
|
||||
mod audio_settings;
|
||||
mod replays;
|
||||
mod rodio_ext;
|
||||
pub use audio_settings::AudioSettings;
|
||||
pub use rodio_ext::RodioExt;
|
||||
|
||||
use crate::audio_settings::LIVE_SETTINGS;
|
||||
|
||||
// NOTE: We used to use WebRTC's mixer which only supported
|
||||
// 16kHz, 32kHz and 48kHz. As 48 is the most common "next step up"
|
||||
// for audio output devices like speakers/bluetooth, we just hard-code
|
||||
// this; and downsample when we need to.
|
||||
//
|
||||
// Since most noise cancelling requires 16kHz we will move to
|
||||
// that in the future.
|
||||
pub const SAMPLE_RATE: NonZero<u32> = nz!(48000);
|
||||
pub const CHANNEL_COUNT: NonZero<u16> = nz!(2);
|
||||
pub const BUFFER_SIZE: usize = // echo canceller and livekit want 10ms of audio
|
||||
(SAMPLE_RATE.get() as usize / 100) * CHANNEL_COUNT.get() as usize;
|
||||
|
||||
pub const REPLAY_DURATION: Duration = Duration::from_secs(30);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AudioSettings::register(cx);
|
||||
LIVE_SETTINGS.initialize(cx);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
|
||||
pub enum Sound {
|
||||
Joined,
|
||||
Leave,
|
||||
@@ -38,32 +75,152 @@ impl Sound {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
output_handle: Option<OutputStream>,
|
||||
output_mixer: Option<Mixer>,
|
||||
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
|
||||
pub echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
|
||||
source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
|
||||
replays: replays::Replays,
|
||||
}
|
||||
|
||||
impl Default for Audio {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
output_handle: Default::default(),
|
||||
output_mixer: Default::default(),
|
||||
#[cfg(not(any(
|
||||
all(target_os = "windows", target_env = "gnu"),
|
||||
target_os = "freebsd"
|
||||
)))]
|
||||
echo_canceller: Arc::new(Mutex::new(apm::AudioProcessingModule::new(
|
||||
true, false, false, false,
|
||||
))),
|
||||
source_cache: Default::default(),
|
||||
replays: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for Audio {}
|
||||
|
||||
impl Audio {
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
|
||||
fn ensure_output_exists(&mut self) -> Result<&Mixer> {
|
||||
if self.output_handle.is_none() {
|
||||
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
|
||||
self.output_handle = Some(
|
||||
OutputStreamBuilder::open_default_stream()
|
||||
.context("Could not open default output stream")?,
|
||||
);
|
||||
if let Some(output_handle) = &self.output_handle {
|
||||
let (mixer, source) = rodio::mixer::mixer(CHANNEL_COUNT, SAMPLE_RATE);
|
||||
// or the mixer will end immediately as its empty.
|
||||
mixer.add(rodio::source::Zero::new(CHANNEL_COUNT, SAMPLE_RATE));
|
||||
self.output_mixer = Some(mixer);
|
||||
|
||||
// The webrtc apm is not yet compiling for windows & freebsd
|
||||
#[cfg(not(any(
|
||||
any(all(target_os = "windows", target_env = "gnu")),
|
||||
target_os = "freebsd"
|
||||
)))]
|
||||
let echo_canceller = Arc::clone(&self.echo_canceller);
|
||||
#[cfg(not(any(
|
||||
any(all(target_os = "windows", target_env = "gnu")),
|
||||
target_os = "freebsd"
|
||||
)))]
|
||||
let source = source.inspect_buffer::<BUFFER_SIZE, _>(move |buffer| {
|
||||
let mut buf: [i16; _] = buffer.map(|s| s.to_sample());
|
||||
echo_canceller
|
||||
.lock()
|
||||
.process_reverse_stream(
|
||||
&mut buf,
|
||||
SAMPLE_RATE.get() as i32,
|
||||
CHANNEL_COUNT.get().into(),
|
||||
)
|
||||
.expect("Audio input and output threads should not panic");
|
||||
});
|
||||
output_handle.mixer().add(source);
|
||||
}
|
||||
}
|
||||
|
||||
self.output_handle.as_ref()
|
||||
Ok(self
|
||||
.output_mixer
|
||||
.as_ref()
|
||||
.expect("we only get here if opening the outputstream succeeded"))
|
||||
}
|
||||
|
||||
pub fn play_source(
|
||||
pub fn save_replays(
|
||||
&self,
|
||||
executor: BackgroundExecutor,
|
||||
) -> gpui::Task<anyhow::Result<(PathBuf, Duration)>> {
|
||||
self.replays.replays_to_tar(executor)
|
||||
}
|
||||
|
||||
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
|
||||
pub fn open_microphone(voip_parts: VoipParts) -> anyhow::Result<impl Source> {
|
||||
let stream = rodio::microphone::MicrophoneBuilder::new()
|
||||
.default_device()?
|
||||
.default_config()?
|
||||
.prefer_sample_rates([SAMPLE_RATE, SAMPLE_RATE.saturating_mul(nz!(2))])
|
||||
.prefer_channel_counts([nz!(1), nz!(2)])
|
||||
.prefer_buffer_sizes(512..)
|
||||
.open_stream()?;
|
||||
info!("Opened microphone: {:?}", stream.config());
|
||||
|
||||
let (replay, stream) = UniformSourceIterator::new(stream, CHANNEL_COUNT, SAMPLE_RATE)
|
||||
.limit(LimitSettings::live_performance())
|
||||
.process_buffer::<BUFFER_SIZE, _>(move |buffer| {
|
||||
let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
|
||||
if voip_parts
|
||||
.echo_canceller
|
||||
.lock()
|
||||
.process_stream(
|
||||
&mut int_buffer,
|
||||
SAMPLE_RATE.get() as i32,
|
||||
CHANNEL_COUNT.get() as i32,
|
||||
)
|
||||
.context("livekit audio processor error")
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
for (sample, processed) in buffer.iter_mut().zip(&int_buffer) {
|
||||
*sample = (*processed).to_sample();
|
||||
}
|
||||
}
|
||||
})
|
||||
.automatic_gain_control(1.0, 4.0, 0.0, 5.0)
|
||||
.periodic_access(Duration::from_millis(100), move |agc_source| {
|
||||
agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
|
||||
})
|
||||
.replayable(REPLAY_DURATION)?;
|
||||
|
||||
voip_parts
|
||||
.replays
|
||||
.add_voip_stream("local microphone".to_string(), replay);
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn play_voip_stream(
|
||||
source: impl rodio::Source + Send + 'static,
|
||||
speaker_name: String,
|
||||
is_staff: bool,
|
||||
cx: &mut App,
|
||||
) -> anyhow::Result<()> {
|
||||
let (replay_source, source) = source
|
||||
.automatic_gain_control(1.0, 4.0, 0.0, 5.0)
|
||||
.periodic_access(Duration::from_millis(100), move |agc_source| {
|
||||
agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
|
||||
})
|
||||
.replayable(REPLAY_DURATION)
|
||||
.expect("REPLAY_DURATION is longer then 100ms");
|
||||
|
||||
cx.update_default_global(|this: &mut Self, _cx| {
|
||||
let output_handle = this
|
||||
let output_mixer = this
|
||||
.ensure_output_exists()
|
||||
.ok_or_else(|| anyhow!("Could not open audio output"))?;
|
||||
output_handle.mixer().add(source);
|
||||
.context("Could not get output mixer")?;
|
||||
output_mixer.add(source);
|
||||
if is_staff {
|
||||
this.replays.add_voip_stream(speaker_name, replay_source);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -71,8 +228,12 @@ impl Audio {
|
||||
pub fn play_sound(sound: Sound, cx: &mut App) {
|
||||
cx.update_default_global(|this: &mut Self, cx| {
|
||||
let source = this.sound_source(sound, cx).log_err()?;
|
||||
let output_handle = this.ensure_output_exists()?;
|
||||
output_handle.mixer().add(source);
|
||||
let output_mixer = this
|
||||
.ensure_output_exists()
|
||||
.context("Could not get output mixer")
|
||||
.log_err()?;
|
||||
|
||||
output_mixer.add(source);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
@@ -103,3 +264,23 @@ impl Audio {
|
||||
Ok(source)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
|
||||
pub struct VoipParts {
|
||||
echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
|
||||
replays: replays::Replays,
|
||||
}
|
||||
|
||||
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
|
||||
impl VoipParts {
|
||||
pub fn new(cx: &AsyncApp) -> anyhow::Result<Self> {
|
||||
let (apm, replays) = cx.try_read_default_global::<Audio, _>(|audio, _| {
|
||||
(Arc::clone(&audio.echo_canceller), audio.replays.clone())
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
echo_canceller: apm,
|
||||
replays,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,55 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
pub struct AudioSettings {
|
||||
/// Opt into the new audio system.
|
||||
#[serde(rename = "experimental.rodio_audio", default)]
|
||||
pub rodio_audio: bool, // default is false
|
||||
/// Requires 'rodio_audio: true'
|
||||
///
|
||||
/// Use the new audio systems automatic gain control for your microphone.
|
||||
/// This affects how loud you sound to others.
|
||||
#[serde(rename = "experimental.control_input_volume", default)]
|
||||
pub control_input_volume: bool,
|
||||
/// Requires 'rodio_audio: 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.
|
||||
#[serde(rename = "experimental.control_output_volume", default)]
|
||||
pub control_output_volume: bool,
|
||||
}
|
||||
|
||||
/// Configuration of audio in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[serde(default)]
|
||||
#[settings_key(key = "audio")]
|
||||
pub struct AudioSettingsContent {
|
||||
/// Whether to use the experimental audio system
|
||||
/// Opt into the new audio system.
|
||||
#[serde(rename = "experimental.rodio_audio", default)]
|
||||
pub rodio_audio: bool,
|
||||
pub rodio_audio: bool, // default is false
|
||||
/// Requires 'rodio_audio: true'
|
||||
///
|
||||
/// Use the new audio systems automatic gain control for your microphone.
|
||||
/// This affects how loud you sound to others.
|
||||
#[serde(rename = "experimental.control_input_volume", default)]
|
||||
pub control_input_volume: bool,
|
||||
/// Requires 'rodio_audio: 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.
|
||||
#[serde(rename = "experimental.control_output_volume", default)]
|
||||
pub control_output_volume: bool,
|
||||
}
|
||||
|
||||
impl Settings for AudioSettings {
|
||||
const KEY: Option<&'static str> = Some("audio");
|
||||
|
||||
type FileContent = AudioSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
|
||||
@@ -31,3 +58,42 @@ impl Settings for AudioSettings {
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
/// See docs on [LIVE_SETTINGS]
|
||||
pub(crate) struct LiveSettings {
|
||||
pub(crate) control_input_volume: AtomicBool,
|
||||
pub(crate) control_output_volume: AtomicBool,
|
||||
}
|
||||
|
||||
impl LiveSettings {
|
||||
pub(crate) fn initialize(&self, cx: &mut App) {
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
LIVE_SETTINGS.control_input_volume.store(
|
||||
AudioSettings::get_global(cx).control_input_volume,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
LIVE_SETTINGS.control_output_volume.store(
|
||||
AudioSettings::get_global(cx).control_output_volume,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
|
||||
let init_settings = AudioSettings::get_global(cx);
|
||||
LIVE_SETTINGS
|
||||
.control_input_volume
|
||||
.store(init_settings.control_input_volume, Ordering::Relaxed);
|
||||
LIVE_SETTINGS
|
||||
.control_output_volume
|
||||
.store(init_settings.control_output_volume, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows access to settings from the audio thread. Updated by
|
||||
/// observer of SettingsStore. Needed because audio playback and recording are
|
||||
/// real time and must each run in a dedicated OS thread, therefore we can not
|
||||
/// use the background executor.
|
||||
pub(crate) static LIVE_SETTINGS: LiveSettings = LiveSettings {
|
||||
control_input_volume: AtomicBool::new(true),
|
||||
control_output_volume: AtomicBool::new(true),
|
||||
};
|
||||
|
||||
77
crates/audio/src/replays.rs
Normal file
77
crates/audio/src/replays.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_tar::{Builder, Header};
|
||||
use gpui::{BackgroundExecutor, Task};
|
||||
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use rodio::Source;
|
||||
use smol::fs::File;
|
||||
use std::{io, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use crate::{REPLAY_DURATION, rodio_ext::Replay};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct Replays(Arc<Mutex<HashMap<String, Replay>>>);
|
||||
|
||||
impl Replays {
|
||||
pub(crate) fn add_voip_stream(&self, stream_name: String, source: Replay) {
|
||||
let mut map = self.0.lock();
|
||||
map.retain(|_, replay| replay.source_is_active());
|
||||
map.insert(stream_name, source);
|
||||
}
|
||||
|
||||
pub(crate) fn replays_to_tar(
|
||||
&self,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Task<anyhow::Result<(PathBuf, Duration)>> {
|
||||
let map = Arc::clone(&self.0);
|
||||
executor.spawn(async move {
|
||||
let recordings: Vec<_> = map
|
||||
.lock()
|
||||
.iter_mut()
|
||||
.map(|(name, replay)| {
|
||||
let queued = REPLAY_DURATION.min(replay.duration_ready());
|
||||
(name.clone(), replay.take_duration(queued).record())
|
||||
})
|
||||
.collect();
|
||||
let longest = recordings
|
||||
.iter()
|
||||
.map(|(_, r)| {
|
||||
r.total_duration()
|
||||
.expect("SamplesBuffer always returns a total duration")
|
||||
})
|
||||
.max()
|
||||
.ok_or(anyhow!("There is no audio to capture"))?;
|
||||
|
||||
let path = std::env::current_dir()
|
||||
.context("Could not get current dir")?
|
||||
.join("replays.tar");
|
||||
let tar = File::create(&path)
|
||||
.await
|
||||
.context("Could not create file for tar")?;
|
||||
|
||||
let mut tar = Builder::new(tar);
|
||||
|
||||
for (name, recording) in recordings {
|
||||
let mut writer = io::Cursor::new(Vec::new());
|
||||
rodio::wav_to_writer(recording, &mut writer).context("failed to encode wav")?;
|
||||
let wav_data = writer.into_inner();
|
||||
let path = name.replace(' ', "_") + ".wav";
|
||||
let mut header = Header::new_gnu();
|
||||
// rw permissions for everyone
|
||||
header.set_mode(0o666);
|
||||
header.set_size(wav_data.len() as u64);
|
||||
tar.append_data(&mut header, path, wav_data.as_slice())
|
||||
.await
|
||||
.context("failed to apped wav to tar")?;
|
||||
}
|
||||
tar.into_inner()
|
||||
.await
|
||||
.context("Could not finish writing tar")?
|
||||
.sync_all()
|
||||
.await
|
||||
.context("Could not flush tar file to disk")?;
|
||||
Ok((path, longest))
|
||||
})
|
||||
}
|
||||
}
|
||||
598
crates/audio/src/rodio_ext.rs
Normal file
598
crates/audio/src/rodio_ext.rs
Normal file
@@ -0,0 +1,598 @@
|
||||
use std::{
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossbeam::queue::ArrayQueue;
|
||||
use rodio::{ChannelCount, Sample, SampleRate, Source};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Replay duration is too short must be >= 100ms")]
|
||||
pub struct ReplayDurationTooShort;
|
||||
|
||||
pub trait RodioExt: Source + Sized {
|
||||
fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
|
||||
where
|
||||
F: FnMut(&mut [Sample; N]);
|
||||
fn inspect_buffer<const N: usize, F>(self, callback: F) -> InspectBuffer<N, Self, F>
|
||||
where
|
||||
F: FnMut(&[Sample; N]);
|
||||
fn replayable(
|
||||
self,
|
||||
duration: Duration,
|
||||
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort>;
|
||||
fn take_samples(self, n: usize) -> TakeSamples<Self>;
|
||||
}
|
||||
|
||||
impl<S: Source> RodioExt for S {
|
||||
fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
|
||||
where
|
||||
F: FnMut(&mut [Sample; N]),
|
||||
{
|
||||
ProcessBuffer {
|
||||
inner: self,
|
||||
callback,
|
||||
buffer: [0.0; N],
|
||||
next: N,
|
||||
}
|
||||
}
|
||||
fn inspect_buffer<const N: usize, F>(self, callback: F) -> InspectBuffer<N, Self, F>
|
||||
where
|
||||
F: FnMut(&[Sample; N]),
|
||||
{
|
||||
InspectBuffer {
|
||||
inner: self,
|
||||
callback,
|
||||
buffer: [0.0; N],
|
||||
free: 0,
|
||||
}
|
||||
}
|
||||
/// Maintains a live replay with a history of at least `duration` seconds.
|
||||
///
|
||||
/// Note:
|
||||
/// History can be 100ms longer if the source drops before or while the
|
||||
/// replay is being read
|
||||
///
|
||||
/// # Errors
|
||||
/// If duration is smaller then 100ms
|
||||
fn replayable(
|
||||
self,
|
||||
duration: Duration,
|
||||
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort> {
|
||||
if duration < Duration::from_millis(100) {
|
||||
return Err(ReplayDurationTooShort);
|
||||
}
|
||||
|
||||
let samples_per_second = self.sample_rate().get() as usize * self.channels().get() as usize;
|
||||
let samples_to_queue = duration.as_secs_f64() * samples_per_second as f64;
|
||||
let samples_to_queue =
|
||||
(samples_to_queue as usize).next_multiple_of(self.channels().get().into());
|
||||
|
||||
let chunk_size =
|
||||
(samples_per_second.div_ceil(10)).next_multiple_of(self.channels().get() as usize);
|
||||
let chunks_to_queue = samples_to_queue.div_ceil(chunk_size);
|
||||
|
||||
let is_active = Arc::new(AtomicBool::new(true));
|
||||
let queue = Arc::new(ReplayQueue::new(chunks_to_queue, chunk_size));
|
||||
Ok((
|
||||
Replay {
|
||||
rx: Arc::clone(&queue),
|
||||
buffer: Vec::new().into_iter(),
|
||||
sleep_duration: duration / 2,
|
||||
sample_rate: self.sample_rate(),
|
||||
channel_count: self.channels(),
|
||||
source_is_active: is_active.clone(),
|
||||
},
|
||||
Replayable {
|
||||
tx: queue,
|
||||
inner: self,
|
||||
buffer: Vec::with_capacity(chunk_size),
|
||||
chunk_size,
|
||||
is_active,
|
||||
},
|
||||
))
|
||||
}
|
||||
fn take_samples(self, n: usize) -> TakeSamples<S> {
|
||||
TakeSamples {
|
||||
inner: self,
|
||||
left_to_take: n,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TakeSamples<S> {
|
||||
inner: S,
|
||||
left_to_take: usize,
|
||||
}
|
||||
|
||||
impl<S: Source> Iterator for TakeSamples<S> {
|
||||
type Item = Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.left_to_take == 0 {
|
||||
None
|
||||
} else {
|
||||
self.left_to_take -= 1;
|
||||
self.inner.next()
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(0, Some(self.left_to_take))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Source> Source for TakeSamples<S> {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
None // does not support spans
|
||||
}
|
||||
|
||||
fn channels(&self) -> ChannelCount {
|
||||
self.inner.channels()
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> SampleRate {
|
||||
self.inner.sample_rate()
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
Some(Duration::from_secs_f64(
|
||||
self.left_to_take as f64
|
||||
/ self.sample_rate().get() as f64
|
||||
/ self.channels().get() as f64,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ReplayQueue {
|
||||
inner: ArrayQueue<Vec<Sample>>,
|
||||
normal_chunk_len: usize,
|
||||
/// The last chunk in the queue may be smaller then
|
||||
/// the normal chunk size. This is always equal to the
|
||||
/// size of the last element in the queue.
|
||||
/// (so normally chunk_size)
|
||||
last_chunk: Mutex<Vec<Sample>>,
|
||||
}
|
||||
|
||||
impl ReplayQueue {
|
||||
fn new(queue_len: usize, chunk_size: usize) -> Self {
|
||||
Self {
|
||||
inner: ArrayQueue::new(queue_len),
|
||||
normal_chunk_len: chunk_size,
|
||||
last_chunk: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
/// Returns the length in samples
|
||||
fn len(&self) -> usize {
|
||||
self.inner.len().saturating_sub(1) * self.normal_chunk_len
|
||||
+ self
|
||||
.last_chunk
|
||||
.lock()
|
||||
.expect("Self::push_last can not poison this lock")
|
||||
.len()
|
||||
}
|
||||
|
||||
fn pop(&self) -> Option<Vec<Sample>> {
|
||||
self.inner.pop() // removes element that was inserted first
|
||||
}
|
||||
|
||||
fn push_last(&self, mut samples: Vec<Sample>) {
|
||||
let mut last_chunk = self
|
||||
.last_chunk
|
||||
.lock()
|
||||
.expect("Self::len can not poison this lock");
|
||||
std::mem::swap(&mut *last_chunk, &mut samples);
|
||||
}
|
||||
|
||||
fn push_normal(&self, samples: Vec<Sample>) {
|
||||
let _pushed_out_of_ringbuf = self.inner.force_push(samples);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcessBuffer<const N: usize, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&mut [Sample; N]),
|
||||
{
|
||||
inner: S,
|
||||
callback: F,
|
||||
/// Buffer used for both input and output.
|
||||
buffer: [Sample; N],
|
||||
/// Next already processed sample is at this index
|
||||
/// in buffer.
|
||||
///
|
||||
/// If this is equal to the length of the buffer we have no more samples and
|
||||
/// we must get new ones and process them
|
||||
next: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize, S, F> Iterator for ProcessBuffer<N, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&mut [Sample; N]),
|
||||
{
|
||||
type Item = Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.next += 1;
|
||||
if self.next < self.buffer.len() {
|
||||
let sample = self.buffer[self.next];
|
||||
return Some(sample);
|
||||
}
|
||||
|
||||
for sample in &mut self.buffer {
|
||||
*sample = self.inner.next()?
|
||||
}
|
||||
(self.callback)(&mut self.buffer);
|
||||
|
||||
self.next = 0;
|
||||
Some(self.buffer[0])
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, S, F> Source for ProcessBuffer<N, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&mut [Sample; N]),
|
||||
{
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn channels(&self) -> rodio::ChannelCount {
|
||||
self.inner.channels()
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> rodio::SampleRate {
|
||||
self.inner.sample_rate()
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<std::time::Duration> {
|
||||
self.inner.total_duration()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InspectBuffer<const N: usize, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&[Sample; N]),
|
||||
{
|
||||
inner: S,
|
||||
callback: F,
|
||||
/// Stores already emitted samples, once its full we call the callback.
|
||||
buffer: [Sample; N],
|
||||
/// Next free element in buffer. If this is equal to the buffer length
|
||||
/// we have no more free lements.
|
||||
free: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize, S, F> Iterator for InspectBuffer<N, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&[Sample; N]),
|
||||
{
|
||||
type Item = Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let Some(sample) = self.inner.next() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
self.buffer[self.free] = sample;
|
||||
self.free += 1;
|
||||
|
||||
if self.free == self.buffer.len() {
|
||||
(self.callback)(&self.buffer);
|
||||
self.free = 0
|
||||
}
|
||||
|
||||
Some(sample)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, S, F> Source for InspectBuffer<N, S, F>
|
||||
where
|
||||
S: Source + Sized,
|
||||
F: FnMut(&[Sample; N]),
|
||||
{
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn channels(&self) -> rodio::ChannelCount {
|
||||
self.inner.channels()
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> rodio::SampleRate {
|
||||
self.inner.sample_rate()
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<std::time::Duration> {
|
||||
self.inner.total_duration()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Replayable<S: Source> {
|
||||
inner: S,
|
||||
buffer: Vec<Sample>,
|
||||
chunk_size: usize,
|
||||
tx: Arc<ReplayQueue>,
|
||||
is_active: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl<S: Source> Iterator for Replayable<S> {
|
||||
type Item = Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(sample) = self.inner.next() {
|
||||
self.buffer.push(sample);
|
||||
// If the buffer is full send it
|
||||
if self.buffer.len() == self.chunk_size {
|
||||
self.tx.push_normal(std::mem::take(&mut self.buffer));
|
||||
}
|
||||
Some(sample)
|
||||
} else {
|
||||
let last_chunk = std::mem::take(&mut self.buffer);
|
||||
self.tx.push_last(last_chunk);
|
||||
self.is_active.store(false, Ordering::Relaxed);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Source> Source for Replayable<S> {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
self.inner.current_span_len()
|
||||
}
|
||||
|
||||
fn channels(&self) -> ChannelCount {
|
||||
self.inner.channels()
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> SampleRate {
|
||||
self.inner.sample_rate()
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
self.inner.total_duration()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Replay {
|
||||
rx: Arc<ReplayQueue>,
|
||||
buffer: std::vec::IntoIter<Sample>,
|
||||
sleep_duration: Duration,
|
||||
sample_rate: SampleRate,
|
||||
channel_count: ChannelCount,
|
||||
source_is_active: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Replay {
|
||||
pub fn source_is_active(&self) -> bool {
|
||||
// - source could return None and not drop
|
||||
// - source could be dropped before returning None
|
||||
self.source_is_active.load(Ordering::Relaxed) && Arc::strong_count(&self.rx) < 2
|
||||
}
|
||||
|
||||
/// Duration of what is in the buffer and can be returned without blocking.
|
||||
pub fn duration_ready(&self) -> Duration {
|
||||
let samples_per_second = self.channels().get() as u32 * self.sample_rate().get();
|
||||
|
||||
let seconds_queued = self.samples_ready() as f64 / samples_per_second as f64;
|
||||
Duration::from_secs_f64(seconds_queued)
|
||||
}
|
||||
|
||||
/// Number of samples in the buffer and can be returned without blocking.
|
||||
pub fn samples_ready(&self) -> usize {
|
||||
self.rx.len() + self.buffer.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Replay {
|
||||
type Item = Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(sample) = self.buffer.next() {
|
||||
return Some(sample);
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Some(new_buffer) = self.rx.pop() {
|
||||
self.buffer = new_buffer.into_iter();
|
||||
return self.buffer.next();
|
||||
}
|
||||
|
||||
if !self.source_is_active() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// The queue does not support blocking on a next item. We want this queue as it
|
||||
// is quite fast and provides a fixed size. We know how many samples are in a
|
||||
// buffer so if we do not get one now we must be getting one after `sleep_duration`.
|
||||
std::thread::sleep(self.sleep_duration);
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
((self.rx.len() + self.buffer.len()), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Source for Replay {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
None // source is not compatible with spans
|
||||
}
|
||||
|
||||
fn channels(&self) -> ChannelCount {
|
||||
self.channel_count
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> SampleRate {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rodio::{nz, static_buffer::StaticSamplesBuffer};
|
||||
|
||||
use super::*;
|
||||
|
||||
const SAMPLES: [Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0];
|
||||
|
||||
fn test_source() -> StaticSamplesBuffer {
|
||||
StaticSamplesBuffer::new(nz!(1), nz!(1), &SAMPLES)
|
||||
}
|
||||
|
||||
mod process_buffer {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn callback_gets_all_samples() {
|
||||
let input = test_source();
|
||||
|
||||
let _ = input
|
||||
.process_buffer::<{ SAMPLES.len() }, _>(|buffer| assert_eq!(*buffer, SAMPLES))
|
||||
.count();
|
||||
}
|
||||
#[test]
|
||||
fn callback_modifies_yielded() {
|
||||
let input = test_source();
|
||||
|
||||
let yielded: Vec<_> = input
|
||||
.process_buffer::<{ SAMPLES.len() }, _>(|buffer| {
|
||||
for sample in buffer {
|
||||
*sample += 1.0;
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
yielded,
|
||||
SAMPLES.into_iter().map(|s| s + 1.0).collect::<Vec<_>>()
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn source_truncates_to_whole_buffers() {
|
||||
let input = test_source();
|
||||
|
||||
let yielded = input
|
||||
.process_buffer::<3, _>(|buffer| assert_eq!(buffer, &SAMPLES[..3]))
|
||||
.count();
|
||||
assert_eq!(yielded, 3)
|
||||
}
|
||||
}
|
||||
|
||||
mod inspect_buffer {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn callback_gets_all_samples() {
|
||||
let input = test_source();
|
||||
|
||||
let _ = input
|
||||
.inspect_buffer::<{ SAMPLES.len() }, _>(|buffer| assert_eq!(*buffer, SAMPLES))
|
||||
.count();
|
||||
}
|
||||
#[test]
|
||||
fn source_does_not_truncate() {
|
||||
let input = test_source();
|
||||
|
||||
let yielded = input
|
||||
.inspect_buffer::<3, _>(|buffer| assert_eq!(buffer, &SAMPLES[..3]))
|
||||
.count();
|
||||
assert_eq!(yielded, SAMPLES.len())
|
||||
}
|
||||
}
|
||||
|
||||
mod instant_replay {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn continues_after_history() {
|
||||
let input = test_source();
|
||||
|
||||
let (mut replay, mut source) = input
|
||||
.replayable(Duration::from_secs(3))
|
||||
.expect("longer then 100ms");
|
||||
|
||||
source.by_ref().take(3).count();
|
||||
let yielded: Vec<Sample> = replay.by_ref().take(3).collect();
|
||||
assert_eq!(&yielded, &SAMPLES[0..3],);
|
||||
|
||||
source.count();
|
||||
let yielded: Vec<Sample> = replay.collect();
|
||||
assert_eq!(&yielded, &SAMPLES[3..5],);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_only_latest() {
|
||||
let input = test_source();
|
||||
|
||||
let (mut replay, mut source) = input
|
||||
.replayable(Duration::from_secs(2))
|
||||
.expect("longer then 100ms");
|
||||
|
||||
source.by_ref().take(5).count(); // get all items but do not end the source
|
||||
let yielded: Vec<Sample> = replay.by_ref().take(2).collect();
|
||||
assert_eq!(&yielded, &SAMPLES[3..5]);
|
||||
source.count(); // exhaust source
|
||||
assert_eq!(replay.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_correct_amount_of_seconds() {
|
||||
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
|
||||
|
||||
let (replay, mut source) = input
|
||||
.replayable(Duration::from_secs(2))
|
||||
.expect("longer then 100ms");
|
||||
|
||||
// exhaust but do not yet end source
|
||||
source.by_ref().take(40_000).count();
|
||||
|
||||
// take all samples we can without blocking
|
||||
let ready = replay.samples_ready();
|
||||
let n_yielded = replay.take_samples(ready).count();
|
||||
|
||||
let max = source.sample_rate().get() * source.channels().get() as u32 * 2;
|
||||
let margin = 16_000 / 10; // 100ms
|
||||
assert!(n_yielded as u32 >= max - margin);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn samples_ready() {
|
||||
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
|
||||
let (mut replay, source) = input
|
||||
.replayable(Duration::from_secs(2))
|
||||
.expect("longer then 100ms");
|
||||
assert_eq!(replay.by_ref().samples_ready(), 0);
|
||||
|
||||
source.take(8000).count(); // half a second
|
||||
let margin = 16_000 / 10; // 100ms
|
||||
let ready = replay.samples_ready();
|
||||
assert!(ready >= 8000 - margin);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use paths::remote_servers_dir;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
use smol::{fs::File, process::Command};
|
||||
use std::{
|
||||
@@ -118,13 +118,12 @@ struct AutoUpdateSetting(bool);
|
||||
/// Whether or not to automatically check for updates.
|
||||
///
|
||||
/// Default: true
|
||||
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)]
|
||||
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)]
|
||||
#[serde(transparent)]
|
||||
#[settings_key(key = "auto_update")]
|
||||
struct AutoUpdateSettingContent(bool);
|
||||
|
||||
impl Settings for AutoUpdateSetting {
|
||||
const KEY: Option<&'static str> = Some("auto_update");
|
||||
|
||||
type FileContent = AutoUpdateSettingContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
|
||||
@@ -88,10 +88,7 @@ fn view_release_notes_locally(
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
let buffer = project.create_local_buffer("", markdown, cx);
|
||||
project
|
||||
.mark_buffer_as_non_searchable(buffer.read(cx).remote_id(), cx);
|
||||
buffer
|
||||
project.create_local_buffer("", markdown, false, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
|
||||
@@ -2044,10 +2044,10 @@ mod tests {
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
fn gen_line(rng: &mut StdRng) -> String {
|
||||
if rng.gen_bool(0.2) {
|
||||
if rng.random_bool(0.2) {
|
||||
"\n".to_owned()
|
||||
} else {
|
||||
let c = rng.gen_range('A'..='Z');
|
||||
let c = rng.random_range('A'..='Z');
|
||||
format!("{c}{c}{c}\n")
|
||||
}
|
||||
}
|
||||
@@ -2066,7 +2066,7 @@ mod tests {
|
||||
old_lines.into_iter()
|
||||
};
|
||||
let mut result = String::new();
|
||||
let unchanged_count = rng.gen_range(0..=old_lines.len());
|
||||
let unchanged_count = rng.random_range(0..=old_lines.len());
|
||||
result +=
|
||||
&old_lines
|
||||
.by_ref()
|
||||
@@ -2076,14 +2076,14 @@ mod tests {
|
||||
s
|
||||
});
|
||||
while old_lines.len() > 0 {
|
||||
let deleted_count = rng.gen_range(0..=old_lines.len());
|
||||
let deleted_count = rng.random_range(0..=old_lines.len());
|
||||
let _advance = old_lines
|
||||
.by_ref()
|
||||
.take(deleted_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
let minimum_added = if deleted_count == 0 { 1 } else { 0 };
|
||||
let added_count = rng.gen_range(minimum_added..=5);
|
||||
let added_count = rng.random_range(minimum_added..=5);
|
||||
let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
|
||||
result += &addition;
|
||||
|
||||
@@ -2092,7 +2092,8 @@ mod tests {
|
||||
if blank_lines == old_lines.len() {
|
||||
break;
|
||||
};
|
||||
let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
|
||||
let unchanged_count =
|
||||
rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
|
||||
result += &old_lines.by_ref().take(unchanged_count).fold(
|
||||
String::new(),
|
||||
|mut s, line| {
|
||||
@@ -2149,7 +2150,7 @@ mod tests {
|
||||
)
|
||||
});
|
||||
let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
|
||||
let mut index_text = if rng.r#gen() {
|
||||
let mut index_text = if rng.random() {
|
||||
Rope::from(head_text.as_str())
|
||||
} else {
|
||||
working_copy.as_rope().clone()
|
||||
@@ -2165,7 +2166,7 @@ mod tests {
|
||||
}
|
||||
|
||||
for _ in 0..operations {
|
||||
let i = rng.gen_range(0..hunks.len());
|
||||
let i = rng.random_range(0..hunks.len());
|
||||
let hunk = &mut hunks[i];
|
||||
let hunk_to_change = hunk.clone();
|
||||
let stage = match hunk.secondary_status {
|
||||
|
||||
@@ -29,6 +29,7 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
feature_flags.workspace = true
|
||||
gpui = { workspace = true, features = ["screen-capture"] }
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -9,6 +9,7 @@ use client::{
|
||||
proto::{self, PeerId},
|
||||
};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
@@ -1322,8 +1323,18 @@ impl Room {
|
||||
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||
};
|
||||
|
||||
let is_staff = cx.is_staff();
|
||||
let user_name = self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.and_then(|user| user.name.clone())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let publication = room.publish_local_microphone_track(cx).await;
|
||||
let publication = room
|
||||
.publish_local_microphone_track(user_name, is_staff, cx)
|
||||
.await;
|
||||
this.update(cx, |this, cx| {
|
||||
let live_kit = this
|
||||
.live_kit
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CallSettings {
|
||||
@@ -11,7 +11,8 @@ pub struct CallSettings {
|
||||
}
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "calls")]
|
||||
pub struct CallSettingsContent {
|
||||
/// Whether the microphone should be muted when joining a channel or a call.
|
||||
///
|
||||
@@ -25,8 +26,6 @@ pub struct CallSettingsContent {
|
||||
}
|
||||
|
||||
impl Settings for CallSettings {
|
||||
const KEY: Option<&'static str> = Some("calls");
|
||||
|
||||
type FileContent = CallSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
|
||||
@@ -129,7 +129,7 @@ impl ChannelChat {
|
||||
loaded_all_messages: false,
|
||||
next_pending_message_id: 0,
|
||||
last_acknowledged_id: None,
|
||||
rng: StdRng::from_entropy(),
|
||||
rng: StdRng::from_os_rng(),
|
||||
first_loaded_message_id: None,
|
||||
_subscription: subscription.set_entity(&cx.entity(), &cx.to_async()),
|
||||
}
|
||||
@@ -183,7 +183,7 @@ impl ChannelChat {
|
||||
|
||||
let channel_id = self.channel_id;
|
||||
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
|
||||
let nonce = self.rng.r#gen();
|
||||
let nonce = self.rng.random();
|
||||
self.insert_messages(
|
||||
SumTree::from_item(
|
||||
ChannelMessage {
|
||||
@@ -257,7 +257,7 @@ impl ChannelChat {
|
||||
cx,
|
||||
);
|
||||
|
||||
let nonce: u128 = self.rng.r#gen();
|
||||
let nonce: u128 = self.rng.random();
|
||||
|
||||
let request = self.rpc.request(proto::UpdateChannelMessage {
|
||||
channel_id: self.channel_id.0,
|
||||
|
||||
@@ -93,6 +93,7 @@ struct Args {
|
||||
/// Example: `me@Ubuntu` or `Ubuntu`.
|
||||
///
|
||||
/// WARN: You should not fill in this field by hand.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[arg(long, value_name = "USER@DISTRO")]
|
||||
wsl: Option<String>,
|
||||
/// Not supported in Zed CLI, only supported on Zed binary
|
||||
@@ -303,6 +304,11 @@ fn main() -> Result<()> {
|
||||
]);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let wsl = args.wsl.as_ref();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let wsl = None;
|
||||
|
||||
for path in args.paths_with_position.iter() {
|
||||
if path.starts_with("zed://")
|
||||
|| path.starts_with("http://")
|
||||
@@ -321,7 +327,7 @@ fn main() -> Result<()> {
|
||||
paths.push(tmp_file.path().to_string_lossy().to_string());
|
||||
let (tmp_file, _) = tmp_file.keep()?;
|
||||
anonymous_fd_tmp_files.push((file, tmp_file));
|
||||
} else if let Some(wsl) = &args.wsl {
|
||||
} else if let Some(wsl) = wsl {
|
||||
urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?));
|
||||
} else {
|
||||
paths.push(parse_path_with_position(path)?);
|
||||
@@ -340,11 +346,16 @@ fn main() -> Result<()> {
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
let (tx, rx) = (handshake.requests, handshake.responses);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let wsl = args.wsl;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let wsl = None;
|
||||
|
||||
tx.send(CliRequest::Open {
|
||||
paths,
|
||||
urls,
|
||||
diff_paths,
|
||||
wsl: args.wsl,
|
||||
wsl,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
env,
|
||||
|
||||
@@ -75,7 +75,7 @@ util = { workspace = true, features = ["test-support"] }
|
||||
windows.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa.workspace = true
|
||||
objc2-foundation.workspace = true
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
|
||||
tokio-native-tls = "0.3"
|
||||
|
||||
@@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel};
|
||||
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
convert::TryFrom,
|
||||
@@ -96,7 +96,8 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
|
||||
#[settings_key(None)]
|
||||
pub struct ClientSettingsContent {
|
||||
server_url: Option<String>,
|
||||
}
|
||||
@@ -107,8 +108,6 @@ pub struct ClientSettings {
|
||||
}
|
||||
|
||||
impl Settings for ClientSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
type FileContent = ClientSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
@@ -122,7 +121,8 @@ impl Settings for ClientSettings {
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)]
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
|
||||
#[settings_key(None)]
|
||||
pub struct ProxySettingsContent {
|
||||
proxy: Option<String>,
|
||||
}
|
||||
@@ -133,8 +133,6 @@ pub struct ProxySettings {
|
||||
}
|
||||
|
||||
impl Settings for ProxySettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
type FileContent = ProxySettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
@@ -527,7 +525,8 @@ pub struct TelemetrySettings {
|
||||
}
|
||||
|
||||
/// Control what info is collected by Zed.
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "telemetry")]
|
||||
pub struct TelemetrySettingsContent {
|
||||
/// Send debug info like crash reports.
|
||||
///
|
||||
@@ -540,8 +539,6 @@ pub struct TelemetrySettingsContent {
|
||||
}
|
||||
|
||||
impl settings::Settings for TelemetrySettings {
|
||||
const KEY: Option<&'static str> = Some("telemetry");
|
||||
|
||||
type FileContent = TelemetrySettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
@@ -691,7 +688,7 @@ impl Client {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
let mut rng = StdRng::seed_from_u64(0);
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let mut rng = StdRng::from_os_rng();
|
||||
|
||||
let mut delay = INITIAL_RECONNECTION_DELAY;
|
||||
loop {
|
||||
@@ -721,8 +718,9 @@ impl Client {
|
||||
},
|
||||
cx,
|
||||
);
|
||||
let jitter =
|
||||
Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64));
|
||||
let jitter = Duration::from_millis(
|
||||
rng.random_range(0..delay.as_millis() as u64),
|
||||
);
|
||||
cx.background_executor().timer(delay + jitter).await;
|
||||
delay = cmp::min(delay * 2, MAX_RECONNECTION_DELAY);
|
||||
} else {
|
||||
|
||||
@@ -84,6 +84,10 @@ static DOTNET_PROJECT_FILES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap()
|
||||
});
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
static MACOS_VERSION_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(\s*\(Build [^)]*[0-9]\))").unwrap());
|
||||
|
||||
pub fn os_name() -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -108,19 +112,16 @@ pub fn os_name() -> String {
|
||||
pub fn os_version() -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use cocoa::base::nil;
|
||||
use cocoa::foundation::NSProcessInfo;
|
||||
|
||||
unsafe {
|
||||
let process_info = cocoa::foundation::NSProcessInfo::processInfo(nil);
|
||||
let version = process_info.operatingSystemVersion();
|
||||
gpui::SemanticVersion::new(
|
||||
version.majorVersion as usize,
|
||||
version.minorVersion as usize,
|
||||
version.patchVersion as usize,
|
||||
)
|
||||
use objc2_foundation::NSProcessInfo;
|
||||
let process_info = NSProcessInfo::processInfo();
|
||||
let version_nsstring = unsafe { process_info.operatingSystemVersionString() };
|
||||
// "Version 15.6.1 (Build 24G90)" -> "15.6.1 (Build 24G90)"
|
||||
let version_string = version_nsstring.to_string().replace("Version ", "");
|
||||
// "15.6.1 (Build 24G90)" -> "15.6.1"
|
||||
// "26.0.0 (Build 25A5349a)" -> unchanged (Beta or Rapid Security Response; ends with letter)
|
||||
MACOS_VERSION_REGEX
|
||||
.replace_all(&version_string, "")
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
{
|
||||
|
||||
@@ -227,7 +227,7 @@ pub async fn verify_access_token(
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rand::thread_rng;
|
||||
use rand::prelude::*;
|
||||
use scrypt::password_hash::{PasswordHasher, SaltString};
|
||||
use sea_orm::EntityTrait;
|
||||
|
||||
@@ -358,9 +358,42 @@ mod test {
|
||||
None,
|
||||
None,
|
||||
params,
|
||||
&SaltString::generate(thread_rng()),
|
||||
&SaltString::generate(PasswordHashRngCompat::new()),
|
||||
)
|
||||
.map_err(anyhow::Error::new)?
|
||||
.to_string())
|
||||
}
|
||||
|
||||
// TODO: remove once we password_hash v0.6 is released.
|
||||
struct PasswordHashRngCompat(rand::rngs::ThreadRng);
|
||||
|
||||
impl PasswordHashRngCompat {
|
||||
fn new() -> Self {
|
||||
Self(rand::rng())
|
||||
}
|
||||
}
|
||||
|
||||
impl scrypt::password_hash::rand_core::RngCore for PasswordHashRngCompat {
|
||||
fn next_u32(&mut self) -> u32 {
|
||||
self.0.next_u32()
|
||||
}
|
||||
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
self.0.next_u64()
|
||||
}
|
||||
|
||||
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
||||
self.0.fill_bytes(dest);
|
||||
}
|
||||
|
||||
fn try_fill_bytes(
|
||||
&mut self,
|
||||
dest: &mut [u8],
|
||||
) -> Result<(), scrypt::password_hash::rand_core::Error> {
|
||||
self.fill_bytes(dest);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl scrypt::password_hash::rand_core::CryptoRng for PasswordHashRngCompat {}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ impl Database {
|
||||
let test_options = self.test_options.as_ref().unwrap();
|
||||
test_options.executor.simulate_random_delay().await;
|
||||
let fail_probability = *test_options.query_failure_probability.lock();
|
||||
if test_options.executor.rng().gen_bool(fail_probability) {
|
||||
if test_options.executor.rng().random_bool(fail_probability) {
|
||||
return Err(anyhow!("simulated query failure"))?;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,10 +75,10 @@ impl TestDb {
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
let _guard = LOCK.lock();
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let mut rng = StdRng::from_os_rng();
|
||||
let url = format!(
|
||||
"postgres://postgres@localhost/zed-test-{}",
|
||||
rng.r#gen::<u128>()
|
||||
rng.random::<u128>()
|
||||
);
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
|
||||
@@ -3425,16 +3425,16 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
assert_eq!(
|
||||
entries,
|
||||
vec![
|
||||
Some(blame_entry("1b1b1b", 0..1)),
|
||||
Some(blame_entry("0d0d0d", 1..2)),
|
||||
Some(blame_entry("3a3a3a", 2..3)),
|
||||
Some(blame_entry("4c4c4c", 3..4)),
|
||||
Some((buffer_id_b, blame_entry("1b1b1b", 0..1))),
|
||||
Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
|
||||
Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
|
||||
Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
|
||||
]
|
||||
);
|
||||
|
||||
blame.update(cx, |blame, _| {
|
||||
for (idx, entry) in entries.iter().flatten().enumerate() {
|
||||
let details = blame.details_for_entry(entry).unwrap();
|
||||
for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
|
||||
let details = blame.details_for_entry(*buffer, entry).unwrap();
|
||||
assert_eq!(details.message, format!("message for idx-{}", idx));
|
||||
assert_eq!(
|
||||
details.permalink.unwrap().to_string(),
|
||||
@@ -3474,9 +3474,9 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
entries,
|
||||
vec![
|
||||
None,
|
||||
Some(blame_entry("0d0d0d", 1..2)),
|
||||
Some(blame_entry("3a3a3a", 2..3)),
|
||||
Some(blame_entry("4c4c4c", 3..4)),
|
||||
Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
|
||||
Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
|
||||
Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -3511,8 +3511,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
vec![
|
||||
None,
|
||||
None,
|
||||
Some(blame_entry("3a3a3a", 2..3)),
|
||||
Some(blame_entry("4c4c4c", 3..4)),
|
||||
Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
|
||||
Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2098,7 +2098,7 @@ async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
share_workspace(&workspace, cx_a).await.unwrap();
|
||||
let buffer = workspace.update(cx_a, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.create_local_buffer(&sample_text(26, 5, 'a'), None, cx)
|
||||
project.create_local_buffer(&sample_text(26, 5, 'a'), None, false, cx)
|
||||
})
|
||||
});
|
||||
let multibuffer = cx_a.new(|cx| {
|
||||
|
||||
@@ -2506,7 +2506,7 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
});
|
||||
|
||||
let new_buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.create_buffer(cx))
|
||||
.update(cx_a, |p, cx| p.create_buffer(false, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -5746,7 +5746,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
|
||||
let definitions;
|
||||
let buffer_b2;
|
||||
if rng.r#gen() {
|
||||
if rng.random() {
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
|
||||
|
||||
@@ -84,7 +84,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||
}
|
||||
|
||||
loop {
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
0..=29 => {
|
||||
let channel_name = client.channel_store().read_with(cx, |store, cx| {
|
||||
store.ordered_channels().find_map(|(_, channel)| {
|
||||
|
||||
@@ -17,7 +17,7 @@ use project::{
|
||||
DEFAULT_COMPLETION_CONTEXT, Project, ProjectPath, search::SearchQuery, search::SearchResult,
|
||||
};
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, DistString},
|
||||
distr::{self, SampleString},
|
||||
prelude::*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -168,19 +168,19 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
) -> ClientOperation {
|
||||
let call = cx.read(ActiveCall::global);
|
||||
loop {
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
// Mutate the call
|
||||
0..=29 => {
|
||||
// Respond to an incoming call
|
||||
if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
|
||||
break if rng.gen_bool(0.7) {
|
||||
break if rng.random_bool(0.7) {
|
||||
ClientOperation::AcceptIncomingCall
|
||||
} else {
|
||||
ClientOperation::RejectIncomingCall
|
||||
};
|
||||
}
|
||||
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
// Invite a contact to the current call
|
||||
0..=70 => {
|
||||
let available_contacts =
|
||||
@@ -212,7 +212,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
|
||||
// Mutate projects
|
||||
30..=59 => match rng.gen_range(0..100_u32) {
|
||||
30..=59 => match rng.random_range(0..100_u32) {
|
||||
// Open a new project
|
||||
0..=70 => {
|
||||
// Open a remote project
|
||||
@@ -270,7 +270,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
|
||||
// Mutate project worktrees
|
||||
81.. => match rng.gen_range(0..100_u32) {
|
||||
81.. => match rng.random_range(0..100_u32) {
|
||||
// Add a worktree to a local project
|
||||
0..=50 => {
|
||||
let Some(project) = client.local_projects().choose(rng).cloned() else {
|
||||
@@ -279,7 +279,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let project_root_name = root_name_for_project(&project, cx);
|
||||
let mut paths = client.fs().paths(false);
|
||||
paths.remove(0);
|
||||
let new_root_path = if paths.is_empty() || rng.r#gen() {
|
||||
let new_root_path = if paths.is_empty() || rng.random() {
|
||||
Path::new(path!("/")).join(plan.next_root_dir_name())
|
||||
} else {
|
||||
paths.choose(rng).unwrap().clone()
|
||||
@@ -309,7 +309,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.choose(rng)
|
||||
});
|
||||
let Some(worktree) = worktree else { continue };
|
||||
let is_dir = rng.r#gen::<bool>();
|
||||
let is_dir = rng.random::<bool>();
|
||||
let mut full_path =
|
||||
worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
|
||||
full_path.push(gen_file_name(rng));
|
||||
@@ -334,7 +334,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let project_root_name = root_name_for_project(&project, cx);
|
||||
let is_local = project.read_with(cx, |project, _| project.is_local());
|
||||
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
// Manipulate an existing buffer
|
||||
0..=70 => {
|
||||
let Some(buffer) = client
|
||||
@@ -349,7 +349,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let full_path = buffer
|
||||
.read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
|
||||
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
// Close the buffer
|
||||
0..=15 => {
|
||||
break ClientOperation::CloseBuffer {
|
||||
@@ -360,7 +360,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
// Save the buffer
|
||||
16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
|
||||
let detach = rng.gen_bool(0.3);
|
||||
let detach = rng.random_bool(0.3);
|
||||
break ClientOperation::SaveBuffer {
|
||||
project_root_name,
|
||||
is_local,
|
||||
@@ -383,17 +383,17 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
_ => {
|
||||
let offset = buffer.read_with(cx, |buffer, _| {
|
||||
buffer.clip_offset(
|
||||
rng.gen_range(0..=buffer.len()),
|
||||
rng.random_range(0..=buffer.len()),
|
||||
language::Bias::Left,
|
||||
)
|
||||
});
|
||||
let detach = rng.r#gen();
|
||||
let detach = rng.random();
|
||||
break ClientOperation::RequestLspDataInBuffer {
|
||||
project_root_name,
|
||||
full_path,
|
||||
offset,
|
||||
is_local,
|
||||
kind: match rng.gen_range(0..5_u32) {
|
||||
kind: match rng.random_range(0..5_u32) {
|
||||
0 => LspRequestKind::Rename,
|
||||
1 => LspRequestKind::Highlights,
|
||||
2 => LspRequestKind::Definition,
|
||||
@@ -407,8 +407,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
|
||||
71..=80 => {
|
||||
let query = rng.gen_range('a'..='z').to_string();
|
||||
let detach = rng.gen_bool(0.3);
|
||||
let query = rng.random_range('a'..='z').to_string();
|
||||
let detach = rng.random_bool(0.3);
|
||||
break ClientOperation::SearchProject {
|
||||
project_root_name,
|
||||
is_local,
|
||||
@@ -460,7 +460,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
|
||||
// Create or update a file or directory
|
||||
96.. => {
|
||||
let is_dir = rng.r#gen::<bool>();
|
||||
let is_dir = rng.random::<bool>();
|
||||
let content;
|
||||
let mut path;
|
||||
let dir_paths = client.fs().directories(false);
|
||||
@@ -470,11 +470,11 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
path = dir_paths.choose(rng).unwrap().clone();
|
||||
path.push(gen_file_name(rng));
|
||||
} else {
|
||||
content = Alphanumeric.sample_string(rng, 16);
|
||||
content = distr::Alphanumeric.sample_string(rng, 16);
|
||||
|
||||
// Create a new file or overwrite an existing file
|
||||
let file_paths = client.fs().files();
|
||||
if file_paths.is_empty() || rng.gen_bool(0.5) {
|
||||
if file_paths.is_empty() || rng.random_bool(0.5) {
|
||||
path = dir_paths.choose(rng).unwrap().clone();
|
||||
path.push(gen_file_name(rng));
|
||||
path.set_extension("rs");
|
||||
@@ -1090,7 +1090,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
move |_, cx| {
|
||||
let background = cx.background_executor();
|
||||
let mut rng = background.rng();
|
||||
let count = rng.gen_range::<usize, _>(1..3);
|
||||
let count = rng.random_range::<usize, _>(1..3);
|
||||
let files = fs.as_fake().files();
|
||||
let files = (0..count)
|
||||
.map(|_| files.choose(&mut rng).unwrap().clone())
|
||||
@@ -1117,12 +1117,12 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let background = cx.background_executor();
|
||||
let mut rng = background.rng();
|
||||
|
||||
let highlight_count = rng.gen_range(1..=5);
|
||||
let highlight_count = rng.random_range(1..=5);
|
||||
for _ in 0..highlight_count {
|
||||
let start_row = rng.gen_range(0..100);
|
||||
let start_column = rng.gen_range(0..100);
|
||||
let end_row = rng.gen_range(0..100);
|
||||
let end_column = rng.gen_range(0..100);
|
||||
let start_row = rng.random_range(0..100);
|
||||
let start_column = rng.random_range(0..100);
|
||||
let end_row = rng.random_range(0..100);
|
||||
let end_column = rng.random_range(0..100);
|
||||
let start = PointUtf16::new(start_row, start_column);
|
||||
let end = PointUtf16::new(end_row, end_column);
|
||||
let range =
|
||||
@@ -1219,8 +1219,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(
|
||||
guest_snapshot.entries(false, 0).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false, 0).collect::<Vec<_>>(),
|
||||
guest_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
|
||||
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
|
||||
client.username,
|
||||
host_snapshot.abs_path(),
|
||||
@@ -1248,6 +1248,18 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// A hack to work around a hack in
|
||||
// https://github.com/zed-industries/zed/pull/16696 that wasn't
|
||||
// detected until we upgraded the rng crate. This whole crate is
|
||||
// going away with DeltaDB soon, so we hold our nose and
|
||||
// continue.
|
||||
fn null_out_entry_size(entry: &project::Entry) -> project::Entry {
|
||||
project::Entry {
|
||||
size: 0,
|
||||
..entry.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let buffers = client.buffers().clone();
|
||||
@@ -1422,7 +1434,7 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
|
||||
.filter(|path| path.starts_with(repo_path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let count = rng.gen_range(0..=paths.len());
|
||||
let count = rng.random_range(0..=paths.len());
|
||||
paths.shuffle(rng);
|
||||
paths.truncate(count);
|
||||
|
||||
@@ -1434,13 +1446,13 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
|
||||
|
||||
let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
|
||||
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
0..=25 => {
|
||||
let file_paths = generate_file_paths(&repo_path, rng, client);
|
||||
|
||||
let contents = file_paths
|
||||
.into_iter()
|
||||
.map(|path| (path, Alphanumeric.sample_string(rng, 16)))
|
||||
.map(|path| (path, distr::Alphanumeric.sample_string(rng, 16)))
|
||||
.collect();
|
||||
|
||||
GitOperation::WriteGitIndex {
|
||||
@@ -1449,7 +1461,8 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
|
||||
}
|
||||
}
|
||||
26..=63 => {
|
||||
let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));
|
||||
let new_branch =
|
||||
(rng.random_range(0..10) > 3).then(|| distr::Alphanumeric.sample_string(rng, 8));
|
||||
|
||||
GitOperation::WriteGitBranch {
|
||||
repo_path,
|
||||
@@ -1596,7 +1609,7 @@ fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Entity
|
||||
fn gen_file_name(rng: &mut StdRng) -> String {
|
||||
let mut name = String::new();
|
||||
for _ in 0..10 {
|
||||
let letter = rng.gen_range('a'..='z');
|
||||
let letter = rng.random_range('a'..='z');
|
||||
name.push(letter);
|
||||
}
|
||||
name
|
||||
@@ -1604,7 +1617,7 @@ fn gen_file_name(rng: &mut StdRng) -> String {
|
||||
|
||||
fn gen_status(rng: &mut StdRng) -> FileStatus {
|
||||
fn gen_tracked_status(rng: &mut StdRng) -> TrackedStatus {
|
||||
match rng.gen_range(0..3) {
|
||||
match rng.random_range(0..3) {
|
||||
0 => TrackedStatus {
|
||||
index_status: StatusCode::Unmodified,
|
||||
worktree_status: StatusCode::Unmodified,
|
||||
@@ -1626,7 +1639,7 @@ fn gen_status(rng: &mut StdRng) -> FileStatus {
|
||||
}
|
||||
|
||||
fn gen_unmerged_status_code(rng: &mut StdRng) -> UnmergedStatusCode {
|
||||
match rng.gen_range(0..3) {
|
||||
match rng.random_range(0..3) {
|
||||
0 => UnmergedStatusCode::Updated,
|
||||
1 => UnmergedStatusCode::Added,
|
||||
2 => UnmergedStatusCode::Deleted,
|
||||
@@ -1634,7 +1647,7 @@ fn gen_status(rng: &mut StdRng) -> FileStatus {
|
||||
}
|
||||
}
|
||||
|
||||
match rng.gen_range(0..2) {
|
||||
match rng.random_range(0..2) {
|
||||
0 => FileStatus::Unmerged(UnmergedStatus {
|
||||
first_head: gen_unmerged_status_code(rng),
|
||||
second_head: gen_unmerged_status_code(rng),
|
||||
|
||||
@@ -208,9 +208,9 @@ pub fn save_randomized_test_plan() {
|
||||
|
||||
impl<T: RandomizedTest> TestPlan<T> {
|
||||
pub async fn new(server: &mut TestServer, mut rng: StdRng) -> Arc<Mutex<Self>> {
|
||||
let allow_server_restarts = rng.gen_bool(0.7);
|
||||
let allow_client_reconnection = rng.gen_bool(0.7);
|
||||
let allow_client_disconnection = rng.gen_bool(0.1);
|
||||
let allow_server_restarts = rng.random_bool(0.7);
|
||||
let allow_client_reconnection = rng.random_bool(0.7);
|
||||
let allow_client_disconnection = rng.random_bool(0.1);
|
||||
|
||||
let mut users = Vec::new();
|
||||
for ix in 0..max_peers() {
|
||||
@@ -407,7 +407,7 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
}
|
||||
|
||||
Some(loop {
|
||||
break match self.rng.gen_range(0..100) {
|
||||
break match self.rng.random_range(0..100) {
|
||||
0..=29 if clients.len() < self.users.len() => {
|
||||
let user = self
|
||||
.users
|
||||
@@ -421,13 +421,13 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
}
|
||||
}
|
||||
30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
|
||||
let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
|
||||
let (client, cx) = &clients[self.rng.random_range(0..clients.len())];
|
||||
let user_id = client.current_user_id(cx);
|
||||
self.operation_ix += 1;
|
||||
ServerOperation::RemoveConnection { user_id }
|
||||
}
|
||||
35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
|
||||
let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
|
||||
let (client, cx) = &clients[self.rng.random_range(0..clients.len())];
|
||||
let user_id = client.current_user_id(cx);
|
||||
self.operation_ix += 1;
|
||||
ServerOperation::BounceConnection { user_id }
|
||||
@@ -439,12 +439,12 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
_ if !clients.is_empty() => {
|
||||
let count = self
|
||||
.rng
|
||||
.gen_range(1..10)
|
||||
.random_range(1..10)
|
||||
.min(self.max_operations - self.operation_ix);
|
||||
let batch_id = util::post_inc(&mut self.next_batch_id);
|
||||
let mut user_ids = (0..count)
|
||||
.map(|_| {
|
||||
let ix = self.rng.gen_range(0..clients.len());
|
||||
let ix = self.rng.random_range(0..clients.len());
|
||||
let (client, cx) = &clients[ix];
|
||||
client.current_user_id(cx)
|
||||
})
|
||||
@@ -453,7 +453,7 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
ServerOperation::MutateClients {
|
||||
user_ids,
|
||||
batch_id,
|
||||
quiesce: self.rng.gen_bool(0.7),
|
||||
quiesce: self.rng.random_bool(0.7),
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user