Compare commits
263 Commits
scroll-rou
...
settings-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32b1576718 | ||
|
|
e83508736e | ||
|
|
3c2f320f0e | ||
|
|
68bd7dae8f | ||
|
|
8c94e3e2a4 | ||
|
|
9980ce274e | ||
|
|
93c506e405 | ||
|
|
8dfbafd345 | ||
|
|
677d6acc9d | ||
|
|
96add6c9de | ||
|
|
f76eecd758 | ||
|
|
bec2bfeb8b | ||
|
|
9edf1f8f04 | ||
|
|
23fe74ebc5 | ||
|
|
46fff9979d | ||
|
|
e7b19ab0b1 | ||
|
|
ce8d5e41a5 | ||
|
|
dac5725246 | ||
|
|
f1db1f3a3c | ||
|
|
02bdba80a4 | ||
|
|
af0cd30a9c | ||
|
|
3ea4b30e8d | ||
|
|
fdf801d90f | ||
|
|
ff50f48980 | ||
|
|
af52cbacf9 | ||
|
|
785cb41565 | ||
|
|
ce20e71abf | ||
|
|
237474a889 | ||
|
|
f6630ed736 | ||
|
|
81cd435e08 | ||
|
|
47a66c938f | ||
|
|
1ca2f9871e | ||
|
|
52cc71e380 | ||
|
|
7a8a328d3c | ||
|
|
eeaf0b5fec | ||
|
|
95780e5baf | ||
|
|
92e765b5d2 | ||
|
|
abc1e67221 | ||
|
|
68bda24bc1 | ||
|
|
3f3d894c8b | ||
|
|
83f0a36733 | ||
|
|
bbb6783fb8 | ||
|
|
998fece3af | ||
|
|
abe1fd5e16 | ||
|
|
deef58bef7 | ||
|
|
e11e39f9b4 | ||
|
|
6dc3e643b4 | ||
|
|
d4b5bb9f17 | ||
|
|
74d92fd733 | ||
|
|
7d260bf4ef | ||
|
|
89bb2de450 | ||
|
|
3d4d8ef6a8 | ||
|
|
42365df12f | ||
|
|
201124e13f | ||
|
|
3ba4b84107 | ||
|
|
f7e7a304e0 | ||
|
|
65a38a27a9 | ||
|
|
d6becab3be | ||
|
|
924e7e61a5 | ||
|
|
18405dece8 | ||
|
|
120faadef8 | ||
|
|
6a9639f62f | ||
|
|
a696e829ac | ||
|
|
eb8510cb39 | ||
|
|
a54cf3c74e | ||
|
|
41cac5e032 | ||
|
|
59c109f77f | ||
|
|
5e78fb0f94 | ||
|
|
63032f6c66 | ||
|
|
5f857ffbb1 | ||
|
|
a78b560b8b | ||
|
|
b9a6660b93 | ||
|
|
a693d44553 | ||
|
|
41ee92e5f2 | ||
|
|
a9eb480f3c | ||
|
|
5698636c92 | ||
|
|
bbd735905f | ||
|
|
3d5ddcccf0 | ||
|
|
4dae3a15cc | ||
|
|
c6373cc26d | ||
|
|
a4ec693e34 | ||
|
|
08a2b6898b | ||
|
|
13b17b3a85 | ||
|
|
e4f0fbbf80 | ||
|
|
98d4c34199 | ||
|
|
c24f365b69 | ||
|
|
2dfde55367 | ||
|
|
e946a06efe | ||
|
|
75067c94ad | ||
|
|
d7143009fc | ||
|
|
a22c29c5f9 | ||
|
|
c543709d5f | ||
|
|
c58931ac04 | ||
|
|
dd5da592f0 | ||
|
|
f1d17fcfbe | ||
|
|
ccfc1ce387 | ||
|
|
3d4f488d46 | ||
|
|
ba2337ffb9 | ||
|
|
37d676e2c6 | ||
|
|
1bb6752e3e | ||
|
|
8c9b42dda8 | ||
|
|
15c4aadb57 | ||
|
|
3d200a5466 | ||
|
|
e077b63915 | ||
|
|
ef839cc207 | ||
|
|
3d0312f4c7 | ||
|
|
c1e3958c26 | ||
|
|
ba937d16e7 | ||
|
|
4dbd186485 | ||
|
|
88887fd292 | ||
|
|
31e75b2235 | ||
|
|
681c19899f | ||
|
|
439add3d23 | ||
|
|
81b98cdd4d | ||
|
|
ca89a40df2 | ||
|
|
f5884e99d0 | ||
|
|
fce931144e | ||
|
|
ef423148fc | ||
|
|
cd656485c8 | ||
|
|
1e149b755f | ||
|
|
e0eeda11ed | ||
|
|
bcef3b5010 | ||
|
|
5fd187769d | ||
|
|
096930817b | ||
|
|
c7d5afedc5 | ||
|
|
d6b1801fb3 | ||
|
|
7c55f7181d | ||
|
|
4684d6b50e | ||
|
|
578e7e4cbd | ||
|
|
a960db6a43 | ||
|
|
5a0f796a44 | ||
|
|
604d56659d | ||
|
|
1d1c799b4b | ||
|
|
70af11ef2a | ||
|
|
5fa4b3bfe8 | ||
|
|
93a5dffea1 | ||
|
|
9ac010043c | ||
|
|
dd3b65f707 | ||
|
|
057b7b1543 | ||
|
|
a9455eb947 | ||
|
|
db3c186af0 | ||
|
|
71856706c7 | ||
|
|
4ec24ebe01 | ||
|
|
4152942a8e | ||
|
|
bbf4bfad6f | ||
|
|
989d172cfc | ||
|
|
1265b229a9 | ||
|
|
294ca25f44 | ||
|
|
5c7907ad2f | ||
|
|
f652c3a14d | ||
|
|
69ac003bc9 | ||
|
|
d615525771 | ||
|
|
8bf37dd130 | ||
|
|
8cb67ec91c | ||
|
|
cd67941598 | ||
|
|
669db62e33 | ||
|
|
41f1835bbe | ||
|
|
791ba9ce4c | ||
|
|
e60a61f7e7 | ||
|
|
b8a6180b82 | ||
|
|
dfce57c7f8 | ||
|
|
15580a867b | ||
|
|
f7bb22fb83 | ||
|
|
7db7ad93a2 | ||
|
|
642643de01 | ||
|
|
391e304c9f | ||
|
|
3106472bf3 | ||
|
|
d04ac864b8 | ||
|
|
f9a2724a8b | ||
|
|
ded73c9d56 | ||
|
|
41cf114d8a | ||
|
|
e765818487 | ||
|
|
84f488879c | ||
|
|
85985fe960 | ||
|
|
3bec885536 | ||
|
|
9a5034ea6d | ||
|
|
64eec67a81 | ||
|
|
ffff56f7fe | ||
|
|
b02b130b7c | ||
|
|
41ac6a8764 | ||
|
|
963204c99d | ||
|
|
f6f11eb544 | ||
|
|
c1e917165d | ||
|
|
a2a7bd139a | ||
|
|
4de13e06ec | ||
|
|
e680dfb0a0 | ||
|
|
31544d294d | ||
|
|
4e932297a4 | ||
|
|
b2f0b1b168 | ||
|
|
94f1faffa7 | ||
|
|
075104a529 | ||
|
|
c80d213227 | ||
|
|
fe9895d112 | ||
|
|
24bc52a15a | ||
|
|
a65a8bea43 | ||
|
|
ea60a7b172 | ||
|
|
a67a55d81a | ||
|
|
1a9f9ccc29 | ||
|
|
6da5945cd2 | ||
|
|
354cc65daa | ||
|
|
2c6a8634cc | ||
|
|
84ec865c44 | ||
|
|
80727a03bf | ||
|
|
e7339fbd42 | ||
|
|
db5b1a31b5 | ||
|
|
bc39ed2575 | ||
|
|
1764337a5d | ||
|
|
3707102702 | ||
|
|
5263f51432 | ||
|
|
93cd10aaa8 | ||
|
|
2c1cc01b81 | ||
|
|
81ada92306 | ||
|
|
4bd7ef8bad | ||
|
|
d1e2a1f20c | ||
|
|
79a8986cb7 | ||
|
|
d2b91eb2bc | ||
|
|
c26937a848 | ||
|
|
da82eec4cb | ||
|
|
2bfcd60b88 | ||
|
|
9c7369f54d | ||
|
|
5160510ed0 | ||
|
|
ee557fb7ea | ||
|
|
f9919f9214 | ||
|
|
0f0974f105 | ||
|
|
e317d98915 | ||
|
|
dada318be7 | ||
|
|
b53f9c8863 | ||
|
|
5b0a2f1ab6 | ||
|
|
d5a4890142 | ||
|
|
cd61bfbd42 | ||
|
|
469ecfbe13 | ||
|
|
46b6adadf9 | ||
|
|
1a9e9c5faa | ||
|
|
eb64ca8758 | ||
|
|
68e6d55596 | ||
|
|
bcd2d269e2 | ||
|
|
b32075cdcb | ||
|
|
21e75b8221 | ||
|
|
978951b79a | ||
|
|
6b980ecad3 | ||
|
|
d9c7f44b0b | ||
|
|
55e68553a4 | ||
|
|
9fe46dc8d2 | ||
|
|
aced13bc9f | ||
|
|
2859cbdba9 | ||
|
|
4443f61c16 | ||
|
|
f0f0beb42f | ||
|
|
6707ff3b50 | ||
|
|
93770e8314 | ||
|
|
f8c617303a | ||
|
|
e5f05a21ce | ||
|
|
f499504b13 | ||
|
|
504216cbbf | ||
|
|
3bf71c690f | ||
|
|
456ba32ea7 | ||
|
|
9aeb617a89 | ||
|
|
fd8bae9b72 | ||
|
|
f71c9122ca | ||
|
|
8441aa49b2 | ||
|
|
7b96e1cf1a | ||
|
|
86322a186f | ||
|
|
1b94d74dc3 | ||
|
|
db825c1141 |
@@ -24,7 +24,7 @@ workspace-members = [
|
||||
third-party = [
|
||||
{ name = "reqwest", version = "0.11.27" },
|
||||
# build of remote_server should not include scap / its x11 dependency
|
||||
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
|
||||
{ name = "zed-scap", git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", version = "0.0.8-zed" },
|
||||
# build of remote_server should not need to include on libalsa through rodio
|
||||
{ name = "rodio", git = "https://github.com/RustAudio/rodio" },
|
||||
]
|
||||
|
||||
35
.github/ISSUE_TEMPLATE/07_bug_windows.yml
vendored
Normal file
35
.github/ISSUE_TEMPLATE/07_bug_windows.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Bug Report (Windows)
|
||||
description: Zed Windows Related Bugs
|
||||
type: "Bug"
|
||||
labels: ["windows"]
|
||||
title: "Windows: <a short description of the Windows bug>"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Describe the bug with a one-line summary, and provide detailed reproduction steps
|
||||
value: |
|
||||
<!-- Please insert a one-line summary of the issue below -->
|
||||
SUMMARY_SENTENCE_HERE
|
||||
|
||||
### Description
|
||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
|
||||
Steps to trigger the problem:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Expected Behavior**:
|
||||
**Actual Behavior**:
|
||||
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
|
||||
placeholder: |
|
||||
Output of "zed: copy system specs into clipboard"
|
||||
validations:
|
||||
required: true
|
||||
2
.github/actions/run_tests/action.yml
vendored
2
.github/actions/run_tests/action.yml
vendored
@@ -20,4 +20,4 @@ runs:
|
||||
|
||||
- name: Run tests
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
||||
|
||||
2
.github/actions/run_tests_windows/action.yml
vendored
2
.github/actions/run_tests_windows/action.yml
vendored
@@ -24,4 +24,4 @@ runs:
|
||||
shell: powershell
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
cargo nextest run --workspace --no-fail-fast
|
||||
cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -826,8 +826,9 @@ jobs:
|
||||
timeout-minutes: 120
|
||||
name: Create a Windows installer
|
||||
runs-on: [self-32vcpu-windows-2022]
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
# if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
|
||||
if: |
|
||||
( startsWith(github.ref, 'refs/tags/v')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling') )
|
||||
needs: [windows_tests]
|
||||
env:
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
|
||||
@@ -865,13 +866,12 @@ jobs:
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
with:
|
||||
name: ZedEditorUserSetup-x64-${{ github.event.pull_request.head.sha || github.sha }}.exe
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.exe
|
||||
path: ${{ env.SETUP_PATH }}
|
||||
|
||||
- name: Upload Artifacts to release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
# Re-enable when we are ready to publish windows preview releases
|
||||
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview
|
||||
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
|
||||
1381
Cargo.lock
generated
1381
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
45
Cargo.toml
45
Cargo.toml
@@ -164,6 +164,7 @@ members = [
|
||||
"crates/sum_tree",
|
||||
"crates/supermaven",
|
||||
"crates/supermaven_api",
|
||||
"crates/codestral",
|
||||
"crates/svg_preview",
|
||||
"crates/system_specs",
|
||||
"crates/tab_switcher",
|
||||
@@ -273,7 +274,7 @@ cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||
cloud_zeta2_prompt = { path = "crates/cloud_zeta2_prompt" }
|
||||
collab = { path = "crates/collab" }
|
||||
collab_ui = { path = "crates/collab_ui" }
|
||||
collections = { path = "crates/collections" }
|
||||
collections = { path = "crates/collections", package = "zed-collections", version = "0.1.0" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
component = { path = "crates/component" }
|
||||
@@ -289,6 +290,7 @@ debug_adapter_extension = { path = "crates/debug_adapter_extension" }
|
||||
debugger_tools = { path = "crates/debugger_tools" }
|
||||
debugger_ui = { path = "crates/debugger_ui" }
|
||||
deepseek = { path = "crates/deepseek" }
|
||||
derive_refineable = { path = "crates/refineable/derive_refineable", package = "zed-derive-refineable", version = "0.1.0" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
extension = { path = "crates/extension" }
|
||||
@@ -307,10 +309,10 @@ 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 }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
gpui_macros = { path = "crates/gpui_macros", package = "gpui-macros", version = "0.1.0" }
|
||||
gpui_tokio = { path = "crates/gpui_tokio" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http_client = { path = "crates/http_client" }
|
||||
http_client = { path = "crates/http_client", package = "zed-http-client", version = "0.1.0" }
|
||||
http_client_tls = { path = "crates/http_client_tls" }
|
||||
icons = { path = "crates/icons" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
@@ -339,7 +341,7 @@ lsp = { path = "crates/lsp" }
|
||||
markdown = { path = "crates/markdown" }
|
||||
markdown_preview = { path = "crates/markdown_preview" }
|
||||
svg_preview = { path = "crates/svg_preview" }
|
||||
media = { path = "crates/media" }
|
||||
media = { path = "crates/media", package = "zed-media", version = "0.1.0" }
|
||||
menu = { path = "crates/menu" }
|
||||
migrator = { path = "crates/migrator" }
|
||||
mistral = { path = "crates/mistral" }
|
||||
@@ -356,7 +358,7 @@ outline = { path = "crates/outline" }
|
||||
outline_panel = { path = "crates/outline_panel" }
|
||||
panel = { path = "crates/panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
perf = { path = "tooling/perf" }
|
||||
perf = { path = "tooling/perf", package = "zed-perf", version = "0.1.0" }
|
||||
picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
plugin_macros = { path = "crates/plugin_macros" }
|
||||
@@ -368,7 +370,7 @@ project_symbols = { path = "crates/project_symbols" }
|
||||
prompt_store = { path = "crates/prompt_store" }
|
||||
proto = { path = "crates/proto" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
refineable = { path = "crates/refineable", package = "zed-refineable", version = "0.1.0" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
scheduler = { path = "crates/scheduler" }
|
||||
remote = { path = "crates/remote" }
|
||||
@@ -381,7 +383,7 @@ rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
semantic_version = { path = "crates/semantic_version", package = "zed-semantic-version", version = "0.1.0" }
|
||||
session = { path = "crates/session" }
|
||||
settings = { path = "crates/settings" }
|
||||
settings_macros = { path = "crates/settings_macros" }
|
||||
@@ -394,9 +396,10 @@ sqlez_macros = { path = "crates/sqlez_macros" }
|
||||
story = { path = "crates/story" }
|
||||
storybook = { path = "crates/storybook" }
|
||||
streaming_diff = { path = "crates/streaming_diff" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
sum_tree = { path = "crates/sum_tree", package = "zed-sum-tree", version = "0.1.0" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
supermaven_api = { path = "crates/supermaven_api" }
|
||||
codestral = { path = "crates/codestral" }
|
||||
system_specs = { path = "crates/system_specs" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
task = { path = "crates/task" }
|
||||
@@ -417,8 +420,8 @@ ui = { path = "crates/ui" }
|
||||
ui_input = { path = "crates/ui_input" }
|
||||
ui_macros = { path = "crates/ui_macros" }
|
||||
ui_prompt = { path = "crates/ui_prompt" }
|
||||
util = { path = "crates/util" }
|
||||
util_macros = { path = "crates/util_macros" }
|
||||
util = { path = "crates/util", package = "zed-util", version = "0.1.0" }
|
||||
util_macros = { path = "crates/util_macros", package = "zed-util-macros", version = "0.1.0" }
|
||||
vercel = { path = "crates/vercel" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
@@ -475,7 +478,6 @@ bitflags = "2.6.0"
|
||||
blade-graphics = { version = "0.7.0" }
|
||||
blade-macros = { version = "0.3.0" }
|
||||
blade-util = { version = "0.3.0" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
@@ -605,7 +607,8 @@ rand = "0.9"
|
||||
rayon = "1.8"
|
||||
ref-cast = "1.0.24"
|
||||
regex = "1.5"
|
||||
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c770a32f1998d6e999cef3e59e0013e6c4415", default-features = false, features = [
|
||||
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
|
||||
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
|
||||
"charset",
|
||||
"http2",
|
||||
"macos-system-configuration",
|
||||
@@ -613,7 +616,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
|
||||
"rustls-tls-native-roots",
|
||||
"socks",
|
||||
"stream",
|
||||
] }
|
||||
], package = "zed-reqwest", version = "0.12.15-zed" }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
@@ -622,7 +625,8 @@ rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = { version = "0.23.26" }
|
||||
rustls-platform-verifier = "0.5.0"
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false }
|
||||
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
||||
@@ -648,9 +652,9 @@ streaming-iterator = "0.1"
|
||||
strsim = "0.11"
|
||||
strum = { version = "0.27.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
syn = { version = "2.0.101", features = ["full", "extra-traits"] }
|
||||
syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] }
|
||||
sys-locale = "0.3.1"
|
||||
sysinfo = "0.31.0"
|
||||
sysinfo = "0.37.0"
|
||||
take-until = "0.2.0"
|
||||
tempfile = "3.20.0"
|
||||
thiserror = "2.0.12"
|
||||
@@ -666,6 +670,7 @@ tiny_http = "0.8"
|
||||
tokio = { version = "1" }
|
||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
||||
toml = "0.8"
|
||||
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.10", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.25.0"
|
||||
@@ -688,7 +693,7 @@ tree-sitter-python = "0.25"
|
||||
tree-sitter-regex = "0.24"
|
||||
tree-sitter-ruby = "0.23"
|
||||
tree-sitter-rust = "0.24"
|
||||
tree-sitter-typescript = "0.23"
|
||||
tree-sitter-typescript = { git = "https://github.com/zed-industries/tree-sitter-typescript", rev = "e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" } # https://github.com/tree-sitter/tree-sitter-typescript/pull/347
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
|
||||
unicase = "2.6"
|
||||
unicode-script = "0.5.7"
|
||||
@@ -800,7 +805,7 @@ wasmtime = { opt-level = 3 }
|
||||
activity_indicator = { codegen-units = 1 }
|
||||
assets = { codegen-units = 1 }
|
||||
breadcrumbs = { codegen-units = 1 }
|
||||
collections = { codegen-units = 1 }
|
||||
zed-collections = { codegen-units = 1 }
|
||||
command_palette = { codegen-units = 1 }
|
||||
command_palette_hooks = { codegen-units = 1 }
|
||||
extension_cli = { codegen-units = 1 }
|
||||
@@ -820,11 +825,11 @@ outline = { codegen-units = 1 }
|
||||
paths = { codegen-units = 1 }
|
||||
prettier = { codegen-units = 1 }
|
||||
project_symbols = { codegen-units = 1 }
|
||||
refineable = { codegen-units = 1 }
|
||||
zed-refineable = { codegen-units = 1 }
|
||||
release_channel = { codegen-units = 1 }
|
||||
reqwest_client = { codegen-units = 1 }
|
||||
rich_text = { codegen-units = 1 }
|
||||
semantic_version = { codegen-units = 1 }
|
||||
zed-semantic-version = { codegen-units = 1 }
|
||||
session = { codegen-units = 1 }
|
||||
snippet = { codegen-units = 1 }
|
||||
snippets_ui = { codegen-units = 1 }
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[build]
|
||||
dockerfile = "Dockerfile-cross"
|
||||
@@ -1,17 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG CROSS_BASE_IMAGE
|
||||
FROM ${CROSS_BASE_IMAGE}
|
||||
WORKDIR /app
|
||||
ARG TZ=Etc/UTC \
|
||||
LANG=C.UTF-8 \
|
||||
LC_ALL=C.UTF-8 \
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
ENV CARGO_TERM_COLOR=always
|
||||
|
||||
COPY script/install-mold script/
|
||||
RUN ./script/install-mold "2.34.0"
|
||||
COPY script/remote-server script/
|
||||
RUN ./script/remote-server
|
||||
|
||||
COPY . .
|
||||
@@ -1,9 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.6" d="M3.5 11V5.5L8.5 8L3.5 11Z" fill="black"/>
|
||||
<path opacity="0.4" d="M8.5 14L3.5 11L8.5 8V14Z" fill="black"/>
|
||||
<path opacity="0.6" d="M8.5 5.5H3.5L8.5 2.5L8.5 5.5Z" fill="black"/>
|
||||
<path opacity="0.8" d="M8.5 5.5V2.5L13.5 5.5H8.5Z" fill="black"/>
|
||||
<path opacity="0.2" d="M13.5 11L8.5 14L11 9.5L13.5 11Z" fill="black"/>
|
||||
<path opacity="0.5" d="M13.5 11L11 9.5L13.5 5V11Z" fill="black"/>
|
||||
<path d="M3.5 11V5L8.5 2.11325L13.5 5V11L8.5 13.8868L3.5 11Z" stroke="black"/>
|
||||
<path d="M13.2806 4.66818L8.26042 1.76982C8.09921 1.67673 7.9003 1.67673 7.73909 1.76982L2.71918 4.66818C2.58367 4.74642 2.5 4.89112 2.5 5.04785V10.8924C2.5 11.0489 2.58367 11.1938 2.71918 11.2721L7.73934 14.1704C7.90054 14.2635 8.09946 14.2635 8.26066 14.1704L13.2808 11.2721C13.4163 11.1938 13.5 11.0491 13.5 10.8924V5.04785C13.5 4.89136 13.4163 4.74642 13.2808 4.66818H13.2806ZM12.9653 5.28212L8.11901 13.676C8.08626 13.7326 7.99977 13.7095 7.99977 13.6439V8.14771C7.99977 8.03788 7.94107 7.9363 7.84586 7.88115L3.08613 5.13317C3.02957 5.10041 3.05266 5.0139 3.11818 5.0139H12.8106C12.9483 5.0139 13.0343 5.1631 12.9655 5.28236H12.9653V5.28212Z" fill="#C4CAD4"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 583 B After Width: | Height: | Size: 769 B |
@@ -31,6 +31,7 @@
|
||||
"ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
|
||||
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-alt-,": "zed::OpenSettingsFile",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"f4": "debugger::Start",
|
||||
"shift-f5": "debugger::Stop",
|
||||
@@ -369,7 +370,8 @@
|
||||
"bindings": {
|
||||
"new": "rules_library::NewRule",
|
||||
"ctrl-n": "rules_library::NewRule",
|
||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
|
||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule",
|
||||
"ctrl-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -525,15 +527,15 @@
|
||||
"ctrl-k ctrl-l": "editor::ToggleFold",
|
||||
"ctrl-k ctrl-[": "editor::FoldRecursive",
|
||||
"ctrl-k ctrl-]": "editor::UnfoldRecursive",
|
||||
"ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
|
||||
"ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
|
||||
"ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
|
||||
"ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
|
||||
"ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
|
||||
"ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
|
||||
"ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
|
||||
"ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
|
||||
"ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
|
||||
"ctrl-k ctrl-1": "editor::FoldAtLevel_1",
|
||||
"ctrl-k ctrl-2": "editor::FoldAtLevel_2",
|
||||
"ctrl-k ctrl-3": "editor::FoldAtLevel_3",
|
||||
"ctrl-k ctrl-4": "editor::FoldAtLevel_4",
|
||||
"ctrl-k ctrl-5": "editor::FoldAtLevel_5",
|
||||
"ctrl-k ctrl-6": "editor::FoldAtLevel_6",
|
||||
"ctrl-k ctrl-7": "editor::FoldAtLevel_7",
|
||||
"ctrl-k ctrl-8": "editor::FoldAtLevel_8",
|
||||
"ctrl-k ctrl-9": "editor::FoldAtLevel_9",
|
||||
"ctrl-k ctrl-0": "editor::FoldAll",
|
||||
"ctrl-k ctrl-j": "editor::UnfoldAll",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
@@ -619,7 +621,7 @@
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
|
||||
"ctrl-k ctrl-s": "zed::OpenKeymap",
|
||||
"ctrl-k ctrl-t": "theme_selector::Toggle",
|
||||
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
|
||||
"ctrl-t": "project_symbols::Toggle",
|
||||
@@ -1227,9 +1229,6 @@
|
||||
"context": "Onboarding",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-1": "onboarding::ActivateBasicsPage",
|
||||
"ctrl-2": "onboarding::ActivateEditingPage",
|
||||
"ctrl-3": "onboarding::ActivateAISetupPage",
|
||||
"ctrl-enter": "onboarding::Finish",
|
||||
"alt-shift-l": "onboarding::SignIn",
|
||||
"alt-shift-a": "onboarding::OpenAccount"
|
||||
@@ -1241,5 +1240,44 @@
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "SettingsWindow",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-w": "workspace::CloseWindow",
|
||||
"escape": "workspace::CloseWindow",
|
||||
"ctrl-m": "settings_editor::Minimize",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"left": "settings_editor::ToggleFocusNav",
|
||||
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
|
||||
// todo(settings_ui): cut this down based on the max files and overflow UI
|
||||
"ctrl-1": ["settings_editor::FocusFile", 0],
|
||||
"ctrl-2": ["settings_editor::FocusFile", 1],
|
||||
"ctrl-3": ["settings_editor::FocusFile", 2],
|
||||
"ctrl-4": ["settings_editor::FocusFile", 3],
|
||||
"ctrl-5": ["settings_editor::FocusFile", 4],
|
||||
"ctrl-6": ["settings_editor::FocusFile", 5],
|
||||
"ctrl-7": ["settings_editor::FocusFile", 6],
|
||||
"ctrl-8": ["settings_editor::FocusFile", 7],
|
||||
"ctrl-9": ["settings_editor::FocusFile", 8],
|
||||
"ctrl-0": ["settings_editor::FocusFile", 9],
|
||||
"ctrl-pageup": "settings_editor::FocusPreviousFile",
|
||||
"ctrl-pagedown": "settings_editor::FocusNextFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "SettingsWindow > NavigationMenu",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "settings_editor::FocusPreviousNavEntry",
|
||||
"down": "settings_editor::FocusNextNavEntry",
|
||||
"right": "settings_editor::ExpandNavEntry",
|
||||
"left": "settings_editor::CollapseNavEntry",
|
||||
"pageup": "settings_editor::FocusPreviousRootNavEntry",
|
||||
"pagedown": "settings_editor::FocusNextRootNavEntry",
|
||||
"home": "settings_editor::FocusFirstNavEntry",
|
||||
"end": "settings_editor::FocusLastNavEntry"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"cmd--": ["zed::DecreaseBufferFontSize", { "persist": false }],
|
||||
"cmd-0": ["zed::ResetBufferFontSize", { "persist": false }],
|
||||
"cmd-,": "zed::OpenSettings",
|
||||
"cmd-alt-,": "zed::OpenSettingsFile",
|
||||
"cmd-q": "zed::Quit",
|
||||
"cmd-h": "zed::Hide",
|
||||
"alt-cmd-h": "zed::HideOthers",
|
||||
@@ -581,15 +582,15 @@
|
||||
"cmd-k cmd-l": "editor::ToggleFold",
|
||||
"cmd-k cmd-[": "editor::FoldRecursive",
|
||||
"cmd-k cmd-]": "editor::UnfoldRecursive",
|
||||
"cmd-k cmd-1": ["editor::FoldAtLevel", 1],
|
||||
"cmd-k cmd-2": ["editor::FoldAtLevel", 2],
|
||||
"cmd-k cmd-3": ["editor::FoldAtLevel", 3],
|
||||
"cmd-k cmd-4": ["editor::FoldAtLevel", 4],
|
||||
"cmd-k cmd-5": ["editor::FoldAtLevel", 5],
|
||||
"cmd-k cmd-6": ["editor::FoldAtLevel", 6],
|
||||
"cmd-k cmd-7": ["editor::FoldAtLevel", 7],
|
||||
"cmd-k cmd-8": ["editor::FoldAtLevel", 8],
|
||||
"cmd-k cmd-9": ["editor::FoldAtLevel", 9],
|
||||
"cmd-k cmd-1": "editor::FoldAtLevel_1",
|
||||
"cmd-k cmd-2": "editor::FoldAtLevel_2",
|
||||
"cmd-k cmd-3": "editor::FoldAtLevel_3",
|
||||
"cmd-k cmd-4": "editor::FoldAtLevel_4",
|
||||
"cmd-k cmd-5": "editor::FoldAtLevel_5",
|
||||
"cmd-k cmd-6": "editor::FoldAtLevel_6",
|
||||
"cmd-k cmd-7": "editor::FoldAtLevel_7",
|
||||
"cmd-k cmd-8": "editor::FoldAtLevel_8",
|
||||
"cmd-k cmd-9": "editor::FoldAtLevel_9",
|
||||
"cmd-k cmd-0": "editor::FoldAll",
|
||||
"cmd-k cmd-j": "editor::UnfoldAll",
|
||||
// Using `ctrl-space` / `ctrl-shift-space` in Zed requires disabling the macOS global shortcut.
|
||||
@@ -689,7 +690,7 @@
|
||||
"cmd-shift-f": "pane::DeploySearch",
|
||||
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"cmd-shift-t": "pane::ReopenClosedItem",
|
||||
"cmd-k cmd-s": "zed::OpenKeymapEditor",
|
||||
"cmd-k cmd-s": "zed::OpenKeymap",
|
||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||
"ctrl-alt-cmd-p": "settings_profile_selector::Toggle",
|
||||
"cmd-t": "project_symbols::Toggle",
|
||||
@@ -1333,10 +1334,7 @@
|
||||
"context": "Onboarding",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-1": "onboarding::ActivateBasicsPage",
|
||||
"cmd-2": "onboarding::ActivateEditingPage",
|
||||
"cmd-3": "onboarding::ActivateAISetupPage",
|
||||
"cmd-escape": "onboarding::Finish",
|
||||
"cmd-enter": "onboarding::Finish",
|
||||
"alt-tab": "onboarding::SignIn",
|
||||
"alt-shift-a": "onboarding::OpenAccount"
|
||||
}
|
||||
@@ -1347,5 +1345,44 @@
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "SettingsWindow",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-w": "workspace::CloseWindow",
|
||||
"escape": "workspace::CloseWindow",
|
||||
"cmd-m": "settings_editor::Minimize",
|
||||
"cmd-f": "search::FocusSearch",
|
||||
"left": "settings_editor::ToggleFocusNav",
|
||||
"cmd-shift-e": "settings_editor::ToggleFocusNav",
|
||||
// todo(settings_ui): cut this down based on the max files and overflow UI
|
||||
"ctrl-1": ["settings_editor::FocusFile", 0],
|
||||
"ctrl-2": ["settings_editor::FocusFile", 1],
|
||||
"ctrl-3": ["settings_editor::FocusFile", 2],
|
||||
"ctrl-4": ["settings_editor::FocusFile", 3],
|
||||
"ctrl-5": ["settings_editor::FocusFile", 4],
|
||||
"ctrl-6": ["settings_editor::FocusFile", 5],
|
||||
"ctrl-7": ["settings_editor::FocusFile", 6],
|
||||
"ctrl-8": ["settings_editor::FocusFile", 7],
|
||||
"ctrl-9": ["settings_editor::FocusFile", 8],
|
||||
"ctrl-0": ["settings_editor::FocusFile", 9],
|
||||
"cmd-{": "settings_editor::FocusPreviousFile",
|
||||
"cmd-}": "settings_editor::FocusNextFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "SettingsWindow > NavigationMenu",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "settings_editor::FocusPreviousNavEntry",
|
||||
"down": "settings_editor::FocusNextNavEntry",
|
||||
"right": "settings_editor::ExpandNavEntry",
|
||||
"left": "settings_editor::CollapseNavEntry",
|
||||
"pageup": "settings_editor::FocusPreviousRootNavEntry",
|
||||
"pagedown": "settings_editor::FocusNextRootNavEntry",
|
||||
"home": "settings_editor::FocusFirstNavEntry",
|
||||
"end": "settings_editor::FocusLastNavEntry"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
|
||||
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-alt-,": "zed::OpenSettingsFile",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"f4": "debugger::Start",
|
||||
"shift-f5": "debugger::Stop",
|
||||
@@ -133,7 +134,7 @@
|
||||
"ctrl-k z": "editor::ToggleSoftWrap",
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
"ctrl-h": "buffer_search::DeployReplace",
|
||||
"ctrl-shift-.": "assistant::QuoteSelection",
|
||||
"ctrl-shift-.": "agent::QuoteSelection",
|
||||
"ctrl-shift-,": "assistant::InsertIntoEditor",
|
||||
"shift-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
@@ -243,7 +244,7 @@
|
||||
"ctrl-shift-i": "agent::ToggleOptionsMenu",
|
||||
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-shift-.": "assistant::QuoteSelection",
|
||||
"ctrl-shift-.": "agent::QuoteSelection",
|
||||
"shift-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-enter": "agent::ContinueThread",
|
||||
@@ -378,7 +379,8 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "rules_library::NewRule",
|
||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
|
||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule",
|
||||
"ctrl-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -534,15 +536,15 @@
|
||||
"ctrl-k ctrl-l": "editor::ToggleFold",
|
||||
"ctrl-k ctrl-[": "editor::FoldRecursive",
|
||||
"ctrl-k ctrl-]": "editor::UnfoldRecursive",
|
||||
"ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
|
||||
"ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
|
||||
"ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
|
||||
"ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
|
||||
"ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
|
||||
"ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
|
||||
"ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
|
||||
"ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
|
||||
"ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
|
||||
"ctrl-k ctrl-1": "editor::FoldAtLevel_1",
|
||||
"ctrl-k ctrl-2": "editor::FoldAtLevel_2",
|
||||
"ctrl-k ctrl-3": "editor::FoldAtLevel_3",
|
||||
"ctrl-k ctrl-4": "editor::FoldAtLevel_4",
|
||||
"ctrl-k ctrl-5": "editor::FoldAtLevel_5",
|
||||
"ctrl-k ctrl-6": "editor::FoldAtLevel_6",
|
||||
"ctrl-k ctrl-7": "editor::FoldAtLevel_7",
|
||||
"ctrl-k ctrl-8": "editor::FoldAtLevel_8",
|
||||
"ctrl-k ctrl-9": "editor::FoldAtLevel_9",
|
||||
"ctrl-k ctrl-0": "editor::FoldAll",
|
||||
"ctrl-k ctrl-j": "editor::UnfoldAll",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
@@ -621,7 +623,7 @@
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
|
||||
"ctrl-k ctrl-s": "zed::OpenKeymap",
|
||||
"ctrl-k ctrl-t": "theme_selector::Toggle",
|
||||
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
|
||||
"ctrl-t": "project_symbols::Toggle",
|
||||
@@ -1255,12 +1257,48 @@
|
||||
"context": "Onboarding",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-1": "onboarding::ActivateBasicsPage",
|
||||
"ctrl-2": "onboarding::ActivateEditingPage",
|
||||
"ctrl-3": "onboarding::ActivateAISetupPage",
|
||||
"ctrl-enter": "onboarding::Finish",
|
||||
"alt-shift-l": "onboarding::SignIn",
|
||||
"shift-alt-a": "onboarding::OpenAccount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "SettingsWindow",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-w": "workspace::CloseWindow",
|
||||
"escape": "workspace::CloseWindow",
|
||||
"ctrl-m": "settings_editor::Minimize",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"left": "settings_editor::ToggleFocusNav",
|
||||
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
|
||||
// todo(settings_ui): cut this down based on the max files and overflow UI
|
||||
"ctrl-1": ["settings_editor::FocusFile", 0],
|
||||
"ctrl-2": ["settings_editor::FocusFile", 1],
|
||||
"ctrl-3": ["settings_editor::FocusFile", 2],
|
||||
"ctrl-4": ["settings_editor::FocusFile", 3],
|
||||
"ctrl-5": ["settings_editor::FocusFile", 4],
|
||||
"ctrl-6": ["settings_editor::FocusFile", 5],
|
||||
"ctrl-7": ["settings_editor::FocusFile", 6],
|
||||
"ctrl-8": ["settings_editor::FocusFile", 7],
|
||||
"ctrl-9": ["settings_editor::FocusFile", 8],
|
||||
"ctrl-0": ["settings_editor::FocusFile", 9],
|
||||
"ctrl-pageup": "settings_editor::FocusPreviousFile",
|
||||
"ctrl-pagedown": "settings_editor::FocusNextFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "SettingsWindow > NavigationMenu",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "settings_editor::FocusPreviousNavEntry",
|
||||
"down": "settings_editor::FocusNextNavEntry",
|
||||
"right": "settings_editor::ExpandNavEntry",
|
||||
"left": "settings_editor::CollapseNavEntry",
|
||||
"pageup": "settings_editor::FocusPreviousRootNavEntry",
|
||||
"pagedown": "settings_editor::FocusNextRootNavEntry",
|
||||
"home": "settings_editor::FocusFirstNavEntry",
|
||||
"end": "settings_editor::FocusLastNavEntry"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-s": "zed::OpenSettings",
|
||||
"ctrl-alt-s": "zed::OpenSettingsFile",
|
||||
"ctrl-{": "pane::ActivatePreviousItem",
|
||||
"ctrl-}": "pane::ActivateNextItem",
|
||||
"shift-escape": null, // Unmap workspace::zoom
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
"delete": "vim::DeleteRight",
|
||||
"g shift-j": "vim::JoinLinesNoWhitespace",
|
||||
"y": "vim::PushYank",
|
||||
"shift-y": "vim::YankToEndOfLine",
|
||||
"shift-y": "vim::YankLine",
|
||||
"x": "vim::DeleteRight",
|
||||
"shift-x": "vim::DeleteLeft",
|
||||
"ctrl-a": "vim::Increment",
|
||||
@@ -393,7 +393,7 @@
|
||||
"escape": "editor::Cancel",
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"shift-y": "vim::YankToEndOfLine",
|
||||
"shift-y": "vim::YankLine",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
"o": "vim::InsertLineBelow",
|
||||
@@ -580,18 +580,18 @@
|
||||
// "q": "vim::AnyQuotes",
|
||||
"q": "vim::MiniQuotes",
|
||||
"|": "vim::VerticalBars",
|
||||
"(": "vim::Parentheses",
|
||||
"(": ["vim::Parentheses", { "opening": true }],
|
||||
")": "vim::Parentheses",
|
||||
"b": "vim::Parentheses",
|
||||
// "b": "vim::AnyBrackets",
|
||||
// "b": "vim::MiniBrackets",
|
||||
"[": "vim::SquareBrackets",
|
||||
"[": ["vim::SquareBrackets", { "opening": true }],
|
||||
"]": "vim::SquareBrackets",
|
||||
"r": "vim::SquareBrackets",
|
||||
"{": "vim::CurlyBrackets",
|
||||
"{": ["vim::CurlyBrackets", { "opening": true }],
|
||||
"}": "vim::CurlyBrackets",
|
||||
"shift-b": "vim::CurlyBrackets",
|
||||
"<": "vim::AngleBrackets",
|
||||
"<": ["vim::AngleBrackets", { "opening": true }],
|
||||
">": "vim::AngleBrackets",
|
||||
"a": "vim::Argument",
|
||||
"i": "vim::IndentObj",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "zed://schemas/settings",
|
||||
/// The displayed name of this project. If not set or empty, the root directory name
|
||||
/// will be displayed.
|
||||
"project_name": "",
|
||||
@@ -74,8 +75,10 @@
|
||||
"ui_font_weight": 400,
|
||||
// The default font size for text in the UI
|
||||
"ui_font_size": 16,
|
||||
// The default font size for text in the agent panel. Falls back to the UI font size if unset.
|
||||
"agent_font_size": null,
|
||||
// The default font size for agent responses in the agent panel. Falls back to the UI font size if unset.
|
||||
"agent_ui_font_size": null,
|
||||
// The default font size for user messages in the agent panel.
|
||||
"agent_buffer_font_size": 12,
|
||||
// How much to fade out unused code.
|
||||
"unnecessary_code_fade": 0.3,
|
||||
// Active pane styling settings.
|
||||
@@ -719,7 +722,9 @@
|
||||
// Whether to enable drag-and-drop operations in the project panel.
|
||||
"drag_and_drop": true,
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
"hide_root": false
|
||||
"hide_root": false,
|
||||
// Whether to hide the hidden entries in the project panel.
|
||||
"hide_hidden": false
|
||||
},
|
||||
"outline_panel": {
|
||||
// Whether to show the outline panel button in the status bar
|
||||
@@ -1099,7 +1104,7 @@
|
||||
// Removes any lines containing only whitespace at the end of the file and
|
||||
// ensures just one newline at the end.
|
||||
"ensure_final_newline_on_save": true,
|
||||
// Whether or not to perform a buffer format before saving: [on, off, prettier, language_server]
|
||||
// Whether or not to perform a buffer format before saving: [on, off]
|
||||
// Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored
|
||||
"format_on_save": "on",
|
||||
// How to perform a buffer format. This setting can take 4 values:
|
||||
@@ -1229,6 +1234,10 @@
|
||||
// 2. Hide the gutter
|
||||
// "git_gutter": "hide"
|
||||
"git_gutter": "tracked_files",
|
||||
/// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
|
||||
///
|
||||
/// Default: 0
|
||||
"gutter_debounce": 0,
|
||||
// Control whether the git blame information is shown inline,
|
||||
// in the currently focused line.
|
||||
"inline_blame": {
|
||||
@@ -1305,15 +1314,18 @@
|
||||
// "proxy": "",
|
||||
// "proxy_no_verify": false
|
||||
// },
|
||||
// Whether edit predictions are enabled when editing text threads.
|
||||
// This setting has no effect if globally disabled.
|
||||
"enabled_in_text_threads": true,
|
||||
|
||||
"copilot": {
|
||||
"enterprise_uri": null,
|
||||
"proxy": null,
|
||||
"proxy_no_verify": null
|
||||
}
|
||||
},
|
||||
"codestral": {
|
||||
"model": null,
|
||||
"max_tokens": null
|
||||
},
|
||||
// Whether edit predictions are enabled when editing text threads.
|
||||
// This setting has no effect if globally disabled.
|
||||
"enabled_in_text_threads": true
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
@@ -1327,6 +1339,8 @@
|
||||
},
|
||||
// Status bar-related settings.
|
||||
"status_bar": {
|
||||
// Whether to show the status bar.
|
||||
"experimental.show": true,
|
||||
// Whether to show the active language button in the status bar.
|
||||
"active_language_button": true,
|
||||
// Whether to show the cursor position button in the status bar.
|
||||
@@ -1393,8 +1407,8 @@
|
||||
// 4. A box drawn around the following character
|
||||
// "hollow"
|
||||
//
|
||||
// Default: not set, defaults to "block"
|
||||
"cursor_shape": null,
|
||||
// Default: "block"
|
||||
"cursor_shape": "block",
|
||||
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
|
||||
// Alternate Scroll mode converts mouse scroll events into up / down key
|
||||
// presses when in the alternate screen (e.g. when running applications
|
||||
@@ -1416,8 +1430,8 @@
|
||||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
// Whether to keep the text selection after copying it to the clipboard
|
||||
"keep_selection_on_copy": false,
|
||||
// Whether to keep the text selection after copying it to the clipboard.
|
||||
"keep_selection_on_copy": true,
|
||||
// Whether to show the terminal button in the status bar
|
||||
"button": true,
|
||||
// Any key-value pairs added to this list will be added to the terminal's
|
||||
@@ -1507,7 +1521,6 @@
|
||||
// A value of 45 preserves colorful themes while ensuring legibility.
|
||||
"minimum_contrast": 45
|
||||
},
|
||||
"code_actions_on_format": {},
|
||||
// Settings related to running tasks.
|
||||
"tasks": {
|
||||
"variables": {},
|
||||
@@ -1562,6 +1575,14 @@
|
||||
"auto_install_extensions": {
|
||||
"html": true
|
||||
},
|
||||
// The capabilities granted to extensions.
|
||||
//
|
||||
// This list can be customized to restrict what extensions are able to do.
|
||||
"granted_extension_capabilities": [
|
||||
{ "kind": "process:exec", "command": "*", "args": ["**"] },
|
||||
{ "kind": "download_file", "host": "*", "path": ["**"] },
|
||||
{ "kind": "npm:install", "package": "*" }
|
||||
],
|
||||
// Controls how completions are processed for this language.
|
||||
"completions": {
|
||||
// Controls how words are completed.
|
||||
@@ -1669,9 +1690,7 @@
|
||||
"preferred_line_length": 72
|
||||
},
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"formatter": [{ "code_action": "source.organizeImports" }, { "language_server": {} }],
|
||||
"debuggers": ["Delve"]
|
||||
},
|
||||
"GraphQL": {
|
||||
@@ -1860,21 +1879,19 @@
|
||||
// Allows to enable/disable formatting with Prettier
|
||||
// and configure default Prettier, used when no project-level Prettier installation is found.
|
||||
"prettier": {
|
||||
// // Whether to consider prettier formatter or not when attempting to format a file.
|
||||
"allowed": false
|
||||
//
|
||||
// // Use regular Prettier json configuration.
|
||||
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
|
||||
// // the project has no other Prettier installed.
|
||||
// "plugins": [],
|
||||
//
|
||||
// // Use regular Prettier json configuration.
|
||||
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
|
||||
// // the project has no other Prettier installed.
|
||||
// Enables or disables formatting with Prettier for any given language.
|
||||
"allowed": false,
|
||||
// Forces Prettier integration to use a specific parser name when formatting files with the language.
|
||||
"plugins": [],
|
||||
// Default Prettier options, in the format as in package.json section for Prettier.
|
||||
// If project installs Prettier via its package.json, these options will be ignored.
|
||||
// "trailingComma": "es5",
|
||||
// "tabWidth": 4,
|
||||
// "semi": false,
|
||||
// "singleQuote": true
|
||||
// Forces Prettier integration to use a specific parser name when formatting files with the language
|
||||
// when set to a non-empty string.
|
||||
"parser": ""
|
||||
},
|
||||
// Settings for auto-closing of JSX tags.
|
||||
"jsx_tag_auto_close": {
|
||||
@@ -2024,7 +2041,7 @@
|
||||
// Examples:
|
||||
// "profiles": {
|
||||
// "Presenting": {
|
||||
// "agent_font_size": 20.0,
|
||||
// "agent_ui_font_size": 20.0,
|
||||
// "buffer_font_size": 20.0,
|
||||
// "theme": "One Light",
|
||||
// "ui_font_size": 20.0
|
||||
@@ -2037,7 +2054,7 @@
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
"profiles": [],
|
||||
"profiles": {},
|
||||
|
||||
// A map of log scopes to the desired log level.
|
||||
// Useful for filtering out noisy logs or enabling more verbose logging.
|
||||
|
||||
@@ -9,6 +9,8 @@ disallowed-methods = [
|
||||
{ path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" },
|
||||
{ path = "std::process::Command::output", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::output" },
|
||||
{ path = "std::process::Command::status", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::status" },
|
||||
{ path = "serde_json::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892. Use `serde_json::from_slice` instead." },
|
||||
{ path = "serde_json_lenient::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892, Use `serde_json_lenient::from_slice` instead." },
|
||||
]
|
||||
disallowed-types = [
|
||||
# { path = "std::collections::HashMap", replacement = "collections::HashMap" },
|
||||
|
||||
@@ -12,7 +12,7 @@ use language::language_settings::FormatOnSave;
|
||||
pub use mention::*;
|
||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use settings::{Settings as _, SettingsLocation};
|
||||
use task::{Shell, ShellBuilder};
|
||||
pub use terminal::*;
|
||||
|
||||
@@ -35,7 +35,7 @@ use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
|
||||
use ui::App;
|
||||
use util::{ResultExt, get_default_system_shell};
|
||||
use util::{ResultExt, get_default_system_shell_preferring_bash};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -2086,7 +2086,16 @@ impl AcpThread {
|
||||
) -> Task<Result<Entity<Terminal>>> {
|
||||
let env = match &cwd {
|
||||
Some(dir) => self.project.update(cx, |project, cx| {
|
||||
let shell = TerminalSettings::get_global(cx).shell.clone();
|
||||
let worktree = project.find_worktree(dir.as_path(), cx);
|
||||
let shell = TerminalSettings::get(
|
||||
worktree.as_ref().map(|(worktree, path)| SettingsLocation {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: &path,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
.shell
|
||||
.clone();
|
||||
project.directory_environment(&shell, dir.as_path().into(), cx)
|
||||
}),
|
||||
None => Task::ready(None).shared(),
|
||||
@@ -2103,24 +2112,24 @@ impl AcpThread {
|
||||
|
||||
let project = self.project.clone();
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
let is_windows = project.read(cx).path_style(cx).is_windows();
|
||||
|
||||
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
|
||||
let terminal_task = cx.spawn({
|
||||
let terminal_id = terminal_id.clone();
|
||||
async move |_this, cx| {
|
||||
let env = env.await;
|
||||
let (task_command, task_args) = ShellBuilder::new(
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project
|
||||
.remote_client()
|
||||
.and_then(|r| r.read(cx).default_system_shell())
|
||||
})?
|
||||
.as_deref(),
|
||||
&Shell::Program(get_default_system_shell()),
|
||||
)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(command.clone()), &args);
|
||||
let shell = project
|
||||
.update(cx, |project, cx| {
|
||||
project
|
||||
.remote_client()
|
||||
.and_then(|r| r.read(cx).default_system_shell())
|
||||
})?
|
||||
.unwrap_or_else(|| get_default_system_shell_preferring_bash());
|
||||
let (task_command, task_args) =
|
||||
ShellBuilder::new(&Shell::Program(shell), is_windows)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(command.clone()), &args);
|
||||
let terminal = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal_task(
|
||||
@@ -2333,12 +2342,10 @@ mod tests {
|
||||
// Create a display-only terminal and then send Created
|
||||
let lower = cx.new(|cx| {
|
||||
let builder = ::terminal::TerminalBuilder::new_display_only(
|
||||
None,
|
||||
::terminal::terminal_settings::CursorShape::default(),
|
||||
::terminal::terminal_settings::AlternateScroll::On,
|
||||
None,
|
||||
0,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
builder.subscribe(cx)
|
||||
@@ -2412,12 +2419,10 @@ mod tests {
|
||||
// Now create a display-only lower-level terminal and send Created
|
||||
let lower = cx.new(|cx| {
|
||||
let builder = ::terminal::TerminalBuilder::new_display_only(
|
||||
None,
|
||||
::terminal::terminal_settings::CursorShape::default(),
|
||||
::terminal::terminal_settings::AlternateScroll::On,
|
||||
None,
|
||||
0,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
builder.subscribe(cx)
|
||||
|
||||
@@ -20,7 +20,6 @@ use std::{
|
||||
cmp::Reverse,
|
||||
collections::HashSet,
|
||||
fmt::Write,
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -328,17 +327,13 @@ impl ActivityIndicator {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn pending_environment_errors<'a>(
|
||||
&'a self,
|
||||
cx: &'a App,
|
||||
) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
|
||||
self.project.read(cx).shell_environment_errors(cx)
|
||||
fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a EnvironmentErrorMessage> {
|
||||
self.project.read(cx).peek_environment_error(cx)
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
|
||||
// Show if any direnv calls failed
|
||||
if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
|
||||
let abs_path = abs_path.clone();
|
||||
if let Some(error) = self.pending_environment_error(cx) {
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Warning)
|
||||
@@ -348,7 +343,7 @@ impl ActivityIndicator {
|
||||
message: error.0.clone(),
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.remove_environment_error(&abs_path, cx);
|
||||
project.pop_environment_error(cx);
|
||||
});
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
|
||||
@@ -1276,62 +1276,6 @@ impl Thread {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn retry_last_completion(
|
||||
&mut self,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Clear any existing error state
|
||||
self.retry_state = None;
|
||||
|
||||
// Use the last error context if available, otherwise fall back to configured model
|
||||
let (model, intent) = if let Some((model, intent)) = self.last_error_context.take() {
|
||||
(model, intent)
|
||||
} else if let Some(configured_model) = self.configured_model.as_ref() {
|
||||
let model = configured_model.model.clone();
|
||||
let intent = if self.has_pending_tool_uses() {
|
||||
CompletionIntent::ToolResults
|
||||
} else {
|
||||
CompletionIntent::UserPrompt
|
||||
};
|
||||
(model, intent)
|
||||
} else if let Some(configured_model) = self.get_or_init_configured_model(cx) {
|
||||
let model = configured_model.model.clone();
|
||||
let intent = if self.has_pending_tool_uses() {
|
||||
CompletionIntent::ToolResults
|
||||
} else {
|
||||
CompletionIntent::UserPrompt
|
||||
};
|
||||
(model, intent)
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.send_to_model(model, intent, window, cx);
|
||||
}
|
||||
|
||||
pub fn enable_burn_mode_and_retry(
|
||||
&mut self,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.completion_mode = CompletionMode::Burn;
|
||||
cx.emit(ThreadEvent::ProfileChanged);
|
||||
self.retry_last_completion(window, cx);
|
||||
}
|
||||
|
||||
pub fn used_tools_since_last_user_message(&self) -> bool {
|
||||
for message in self.messages.iter().rev() {
|
||||
if self.tool_use.message_has_tool_results(message.id) {
|
||||
return true;
|
||||
} else if message.role == Role::User {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
@@ -3276,7 +3220,6 @@ mod tests {
|
||||
use settings::{LanguageModelParameters, Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use util::path;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -5337,7 +5280,7 @@ fn main() {{
|
||||
thread_store::init(fs.clone(), cx);
|
||||
workspace::init_settings(cx);
|
||||
language_model::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
ToolRegistry::default_global(cx);
|
||||
assistant_tool::init(cx);
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ use std::{
|
||||
cell::{Ref, RefCell},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{Arc, Mutex},
|
||||
sync::{Arc, LazyLock, Mutex},
|
||||
};
|
||||
use util::{ResultExt as _, rel_path::RelPath};
|
||||
|
||||
@@ -74,17 +74,19 @@ impl Column for DataType {
|
||||
}
|
||||
}
|
||||
|
||||
const RULES_FILE_NAMES: [&str; 9] = [
|
||||
".rules",
|
||||
".cursorrules",
|
||||
".windsurfrules",
|
||||
".clinerules",
|
||||
".github/copilot-instructions.md",
|
||||
"CLAUDE.md",
|
||||
"AGENT.md",
|
||||
"AGENTS.md",
|
||||
"GEMINI.md",
|
||||
];
|
||||
static RULES_FILE_NAMES: LazyLock<[&RelPath; 9]> = LazyLock::new(|| {
|
||||
[
|
||||
RelPath::unix(".rules").unwrap(),
|
||||
RelPath::unix(".cursorrules").unwrap(),
|
||||
RelPath::unix(".windsurfrules").unwrap(),
|
||||
RelPath::unix(".clinerules").unwrap(),
|
||||
RelPath::unix(".github/copilot-instructions.md").unwrap(),
|
||||
RelPath::unix("CLAUDE.md").unwrap(),
|
||||
RelPath::unix("AGENT.md").unwrap(),
|
||||
RelPath::unix("AGENTS.md").unwrap(),
|
||||
RelPath::unix("GEMINI.md").unwrap(),
|
||||
]
|
||||
});
|
||||
|
||||
pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
ThreadsDatabase::init(fs, cx);
|
||||
@@ -232,11 +234,10 @@ impl ThreadStore {
|
||||
self.enqueue_system_prompt_reload();
|
||||
}
|
||||
project::Event::WorktreeUpdatedEntries(_, items) => {
|
||||
if items.iter().any(|(path, _, _)| {
|
||||
RULES_FILE_NAMES
|
||||
.iter()
|
||||
.any(|name| path.as_ref() == RelPath::unix(name).unwrap())
|
||||
}) {
|
||||
if items
|
||||
.iter()
|
||||
.any(|(path, _, _)| RULES_FILE_NAMES.iter().any(|name| path.as_ref() == *name))
|
||||
{
|
||||
self.enqueue_system_prompt_reload();
|
||||
}
|
||||
}
|
||||
@@ -368,7 +369,7 @@ impl ThreadStore {
|
||||
.into_iter()
|
||||
.filter_map(|name| {
|
||||
worktree
|
||||
.entry_for_path(RelPath::unix(name).unwrap())
|
||||
.entry_for_path(name)
|
||||
.filter(|entry| entry.is_file())
|
||||
.map(|entry| entry.path.clone())
|
||||
})
|
||||
|
||||
@@ -1418,7 +1418,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_save_load_thread(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -1498,7 +1497,8 @@ mod tests {
|
||||
model.send_last_completion_stream_text_chunk("Lorem.");
|
||||
model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
summary_model.send_last_completion_stream_text_chunk("Explaining /a/b.md");
|
||||
summary_model
|
||||
.send_last_completion_stream_text_chunk(&format!("Explaining {}", path!("/a/b.md")));
|
||||
summary_model.end_last_completion_stream();
|
||||
|
||||
send.await.unwrap();
|
||||
@@ -1538,7 +1538,7 @@ mod tests {
|
||||
history_entries(&history_store, cx),
|
||||
vec![(
|
||||
HistoryEntryId::AcpThread(session_id.clone()),
|
||||
"Explaining /a/b.md".into()
|
||||
format!("Explaining {}", path!("/a/b.md"))
|
||||
)]
|
||||
);
|
||||
let acp_thread = agent
|
||||
|
||||
@@ -15,10 +15,11 @@ use agent_settings::{
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::adapt_schema_to_format;
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{ModelRequestUsage, RequestUsage};
|
||||
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
use client::{ModelRequestUsage, RequestUsage, UserStore};
|
||||
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use fs::Fs;
|
||||
use futures::stream;
|
||||
use futures::{
|
||||
FutureExt,
|
||||
channel::{mpsc, oneshot},
|
||||
@@ -34,7 +35,7 @@ use language_model::{
|
||||
LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage,
|
||||
LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use project::{
|
||||
Project,
|
||||
@@ -585,6 +586,7 @@ pub struct Thread {
|
||||
pending_title_generation: Option<Task<()>>,
|
||||
summary: Option<SharedString>,
|
||||
messages: Vec<Message>,
|
||||
user_store: Entity<UserStore>,
|
||||
completion_mode: CompletionMode,
|
||||
/// Holds the task that handles agent interaction until the end of the turn.
|
||||
/// Survives across multiple requests as the model performs tool calls and
|
||||
@@ -641,6 +643,7 @@ impl Thread {
|
||||
pending_title_generation: None,
|
||||
summary: None,
|
||||
messages: Vec::new(),
|
||||
user_store: project.read(cx).user_store(),
|
||||
completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
|
||||
running_turn: None,
|
||||
pending_message: None,
|
||||
@@ -820,6 +823,7 @@ impl Thread {
|
||||
pending_title_generation: None,
|
||||
summary: db_thread.detailed_summary,
|
||||
messages: db_thread.messages,
|
||||
user_store: project.read(cx).user_store(),
|
||||
completion_mode: db_thread.completion_mode.unwrap_or_default(),
|
||||
running_turn: None,
|
||||
pending_message: None,
|
||||
@@ -1249,12 +1253,12 @@ impl Thread {
|
||||
);
|
||||
|
||||
log::debug!("Calling model.stream_completion, attempt {}", attempt);
|
||||
let mut events = model
|
||||
.stream_completion(request, cx)
|
||||
.await
|
||||
.map_err(|error| anyhow!(error))?;
|
||||
|
||||
let (mut events, mut error) = match model.stream_completion(request, cx).await {
|
||||
Ok(events) => (events, None),
|
||||
Err(err) => (stream::empty().boxed(), Some(err)),
|
||||
};
|
||||
let mut tool_results = FuturesUnordered::new();
|
||||
let mut error = None;
|
||||
while let Some(event) = events.next().await {
|
||||
log::trace!("Received completion event: {:?}", event);
|
||||
match event {
|
||||
@@ -1302,8 +1306,10 @@ impl Thread {
|
||||
|
||||
if let Some(error) = error {
|
||||
attempt += 1;
|
||||
let retry =
|
||||
this.update(cx, |this, _| this.handle_completion_error(error, attempt))??;
|
||||
let retry = this.update(cx, |this, cx| {
|
||||
let user_store = this.user_store.read(cx);
|
||||
this.handle_completion_error(error, attempt, user_store.plan())
|
||||
})??;
|
||||
let timer = cx.background_executor().timer(retry.duration);
|
||||
event_stream.send_retry(retry);
|
||||
timer.await;
|
||||
@@ -1330,8 +1336,23 @@ impl Thread {
|
||||
&mut self,
|
||||
error: LanguageModelCompletionError,
|
||||
attempt: u8,
|
||||
plan: Option<Plan>,
|
||||
) -> Result<acp_thread::RetryStatus> {
|
||||
if self.completion_mode == CompletionMode::Normal {
|
||||
let Some(model) = self.model.as_ref() else {
|
||||
return Err(anyhow!(error));
|
||||
};
|
||||
|
||||
let auto_retry = if model.provider_id() == ZED_CLOUD_PROVIDER_ID {
|
||||
match plan {
|
||||
Some(Plan::V2(_)) => true,
|
||||
Some(Plan::V1(_)) => self.completion_mode == CompletionMode::Burn,
|
||||
None => false,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if !auto_retry {
|
||||
return Err(anyhow!(error));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
use serde::Deserialize;
|
||||
use task::Shell;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -81,7 +82,7 @@ impl AcpConnection {
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut child = util::command::new_smol_command(command.path);
|
||||
let mut child = util::command::new_smol_command(&command.path);
|
||||
child
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.envs(command.env.iter().flatten())
|
||||
@@ -96,6 +97,11 @@ impl AcpConnection {
|
||||
let stdout = child.stdout.take().context("Failed to take stdout")?;
|
||||
let stdin = child.stdin.take().context("Failed to take stdin")?;
|
||||
let stderr = child.stderr.take().context("Failed to take stderr")?;
|
||||
log::info!(
|
||||
"Spawning external agent server: {:?}, {:?}",
|
||||
command.path,
|
||||
command.args
|
||||
);
|
||||
log::trace!("Spawned (pid: {})", child.id());
|
||||
|
||||
let sessions = Rc::new(RefCell::new(HashMap::default()));
|
||||
@@ -712,23 +718,24 @@ impl acp::Client for ClientDelegate {
|
||||
if let Some(id_str) = terminal_info.get("terminal_id").and_then(|v| v.as_str())
|
||||
{
|
||||
let terminal_id = acp::TerminalId(id_str.into());
|
||||
let cwd = terminal_info
|
||||
.get("cwd")
|
||||
.and_then(|v| v.as_str().map(PathBuf::from));
|
||||
|
||||
// Create a minimal display-only lower-level terminal and register it.
|
||||
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
|
||||
let builder = TerminalBuilder::new_display_only(
|
||||
None,
|
||||
CursorShape::default(),
|
||||
AlternateScroll::On,
|
||||
None,
|
||||
0,
|
||||
cx,
|
||||
)?;
|
||||
let lower = cx.new(|cx| builder.subscribe(cx));
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Created {
|
||||
terminal_id: terminal_id.clone(),
|
||||
label: tc.title.clone(),
|
||||
cwd: None,
|
||||
cwd,
|
||||
output_byte_limit: None,
|
||||
terminal: lower,
|
||||
},
|
||||
@@ -820,15 +827,20 @@ impl acp::Client for ClientDelegate {
|
||||
}
|
||||
|
||||
// Use remote shell or default system shell, as appropriate
|
||||
let remote_shell = project.update(&mut self.cx.clone(), |project, cx| {
|
||||
project
|
||||
.remote_client()
|
||||
.and_then(|r| r.read(cx).default_system_shell())
|
||||
})?;
|
||||
let (task_command, task_args) =
|
||||
task::ShellBuilder::new(remote_shell.as_deref(), &task::Shell::System)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(args.command.clone()), &args.args);
|
||||
let shell = project
|
||||
.update(&mut self.cx.clone(), |project, cx| {
|
||||
project
|
||||
.remote_client()
|
||||
.and_then(|r| r.read(cx).default_system_shell())
|
||||
.map(Shell::Program)
|
||||
})?
|
||||
.unwrap_or(task::Shell::System);
|
||||
let is_windows = project
|
||||
.read_with(&self.cx, |project, cx| project.path_style(cx).is_windows())
|
||||
.unwrap_or(cfg!(windows));
|
||||
let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(args.command.clone()), &args.args);
|
||||
|
||||
let terminal_entity = project
|
||||
.update(&mut self.cx.clone(), |project, cx| {
|
||||
|
||||
@@ -151,7 +151,7 @@ impl Default for AgentProfileId {
|
||||
}
|
||||
|
||||
impl Settings for AgentSettings {
|
||||
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
let agent = content.agent.clone().unwrap();
|
||||
Self {
|
||||
enabled: agent.enabled.unwrap(),
|
||||
|
||||
@@ -27,7 +27,7 @@ use util::rel_path::RelPath;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::AgentPanel;
|
||||
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::acp::message_editor::MessageEditor;
|
||||
use crate::context_picker::file_context_picker::{FileMatch, search_files};
|
||||
use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
|
||||
use crate::context_picker::symbol_context_picker::SymbolMatch;
|
||||
@@ -759,13 +759,13 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
let editor = editor.clone();
|
||||
move |cx| {
|
||||
editor
|
||||
.update(cx, |_editor, cx| {
|
||||
.update(cx, |editor, cx| {
|
||||
match intent {
|
||||
CompletionIntent::Complete
|
||||
| CompletionIntent::CompleteWithInsert
|
||||
| CompletionIntent::CompleteWithReplace => {
|
||||
if !is_missing_argument {
|
||||
cx.emit(MessageEditorEvent::Send);
|
||||
editor.send(cx);
|
||||
}
|
||||
}
|
||||
CompletionIntent::Compose => {}
|
||||
|
||||
@@ -203,7 +203,7 @@ impl EntryViewState {
|
||||
self.entries.drain(range);
|
||||
}
|
||||
|
||||
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
|
||||
pub fn agent_ui_font_size_changed(&mut self, cx: &mut App) {
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||
@@ -387,7 +387,7 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
|
||||
font_size: Some(
|
||||
TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
|
||||
.to_pixels(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
@@ -414,7 +414,6 @@ mod tests {
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use theme::ThemeSettings;
|
||||
use util::path;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -544,7 +543,7 @@ mod tests {
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
EditorSettings::register(cx);
|
||||
});
|
||||
|
||||
@@ -141,7 +141,9 @@ impl MessageEditor {
|
||||
|
||||
subscriptions.push(cx.subscribe_in(&editor, window, {
|
||||
move |this, editor, event, window, cx| {
|
||||
if let EditorEvent::Edited { .. } = event {
|
||||
if let EditorEvent::Edited { .. } = event
|
||||
&& !editor.read(cx).read_only(cx)
|
||||
{
|
||||
let snapshot = editor.update(cx, |editor, cx| {
|
||||
let new_hints = this
|
||||
.command_hint(editor.buffer(), cx)
|
||||
@@ -290,18 +292,18 @@ impl MessageEditor {
|
||||
let snapshot = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else {
|
||||
let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot().as_singleton() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
let Some(start_anchor) = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_in_excerpt(*excerpt_id, start)
|
||||
else {
|
||||
return Task::ready(());
|
||||
};
|
||||
let end_anchor = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1);
|
||||
.buffer_snapshot()
|
||||
.anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1);
|
||||
|
||||
let crease = if let MentionUri::File { abs_path } = &mention_uri
|
||||
&& let Some(extension) = abs_path.extension()
|
||||
@@ -718,7 +720,7 @@ impl MessageEditor {
|
||||
continue;
|
||||
};
|
||||
|
||||
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
|
||||
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot());
|
||||
if crease_range.start > ix {
|
||||
//todo(): Custom slash command ContentBlock?
|
||||
// let chunk = if prevent_slash_commands
|
||||
@@ -823,13 +825,20 @@ impl MessageEditor {
|
||||
});
|
||||
}
|
||||
|
||||
fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
|
||||
pub fn send(&mut self, cx: &mut Context<Self>) {
|
||||
if self.is_empty(cx) {
|
||||
return;
|
||||
}
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.clear_inlay_hints(cx);
|
||||
});
|
||||
cx.emit(MessageEditorEvent::Send)
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.send(cx);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(MessageEditorEvent::Cancel)
|
||||
}
|
||||
@@ -865,11 +874,11 @@ impl MessageEditor {
|
||||
self.editor.update(cx, |message_editor, cx| {
|
||||
let snapshot = message_editor.snapshot(window, cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.buffer_snapshot.as_singleton().unwrap();
|
||||
snapshot.buffer_snapshot().as_singleton().unwrap();
|
||||
|
||||
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
|
||||
let multibuffer_anchor = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_in_excerpt(*excerpt_id, text_anchor);
|
||||
message_editor.edit(
|
||||
[(
|
||||
@@ -1030,6 +1039,7 @@ impl MessageEditor {
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.editor.update(cx, |message_editor, cx| {
|
||||
message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
|
||||
});
|
||||
@@ -1287,7 +1297,7 @@ impl Render for MessageEditor {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::send))
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.flex_1()
|
||||
@@ -1299,7 +1309,7 @@ impl Render for MessageEditor {
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_size: settings.agent_buffer_font_size(cx).into(),
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -1550,7 +1560,7 @@ impl MentionSet {
|
||||
|
||||
fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
|
||||
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
|
||||
if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
|
||||
if !crease.range().start.is_valid(&snapshot.buffer_snapshot()) {
|
||||
self.mentions.remove(&crease_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ use gpui::{
|
||||
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
|
||||
ListOffset, ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task,
|
||||
TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, WindowHandle, div,
|
||||
ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*, pulsating_between,
|
||||
ease_in_out, linear_color_stop, linear_gradient, list, point, pulsating_between,
|
||||
};
|
||||
use language::Buffer;
|
||||
|
||||
@@ -289,8 +289,9 @@ pub struct AcpThreadView {
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
is_loading_contents: bool,
|
||||
new_server_version_available: Option<SharedString>,
|
||||
resume_thread_metadata: Option<DbThreadMetadata>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 4],
|
||||
_subscriptions: [Subscription; 5],
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -380,11 +381,17 @@ impl AcpThreadView {
|
||||
)
|
||||
});
|
||||
|
||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||
let subscriptions = [
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_font_size_changed),
|
||||
cx.observe_global_in::<AgentFontSize>(window, Self::agent_font_size_changed),
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
|
||||
cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_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),
|
||||
cx.subscribe_in(
|
||||
&agent_server_store,
|
||||
window,
|
||||
Self::handle_agent_servers_updated,
|
||||
),
|
||||
];
|
||||
|
||||
Self {
|
||||
@@ -392,7 +399,14 @@ impl AcpThreadView {
|
||||
workspace: workspace.clone(),
|
||||
project: project.clone(),
|
||||
entry_view_state,
|
||||
thread_state: Self::initial_state(agent, resume_thread, workspace, project, window, cx),
|
||||
thread_state: Self::initial_state(
|
||||
agent.clone(),
|
||||
resume_thread.clone(),
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
login: None,
|
||||
message_editor,
|
||||
model_selector: None,
|
||||
@@ -421,13 +435,14 @@ impl AcpThreadView {
|
||||
_cancel_task: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
new_server_version_available: None,
|
||||
resume_thread_metadata: resume_thread,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread_state = Self::initial_state(
|
||||
self.agent.clone(),
|
||||
None,
|
||||
self.resume_thread_metadata.clone(),
|
||||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
window,
|
||||
@@ -775,6 +790,25 @@ impl AcpThreadView {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_agent_servers_updated(
|
||||
&mut self,
|
||||
_agent_server_store: &Entity<project::AgentServerStore>,
|
||||
_event: &project::AgentServersUpdated,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// If we're in a LoadError state OR have a thread_error set (which can happen
|
||||
// when agent.connect() fails during loading), retry loading the thread.
|
||||
// This handles the case where a thread is restored before authentication completes.
|
||||
let should_retry =
|
||||
matches!(&self.thread_state, ThreadState::LoadError(_)) || self.thread_error.is_some();
|
||||
|
||||
if should_retry {
|
||||
self.thread_error = None;
|
||||
self.reset(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn workspace(&self) -> &WeakEntity<Workspace> {
|
||||
&self.workspace
|
||||
}
|
||||
@@ -1011,33 +1045,37 @@ impl AcpThreadView {
|
||||
return;
|
||||
};
|
||||
|
||||
self.message_editor
|
||||
.update(cx, |editor, cx| editor.clear(window, cx));
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let auth_methods = connection.auth_methods();
|
||||
let has_supported_auth = auth_methods.iter().any(|method| {
|
||||
let id = method.id.0.as_ref();
|
||||
id == "claude-login" || id == "spawn-gemini-cli"
|
||||
});
|
||||
let can_login = has_supported_auth || auth_methods.is_empty() || self.login.is_some();
|
||||
if !can_login {
|
||||
let can_login = !connection.auth_methods().is_empty() || self.login.is_some();
|
||||
// Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
|
||||
let logout_supported = text == "/logout"
|
||||
&& self
|
||||
.available_commands
|
||||
.borrow()
|
||||
.iter()
|
||||
.any(|command| command.name == "logout");
|
||||
if can_login && !logout_supported {
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
||||
self.send_impl(self.message_editor.clone(), window, cx)
|
||||
@@ -2693,7 +2731,7 @@ impl AcpThreadView {
|
||||
let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
|
||||
|
||||
let command_failed = command_finished
|
||||
&& output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
|
||||
&& output.is_some_and(|o| o.exit_status.is_some_and(|status| !status.success()));
|
||||
|
||||
let time_elapsed = if let Some(output) = output {
|
||||
output.ended_at.duration_since(started_at)
|
||||
@@ -2716,7 +2754,7 @@ impl AcpThreadView {
|
||||
|
||||
let working_dir = working_dir
|
||||
.as_ref()
|
||||
.map(|path| format!("{}", path.display()))
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| "current directory".to_string());
|
||||
|
||||
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
|
||||
@@ -3248,6 +3286,12 @@ impl AcpThreadView {
|
||||
this.style(ButtonStyle::Outlined)
|
||||
}
|
||||
})
|
||||
.when_some(
|
||||
method.description.clone(),
|
||||
|this, description| {
|
||||
this.tooltip(Tooltip::text(description))
|
||||
},
|
||||
)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
@@ -3354,6 +3398,12 @@ impl AcpThreadView {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn activity_bar_bg(&self, cx: &Context<Self>) -> Hsla {
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let active_color = cx.theme().colors().element_selected;
|
||||
editor_bg_color.blend(active_color.opacity(0.3))
|
||||
}
|
||||
|
||||
fn render_activity_bar(
|
||||
&self,
|
||||
thread_entity: &Entity<AcpThread>,
|
||||
@@ -3369,10 +3419,6 @@ impl AcpThreadView {
|
||||
return None;
|
||||
}
|
||||
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let active_color = cx.theme().colors().element_selected;
|
||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||
|
||||
// Temporarily always enable ACP edit controls. This is temporary, to lessen the
|
||||
// impact of a nasty bug that causes them to sometimes be disabled when they shouldn't
|
||||
// be, which blocks you from being able to accept or reject edits. This switches the
|
||||
@@ -3383,7 +3429,7 @@ impl AcpThreadView {
|
||||
v_flex()
|
||||
.mt_1()
|
||||
.mx_2()
|
||||
.bg(bg_edit_files_disclosure)
|
||||
.bg(self.activity_bar_bg(cx))
|
||||
.border_1()
|
||||
.border_b_0()
|
||||
.border_color(cx.theme().colors().border)
|
||||
@@ -3424,27 +3470,33 @@ impl AcpThreadView {
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
|
||||
fn render_plan_summary(
|
||||
&self,
|
||||
plan: &Plan,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let stats = plan.stats();
|
||||
|
||||
let title = if let Some(entry) = stats.in_progress_entry
|
||||
&& !self.plan_expanded
|
||||
{
|
||||
h_flex()
|
||||
.w_full()
|
||||
.cursor_default()
|
||||
.relative()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.justify_between()
|
||||
.truncate()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Current:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
Label::new("Current:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.line_clamp(1)
|
||||
.child(MarkdownElement::new(
|
||||
entry.content.clone(),
|
||||
plan_label_markdown_style(&entry.status, window, cx),
|
||||
@@ -3452,10 +3504,23 @@ impl AcpThreadView {
|
||||
)
|
||||
.when(stats.pending > 0, |this| {
|
||||
this.child(
|
||||
Label::new(format!("{} left", stats.pending))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mr_1(),
|
||||
h_flex()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.h_full()
|
||||
.child(div().min_w_8().h_full().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(self.activity_bar_bg(cx), 1.),
|
||||
linear_color_stop(self.activity_bar_bg(cx).opacity(0.2), 0.),
|
||||
)))
|
||||
.child(
|
||||
div().pr_0p5().bg(self.activity_bar_bg(cx)).child(
|
||||
Label::new(format!("{} left", stats.pending))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
@@ -3485,23 +3550,19 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("plan_summary")
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.when(self.plan_expanded, |this| {
|
||||
this.border_b_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id("plan_summary")
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Disclosure::new("plan_disclosure", self.plan_expanded))
|
||||
.child(title)
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.plan_expanded = !this.plan_expanded;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(Disclosure::new("plan_disclosure", self.plan_expanded))
|
||||
.child(title)
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.plan_expanded = !this.plan_expanded;
|
||||
cx.notify();
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
|
||||
@@ -3750,7 +3811,7 @@ impl AcpThreadView {
|
||||
.id(("file-name", index))
|
||||
.pr_8()
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(file_icon)
|
||||
.child(h_flex().gap_0p5().children(file_name).children(file_path))
|
||||
@@ -4902,9 +4963,9 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
fn agent_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn agent_ui_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.agent_font_size_changed(cx);
|
||||
entry_view_state.agent_ui_font_size_changed(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4920,10 +4981,12 @@ impl AcpThreadView {
|
||||
})
|
||||
}
|
||||
|
||||
/// Inserts the selected text into the message editor or the message being
|
||||
/// edited, if any.
|
||||
pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.message_editor.update(cx, |message_editor, cx| {
|
||||
message_editor.insert_selections(window, cx);
|
||||
})
|
||||
self.active_editor(cx).update(cx, |editor, cx| {
|
||||
editor.insert_selections(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_thread_retry_status_callout(
|
||||
@@ -5334,6 +5397,23 @@ impl AcpThreadView {
|
||||
};
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
/// Returns the currently active editor, either for a message that is being
|
||||
/// edited or the editor for a new message.
|
||||
fn active_editor(&self, cx: &App) -> Entity<MessageEditor> {
|
||||
if let Some(index) = self.editing_message
|
||||
&& let Some(editor) = self
|
||||
.entry_view_state
|
||||
.read(cx)
|
||||
.entry(index)
|
||||
.and_then(|e| e.message_editor())
|
||||
.cloned()
|
||||
{
|
||||
editor
|
||||
} else {
|
||||
self.message_editor.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn loading_contents_spinner(size: IconSize) -> AnyElement {
|
||||
@@ -5348,7 +5428,7 @@ impl Focusable for AcpThreadView {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match self.thread_state {
|
||||
ThreadState::Loading { .. } | ThreadState::Ready { .. } => {
|
||||
self.message_editor.focus_handle(cx)
|
||||
self.active_editor(cx).focus_handle(cx)
|
||||
}
|
||||
ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => {
|
||||
self.focus_handle.clone()
|
||||
@@ -6035,7 +6115,7 @@ pub(crate) mod tests {
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
EditorSettings::register(cx);
|
||||
prompt_store::init(cx)
|
||||
@@ -6609,4 +6689,146 @@ pub(crate) mod tests {
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_message_editing_insert_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let connection = StubAgentConnection::new();
|
||||
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
|
||||
content: acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Response".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
}]);
|
||||
|
||||
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
|
||||
add_to_workspace(thread_view.clone(), cx);
|
||||
|
||||
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||
message_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Original message to edit", window, cx)
|
||||
});
|
||||
thread_view.update_in(cx, |thread_view, window, cx| thread_view.send(window, cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
let user_message_editor = thread_view.read_with(cx, |thread_view, cx| {
|
||||
thread_view
|
||||
.entry_view_state
|
||||
.read(cx)
|
||||
.entry(0)
|
||||
.expect("Should have at least one entry")
|
||||
.message_editor()
|
||||
.expect("Should have message editor")
|
||||
.clone()
|
||||
});
|
||||
|
||||
cx.focus(&user_message_editor);
|
||||
thread_view.read_with(cx, |thread_view, _cx| {
|
||||
assert_eq!(thread_view.editing_message, Some(0));
|
||||
});
|
||||
|
||||
// Ensure to edit the focused message before proceeding otherwise, since
|
||||
// its content is not different from what was sent, focus will be lost.
|
||||
user_message_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Original message to edit with ", window, cx)
|
||||
});
|
||||
|
||||
// Create a simple buffer with some text so we can create a selection
|
||||
// that will then be added to the message being edited.
|
||||
let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
|
||||
(thread_view.workspace.clone(), thread_view.project.clone())
|
||||
});
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer("let a = 10 + 10;", None, false, cx)
|
||||
});
|
||||
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
|
||||
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_ranges([8..15]);
|
||||
});
|
||||
|
||||
editor
|
||||
});
|
||||
workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||
assert_eq!(thread_view.editing_message, Some(0));
|
||||
thread_view.insert_selections(window, cx);
|
||||
});
|
||||
|
||||
user_message_editor.read_with(cx, |editor, cx| {
|
||||
let text = editor.editor().read(cx).text(cx);
|
||||
let expected_text = String::from("Original message to edit with selection ");
|
||||
|
||||
assert_eq!(text, expected_text);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let connection = StubAgentConnection::new();
|
||||
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
|
||||
content: acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Response".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
}]);
|
||||
|
||||
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
|
||||
add_to_workspace(thread_view.clone(), cx);
|
||||
|
||||
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||
message_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Can you review this snippet ", window, cx)
|
||||
});
|
||||
|
||||
// Create a simple buffer with some text so we can create a selection
|
||||
// that will then be added to the message being edited.
|
||||
let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
|
||||
(thread_view.workspace.clone(), thread_view.project.clone())
|
||||
});
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer("let a = 10 + 10;", None, false, cx)
|
||||
});
|
||||
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
|
||||
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_ranges([8..15]);
|
||||
});
|
||||
|
||||
editor
|
||||
});
|
||||
workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||
assert_eq!(thread_view.editing_message, None);
|
||||
thread_view.insert_selections(window, cx);
|
||||
});
|
||||
|
||||
thread_view.read_with(cx, |thread_view, cx| {
|
||||
let text = thread_view.message_editor.read(cx).text(cx);
|
||||
let expected_txt = String::from("Can you review this snippet selection ");
|
||||
|
||||
assert_eq!(text, expected_txt);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ mod tool_picker;
|
||||
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use cloud_llm_client::{Plan, PlanV1, PlanV2};
|
||||
@@ -15,7 +14,6 @@ use context_server::ContextServerId;
|
||||
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
|
||||
use extension::ExtensionManifest;
|
||||
use extension_host::ExtensionStore;
|
||||
use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
@@ -30,10 +28,10 @@ use project::{
|
||||
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use settings::{SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
|
||||
Indicator, PopoverMenu, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*,
|
||||
Indicator, PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Workspace, create_and_open_local_file};
|
||||
@@ -403,101 +401,6 @@ impl AgentConfiguration {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
SwitchField::new(
|
||||
"always-allow-tool-actions-switch",
|
||||
"Allow running commands without asking for confirmation",
|
||||
Some(
|
||||
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
|
||||
),
|
||||
always_allow_tool_actions,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings.agent.get_or_insert_default().set_always_allow_tool_actions(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_single_file_review(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let single_file_review = AgentSettings::get_global(cx).single_file_review;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
SwitchField::new(
|
||||
"single-file-review",
|
||||
"Enable single-file agent reviews",
|
||||
Some("Agent edits are also displayed in single-file editors for review.".into()),
|
||||
single_file_review,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_single_file_review(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
SwitchField::new(
|
||||
"sound-notification",
|
||||
"Play sound when finished generating",
|
||||
Some(
|
||||
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
|
||||
),
|
||||
play_sound_when_agent_done,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings.agent.get_or_insert_default().set_play_sound_when_agent_done(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_modifier_to_send(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let use_modifier_to_send = AgentSettings::get_global(cx).use_modifier_to_send;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
SwitchField::new(
|
||||
"modifier-send",
|
||||
"Use modifier to submit a message",
|
||||
Some(
|
||||
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
|
||||
),
|
||||
use_modifier_to_send,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings.agent.get_or_insert_default().set_use_modifier_to_send(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Headline::new("General Settings"))
|
||||
.child(self.render_command_permission(cx))
|
||||
.child(self.render_single_file_review(cx))
|
||||
.child(self.render_sound_notification(cx))
|
||||
.child(self.render_modifier_to_send(cx))
|
||||
}
|
||||
|
||||
fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if let Some(plan) = plan {
|
||||
let free_chip_bg = cx
|
||||
@@ -1085,14 +988,11 @@ impl AgentConfiguration {
|
||||
"Claude Code",
|
||||
))
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
.when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
|
||||
this
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiOpenAi,
|
||||
"Codex",
|
||||
))
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
})
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiOpenAi,
|
||||
"Codex",
|
||||
))
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiGemini,
|
||||
"Gemini CLI",
|
||||
@@ -1145,7 +1045,6 @@ impl Render for AgentConfiguration {
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_general_settings_section(cx))
|
||||
.child(self.render_agent_servers_section(cx))
|
||||
.child(self.render_context_servers_section(window, cx))
|
||||
.child(self.render_provider_configuration_section(cx)),
|
||||
|
||||
@@ -619,10 +619,10 @@ mod tests {
|
||||
cx.update(|_window, cx| {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.register_provider(
|
||||
FakeLanguageModelProvider::new(
|
||||
Arc::new(FakeLanguageModelProvider::new(
|
||||
LanguageModelProviderId::new("someprovider"),
|
||||
LanguageModelProviderName::new("Some Provider"),
|
||||
),
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -562,10 +562,6 @@ impl Item for AgentDiffPane {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
nav_history: ItemNavHistory,
|
||||
@@ -850,7 +846,7 @@ fn render_diff_hunk_controls(
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let position =
|
||||
hunk_range.end.to_point(&snapshot.buffer_snapshot);
|
||||
hunk_range.end.to_point(&snapshot.buffer_snapshot());
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
position,
|
||||
@@ -886,7 +882,7 @@ fn render_diff_hunk_controls(
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let point =
|
||||
hunk_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
hunk_range.start.to_point(&snapshot.buffer_snapshot());
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
point,
|
||||
@@ -1818,7 +1814,6 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{path::Path, rc::Rc};
|
||||
use theme::ThemeSettings;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1831,7 +1826,7 @@ mod tests {
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
EditorSettings::register(cx);
|
||||
language_model::init_settings(cx);
|
||||
});
|
||||
@@ -1983,7 +1978,7 @@ mod tests {
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
EditorSettings::register(cx);
|
||||
language_model::init_settings(cx);
|
||||
workspace::register_project_item::<Editor>(cx);
|
||||
|
||||
@@ -53,7 +53,7 @@ use gpui::{
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{ConfigurationError, LanguageModelRegistry};
|
||||
use project::{DisableAiSettings, Project, ProjectPath, Worktree};
|
||||
use project::{Project, ProjectPath, Worktree};
|
||||
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
||||
use rules_library::{RulesLibrary, open_rules_library};
|
||||
use search::{BufferSearchBar, buffer_search};
|
||||
@@ -75,7 +75,6 @@ use zed_actions::{
|
||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||
};
|
||||
|
||||
use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
|
||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -519,6 +518,7 @@ impl AgentPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
panel.as_mut(cx).loading = true;
|
||||
if let Some(serialized_panel) = serialized_panel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
@@ -670,43 +670,6 @@ impl AgentPanel {
|
||||
)
|
||||
});
|
||||
|
||||
let mut old_disable_ai = false;
|
||||
cx.observe_global_in::<SettingsStore>(window, move |panel, window, cx| {
|
||||
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
||||
if old_disable_ai != disable_ai {
|
||||
let agent_panel_id = cx.entity_id();
|
||||
let agent_panel_visible = panel
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let agent_dock_position = panel.position(window, cx);
|
||||
let agent_dock = workspace.dock_at_position(agent_dock_position);
|
||||
let agent_panel_focused = agent_dock
|
||||
.read(cx)
|
||||
.active_panel()
|
||||
.is_some_and(|panel| panel.panel_id() == agent_panel_id);
|
||||
|
||||
let active_panel_visible = agent_dock
|
||||
.read(cx)
|
||||
.visible_panel()
|
||||
.is_some_and(|panel| panel.panel_id() == agent_panel_id);
|
||||
|
||||
if agent_panel_focused {
|
||||
cx.dispatch_action(&ToggleFocus);
|
||||
}
|
||||
|
||||
active_panel_visible
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if agent_panel_visible {
|
||||
cx.emit(PanelEvent::Close);
|
||||
}
|
||||
|
||||
old_disable_ai = disable_ai;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
active_view,
|
||||
workspace,
|
||||
@@ -1108,15 +1071,15 @@ impl AgentPanel {
|
||||
WhichFontSize::AgentFont => {
|
||||
if persist {
|
||||
update_settings_file(self.fs.clone(), cx, move |settings, cx| {
|
||||
let agent_font_size =
|
||||
ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
|
||||
let agent_ui_font_size =
|
||||
ThemeSettings::get_global(cx).agent_ui_font_size(cx) + delta;
|
||||
let _ = settings
|
||||
.theme
|
||||
.agent_font_size
|
||||
.insert(theme::clamp_font_size(agent_font_size).into());
|
||||
.agent_ui_font_size
|
||||
.insert(theme::clamp_font_size(agent_ui_font_size).into());
|
||||
});
|
||||
} else {
|
||||
theme::adjust_agent_font_size(cx, |size| size + delta);
|
||||
theme::adjust_agent_ui_font_size(cx, |size| size + delta);
|
||||
}
|
||||
}
|
||||
WhichFontSize::BufferFont => {
|
||||
@@ -1136,10 +1099,10 @@ impl AgentPanel {
|
||||
) {
|
||||
if action.persist {
|
||||
update_settings_file(self.fs.clone(), cx, move |settings, _| {
|
||||
settings.theme.agent_font_size = None;
|
||||
settings.theme.agent_ui_font_size = None;
|
||||
});
|
||||
} else {
|
||||
theme::reset_agent_font_size(cx);
|
||||
theme::reset_agent_ui_font_size(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1975,34 +1938,32 @@ impl AgentPanel {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
|
||||
this.item(
|
||||
ContextMenuEntry::new("New Codex Thread")
|
||||
.icon(IconName::AiOpenAi)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) =
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
AgentType::Codex,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
.item(
|
||||
ContextMenuEntry::new("New Codex Thread")
|
||||
.icon(IconName::AiOpenAi)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) =
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(
|
||||
AgentType::Codex,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Gemini CLI Thread")
|
||||
.icon(IconName::AiGemini)
|
||||
@@ -2570,7 +2531,7 @@ impl Render for AgentPanel {
|
||||
|
||||
match self.active_view.which_font_size_used() {
|
||||
WhichFontSize::AgentFont => {
|
||||
WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
|
||||
WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
|
||||
.size_full()
|
||||
.child(content)
|
||||
.into_any()
|
||||
|
||||
@@ -382,7 +382,7 @@ impl InlineAssistant {
|
||||
if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
|
||||
for assist_id in &editor_assists.assist_ids {
|
||||
let assist = &self.assists[assist_id];
|
||||
let range = assist.range.to_point(&snapshot.buffer_snapshot);
|
||||
let range = assist.range.to_point(&snapshot.buffer_snapshot());
|
||||
if range.start.row <= newest_selection.start.row
|
||||
&& newest_selection.end.row <= range.end.row
|
||||
{
|
||||
@@ -402,16 +402,16 @@ impl InlineAssistant {
|
||||
selection.end.row -= 1;
|
||||
}
|
||||
selection.end.column = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.line_len(MultiBufferRow(selection.end.row));
|
||||
} else if let Some(fold) =
|
||||
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
|
||||
{
|
||||
selection.start = fold.range().start;
|
||||
selection.end = fold.range().end;
|
||||
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot.max_row() {
|
||||
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot().max_row() {
|
||||
let chars = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.chars_at(Point::new(selection.end.row + 1, 0));
|
||||
|
||||
for c in chars {
|
||||
@@ -427,7 +427,7 @@ impl InlineAssistant {
|
||||
{
|
||||
selection.end.row += 1;
|
||||
selection.end.column = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.line_len(MultiBufferRow(selection.end.row));
|
||||
}
|
||||
}
|
||||
@@ -447,7 +447,7 @@ impl InlineAssistant {
|
||||
}
|
||||
selections.push(selection);
|
||||
}
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
let snapshot = &snapshot.buffer_snapshot();
|
||||
let newest_selection = newest_selection.unwrap();
|
||||
|
||||
let mut codegen_ranges = Vec::new();
|
||||
|
||||
@@ -15,7 +15,8 @@ use std::{
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::{
|
||||
HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
|
||||
DocumentationAside, DocumentationEdge, DocumentationSide, HighlightedLabel, LabelSize,
|
||||
ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
|
||||
};
|
||||
|
||||
/// Trait for types that can provide and manage agent profiles
|
||||
@@ -86,8 +87,8 @@ impl ProfileSelector {
|
||||
let picker = cx.new(|cx| {
|
||||
Picker::list(delegate, window, cx)
|
||||
.show_scrollbar(true)
|
||||
.width(rems(20.))
|
||||
.max_height(Some(rems(16.).into()))
|
||||
.width(rems(18.))
|
||||
.max_height(Some(rems(20.).into()))
|
||||
});
|
||||
|
||||
self.picker = Some(picker);
|
||||
@@ -536,18 +537,10 @@ impl PickerDelegate for ProfilePickerDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(
|
||||
v_flex()
|
||||
.child(HighlightedLabel::new(
|
||||
candidate.name.clone(),
|
||||
entry.positions.clone(),
|
||||
))
|
||||
.when_some(Self::documentation(candidate), |this, doc| {
|
||||
this.child(
|
||||
Label::new(doc).size(LabelSize::Small).color(Color::Muted),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(HighlightedLabel::new(
|
||||
candidate.name.clone(),
|
||||
entry.positions.clone(),
|
||||
))
|
||||
.when(is_active, |this| {
|
||||
this.end_slot(
|
||||
div()
|
||||
@@ -561,6 +554,36 @@ impl PickerDelegate for ProfilePickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn documentation_aside(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<DocumentationAside> {
|
||||
use std::rc::Rc;
|
||||
|
||||
let entry = match self.filtered_entries.get(self.selected_index)? {
|
||||
ProfilePickerEntry::Profile(entry) => entry,
|
||||
ProfilePickerEntry::Header(_) => return None,
|
||||
};
|
||||
|
||||
let candidate = self.candidates.get(entry.candidate_index)?;
|
||||
let docs_aside = Self::documentation(candidate)?.to_string();
|
||||
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let side = match settings.dock {
|
||||
settings::DockPosition::Left => DocumentationSide::Right,
|
||||
settings::DockPosition::Bottom | settings::DockPosition::Right => {
|
||||
DocumentationSide::Left
|
||||
}
|
||||
};
|
||||
|
||||
Some(DocumentationAside {
|
||||
side,
|
||||
edge: DocumentationEdge::Top,
|
||||
render: Rc::new(move |_| Label::new(docs_aside.clone()).into_any_element()),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_footer(
|
||||
&self,
|
||||
_: &mut Window,
|
||||
|
||||
@@ -16,8 +16,8 @@ anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
net.workspace = true
|
||||
proto.workspace = true
|
||||
smol.workspace = true
|
||||
log.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
@@ -25,3 +25,6 @@ zeroize.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["log"]
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
mod encrypted_password;
|
||||
|
||||
pub use encrypted_password::{EncryptedPassword, ProcessExt};
|
||||
pub use encrypted_password::{EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use net::async_net::UnixListener;
|
||||
use smol::lock::Mutex;
|
||||
use util::fs::make_file_executable;
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::ops::ControlFlow;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::{ffi::OsStr, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
@@ -14,9 +20,13 @@ use futures::{
|
||||
};
|
||||
use gpui::{AsyncApp, BackgroundExecutor, Task};
|
||||
use smol::fs;
|
||||
use util::ResultExt as _;
|
||||
use util::{ResultExt as _, debug_panic, maybe, paths::PathExt};
|
||||
|
||||
use crate::encrypted_password::decrypt;
|
||||
/// Path to the program used for askpass
|
||||
///
|
||||
/// On Unix and remote servers, this defaults to the current executable
|
||||
/// On Windows, this is set to the CLI variant of zed
|
||||
static ASKPASS_PROGRAM: OnceLock<std::path::PathBuf> = OnceLock::new();
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum AskPassResult {
|
||||
@@ -26,6 +36,7 @@ pub enum AskPassResult {
|
||||
|
||||
pub struct AskPassDelegate {
|
||||
tx: mpsc::UnboundedSender<(String, oneshot::Sender<EncryptedPassword>)>,
|
||||
executor: BackgroundExecutor,
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
@@ -43,24 +54,27 @@ impl AskPassDelegate {
|
||||
password_prompt(prompt, channel, cx);
|
||||
}
|
||||
});
|
||||
Self { tx, _task: task }
|
||||
Self {
|
||||
tx,
|
||||
_task: task,
|
||||
executor: cx.background_executor().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn ask_password(&mut self, prompt: String) -> Result<EncryptedPassword> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx.send((prompt, tx)).await?;
|
||||
Ok(rx.await?)
|
||||
pub fn ask_password(&mut self, prompt: String) -> Task<Option<EncryptedPassword>> {
|
||||
let mut this_tx = self.tx.clone();
|
||||
self.executor.spawn(async move {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
this_tx.send((prompt, tx)).await.ok()?;
|
||||
rx.await.ok()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AskPassSession {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
script_path: std::path::PathBuf,
|
||||
#[cfg(target_os = "windows")]
|
||||
askpass_helper: String,
|
||||
#[cfg(target_os = "windows")]
|
||||
secret: std::sync::Arc<OnceLock<EncryptedPassword>>,
|
||||
_askpass_task: Task<()>,
|
||||
askpass_task: PasswordProxy,
|
||||
askpass_opened_rx: Option<oneshot::Receiver<()>>,
|
||||
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
|
||||
}
|
||||
@@ -75,101 +89,57 @@ impl AskPassSession {
|
||||
/// You must retain this session until the master process exits.
|
||||
#[must_use]
|
||||
pub async fn new(executor: &BackgroundExecutor, mut delegate: AskPassDelegate) -> Result<Self> {
|
||||
use net::async_net::UnixListener;
|
||||
use util::fs::make_file_executable;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let secret = std::sync::Arc::new(OnceLock::new());
|
||||
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
|
||||
let askpass_socket = temp_dir.path().join("askpass.sock");
|
||||
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
|
||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
|
||||
let zed_cli_path =
|
||||
util::get_shell_safe_zed_cli_path().context("getting zed-cli path for askpass")?;
|
||||
|
||||
let askpass_opened_tx = Arc::new(Mutex::new(Some(askpass_opened_tx)));
|
||||
|
||||
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
|
||||
let mut kill_tx = Some(askpass_kill_master_tx);
|
||||
let kill_tx = Arc::new(Mutex::new(Some(askpass_kill_master_tx)));
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let askpass_secret = secret.clone();
|
||||
let askpass_task = executor.spawn(async move {
|
||||
let mut askpass_opened_tx = Some(askpass_opened_tx);
|
||||
let get_password = {
|
||||
let executor = executor.clone();
|
||||
|
||||
while let Ok((mut stream, _)) = listener.accept().await {
|
||||
if let Some(askpass_opened_tx) = askpass_opened_tx.take() {
|
||||
askpass_opened_tx.send(()).ok();
|
||||
}
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(&mut stream);
|
||||
if reader.read_until(b'\0', &mut buffer).await.is_err() {
|
||||
buffer.clear();
|
||||
}
|
||||
let prompt = String::from_utf8_lossy(&buffer);
|
||||
if let Some(password) = delegate
|
||||
.ask_password(prompt.to_string())
|
||||
.await
|
||||
.context("getting askpass password")
|
||||
.log_err()
|
||||
{
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
askpass_secret.get_or_init(|| password.clone());
|
||||
move |prompt| {
|
||||
let prompt = delegate.ask_password(prompt);
|
||||
let kill_tx = kill_tx.clone();
|
||||
let askpass_opened_tx = askpass_opened_tx.clone();
|
||||
#[cfg(target_os = "windows")]
|
||||
let askpass_secret = askpass_secret.clone();
|
||||
executor.spawn(async move {
|
||||
if let Some(askpass_opened_tx) = askpass_opened_tx.lock().await.take() {
|
||||
askpass_opened_tx.send(()).ok();
|
||||
}
|
||||
if let Ok(decrypted) = decrypt(password) {
|
||||
stream.write_all(decrypted.as_bytes()).await.log_err();
|
||||
if let Some(password) = prompt.await {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
_ = askpass_secret.set(password.clone());
|
||||
}
|
||||
ControlFlow::Continue(Ok(password))
|
||||
} else {
|
||||
if let Some(kill_tx) = kill_tx.lock().await.take() {
|
||||
kill_tx.send(()).log_err();
|
||||
}
|
||||
ControlFlow::Break(())
|
||||
}
|
||||
} else {
|
||||
if let Some(kill_tx) = kill_tx.take() {
|
||||
kill_tx.send(()).log_err();
|
||||
}
|
||||
// note: we expect the caller to drop this task when it's done.
|
||||
// We need to keep the stream open until the caller is done to avoid
|
||||
// spurious errors from ssh.
|
||||
std::future::pending::<()>().await;
|
||||
drop(stream);
|
||||
}
|
||||
})
|
||||
}
|
||||
drop(temp_dir)
|
||||
});
|
||||
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = generate_askpass_script(&zed_cli_path, &askpass_socket);
|
||||
fs::write(&askpass_script_path, askpass_script)
|
||||
.await
|
||||
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
|
||||
make_file_executable(&askpass_script_path).await?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let askpass_helper = format!(
|
||||
"powershell.exe -ExecutionPolicy Bypass -File {}",
|
||||
askpass_script_path.display()
|
||||
);
|
||||
};
|
||||
let askpass_task = PasswordProxy::new(get_password, executor.clone()).await?;
|
||||
|
||||
Ok(Self {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
script_path: askpass_script_path,
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
secret,
|
||||
#[cfg(target_os = "windows")]
|
||||
askpass_helper,
|
||||
|
||||
_askpass_task: askpass_task,
|
||||
askpass_task,
|
||||
askpass_kill_master_rx: Some(askpass_kill_master_rx),
|
||||
askpass_opened_rx: Some(askpass_opened_rx),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn script_path(&self) -> impl AsRef<OsStr> {
|
||||
&self.script_path
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn script_path(&self) -> impl AsRef<OsStr> {
|
||||
&self.askpass_helper
|
||||
}
|
||||
|
||||
// This will run the askpass task forever, resolving as many authentication requests as needed.
|
||||
// The caller is responsible for examining the result of their own commands and cancelling this
|
||||
// future when this is no longer needed. Note that this can only be called once, but due to the
|
||||
@@ -201,8 +171,109 @@ impl AskPassSession {
|
||||
pub fn get_password(&self) -> Option<EncryptedPassword> {
|
||||
self.secret.get().cloned()
|
||||
}
|
||||
|
||||
pub fn script_path(&self) -> impl AsRef<OsStr> {
|
||||
self.askpass_task.script_path()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PasswordProxy {
|
||||
_task: Task<()>,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
askpass_script_path: std::path::PathBuf,
|
||||
#[cfg(target_os = "windows")]
|
||||
askpass_helper: String,
|
||||
}
|
||||
|
||||
impl PasswordProxy {
|
||||
pub async fn new(
|
||||
mut get_password: impl FnMut(String) -> Task<ControlFlow<(), Result<EncryptedPassword>>>
|
||||
+ 'static
|
||||
+ Send
|
||||
+ Sync,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Result<Self> {
|
||||
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
|
||||
let askpass_socket = temp_dir.path().join("askpass.sock");
|
||||
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
|
||||
let current_exec =
|
||||
std::env::current_exe().context("Failed to determine current zed executable path.")?;
|
||||
|
||||
let askpass_program = ASKPASS_PROGRAM
|
||||
.get_or_init(|| current_exec)
|
||||
.try_shell_safe()
|
||||
.context("Failed to shell-escape Askpass program path.")?
|
||||
.to_string();
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = generate_askpass_script(&askpass_program, &askpass_socket);
|
||||
let _task = executor.spawn(async move {
|
||||
maybe!(async move {
|
||||
let listener =
|
||||
UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
|
||||
|
||||
while let Ok((mut stream, _)) = listener.accept().await {
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(&mut stream);
|
||||
if reader.read_until(b'\0', &mut buffer).await.is_err() {
|
||||
buffer.clear();
|
||||
}
|
||||
let prompt = String::from_utf8_lossy(&buffer).into_owned();
|
||||
let password = get_password(prompt).await;
|
||||
match password {
|
||||
ControlFlow::Continue(password) => {
|
||||
if let Ok(password) = password
|
||||
&& let Ok(decrypted) =
|
||||
password.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)
|
||||
{
|
||||
stream.write_all(decrypted.as_bytes()).await.log_err();
|
||||
}
|
||||
}
|
||||
ControlFlow::Break(()) => {
|
||||
// note: we expect the caller to drop this task when it's done.
|
||||
// We need to keep the stream open until the caller is done to avoid
|
||||
// spurious errors from ssh.
|
||||
std::future::pending::<()>().await;
|
||||
drop(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(temp_dir);
|
||||
Result::<_, anyhow::Error>::Ok(())
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
});
|
||||
|
||||
fs::write(&askpass_script_path, askpass_script)
|
||||
.await
|
||||
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
|
||||
make_file_executable(&askpass_script_path).await?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let askpass_helper = format!(
|
||||
"powershell.exe -ExecutionPolicy Bypass -File {}",
|
||||
askpass_script_path.display()
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
_task,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
askpass_script_path,
|
||||
#[cfg(target_os = "windows")]
|
||||
askpass_helper,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn script_path(&self) -> impl AsRef<OsStr> {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
&self.askpass_script_path
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
&self.askpass_helper
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The main function for when Zed is running in netcat mode for use in askpass.
|
||||
/// Called from both the remote server binary and the zed binary in their respective main functions.
|
||||
pub fn main(socket: &str) {
|
||||
@@ -249,12 +320,17 @@ pub fn main(socket: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_askpass_program(path: std::path::PathBuf) {
|
||||
if ASKPASS_PROGRAM.set(path).is_err() {
|
||||
debug_panic!("askpass program has already been set");
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path) -> String {
|
||||
fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
|
||||
format!(
|
||||
"{shebang}\n{print_args} | {zed_cli} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
zed_cli = zed_cli_path,
|
||||
"{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
@@ -263,13 +339,12 @@ fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path)
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path) -> String {
|
||||
fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
|
||||
format!(
|
||||
r#"
|
||||
$ErrorActionPreference = 'Stop';
|
||||
($args -join [char]0) | & "{zed_cli}" --askpass={askpass_socket} 2> $null
|
||||
($args -join [char]0) | & "{askpass_program}" --askpass={askpass_socket} 2> $null
|
||||
"#,
|
||||
zed_cli = zed_cli_path,
|
||||
askpass_socket = askpass_socket.display(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,27 +21,6 @@ type LengthWithoutPadding = u32;
|
||||
#[derive(Clone)]
|
||||
pub struct EncryptedPassword(Vec<u8>, LengthWithoutPadding);
|
||||
|
||||
pub trait ProcessExt {
|
||||
fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self;
|
||||
}
|
||||
|
||||
impl ProcessExt for smol::process::Command {
|
||||
fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self {
|
||||
if let Ok(password) = decrypt(value) {
|
||||
self.env(name, password);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<EncryptedPassword> for proto::AskPassResponse {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(pw: EncryptedPassword) -> Result<Self, Self::Error> {
|
||||
let pw = decrypt(pw)?;
|
||||
Ok(Self { response: pw })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EncryptedPassword {
|
||||
fn drop(&mut self) {
|
||||
self.0.zeroize();
|
||||
@@ -79,38 +58,45 @@ impl TryFrom<&str> for EncryptedPassword {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt(mut password: EncryptedPassword) -> Result<String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use anyhow::Context;
|
||||
use windows::Win32::Security::Cryptography::{
|
||||
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS, CryptUnprotectMemory,
|
||||
};
|
||||
assert_eq!(
|
||||
password.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
|
||||
0,
|
||||
"Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
|
||||
password.0.len(),
|
||||
CRYPTPROTECTMEMORY_BLOCK_SIZE
|
||||
);
|
||||
if password.1 != 0 {
|
||||
unsafe {
|
||||
CryptUnprotectMemory(
|
||||
password.0.as_mut_ptr() as _,
|
||||
password.0.len().try_into()?,
|
||||
CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
)
|
||||
.context("while decrypting a SSH password")?
|
||||
/// Read the docs for [EncryptedPassword]; please take care of not storing the plaintext string in memory for extended
|
||||
/// periods of time.
|
||||
pub struct IKnowWhatIAmDoingAndIHaveReadTheDocs;
|
||||
|
||||
impl EncryptedPassword {
|
||||
pub fn decrypt(mut self, _: IKnowWhatIAmDoingAndIHaveReadTheDocs) -> Result<String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use anyhow::Context;
|
||||
use windows::Win32::Security::Cryptography::{
|
||||
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
CryptUnprotectMemory,
|
||||
};
|
||||
assert_eq!(
|
||||
self.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
|
||||
0,
|
||||
"Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
|
||||
self.0.len(),
|
||||
CRYPTPROTECTMEMORY_BLOCK_SIZE
|
||||
);
|
||||
if self.1 != 0 {
|
||||
unsafe {
|
||||
CryptUnprotectMemory(
|
||||
self.0.as_mut_ptr() as _,
|
||||
self.0.len().try_into()?,
|
||||
CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
)
|
||||
.context("while decrypting a SSH password")?
|
||||
};
|
||||
|
||||
{
|
||||
// Remove padding
|
||||
_ = password.0.drain(password.1 as usize..);
|
||||
{
|
||||
// Remove padding
|
||||
_ = self.0.drain(self.1 as usize..);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(std::mem::take(&mut password.0))?)
|
||||
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
Ok(String::from_utf8(std::mem::take(&mut password.0))?)
|
||||
}
|
||||
|
||||
@@ -1003,7 +1003,7 @@ impl ToolCard for EditFileToolCard {
|
||||
font_size: Some(
|
||||
TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
|
||||
.to_pixels(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
|
||||
.into(),
|
||||
),
|
||||
..TextStyleRefinement::default()
|
||||
|
||||
@@ -18,7 +18,7 @@ use portable_pty::{CommandBuilder, PtySize, native_pty_system};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsLocation};
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
@@ -32,8 +32,8 @@ use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
|
||||
use util::{
|
||||
ResultExt, get_default_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
|
||||
time::duration_alt_display,
|
||||
ResultExt, get_default_system_shell_preferring_bash, markdown::MarkdownInlineCode,
|
||||
size::format_file_size, time::duration_alt_display,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -122,16 +122,28 @@ impl Tool for TerminalTool {
|
||||
let cwd = working_dir.clone();
|
||||
let env = match &cwd {
|
||||
Some(dir) => project.update(cx, |project, cx| {
|
||||
let shell = TerminalSettings::get_global(cx).shell.clone();
|
||||
let worktree = project.find_worktree(dir.as_path(), cx);
|
||||
let shell = TerminalSettings::get(
|
||||
worktree.as_ref().map(|(worktree, path)| SettingsLocation {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: &path,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
.shell
|
||||
.clone();
|
||||
project.directory_environment(&shell, dir.as_path().into(), cx)
|
||||
}),
|
||||
None => Task::ready(None).shared(),
|
||||
};
|
||||
let remote_shell = project.update(cx, |project, cx| {
|
||||
project
|
||||
.remote_client()
|
||||
.and_then(|r| r.read(cx).default_system_shell())
|
||||
});
|
||||
let is_windows = project.read(cx).path_style(cx).is_windows();
|
||||
let shell = project
|
||||
.update(cx, |project, cx| {
|
||||
project
|
||||
.remote_client()
|
||||
.and_then(|r| r.read(cx).default_system_shell())
|
||||
})
|
||||
.unwrap_or_else(|| get_default_system_shell_preferring_bash());
|
||||
|
||||
let env = cx.spawn(async move |_| {
|
||||
let mut env = env.await.unwrap_or_default();
|
||||
@@ -144,12 +156,9 @@ impl Tool for TerminalTool {
|
||||
let build_cmd = {
|
||||
let input_command = input.command.clone();
|
||||
move || {
|
||||
ShellBuilder::new(
|
||||
remote_shell.as_deref(),
|
||||
&Shell::Program(get_default_system_shell()),
|
||||
)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(input_command.clone()), &[])
|
||||
ShellBuilder::new(&Shell::Program(shell), is_windows)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(input_command), &[])
|
||||
}
|
||||
};
|
||||
|
||||
@@ -478,7 +487,7 @@ impl ToolCard for TerminalToolCard {
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.or_else(|| env::current_dir().ok())
|
||||
.map(|path| format!("{}", path.display()))
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| "current directory".to_string());
|
||||
|
||||
let header = h_flex()
|
||||
@@ -696,7 +705,6 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use terminal::terminal_settings::TerminalSettings;
|
||||
use theme::ThemeSettings;
|
||||
use util::{ResultExt as _, test::TempTree};
|
||||
|
||||
use super::*;
|
||||
@@ -711,7 +719,7 @@ mod tests {
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
TerminalSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, BackgroundExecutor, BorrowAppContext, Global};
|
||||
use log::info;
|
||||
|
||||
#[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;
|
||||
|
||||
@@ -42,7 +42,7 @@ pub struct AudioSettings {
|
||||
|
||||
/// Configuration of audio in Zed
|
||||
impl Settings for AudioSettings {
|
||||
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
let audio = &content.audio.as_ref().unwrap();
|
||||
AudioSettings {
|
||||
rodio_audio: audio.rodio_audio.unwrap(),
|
||||
|
||||
@@ -127,7 +127,7 @@ struct AutoUpdateSetting(bool);
|
||||
///
|
||||
/// Default: true
|
||||
impl Settings for AutoUpdateSetting {
|
||||
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
Self(content.auto_update.unwrap())
|
||||
}
|
||||
}
|
||||
@@ -649,7 +649,7 @@ impl AutoUpdater {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
anyhow::ensure!(
|
||||
which::which("rsync").is_ok(),
|
||||
"Aborting. Could not find rsync which is required for auto-updates."
|
||||
"Could not auto-update because the required rsync utility was not found."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -658,7 +658,7 @@ impl AutoUpdater {
|
||||
let filename = match OS {
|
||||
"macos" => anyhow::Ok("Zed.dmg"),
|
||||
"linux" => Ok("zed.tar.gz"),
|
||||
"windows" => Ok("zed_editor_installer.exe"),
|
||||
"windows" => Ok("Zed.exe"),
|
||||
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
|
||||
}?;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::{
|
||||
os::windows::process::CommandExt,
|
||||
path::Path,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -7,7 +6,6 @@ use std::{
|
||||
use anyhow::{Context as _, Result};
|
||||
use windows::Win32::{
|
||||
Foundation::{HWND, LPARAM, WPARAM},
|
||||
System::Threading::CREATE_NEW_PROCESS_GROUP,
|
||||
UI::WindowsAndMessaging::PostMessageW,
|
||||
};
|
||||
|
||||
@@ -207,9 +205,7 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool)
|
||||
}
|
||||
if launch {
|
||||
#[allow(clippy::disallowed_methods, reason = "doesn't run in the main binary")]
|
||||
let _ = std::process::Command::new(app_dir.join("Zed.exe"))
|
||||
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
|
||||
.spawn();
|
||||
let _ = std::process::Command::new(app_dir.join("Zed.exe")).spawn();
|
||||
}
|
||||
log::info!("Update completed successfully");
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use gpui::App;
|
||||
use settings::Settings;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -8,17 +7,11 @@ pub struct CallSettings {
|
||||
}
|
||||
|
||||
impl Settings for CallSettings {
|
||||
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
let call = content.calls.clone().unwrap();
|
||||
CallSettings {
|
||||
mute_on_join: call.mute_on_join.unwrap(),
|
||||
share_on_join: call.share_on_join.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn import_from_vscode(
|
||||
_vscode: &settings::VsCodeSettings,
|
||||
_current: &mut settings::SettingsContent,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,15 +731,15 @@ mod windows {
|
||||
Storage::FileSystem::{
|
||||
CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
|
||||
},
|
||||
System::Threading::{CREATE_NEW_PROCESS_GROUP, CreateMutexW},
|
||||
System::Threading::CreateMutexW,
|
||||
},
|
||||
core::HSTRING,
|
||||
};
|
||||
|
||||
use crate::{Detect, InstalledApp};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitStatus;
|
||||
use std::{io, os::windows::process::CommandExt};
|
||||
|
||||
fn check_single_instance() -> bool {
|
||||
let mutex = unsafe {
|
||||
@@ -778,7 +778,6 @@ mod windows {
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
|
||||
if check_single_instance() {
|
||||
std::process::Command::new(self.0.clone())
|
||||
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
|
||||
.arg(ipc_url)
|
||||
.spawn()?;
|
||||
} else {
|
||||
|
||||
@@ -101,7 +101,7 @@ pub struct ClientSettings {
|
||||
}
|
||||
|
||||
impl Settings for ClientSettings {
|
||||
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
if let Some(server_url) = &*ZED_SERVER_URL {
|
||||
return Self {
|
||||
server_url: server_url.clone(),
|
||||
@@ -133,7 +133,7 @@ impl ProxySettings {
|
||||
}
|
||||
|
||||
impl Settings for ProxySettings {
|
||||
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
Self {
|
||||
proxy: content.proxy.clone(),
|
||||
}
|
||||
@@ -519,7 +519,7 @@ pub struct TelemetrySettings {
|
||||
}
|
||||
|
||||
impl settings::Settings for TelemetrySettings {
|
||||
fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
|
||||
fn from_settings(content: &SettingsContent) -> Self {
|
||||
Self {
|
||||
diagnostics: content.telemetry.as_ref().unwrap().diagnostics.unwrap(),
|
||||
metrics: content.telemetry.as_ref().unwrap().metrics.unwrap(),
|
||||
|
||||
@@ -127,7 +127,6 @@ pub struct DeclarationScoreComponents {
|
||||
pub declaration_count: usize,
|
||||
pub reference_line_distance: u32,
|
||||
pub declaration_line_distance: u32,
|
||||
pub declaration_line_distance_rank: usize,
|
||||
pub excerpt_vs_item_jaccard: f32,
|
||||
pub excerpt_vs_signature_jaccard: f32,
|
||||
pub adjacent_vs_item_jaccard: f32,
|
||||
@@ -136,6 +135,15 @@ pub struct DeclarationScoreComponents {
|
||||
pub excerpt_vs_signature_weighted_overlap: f32,
|
||||
pub adjacent_vs_item_weighted_overlap: f32,
|
||||
pub adjacent_vs_signature_weighted_overlap: f32,
|
||||
pub path_import_match_count: usize,
|
||||
pub wildcard_path_import_match_count: usize,
|
||||
pub import_similarity: f32,
|
||||
pub max_import_similarity: f32,
|
||||
pub normalized_import_similarity: f32,
|
||||
pub wildcard_import_similarity: f32,
|
||||
pub normalized_wildcard_import_similarity: f32,
|
||||
pub included_by_others: usize,
|
||||
pub includes_others: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
28
crates/codestral/Cargo.toml
Normal file
28
crates/codestral/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "codestral"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
path = "src/codestral.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
edit_prediction_context.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
mistral.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
text.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
1
crates/codestral/LICENSE-GPL
Symbolic link
1
crates/codestral/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
381
crates/codestral/src/codestral.rs
Normal file
381
crates/codestral/src/codestral.rs
Normal file
@@ -0,0 +1,381 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use edit_prediction::{Direction, EditPrediction, EditPredictionProvider};
|
||||
use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{App, Context, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
use language::{
|
||||
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview, ToPoint,
|
||||
};
|
||||
use language_models::MistralLanguageModelProvider;
|
||||
use mistral::CODESTRAL_API_URL;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use text::ToOffset;
|
||||
|
||||
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(150);
|
||||
|
||||
const EXCERPT_OPTIONS: EditPredictionExcerptOptions = EditPredictionExcerptOptions {
|
||||
max_bytes: 1050,
|
||||
min_bytes: 525,
|
||||
target_before_cursor_over_total_bytes: 0.66,
|
||||
};
|
||||
|
||||
/// Represents a completion that has been received and processed from Codestral.
|
||||
/// This struct maintains the state needed to interpolate the completion as the user types.
|
||||
#[derive(Clone)]
|
||||
struct CurrentCompletion {
|
||||
/// The buffer snapshot at the time the completion was generated.
|
||||
/// Used to detect changes and interpolate edits.
|
||||
snapshot: BufferSnapshot,
|
||||
/// The edits that should be applied to transform the original text into the predicted text.
|
||||
/// Each edit is a range in the buffer and the text to replace it with.
|
||||
edits: Arc<[(Range<Anchor>, String)]>,
|
||||
/// Preview of how the buffer will look after applying the edits.
|
||||
edit_preview: EditPreview,
|
||||
}
|
||||
|
||||
impl CurrentCompletion {
|
||||
/// Attempts to adjust the edits based on changes made to the buffer since the completion was generated.
|
||||
/// Returns None if the user's edits conflict with the predicted edits.
|
||||
fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
|
||||
edit_prediction::interpolate_edits(&self.snapshot, new_snapshot, &self.edits)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodestralCompletionProvider {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
pending_request: Option<Task<Result<()>>>,
|
||||
current_completion: Option<CurrentCompletion>,
|
||||
}
|
||||
|
||||
impl CodestralCompletionProvider {
|
||||
pub fn new(http_client: Arc<dyn HttpClient>) -> Self {
|
||||
Self {
|
||||
http_client,
|
||||
pending_request: None,
|
||||
current_completion: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_api_key(cx: &App) -> bool {
|
||||
Self::api_key(cx).is_some()
|
||||
}
|
||||
|
||||
fn api_key(cx: &App) -> Option<Arc<str>> {
|
||||
MistralLanguageModelProvider::try_global(cx)
|
||||
.and_then(|provider| provider.codestral_api_key(CODESTRAL_API_URL, cx))
|
||||
}
|
||||
|
||||
/// Uses Codestral's Fill-in-the-Middle API for code completion.
|
||||
async fn fetch_completion(
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
api_key: &str,
|
||||
prompt: String,
|
||||
suffix: String,
|
||||
model: String,
|
||||
max_tokens: Option<u32>,
|
||||
) -> Result<String> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
log::debug!(
|
||||
"Codestral: Requesting completion (model: {}, max_tokens: {:?})",
|
||||
model,
|
||||
max_tokens
|
||||
);
|
||||
|
||||
let request = CodestralRequest {
|
||||
model,
|
||||
prompt,
|
||||
suffix: if suffix.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(suffix)
|
||||
},
|
||||
max_tokens: max_tokens.or(Some(350)),
|
||||
temperature: Some(0.2),
|
||||
top_p: Some(1.0),
|
||||
stream: Some(false),
|
||||
stop: None,
|
||||
random_seed: None,
|
||||
min_tokens: None,
|
||||
};
|
||||
|
||||
let request_body = serde_json::to_string(&request)?;
|
||||
|
||||
log::debug!("Codestral: Sending FIM request");
|
||||
|
||||
let http_request = http_client::Request::builder()
|
||||
.method(http_client::Method::POST)
|
||||
.uri(format!("{}/v1/fim/completions", CODESTRAL_API_URL))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.body(http_client::AsyncBody::from(request_body))?;
|
||||
|
||||
let mut response = http_client.send(http_request).await?;
|
||||
let status = response.status();
|
||||
|
||||
log::debug!("Codestral: Response status: {}", status);
|
||||
|
||||
if !status.is_success() {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
return Err(anyhow::anyhow!(
|
||||
"Codestral API error: {} - {}",
|
||||
status,
|
||||
body
|
||||
));
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
let codestral_response: CodestralResponse = serde_json::from_str(&body)?;
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
if let Some(choice) = codestral_response.choices.first() {
|
||||
let completion = &choice.message.content;
|
||||
|
||||
log::debug!(
|
||||
"Codestral: Completion received ({} tokens, {:.2}s)",
|
||||
codestral_response.usage.completion_tokens,
|
||||
elapsed.as_secs_f64()
|
||||
);
|
||||
|
||||
// Return just the completion text for insertion at cursor
|
||||
Ok(completion.clone())
|
||||
} else {
|
||||
log::error!("Codestral: No completion returned in response");
|
||||
Err(anyhow::anyhow!("No completion returned from Codestral"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EditPredictionProvider for CodestralCompletionProvider {
|
||||
fn name() -> &'static str {
|
||||
"codestral"
|
||||
}
|
||||
|
||||
fn display_name() -> &'static str {
|
||||
"Codestral"
|
||||
}
|
||||
|
||||
fn show_completions_in_menu() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_enabled(&self, _buffer: &Entity<Buffer>, _cursor_position: Anchor, cx: &App) -> bool {
|
||||
Self::api_key(cx).is_some()
|
||||
}
|
||||
|
||||
fn is_refreshing(&self) -> bool {
|
||||
self.pending_request.is_some()
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
debounce: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
log::debug!("Codestral: Refresh called (debounce: {})", debounce);
|
||||
|
||||
let Some(api_key) = Self::api_key(cx) else {
|
||||
log::warn!("Codestral: No API key configured, skipping refresh");
|
||||
return;
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
// Check if current completion is still valid
|
||||
if let Some(current_completion) = self.current_completion.as_ref() {
|
||||
if current_completion.interpolate(&snapshot).is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let http_client = self.http_client.clone();
|
||||
|
||||
// Get settings
|
||||
let settings = all_language_settings(None, cx);
|
||||
let model = settings
|
||||
.edit_predictions
|
||||
.codestral
|
||||
.model
|
||||
.clone()
|
||||
.unwrap_or_else(|| "codestral-latest".to_string());
|
||||
let max_tokens = settings.edit_predictions.codestral.max_tokens;
|
||||
|
||||
self.pending_request = Some(cx.spawn(async move |this, cx| {
|
||||
if debounce {
|
||||
log::debug!("Codestral: Debouncing for {:?}", DEBOUNCE_TIMEOUT);
|
||||
smol::Timer::after(DEBOUNCE_TIMEOUT).await;
|
||||
}
|
||||
|
||||
let cursor_offset = cursor_position.to_offset(&snapshot);
|
||||
let cursor_point = cursor_offset.to_point(&snapshot);
|
||||
let excerpt = EditPredictionExcerpt::select_from_buffer(
|
||||
cursor_point,
|
||||
&snapshot,
|
||||
&EXCERPT_OPTIONS,
|
||||
None,
|
||||
)
|
||||
.context("Line containing cursor doesn't fit in excerpt max bytes")?;
|
||||
|
||||
let excerpt_text = excerpt.text(&snapshot);
|
||||
let cursor_within_excerpt = cursor_offset
|
||||
.saturating_sub(excerpt.range.start)
|
||||
.min(excerpt_text.body.len());
|
||||
let prompt = excerpt_text.body[..cursor_within_excerpt].to_string();
|
||||
let suffix = excerpt_text.body[cursor_within_excerpt..].to_string();
|
||||
|
||||
let completion_text = match Self::fetch_completion(
|
||||
http_client,
|
||||
&api_key,
|
||||
prompt,
|
||||
suffix,
|
||||
model,
|
||||
max_tokens,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(completion) => completion,
|
||||
Err(e) => {
|
||||
log::error!("Codestral: Failed to fetch completion: {}", e);
|
||||
this.update(cx, |this, cx| {
|
||||
this.pending_request = None;
|
||||
cx.notify();
|
||||
})?;
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
if completion_text.trim().is_empty() {
|
||||
log::debug!("Codestral: Completion was empty after trimming; ignoring");
|
||||
this.update(cx, |this, cx| {
|
||||
this.pending_request = None;
|
||||
cx.notify();
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let edits: Arc<[(Range<Anchor>, String)]> =
|
||||
vec![(cursor_position..cursor_position, completion_text)].into();
|
||||
let edit_preview = buffer
|
||||
.read_with(cx, |buffer, cx| buffer.preview_edits(edits.clone(), cx))?
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.current_completion = Some(CurrentCompletion {
|
||||
snapshot,
|
||||
edits,
|
||||
edit_preview,
|
||||
});
|
||||
this.pending_request = None;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_cursor_position: Anchor,
|
||||
_direction: Direction,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
// Codestral doesn't support multiple completions, so cycling does nothing
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut Context<Self>) {
|
||||
log::debug!("Codestral: Completion accepted");
|
||||
self.pending_request = None;
|
||||
self.current_completion = None;
|
||||
}
|
||||
|
||||
fn discard(&mut self, _cx: &mut Context<Self>) {
|
||||
log::debug!("Codestral: Completion discarded");
|
||||
self.pending_request = None;
|
||||
self.current_completion = None;
|
||||
}
|
||||
|
||||
/// Returns the completion suggestion, adjusted or invalidated based on user edits
|
||||
fn suggest(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
_cursor_position: Anchor,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<EditPrediction> {
|
||||
let current_completion = self.current_completion.as_ref()?;
|
||||
let buffer = buffer.read(cx);
|
||||
let edits = current_completion.interpolate(&buffer.snapshot())?;
|
||||
if edits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(EditPrediction::Local {
|
||||
id: None,
|
||||
edits,
|
||||
edit_preview: Some(current_completion.edit_preview.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CodestralRequest {
|
||||
pub model: String,
|
||||
pub prompt: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suffix: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_tokens: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub top_p: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stop: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub random_seed: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CodestralResponse {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub model: String,
|
||||
pub usage: Usage,
|
||||
pub created: u64,
|
||||
pub choices: Vec<Choice>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Usage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Choice {
|
||||
pub index: u32,
|
||||
pub message: Message,
|
||||
pub finish_reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Message {
|
||||
pub content: String,
|
||||
pub role: String,
|
||||
}
|
||||
@@ -97,6 +97,7 @@ CREATE TABLE "worktree_entries" (
|
||||
"is_external" BOOL NOT NULL,
|
||||
"is_ignored" BOOL NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
"is_hidden" BOOL NOT NULL,
|
||||
"git_status" INTEGER,
|
||||
"is_fifo" BOOL NOT NULL,
|
||||
PRIMARY KEY (project_id, worktree_id, id),
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "worktree_entries"
|
||||
ADD "is_hidden" BOOL NOT NULL DEFAULT FALSE;
|
||||
@@ -282,6 +282,7 @@ impl Database {
|
||||
git_status: ActiveValue::set(None),
|
||||
is_external: ActiveValue::set(entry.is_external),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
is_hidden: ActiveValue::set(entry.is_hidden),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_fifo: ActiveValue::set(entry.is_fifo),
|
||||
}
|
||||
@@ -300,6 +301,7 @@ impl Database {
|
||||
worktree_entry::Column::MtimeNanos,
|
||||
worktree_entry::Column::CanonicalPath,
|
||||
worktree_entry::Column::IsIgnored,
|
||||
worktree_entry::Column::IsHidden,
|
||||
worktree_entry::Column::ScanId,
|
||||
])
|
||||
.to_owned(),
|
||||
@@ -905,6 +907,7 @@ impl Database {
|
||||
canonical_path: db_entry.canonical_path,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
is_external: db_entry.is_external,
|
||||
is_hidden: db_entry.is_hidden,
|
||||
// This is only used in the summarization backlog, so if it's None,
|
||||
// that just means we won't be able to detect when to resummarize
|
||||
// based on total number of backlogged bytes - instead, we'd go
|
||||
|
||||
@@ -671,6 +671,7 @@ impl Database {
|
||||
canonical_path: db_entry.canonical_path,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
is_external: db_entry.is_external,
|
||||
is_hidden: db_entry.is_hidden,
|
||||
// This is only used in the summarization backlog, so if it's None,
|
||||
// that just means we won't be able to detect when to resummarize
|
||||
// based on total number of backlogged bytes - instead, we'd go
|
||||
|
||||
@@ -19,6 +19,7 @@ pub struct Model {
|
||||
pub is_ignored: bool,
|
||||
pub is_external: bool,
|
||||
pub is_deleted: bool,
|
||||
pub is_hidden: bool,
|
||||
pub scan_id: i64,
|
||||
pub is_fifo: bool,
|
||||
pub canonical_path: Option<String>,
|
||||
|
||||
@@ -323,8 +323,8 @@ fn assert_remote_selections(
|
||||
let CollaboratorId::PeerId(peer_id) = s.collaborator_id else {
|
||||
panic!("unexpected collaborator id");
|
||||
};
|
||||
let start = s.selection.start.to_offset(&snapshot.buffer_snapshot);
|
||||
let end = s.selection.end.to_offset(&snapshot.buffer_snapshot);
|
||||
let start = s.selection.start.to_offset(snapshot.buffer_snapshot());
|
||||
let end = s.selection.end.to_offset(snapshot.buffer_snapshot());
|
||||
let user_id = collaborators.get(&peer_id).unwrap().user_id;
|
||||
let participant_index = hub.user_participant_indices(cx).get(&user_id).copied();
|
||||
(participant_index, start..end)
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use editor::{
|
||||
DocumentColorsRenderMode, Editor, RowInfo, SelectionEffects,
|
||||
DocumentColorsRenderMode, Editor, FETCH_COLORS_DEBOUNCE_TIMEOUT, RowInfo, SelectionEffects,
|
||||
actions::{
|
||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
|
||||
ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
|
||||
@@ -1272,7 +1272,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
fake_language_server.start_progress("the-token").await;
|
||||
|
||||
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
|
||||
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
||||
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||
token: lsp::NumberOrString::String("the-token".to_string()),
|
||||
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
|
||||
lsp::WorkDoneProgressReport {
|
||||
@@ -1306,7 +1306,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
});
|
||||
|
||||
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
|
||||
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
||||
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||
token: lsp::NumberOrString::String("the-token".to_string()),
|
||||
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
|
||||
lsp::WorkDoneProgressReport {
|
||||
@@ -2041,6 +2041,10 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
});
|
||||
}
|
||||
|
||||
// This test started hanging on seed 2 after the theme settings
|
||||
// PR. The hypothesis is that it's been buggy for a while, but got lucky
|
||||
// on seeds.
|
||||
#[ignore]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_inlay_hint_refresh_is_forwarded(
|
||||
cx_a: &mut TestAppContext,
|
||||
@@ -2405,6 +2409,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
|
||||
.unwrap();
|
||||
|
||||
color_request_handle.next().await.unwrap();
|
||||
executor.advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
|
||||
executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
@@ -2844,7 +2849,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
});
|
||||
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
range: lsp::Range {
|
||||
@@ -2865,7 +2870,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
},
|
||||
);
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
range: lsp::Range {
|
||||
@@ -2887,7 +2892,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
);
|
||||
|
||||
if should_stream_workspace_diagnostic {
|
||||
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
||||
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||
token: expected_workspace_diagnostic_token.clone(),
|
||||
value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
|
||||
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
|
||||
@@ -3069,7 +3074,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
});
|
||||
|
||||
if should_stream_workspace_diagnostic {
|
||||
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
||||
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||
token: expected_workspace_diagnostic_token.clone(),
|
||||
value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
|
||||
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
|
||||
|
||||
@@ -84,7 +84,11 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
|
||||
diff.update(cx_b, |diff, cx| {
|
||||
assert_eq!(
|
||||
diff.excerpt_paths(cx),
|
||||
vec!["changed.txt", "deleted.txt", "created.txt"]
|
||||
vec![
|
||||
rel_path("changed.txt").into_arc(),
|
||||
rel_path("deleted.txt").into_arc(),
|
||||
rel_path("created.txt").into_arc()
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -121,7 +125,11 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
|
||||
diff.update(cx_b, |diff, cx| {
|
||||
assert_eq!(
|
||||
diff.excerpt_paths(cx),
|
||||
vec!["deleted.txt", "unchanged.txt", "created.txt"]
|
||||
vec![
|
||||
rel_path("deleted.txt").into_arc(),
|
||||
rel_path("unchanged.txt").into_arc(),
|
||||
rel_path("created.txt").into_arc()
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4077,7 +4077,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await;
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
@@ -4097,7 +4097,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
.await
|
||||
.unwrap();
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
@@ -4171,7 +4171,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
|
||||
// Simulate a language server reporting more errors for a file.
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![
|
||||
@@ -4269,7 +4269,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
|
||||
// Simulate a language server reporting no errors for a file.
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
version: None,
|
||||
diagnostics: Vec::new(),
|
||||
@@ -4365,7 +4365,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
.await
|
||||
.into_response()
|
||||
.unwrap();
|
||||
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
||||
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
|
||||
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
|
||||
lsp::WorkDoneProgressBegin {
|
||||
@@ -4376,7 +4376,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
});
|
||||
for file_name in file_names {
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Uri::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
@@ -4389,7 +4389,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
},
|
||||
);
|
||||
}
|
||||
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
||||
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
|
||||
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
|
||||
lsp::WorkDoneProgressEnd { message: None },
|
||||
|
||||
@@ -183,9 +183,10 @@ pub async fn run_randomized_test<T: RandomizedTest>(
|
||||
|
||||
for (client, cx) in clients {
|
||||
cx.update(|cx| {
|
||||
let store = cx.remove_global::<SettingsStore>();
|
||||
let settings = cx.remove_global::<SettingsStore>();
|
||||
cx.clear_globals();
|
||||
cx.set_global(store);
|
||||
cx.set_global(settings);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
drop(client);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,6 +172,7 @@ impl TestServer {
|
||||
}
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
});
|
||||
|
||||
@@ -248,7 +248,7 @@ impl ChannelView {
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
|
||||
if let Some(outline) = snapshot.buffer_snapshot.outline(None)
|
||||
if let Some(outline) = snapshot.buffer_snapshot().outline(None)
|
||||
&& let Some(item) = outline
|
||||
.items
|
||||
.iter()
|
||||
@@ -305,7 +305,7 @@ impl ChannelView {
|
||||
|
||||
let mut closest_heading = None;
|
||||
|
||||
if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
|
||||
if let Some(outline) = snapshot.buffer_snapshot().outline(None) {
|
||||
for item in outline.items {
|
||||
if item.range.start.to_display_point(&snapshot) > position {
|
||||
break;
|
||||
@@ -508,10 +508,6 @@ impl Item for ChannelView {
|
||||
}))
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
&mut self,
|
||||
data: Box<dyn Any>,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub struct NotificationPanelSettings {
|
||||
}
|
||||
|
||||
impl Settings for CollaborationPanelSettings {
|
||||
fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
let panel = content.collaboration_panel.as_ref().unwrap();
|
||||
|
||||
Self {
|
||||
@@ -30,7 +30,7 @@ impl Settings for CollaborationPanelSettings {
|
||||
}
|
||||
|
||||
impl Settings for NotificationPanelSettings {
|
||||
fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
let panel = content.notification_panel.as_ref().unwrap();
|
||||
return Self {
|
||||
button: panel.button.unwrap(),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "collections"
|
||||
name = "zed-collections"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
publish = true
|
||||
license = "Apache-2.0"
|
||||
description = "Standard collection type re-exports used by Zed and GPUI"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -97,11 +97,10 @@ impl CommandPaletteFilter {
|
||||
pub struct CommandInterceptResult {
|
||||
/// The action produced as a result of the interception.
|
||||
pub action: Box<dyn Action>,
|
||||
// TODO: Document this field.
|
||||
#[allow(missing_docs)]
|
||||
/// The display string to show in the command palette for this result.
|
||||
pub string: String,
|
||||
// TODO: Document this field.
|
||||
#[allow(missing_docs)]
|
||||
/// The character positions in the string that match the query.
|
||||
/// Used for highlighting matched characters in the command palette UI.
|
||||
pub positions: Vec<usize>,
|
||||
}
|
||||
|
||||
|
||||
@@ -41,12 +41,9 @@ impl StdioTransport {
|
||||
command.current_dir(working_directory);
|
||||
}
|
||||
|
||||
let mut server = command.spawn().with_context(|| {
|
||||
format!(
|
||||
"failed to spawn command. (path={:?}, args={:?})",
|
||||
binary.executable, &binary.args
|
||||
)
|
||||
})?;
|
||||
let mut server = command
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to spawn command {command:?})",))?;
|
||||
|
||||
let stdin = server.stdin.take().unwrap();
|
||||
let stdout = server.stdout.take().unwrap();
|
||||
|
||||
@@ -270,7 +270,7 @@ impl RegisteredBuffer {
|
||||
server
|
||||
.lsp
|
||||
.notify::<lsp::notification::DidChangeTextDocument>(
|
||||
&lsp::DidChangeTextDocumentParams {
|
||||
lsp::DidChangeTextDocumentParams {
|
||||
text_document: lsp::VersionedTextDocumentIdentifier::new(
|
||||
buffer.uri.clone(),
|
||||
buffer.snapshot_version,
|
||||
@@ -744,7 +744,7 @@ impl Copilot {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
server
|
||||
.notify::<lsp::notification::DidOpenTextDocument>(
|
||||
&lsp::DidOpenTextDocumentParams {
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem {
|
||||
uri: uri.clone(),
|
||||
language_id: language_id.clone(),
|
||||
@@ -792,13 +792,14 @@ impl Copilot {
|
||||
server
|
||||
.lsp
|
||||
.notify::<lsp::notification::DidSaveTextDocument>(
|
||||
&lsp::DidSaveTextDocumentParams {
|
||||
lsp::DidSaveTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(
|
||||
registered_buffer.uri.clone(),
|
||||
),
|
||||
text: None,
|
||||
},
|
||||
)?;
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
language::BufferEvent::FileHandleChanged
|
||||
| language::BufferEvent::LanguageChanged => {
|
||||
@@ -814,14 +815,15 @@ impl Copilot {
|
||||
server
|
||||
.lsp
|
||||
.notify::<lsp::notification::DidCloseTextDocument>(
|
||||
&lsp::DidCloseTextDocumentParams {
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(old_uri),
|
||||
},
|
||||
)?;
|
||||
)
|
||||
.ok();
|
||||
server
|
||||
.lsp
|
||||
.notify::<lsp::notification::DidOpenTextDocument>(
|
||||
&lsp::DidOpenTextDocumentParams {
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
registered_buffer.uri.clone(),
|
||||
registered_buffer.language_id.clone(),
|
||||
@@ -829,7 +831,8 @@ impl Copilot {
|
||||
registered_buffer.snapshot.text(),
|
||||
),
|
||||
},
|
||||
)?;
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -846,7 +849,7 @@ impl Copilot {
|
||||
server
|
||||
.lsp
|
||||
.notify::<lsp::notification::DidCloseTextDocument>(
|
||||
&lsp::DidCloseTextDocumentParams {
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
|
||||
},
|
||||
)
|
||||
@@ -1151,9 +1154,12 @@ fn notify_did_change_config_to_server(
|
||||
}
|
||||
});
|
||||
|
||||
server.notify::<lsp::notification::DidChangeConfiguration>(&lsp::DidChangeConfigurationParams {
|
||||
settings,
|
||||
})
|
||||
server
|
||||
.notify::<lsp::notification::DidChangeConfiguration>(lsp::DidChangeConfigurationParams {
|
||||
settings,
|
||||
})
|
||||
.ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn clear_copilot_dir() {
|
||||
|
||||
@@ -46,6 +46,7 @@ pub trait DapDelegate: Send + Sync + 'static {
|
||||
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
|
||||
async fn read_text_file(&self, path: &RelPath) -> Result<String>;
|
||||
async fn shell_env(&self) -> collections::HashMap<String, String>;
|
||||
fn is_headless(&self) -> bool;
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use dap_types::SteppingGranularity;
|
||||
use gpui::App;
|
||||
use settings::{Settings, SettingsContent};
|
||||
|
||||
pub struct DebuggerSettings {
|
||||
@@ -34,7 +33,7 @@ pub struct DebuggerSettings {
|
||||
}
|
||||
|
||||
impl Settings for DebuggerSettings {
|
||||
fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
|
||||
fn from_settings(content: &SettingsContent) -> Self {
|
||||
let content = content.debugger.clone().unwrap();
|
||||
Self {
|
||||
stepping_granularity: dap_granularity_from_settings(
|
||||
|
||||
@@ -674,13 +674,7 @@ impl StdioTransport {
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
|
||||
let mut process = Child::spawn(command, Stdio::piped()).with_context(|| {
|
||||
format!(
|
||||
"failed to spawn command `{} {}`.",
|
||||
binary_command,
|
||||
binary.arguments.join(" ")
|
||||
)
|
||||
})?;
|
||||
let mut process = Child::spawn(command, Stdio::piped())?;
|
||||
|
||||
let err_task = process.stderr.take().map(|stderr| {
|
||||
cx.background_spawn(TransportDelegate::handle_adapter_log(
|
||||
@@ -1058,11 +1052,13 @@ impl Child {
|
||||
#[cfg(not(windows))]
|
||||
fn spawn(mut command: std::process::Command, stdin: Stdio) -> Result<Self> {
|
||||
util::set_pre_exec_to_start_new_session(&mut command);
|
||||
let process = smol::process::Command::from(command)
|
||||
let mut command = smol::process::Command::from(command);
|
||||
let process = command
|
||||
.stdin(stdin)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to spawn command `{command:?}`",))?;
|
||||
Ok(Self { process })
|
||||
}
|
||||
|
||||
@@ -1070,11 +1066,13 @@ impl Child {
|
||||
fn spawn(command: std::process::Command, stdin: Stdio) -> Result<Self> {
|
||||
// TODO(windows): create a job object and add the child process handle to it,
|
||||
// see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
|
||||
let process = smol::process::Command::from(command)
|
||||
let mut command = smol::process::Command::from(command);
|
||||
let process = command
|
||||
.stdin(stdin)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to spawn command `{command:?}`",))?;
|
||||
Ok(Self { process })
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,13 @@ impl JsDebugAdapter {
|
||||
configuration
|
||||
.entry("sourceMapRenames")
|
||||
.or_insert(true.into());
|
||||
|
||||
// Set up remote browser debugging
|
||||
if delegate.is_headless() {
|
||||
configuration
|
||||
.entry("browserLaunchLocation")
|
||||
.or_insert("ui".into());
|
||||
}
|
||||
}
|
||||
|
||||
let adapter_path = if let Some(user_installed_path) = user_installed_path {
|
||||
|
||||
@@ -963,26 +963,21 @@ pub fn init(cx: &mut App) {
|
||||
};
|
||||
|
||||
let project = workspace.project();
|
||||
if project.read(cx).is_local() {
|
||||
log_store.update(cx, |store, cx| {
|
||||
store.add_project(project, cx);
|
||||
});
|
||||
}
|
||||
log_store.update(cx, |store, cx| {
|
||||
store.add_project(project, cx);
|
||||
});
|
||||
|
||||
let log_store = log_store.clone();
|
||||
workspace.register_action(move |workspace, _: &OpenDebugAdapterLogs, window, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
if project.is_local() {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new(|cx| {
|
||||
DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
|
||||
})),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new(|cx| {
|
||||
DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
|
||||
})),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -41,8 +41,8 @@ use serde_json::Value;
|
||||
use settings::Settings;
|
||||
use stack_frame_list::StackFrameList;
|
||||
use task::{
|
||||
BuildTaskDefinition, DebugScenario, ShellBuilder, SpawnInTerminal, TaskContext, ZedDebugConfig,
|
||||
substitute_variables_in_str,
|
||||
BuildTaskDefinition, DebugScenario, Shell, ShellBuilder, SpawnInTerminal, TaskContext,
|
||||
ZedDebugConfig, substitute_variables_in_str,
|
||||
};
|
||||
use terminal_view::TerminalView;
|
||||
use ui::{
|
||||
@@ -937,6 +937,7 @@ impl RunningState {
|
||||
let task_store = project.read(cx).task_store().downgrade();
|
||||
let weak_project = project.downgrade();
|
||||
let weak_workspace = workspace.downgrade();
|
||||
let is_windows = project.read(cx).path_style(cx).is_windows();
|
||||
let remote_shell = project
|
||||
.read(cx)
|
||||
.remote_client()
|
||||
@@ -988,7 +989,7 @@ impl RunningState {
|
||||
(task, None)
|
||||
}
|
||||
};
|
||||
let Some(task) = task_template.resolve_task("debug-build-task", &task_context) else {
|
||||
let Some(mut task) = task_template.resolve_task("debug-build-task", &task_context) else {
|
||||
anyhow::bail!("Could not resolve task variables within a debug scenario");
|
||||
};
|
||||
|
||||
@@ -1025,7 +1026,11 @@ impl RunningState {
|
||||
None
|
||||
};
|
||||
|
||||
let builder = ShellBuilder::new(remote_shell.as_deref(), &task.resolved.shell);
|
||||
if let Some(remote_shell) = remote_shell && task.resolved.shell == Shell::System {
|
||||
task.resolved.shell = Shell::Program(remote_shell);
|
||||
}
|
||||
|
||||
let builder = ShellBuilder::new(&task.resolved.shell, is_windows);
|
||||
let command_label = builder.command_label(task.resolved.command.as_deref().unwrap_or(""));
|
||||
let (command, args) =
|
||||
builder.build(task.resolved.command.clone(), &task.resolved.args);
|
||||
@@ -1228,7 +1233,6 @@ impl RunningState {
|
||||
|
||||
terminal.read_with(cx, |terminal, _| {
|
||||
terminal
|
||||
.pty_info
|
||||
.pid()
|
||||
.map(|pid| pid.as_u32())
|
||||
.context("Terminal was spawned but PID was not available")
|
||||
|
||||
@@ -59,7 +59,7 @@ impl StackTraceView {
|
||||
|
||||
editor
|
||||
.snapshot(window, cx)
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.excerpt_containing(position..position)
|
||||
.map(|excerpt| excerpt.id())
|
||||
});
|
||||
@@ -259,7 +259,7 @@ impl StackTraceView {
|
||||
let mut is_first = true;
|
||||
|
||||
for (_, highlight) in self.highlights.iter().skip(active_idx) {
|
||||
let position = highlight.to_point(&snapshot.buffer_snapshot);
|
||||
let position = highlight.to_point(&snapshot.buffer_snapshot());
|
||||
let color = if is_first {
|
||||
is_first = false;
|
||||
first_color
|
||||
@@ -268,11 +268,11 @@ impl StackTraceView {
|
||||
};
|
||||
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.clip_point(Point::new(position.row, 0), Bias::Left);
|
||||
let end = start + Point::new(1, 0);
|
||||
let start = snapshot.buffer_snapshot.anchor_before(start);
|
||||
let end = snapshot.buffer_snapshot.anchor_before(end);
|
||||
let start = snapshot.buffer_snapshot().anchor_before(start);
|
||||
let end = snapshot.buffer_snapshot().anchor_before(end);
|
||||
editor.highlight_rows::<DebugStackFrameLine>(
|
||||
start..end,
|
||||
color,
|
||||
@@ -354,10 +354,6 @@ impl Item for StackTraceView {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
nav_history: ItemNavHistory,
|
||||
|
||||
@@ -1604,7 +1604,7 @@ async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut T
|
||||
|
||||
let point = editor
|
||||
.snapshot(window, cx)
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
|
||||
|
||||
assert_eq!(point.row, 1);
|
||||
@@ -1679,7 +1679,7 @@ async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut T
|
||||
|
||||
let point = editor
|
||||
.snapshot(window, cx)
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
|
||||
|
||||
assert_eq!(point.row, 2);
|
||||
|
||||
@@ -341,8 +341,8 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
editor
|
||||
.highlighted_rows::<editor::ActiveDebugLine>()
|
||||
.map(|(range, _)| {
|
||||
let start = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
let start = range.start.to_point(&snapshot.buffer_snapshot());
|
||||
let end = range.end.to_point(&snapshot.buffer_snapshot());
|
||||
start.row..end.row
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -404,8 +404,8 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
editor
|
||||
.highlighted_rows::<editor::ActiveDebugLine>()
|
||||
.map(|(range, _)| {
|
||||
let start = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
let start = range.start.to_point(&snapshot.buffer_snapshot());
|
||||
let end = range.end.to_point(&snapshot.buffer_snapshot());
|
||||
start.row..end.row
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
@@ -730,10 +730,6 @@ impl Item for BufferDiagnosticsEditor {
|
||||
self.multibuffer.read(cx).is_dirty(cx)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
&mut self,
|
||||
data: Box<dyn Any>,
|
||||
|
||||
@@ -138,7 +138,7 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||
BlockProperties {
|
||||
placement: BlockPlacement::Near(
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_after(block.initial_range.start),
|
||||
),
|
||||
height: Some(1),
|
||||
@@ -278,7 +278,7 @@ impl DiagnosticBlock {
|
||||
}
|
||||
} else if let Some(diagnostic) = editor
|
||||
.snapshot(window, cx)
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.diagnostic_group(buffer_id, group_id)
|
||||
.nth(ix)
|
||||
{
|
||||
|
||||
@@ -716,10 +716,6 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
nav_history: ItemNavHistory,
|
||||
@@ -969,10 +965,11 @@ async fn heuristic_syntactic_expand(
|
||||
let row_count = node_end.row - node_start.row + 1;
|
||||
let mut ancestor_range = None;
|
||||
let reached_outline_node = cx.background_executor().scoped({
|
||||
let node_range = node_range.clone();
|
||||
let outline_range = outline_range.clone();
|
||||
let ancestor_range = &mut ancestor_range;
|
||||
|scope| {scope.spawn(async move {
|
||||
let node_range = node_range.clone();
|
||||
let outline_range = outline_range.clone();
|
||||
let ancestor_range = &mut ancestor_range;
|
||||
|scope| {
|
||||
scope.spawn(async move {
|
||||
// Stop if we've exceeded the row count or reached an outline node. Then, find the interval
|
||||
// of node children which contains the query range. For example, this allows just returning
|
||||
// the header of a declaration rather than the entire declaration.
|
||||
@@ -984,8 +981,11 @@ async fn heuristic_syntactic_expand(
|
||||
if cursor.goto_first_child() {
|
||||
loop {
|
||||
let child_node = cursor.node();
|
||||
let child_range = previous_end..Point::from_ts_point(child_node.end_position());
|
||||
if included_child_start.is_none() && child_range.contains(&input_range.start) {
|
||||
let child_range =
|
||||
previous_end..Point::from_ts_point(child_node.end_position());
|
||||
if included_child_start.is_none()
|
||||
&& child_range.contains(&input_range.start)
|
||||
{
|
||||
included_child_start = Some(child_range.start);
|
||||
}
|
||||
if child_range.contains(&input_range.end) {
|
||||
@@ -1001,19 +1001,22 @@ async fn heuristic_syntactic_expand(
|
||||
if let Some(start) = included_child_start {
|
||||
let row_count = end.row - start.row;
|
||||
if row_count < max_row_count {
|
||||
*ancestor_range = Some(Some(RangeInclusive::new(start.row, end.row)));
|
||||
*ancestor_range =
|
||||
Some(Some(RangeInclusive::new(start.row, end.row)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Expanding to ancestor started on {} node exceeding row limit of {max_row_count}.",
|
||||
"Expanding to ancestor started on {} node\
|
||||
exceeding row limit of {max_row_count}.",
|
||||
node.grammar_name()
|
||||
);
|
||||
*ancestor_range = Some(None);
|
||||
}
|
||||
})
|
||||
}});
|
||||
}
|
||||
});
|
||||
reached_outline_node.await;
|
||||
if let Some(node) = ancestor_range {
|
||||
return node;
|
||||
|
||||
@@ -863,20 +863,20 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
|
||||
21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
|
||||
diagnostics.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
if !snapshot.buffer_snapshot.is_empty() {
|
||||
let position = rng.random_range(0..snapshot.buffer_snapshot.len());
|
||||
let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
|
||||
if !snapshot.buffer_snapshot().is_empty() {
|
||||
let position = rng.random_range(0..snapshot.buffer_snapshot().len());
|
||||
let position = snapshot.buffer_snapshot().clip_offset(position, Bias::Left);
|
||||
log::info!(
|
||||
"adding inlay at {position}/{}: {:?}",
|
||||
snapshot.buffer_snapshot.len(),
|
||||
snapshot.buffer_snapshot.text(),
|
||||
snapshot.buffer_snapshot().len(),
|
||||
snapshot.buffer_snapshot().text(),
|
||||
);
|
||||
|
||||
editor.splice_inlays(
|
||||
&[],
|
||||
vec![Inlay::edit_prediction(
|
||||
post_inc(&mut next_inlay_id),
|
||||
snapshot.buffer_snapshot.anchor_before(position),
|
||||
snapshot.buffer_snapshot().anchor_before(position),
|
||||
Rope::from_iter(["Test inlay ", "next_inlay_id"]),
|
||||
)],
|
||||
cx,
|
||||
|
||||
@@ -20,6 +20,8 @@ util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed.workspace = true
|
||||
zlog.workspace = true
|
||||
task.workspace = true
|
||||
theme.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -53,9 +53,20 @@ fn main() -> Result<()> {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
enum PreprocessorError {
|
||||
ActionNotFound { action_name: String },
|
||||
DeprecatedActionUsed { used: String, should_be: String },
|
||||
ActionNotFound {
|
||||
action_name: String,
|
||||
},
|
||||
DeprecatedActionUsed {
|
||||
used: String,
|
||||
should_be: String,
|
||||
},
|
||||
InvalidFrontmatterLine(String),
|
||||
InvalidSettingsJson {
|
||||
file: std::path::PathBuf,
|
||||
line: usize,
|
||||
snippet: String,
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl PreprocessorError {
|
||||
@@ -72,6 +83,20 @@ impl PreprocessorError {
|
||||
}
|
||||
PreprocessorError::ActionNotFound { action_name }
|
||||
}
|
||||
|
||||
fn new_for_invalid_settings_json(
|
||||
chapter: &Chapter,
|
||||
location: usize,
|
||||
snippet: String,
|
||||
error: String,
|
||||
) -> Self {
|
||||
PreprocessorError::InvalidSettingsJson {
|
||||
file: chapter.path.clone().expect("chapter has path"),
|
||||
line: chapter.content[..location].lines().count() + 1,
|
||||
snippet,
|
||||
error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PreprocessorError {
|
||||
@@ -88,6 +113,21 @@ impl std::fmt::Display for PreprocessorError {
|
||||
"Deprecated action used: {} should be {}",
|
||||
used, should_be
|
||||
),
|
||||
PreprocessorError::InvalidSettingsJson {
|
||||
file,
|
||||
line,
|
||||
snippet,
|
||||
error,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid settings JSON at {}:{}\nError: {}\n\n{}",
|
||||
file.display(),
|
||||
line,
|
||||
error,
|
||||
snippet
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,11 +140,11 @@ fn handle_preprocessing() -> Result<()> {
|
||||
let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;
|
||||
|
||||
let mut errors = HashSet::<PreprocessorError>::new();
|
||||
|
||||
handle_frontmatter(&mut book, &mut errors);
|
||||
template_big_table_of_actions(&mut book);
|
||||
template_and_validate_keybindings(&mut book, &mut errors);
|
||||
template_and_validate_actions(&mut book, &mut errors);
|
||||
template_and_validate_json_snippets(&mut book, &mut errors);
|
||||
|
||||
if !errors.is_empty() {
|
||||
const ANSI_RED: &str = "\x1b[31m";
|
||||
@@ -235,6 +275,161 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||
fn for_each_labeled_code_block_mut(
|
||||
book: &mut Book,
|
||||
errors: &mut HashSet<PreprocessorError>,
|
||||
f: impl Fn(&str, &str) -> anyhow::Result<()>,
|
||||
) {
|
||||
const TAGGED_JSON_BLOCK_START: &'static str = "```json [";
|
||||
const JSON_BLOCK_END: &'static str = "```";
|
||||
|
||||
for_each_chapter_mut(book, |chapter| {
|
||||
let mut offset = 0;
|
||||
while let Some(loc) = chapter.content[offset..].find(TAGGED_JSON_BLOCK_START) {
|
||||
let loc = loc + offset;
|
||||
let tag_start = loc + TAGGED_JSON_BLOCK_START.len();
|
||||
offset = tag_start;
|
||||
let Some(tag_end) = chapter.content[tag_start..].find(']') else {
|
||||
errors.insert(PreprocessorError::new_for_invalid_settings_json(
|
||||
chapter,
|
||||
loc,
|
||||
chapter.content[loc..tag_start].to_string(),
|
||||
"Unclosed JSON block tag".to_string(),
|
||||
));
|
||||
continue;
|
||||
};
|
||||
let tag_end = tag_end + tag_start;
|
||||
|
||||
let tag = &chapter.content[tag_start..tag_end];
|
||||
|
||||
if tag.contains('\n') {
|
||||
errors.insert(PreprocessorError::new_for_invalid_settings_json(
|
||||
chapter,
|
||||
loc,
|
||||
chapter.content[loc..tag_start].to_string(),
|
||||
"Unclosed JSON block tag".to_string(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
let snippet_start = tag_end + 1;
|
||||
offset = snippet_start;
|
||||
|
||||
let Some(snippet_end) = chapter.content[snippet_start..].find(JSON_BLOCK_END)
|
||||
else {
|
||||
errors.insert(PreprocessorError::new_for_invalid_settings_json(
|
||||
chapter,
|
||||
loc,
|
||||
chapter.content[loc..tag_end + 1].to_string(),
|
||||
"Missing closing code block".to_string(),
|
||||
));
|
||||
continue;
|
||||
};
|
||||
let snippet_end = snippet_start + snippet_end;
|
||||
let snippet_json = &chapter.content[snippet_start..snippet_end];
|
||||
offset = snippet_end + 3;
|
||||
|
||||
if let Err(err) = f(tag, snippet_json) {
|
||||
errors.insert(PreprocessorError::new_for_invalid_settings_json(
|
||||
chapter,
|
||||
loc,
|
||||
chapter.content[loc..snippet_end + 3].to_string(),
|
||||
err.to_string(),
|
||||
));
|
||||
continue;
|
||||
};
|
||||
let tag_range_complete = tag_start - 1..tag_end + 1;
|
||||
offset -= tag_range_complete.len();
|
||||
chapter.content.replace_range(tag_range_complete, "");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for_each_labeled_code_block_mut(book, errors, |label, snippet_json| {
|
||||
let mut snippet_json_fixed = snippet_json
|
||||
.to_string()
|
||||
.replace("\n>", "\n")
|
||||
.trim()
|
||||
.to_string();
|
||||
while snippet_json_fixed.starts_with("//") {
|
||||
if let Some(line_end) = snippet_json_fixed.find('\n') {
|
||||
snippet_json_fixed.replace_range(0..line_end, "");
|
||||
snippet_json_fixed = snippet_json_fixed.trim().to_string();
|
||||
}
|
||||
}
|
||||
match label {
|
||||
"settings" => {
|
||||
if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {
|
||||
snippet_json_fixed.insert(0, '{');
|
||||
snippet_json_fixed.push_str("\n}");
|
||||
}
|
||||
settings::parse_json_with_comments::<settings::SettingsContent>(
|
||||
&snippet_json_fixed,
|
||||
)?;
|
||||
}
|
||||
"keymap" => {
|
||||
if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
|
||||
snippet_json_fixed.insert(0, '[');
|
||||
snippet_json_fixed.push_str("\n]");
|
||||
}
|
||||
|
||||
let keymap = settings::KeymapFile::parse(&snippet_json_fixed)
|
||||
.context("Failed to parse keymap JSON")?;
|
||||
for section in keymap.sections() {
|
||||
for (keystrokes, action) in section.bindings() {
|
||||
keystrokes
|
||||
.split_whitespace()
|
||||
.map(|source| gpui::Keystroke::parse(source))
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.context("Failed to parse keystroke")?;
|
||||
if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
|
||||
.map_err(|err| anyhow::format_err!(err))
|
||||
.context("Failed to parse action")?
|
||||
{
|
||||
anyhow::ensure!(
|
||||
find_action_by_name(action_name).is_some(),
|
||||
"Action not found: {}",
|
||||
action_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"debug" => {
|
||||
if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
|
||||
snippet_json_fixed.insert(0, '[');
|
||||
snippet_json_fixed.push_str("\n]");
|
||||
}
|
||||
|
||||
settings::parse_json_with_comments::<task::DebugTaskFile>(&snippet_json_fixed)?;
|
||||
}
|
||||
"tasks" => {
|
||||
if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
|
||||
snippet_json_fixed.insert(0, '[');
|
||||
snippet_json_fixed.push_str("\n]");
|
||||
}
|
||||
|
||||
settings::parse_json_with_comments::<task::TaskTemplates>(&snippet_json_fixed)?;
|
||||
}
|
||||
"icon-theme" => {
|
||||
if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {
|
||||
snippet_json_fixed.insert(0, '{');
|
||||
snippet_json_fixed.push_str("\n}");
|
||||
}
|
||||
|
||||
settings::parse_json_with_comments::<theme::IconThemeFamilyContent>(
|
||||
&snippet_json_fixed,
|
||||
)?;
|
||||
}
|
||||
label => {
|
||||
anyhow::bail!("Unexpected JSON code block tag: {}", label)
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
/// Removes any configurable options from the stringified action if existing,
|
||||
/// ensuring that only the actual action name is returned. If the action consists
|
||||
/// only of a string and nothing else, the string is returned as-is.
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::ops::Range;
|
||||
|
||||
use client::EditPredictionUsage;
|
||||
use gpui::{App, Context, Entity, SharedString};
|
||||
use language::Buffer;
|
||||
use language::{Anchor, Buffer, BufferSnapshot, OffsetRangeExt};
|
||||
|
||||
// TODO: Find a better home for `Direction`.
|
||||
//
|
||||
@@ -242,3 +242,51 @@ where
|
||||
self.update(cx, |this, cx| this.suggest(buffer, cursor_position, cx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns edits updated based on user edits since the old snapshot. None is returned if any user
|
||||
/// edit is not a prefix of a predicted insertion.
|
||||
pub fn interpolate_edits(
|
||||
old_snapshot: &BufferSnapshot,
|
||||
new_snapshot: &BufferSnapshot,
|
||||
current_edits: &[(Range<Anchor>, String)],
|
||||
) -> Option<Vec<(Range<Anchor>, String)>> {
|
||||
let mut edits = Vec::new();
|
||||
|
||||
let mut model_edits = current_edits.iter().peekable();
|
||||
for user_edit in new_snapshot.edits_since::<usize>(&old_snapshot.version) {
|
||||
while let Some((model_old_range, _)) = model_edits.peek() {
|
||||
let model_old_range = model_old_range.to_offset(old_snapshot);
|
||||
if model_old_range.end < user_edit.old.start {
|
||||
let (model_old_range, model_new_text) = model_edits.next().unwrap();
|
||||
edits.push((model_old_range.clone(), model_new_text.clone()));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((model_old_range, model_new_text)) = model_edits.peek() {
|
||||
let model_old_offset_range = model_old_range.to_offset(old_snapshot);
|
||||
if user_edit.old == model_old_offset_range {
|
||||
let user_new_text = new_snapshot
|
||||
.text_for_range(user_edit.new.clone())
|
||||
.collect::<String>();
|
||||
|
||||
if let Some(model_suffix) = model_new_text.strip_prefix(&user_new_text) {
|
||||
if !model_suffix.is_empty() {
|
||||
let anchor = old_snapshot.anchor_after(user_edit.old.end);
|
||||
edits.push((anchor..anchor, model_suffix.to_string()));
|
||||
}
|
||||
|
||||
model_edits.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
edits.extend(model_edits.cloned());
|
||||
|
||||
if edits.is_empty() { None } else { Some(edits) }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
codestral.workspace = true
|
||||
copilot.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use client::{UserStore, zed_urls};
|
||||
use cloud_llm_client::UsageLimit;
|
||||
use codestral::CodestralCompletionProvider;
|
||||
use copilot::{Copilot, Status};
|
||||
use editor::{Editor, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll};
|
||||
use feature_flags::{FeatureFlagAppExt, PredictEditsRateCompletionsFeatureFlag};
|
||||
@@ -234,6 +235,67 @@ impl Render for EditPredictionButton {
|
||||
)
|
||||
}
|
||||
|
||||
EditPredictionProvider::Codestral => {
|
||||
let enabled = self.editor_enabled.unwrap_or(true);
|
||||
let has_api_key = CodestralCompletionProvider::has_api_key(cx);
|
||||
let fs = self.fs.clone();
|
||||
let this = cx.entity();
|
||||
|
||||
div().child(
|
||||
PopoverMenu::new("codestral")
|
||||
.menu(move |window, cx| {
|
||||
if has_api_key {
|
||||
Some(this.update(cx, |this, cx| {
|
||||
this.build_codestral_context_menu(window, cx)
|
||||
}))
|
||||
} else {
|
||||
Some(ContextMenu::build(window, cx, |menu, _, _| {
|
||||
let fs = fs.clone();
|
||||
menu.entry("Use Zed AI instead", None, move |_, cx| {
|
||||
set_completion_provider(
|
||||
fs.clone(),
|
||||
cx,
|
||||
EditPredictionProvider::Zed,
|
||||
)
|
||||
})
|
||||
.separator()
|
||||
.entry(
|
||||
"Configure Codestral API Key",
|
||||
None,
|
||||
move |window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::agent::OpenSettings.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
},
|
||||
)
|
||||
}))
|
||||
}
|
||||
})
|
||||
.anchor(Corner::BottomRight)
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("codestral-icon", IconName::AiMistral)
|
||||
.shape(IconButtonShape::Square)
|
||||
.when(!has_api_key, |this| {
|
||||
this.indicator(Indicator::dot().color(Color::Error))
|
||||
.indicator_border_color(Some(
|
||||
cx.theme().colors().status_bar_background,
|
||||
))
|
||||
})
|
||||
.when(has_api_key && !enabled, |this| {
|
||||
this.indicator(Indicator::dot().color(Color::Ignored))
|
||||
.indicator_border_color(Some(
|
||||
cx.theme().colors().status_bar_background,
|
||||
))
|
||||
}),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action("Codestral", &ToggleMenu, window, cx)
|
||||
},
|
||||
)
|
||||
.with_handle(self.popover_menu_handle.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
EditPredictionProvider::Zed => {
|
||||
let enabled = self.editor_enabled.unwrap_or(true);
|
||||
|
||||
@@ -493,6 +555,7 @@ impl EditPredictionButton {
|
||||
EditPredictionProvider::Zed
|
||||
| EditPredictionProvider::Copilot
|
||||
| EditPredictionProvider::Supermaven
|
||||
| EditPredictionProvider::Codestral
|
||||
) {
|
||||
menu = menu
|
||||
.separator()
|
||||
@@ -719,6 +782,25 @@ impl EditPredictionButton {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_codestral_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
ContextMenu::build(window, cx, |menu, window, cx| {
|
||||
self.build_language_settings_menu(menu, window, cx)
|
||||
.separator()
|
||||
.entry("Use Zed AI instead", None, move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
|
||||
})
|
||||
.separator()
|
||||
.entry("Configure Codestral API Key", None, move |window, cx| {
|
||||
window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn build_zeta_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
|
||||
@@ -19,6 +19,7 @@ collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
hashbrown.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
@@ -45,5 +46,8 @@ project = {workspace= true, features = ["test-support"]}
|
||||
serde_json.workspace = true
|
||||
settings = {workspace= true, features = ["test-support"]}
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-c.workspace = true
|
||||
tree-sitter-cpp.workspace = true
|
||||
tree-sitter-go.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use language::LanguageId;
|
||||
use language::{Language, LanguageId};
|
||||
use project::ProjectEntryId;
|
||||
use std::borrow::Cow;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::{borrow::Cow, path::Path};
|
||||
use text::{Bias, BufferId, Rope};
|
||||
use util::paths::{path_ends_with, strip_path_suffix};
|
||||
use util::rel_path::RelPath;
|
||||
|
||||
use crate::outline::OutlineDeclaration;
|
||||
|
||||
@@ -22,12 +24,14 @@ pub enum Declaration {
|
||||
File {
|
||||
project_entry_id: ProjectEntryId,
|
||||
declaration: FileDeclaration,
|
||||
cached_path: CachedDeclarationPath,
|
||||
},
|
||||
Buffer {
|
||||
project_entry_id: ProjectEntryId,
|
||||
buffer_id: BufferId,
|
||||
rope: Rope,
|
||||
declaration: BufferDeclaration,
|
||||
cached_path: CachedDeclarationPath,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,6 +77,13 @@ impl Declaration {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cached_path(&self) -> &CachedDeclarationPath {
|
||||
match self {
|
||||
Declaration::File { cached_path, .. } => cached_path,
|
||||
Declaration::Buffer { cached_path, .. } => cached_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_range(&self) -> Range<usize> {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => declaration.item_range.clone(),
|
||||
@@ -235,3 +246,69 @@ impl BufferDeclaration {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedDeclarationPath {
|
||||
pub worktree_abs_path: Arc<Path>,
|
||||
pub rel_path: Arc<RelPath>,
|
||||
/// The relative path of the file, possibly stripped according to `import_path_strip_regex`.
|
||||
pub rel_path_after_regex_stripping: Arc<RelPath>,
|
||||
}
|
||||
|
||||
impl CachedDeclarationPath {
|
||||
pub fn new(
|
||||
worktree_abs_path: Arc<Path>,
|
||||
path: &Arc<RelPath>,
|
||||
language: Option<&Arc<Language>>,
|
||||
) -> Self {
|
||||
let rel_path = path.clone();
|
||||
let rel_path_after_regex_stripping = if let Some(language) = language
|
||||
&& let Some(strip_regex) = language.config().import_path_strip_regex.as_ref()
|
||||
&& let Ok(stripped) = RelPath::unix(&Path::new(
|
||||
strip_regex.replace_all(rel_path.as_unix_str(), "").as_ref(),
|
||||
)) {
|
||||
Arc::from(stripped)
|
||||
} else {
|
||||
rel_path.clone()
|
||||
};
|
||||
CachedDeclarationPath {
|
||||
worktree_abs_path,
|
||||
rel_path,
|
||||
rel_path_after_regex_stripping,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_for_test(worktree_abs_path: &str, rel_path: &str) -> Self {
|
||||
let rel_path: Arc<RelPath> = util::rel_path::rel_path(rel_path).into();
|
||||
CachedDeclarationPath {
|
||||
worktree_abs_path: std::path::PathBuf::from(worktree_abs_path).into(),
|
||||
rel_path_after_regex_stripping: rel_path.clone(),
|
||||
rel_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ends_with_posix_path(&self, path: &Path) -> bool {
|
||||
if path.as_os_str().len() <= self.rel_path_after_regex_stripping.as_unix_str().len() {
|
||||
path_ends_with(self.rel_path_after_regex_stripping.as_std_path(), path)
|
||||
} else {
|
||||
if let Some(remaining) =
|
||||
strip_path_suffix(path, self.rel_path_after_regex_stripping.as_std_path())
|
||||
{
|
||||
path_ends_with(&self.worktree_abs_path, remaining)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn equals_absolute_path(&self, path: &Path) -> bool {
|
||||
if let Some(remaining) =
|
||||
strip_path_suffix(path, &self.rel_path_after_regex_stripping.as_std_path())
|
||||
{
|
||||
self.worktree_abs_path.as_ref() == remaining
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use cloud_llm_client::predict_edits_v3::DeclarationScoreComponents;
|
||||
use collections::HashMap;
|
||||
use itertools::Itertools as _;
|
||||
use language::BufferSnapshot;
|
||||
use ordered_float::OrderedFloat;
|
||||
use project::ProjectEntryId;
|
||||
use serde::Serialize;
|
||||
use std::{cmp::Reverse, ops::Range};
|
||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||
use strum::EnumIter;
|
||||
use text::{Point, ToPoint};
|
||||
use util::RangeExt as _;
|
||||
|
||||
use crate::{
|
||||
Declaration, EditPredictionExcerpt, Identifier,
|
||||
CachedDeclarationPath, Declaration, EditPredictionExcerpt, Identifier,
|
||||
imports::{Import, Imports, Module},
|
||||
reference::{Reference, ReferenceRegion},
|
||||
syntax_index::SyntaxIndexState,
|
||||
text_similarity::{Occurrences, jaccard_similarity, weighted_overlap_coefficient},
|
||||
@@ -17,12 +19,17 @@ use crate::{
|
||||
|
||||
const MAX_IDENTIFIER_DECLARATION_COUNT: usize = 16;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EditPredictionScoreOptions {
|
||||
pub omit_excerpt_overlaps: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScoredDeclaration {
|
||||
/// identifier used by the local reference
|
||||
pub identifier: Identifier,
|
||||
pub declaration: Declaration,
|
||||
pub score_components: DeclarationScoreComponents,
|
||||
pub scores: DeclarationScores,
|
||||
pub components: DeclarationScoreComponents,
|
||||
}
|
||||
|
||||
#[derive(EnumIter, Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
@@ -31,15 +38,61 @@ pub enum DeclarationStyle {
|
||||
Declaration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Default)]
|
||||
pub struct DeclarationScores {
|
||||
pub signature: f32,
|
||||
pub declaration: f32,
|
||||
pub retrieval: f32,
|
||||
}
|
||||
|
||||
impl ScoredDeclaration {
|
||||
/// Returns the score for this declaration with the specified style.
|
||||
pub fn score(&self, style: DeclarationStyle) -> f32 {
|
||||
// TODO: handle truncation
|
||||
|
||||
// Score related to how likely this is the correct declaration, range 0 to 1
|
||||
let retrieval = self.retrieval_score();
|
||||
|
||||
// Score related to the distance between the reference and cursor, range 0 to 1
|
||||
let distance_score = if self.components.is_referenced_nearby {
|
||||
1.0 / (1.0 + self.components.reference_line_distance as f32 / 10.0).powf(2.0)
|
||||
} else {
|
||||
// same score as ~14 lines away, rationale is to not overly penalize references from parent signatures
|
||||
0.5
|
||||
};
|
||||
|
||||
// For now instead of linear combination, the scores are just multiplied together.
|
||||
let combined_score = 10.0 * retrieval * distance_score;
|
||||
|
||||
match style {
|
||||
DeclarationStyle::Signature => self.scores.signature,
|
||||
DeclarationStyle::Declaration => self.scores.declaration,
|
||||
DeclarationStyle::Signature => {
|
||||
combined_score * self.components.excerpt_vs_signature_weighted_overlap
|
||||
}
|
||||
DeclarationStyle::Declaration => {
|
||||
2.0 * combined_score * self.components.excerpt_vs_item_weighted_overlap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retrieval_score(&self) -> f32 {
|
||||
let mut score = if self.components.is_same_file {
|
||||
10.0 / self.components.same_file_declaration_count as f32
|
||||
} else if self.components.path_import_match_count > 0 {
|
||||
3.0
|
||||
} else if self.components.wildcard_path_import_match_count > 0 {
|
||||
1.0
|
||||
} else if self.components.normalized_import_similarity > 0.0 {
|
||||
self.components.normalized_import_similarity
|
||||
} else if self.components.normalized_wildcard_import_similarity > 0.0 {
|
||||
0.5 * self.components.normalized_wildcard_import_similarity
|
||||
} else {
|
||||
1.0 / self.components.declaration_count as f32
|
||||
};
|
||||
score *= 1. + self.components.included_by_others as f32 / 2.;
|
||||
score *= 1. + self.components.includes_others as f32 / 4.;
|
||||
score
|
||||
}
|
||||
|
||||
pub fn size(&self, style: DeclarationStyle) -> usize {
|
||||
match &self.declaration {
|
||||
Declaration::File { declaration, .. } => match style {
|
||||
@@ -54,110 +107,259 @@ impl ScoredDeclaration {
|
||||
}
|
||||
|
||||
pub fn score_density(&self, style: DeclarationStyle) -> f32 {
|
||||
self.score(style) / (self.size(style)) as f32
|
||||
self.score(style) / self.size(style) as f32
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scored_declarations(
|
||||
options: &EditPredictionScoreOptions,
|
||||
index: &SyntaxIndexState,
|
||||
excerpt: &EditPredictionExcerpt,
|
||||
excerpt_occurrences: &Occurrences,
|
||||
adjacent_occurrences: &Occurrences,
|
||||
imports: &Imports,
|
||||
identifier_to_references: HashMap<Identifier, Vec<Reference>>,
|
||||
cursor_offset: usize,
|
||||
current_buffer: &BufferSnapshot,
|
||||
) -> Vec<ScoredDeclaration> {
|
||||
let cursor_point = cursor_offset.to_point(¤t_buffer);
|
||||
|
||||
let mut declarations = identifier_to_references
|
||||
.into_iter()
|
||||
.flat_map(|(identifier, references)| {
|
||||
let declarations =
|
||||
index.declarations_for_identifier::<MAX_IDENTIFIER_DECLARATION_COUNT>(&identifier);
|
||||
let declaration_count = declarations.len();
|
||||
let mut wildcard_import_occurrences = Vec::new();
|
||||
let mut wildcard_import_paths = Vec::new();
|
||||
for wildcard_import in imports.wildcard_modules.iter() {
|
||||
match wildcard_import {
|
||||
Module::Namespace(namespace) => {
|
||||
wildcard_import_occurrences.push(namespace.occurrences())
|
||||
}
|
||||
Module::SourceExact(path) => wildcard_import_paths.push(path),
|
||||
Module::SourceFuzzy(path) => {
|
||||
wildcard_import_occurrences.push(Occurrences::from_path(&path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declarations
|
||||
.into_iter()
|
||||
.filter_map(|(declaration_id, declaration)| match declaration {
|
||||
Declaration::Buffer {
|
||||
buffer_id,
|
||||
declaration: buffer_declaration,
|
||||
..
|
||||
} => {
|
||||
let is_same_file = buffer_id == ¤t_buffer.remote_id();
|
||||
let mut scored_declarations = Vec::new();
|
||||
let mut project_entry_id_to_outline_ranges: HashMap<ProjectEntryId, Vec<Range<usize>>> =
|
||||
HashMap::default();
|
||||
for (identifier, references) in identifier_to_references {
|
||||
let mut import_occurrences = Vec::new();
|
||||
let mut import_paths = Vec::new();
|
||||
let mut found_external_identifier: Option<&Identifier> = None;
|
||||
|
||||
if is_same_file {
|
||||
let overlaps_excerpt =
|
||||
range_intersection(&buffer_declaration.item_range, &excerpt.range)
|
||||
.is_some();
|
||||
if overlaps_excerpt
|
||||
if let Some(imports) = imports.identifier_to_imports.get(&identifier) {
|
||||
// only use alias when it's the only import, could be generalized if some language
|
||||
// has overlapping aliases
|
||||
//
|
||||
// TODO: when an aliased declaration is included in the prompt, should include the
|
||||
// aliasing in the prompt.
|
||||
//
|
||||
// TODO: For SourceFuzzy consider having componentwise comparison that pays
|
||||
// attention to ordering.
|
||||
if let [
|
||||
Import::Alias {
|
||||
module,
|
||||
external_identifier,
|
||||
},
|
||||
] = imports.as_slice()
|
||||
{
|
||||
match module {
|
||||
Module::Namespace(namespace) => {
|
||||
import_occurrences.push(namespace.occurrences())
|
||||
}
|
||||
Module::SourceExact(path) => import_paths.push(path),
|
||||
Module::SourceFuzzy(path) => {
|
||||
import_occurrences.push(Occurrences::from_path(&path))
|
||||
}
|
||||
}
|
||||
found_external_identifier = Some(&external_identifier);
|
||||
} else {
|
||||
for import in imports {
|
||||
match import {
|
||||
Import::Direct { module } => match module {
|
||||
Module::Namespace(namespace) => {
|
||||
import_occurrences.push(namespace.occurrences())
|
||||
}
|
||||
Module::SourceExact(path) => import_paths.push(path),
|
||||
Module::SourceFuzzy(path) => {
|
||||
import_occurrences.push(Occurrences::from_path(&path))
|
||||
}
|
||||
},
|
||||
Import::Alias { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let identifier_to_lookup = found_external_identifier.unwrap_or(&identifier);
|
||||
// TODO: update this to be able to return more declarations? Especially if there is the
|
||||
// ability to quickly filter a large list (based on imports)
|
||||
let identifier_declarations = index
|
||||
.declarations_for_identifier::<MAX_IDENTIFIER_DECLARATION_COUNT>(&identifier_to_lookup);
|
||||
let declaration_count = identifier_declarations.len();
|
||||
|
||||
if declaration_count == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: option to filter out other candidates when same file / import match
|
||||
let mut checked_declarations = Vec::with_capacity(declaration_count);
|
||||
for (declaration_id, declaration) in identifier_declarations {
|
||||
match declaration {
|
||||
Declaration::Buffer {
|
||||
buffer_id,
|
||||
declaration: buffer_declaration,
|
||||
..
|
||||
} => {
|
||||
if buffer_id == ¤t_buffer.remote_id() {
|
||||
let already_included_in_prompt =
|
||||
range_intersection(&buffer_declaration.item_range, &excerpt.range)
|
||||
.is_some()
|
||||
|| excerpt
|
||||
.parent_declarations
|
||||
.iter()
|
||||
.any(|(excerpt_parent, _)| excerpt_parent == &declaration_id)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
let declaration_line = buffer_declaration
|
||||
.item_range
|
||||
.start
|
||||
.to_point(current_buffer)
|
||||
.row;
|
||||
Some((
|
||||
true,
|
||||
(cursor_point.row as i32 - declaration_line as i32)
|
||||
.unsigned_abs(),
|
||||
declaration,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some((false, u32::MAX, declaration))
|
||||
.any(|(excerpt_parent, _)| excerpt_parent == &declaration_id);
|
||||
if !options.omit_excerpt_overlaps || !already_included_in_prompt {
|
||||
let declaration_line = buffer_declaration
|
||||
.item_range
|
||||
.start
|
||||
.to_point(current_buffer)
|
||||
.row;
|
||||
let declaration_line_distance =
|
||||
(cursor_point.row as i32 - declaration_line as i32).unsigned_abs();
|
||||
checked_declarations.push(CheckedDeclaration {
|
||||
declaration,
|
||||
same_file_line_distance: Some(declaration_line_distance),
|
||||
path_import_match_count: 0,
|
||||
wildcard_path_import_match_count: 0,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
}
|
||||
Declaration::File { .. } => {
|
||||
// We can assume that a file declaration is in a different file,
|
||||
// because the current one must be open
|
||||
Some((false, u32::MAX, declaration))
|
||||
}
|
||||
}
|
||||
Declaration::File { .. } => {}
|
||||
}
|
||||
let declaration_path = declaration.cached_path();
|
||||
let path_import_match_count = import_paths
|
||||
.iter()
|
||||
.filter(|import_path| {
|
||||
declaration_path_matches_import(&declaration_path, import_path)
|
||||
})
|
||||
.sorted_by_key(|&(_, distance, _)| distance)
|
||||
.enumerate()
|
||||
.map(
|
||||
|(
|
||||
declaration_line_distance_rank,
|
||||
(is_same_file, declaration_line_distance, declaration),
|
||||
)| {
|
||||
let same_file_declaration_count = index.file_declaration_count(declaration);
|
||||
.count();
|
||||
let wildcard_path_import_match_count = wildcard_import_paths
|
||||
.iter()
|
||||
.filter(|import_path| {
|
||||
declaration_path_matches_import(&declaration_path, import_path)
|
||||
})
|
||||
.count();
|
||||
checked_declarations.push(CheckedDeclaration {
|
||||
declaration,
|
||||
same_file_line_distance: None,
|
||||
path_import_match_count,
|
||||
wildcard_path_import_match_count,
|
||||
});
|
||||
}
|
||||
|
||||
score_declaration(
|
||||
&identifier,
|
||||
&references,
|
||||
declaration.clone(),
|
||||
is_same_file,
|
||||
declaration_line_distance,
|
||||
declaration_line_distance_rank,
|
||||
same_file_declaration_count,
|
||||
declaration_count,
|
||||
&excerpt_occurrences,
|
||||
&adjacent_occurrences,
|
||||
cursor_point,
|
||||
current_buffer,
|
||||
)
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
let mut max_import_similarity = 0.0;
|
||||
let mut max_wildcard_import_similarity = 0.0;
|
||||
|
||||
declarations.sort_unstable_by_key(|declaration| {
|
||||
let score_density = declaration
|
||||
.score_density(DeclarationStyle::Declaration)
|
||||
.max(declaration.score_density(DeclarationStyle::Signature));
|
||||
Reverse(OrderedFloat(score_density))
|
||||
let mut scored_declarations_for_identifier = Vec::with_capacity(checked_declarations.len());
|
||||
for checked_declaration in checked_declarations {
|
||||
let same_file_declaration_count =
|
||||
index.file_declaration_count(checked_declaration.declaration);
|
||||
|
||||
let declaration = score_declaration(
|
||||
&identifier,
|
||||
&references,
|
||||
checked_declaration,
|
||||
same_file_declaration_count,
|
||||
declaration_count,
|
||||
&excerpt_occurrences,
|
||||
&adjacent_occurrences,
|
||||
&import_occurrences,
|
||||
&wildcard_import_occurrences,
|
||||
cursor_point,
|
||||
current_buffer,
|
||||
);
|
||||
|
||||
if declaration.components.import_similarity > max_import_similarity {
|
||||
max_import_similarity = declaration.components.import_similarity;
|
||||
}
|
||||
|
||||
if declaration.components.wildcard_import_similarity > max_wildcard_import_similarity {
|
||||
max_wildcard_import_similarity = declaration.components.wildcard_import_similarity;
|
||||
}
|
||||
|
||||
project_entry_id_to_outline_ranges
|
||||
.entry(declaration.declaration.project_entry_id())
|
||||
.or_default()
|
||||
.push(declaration.declaration.item_range());
|
||||
scored_declarations_for_identifier.push(declaration);
|
||||
}
|
||||
|
||||
if max_import_similarity > 0.0 || max_wildcard_import_similarity > 0.0 {
|
||||
for declaration in scored_declarations_for_identifier.iter_mut() {
|
||||
if max_import_similarity > 0.0 {
|
||||
declaration.components.max_import_similarity = max_import_similarity;
|
||||
declaration.components.normalized_import_similarity =
|
||||
declaration.components.import_similarity / max_import_similarity;
|
||||
}
|
||||
if max_wildcard_import_similarity > 0.0 {
|
||||
declaration.components.normalized_wildcard_import_similarity =
|
||||
declaration.components.wildcard_import_similarity
|
||||
/ max_wildcard_import_similarity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scored_declarations.extend(scored_declarations_for_identifier);
|
||||
}
|
||||
|
||||
// TODO: Inform this via import / retrieval scores of outline items
|
||||
// TODO: Consider using a sweepline
|
||||
for scored_declaration in scored_declarations.iter_mut() {
|
||||
let project_entry_id = scored_declaration.declaration.project_entry_id();
|
||||
let Some(ranges) = project_entry_id_to_outline_ranges.get(&project_entry_id) else {
|
||||
continue;
|
||||
};
|
||||
for range in ranges {
|
||||
if range.contains_inclusive(&scored_declaration.declaration.item_range()) {
|
||||
scored_declaration.components.included_by_others += 1
|
||||
} else if scored_declaration
|
||||
.declaration
|
||||
.item_range()
|
||||
.contains_inclusive(range)
|
||||
{
|
||||
scored_declaration.components.includes_others += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scored_declarations.sort_unstable_by_key(|declaration| {
|
||||
Reverse(OrderedFloat(
|
||||
declaration.score(DeclarationStyle::Declaration),
|
||||
))
|
||||
});
|
||||
|
||||
declarations
|
||||
scored_declarations
|
||||
}
|
||||
|
||||
struct CheckedDeclaration<'a> {
|
||||
declaration: &'a Declaration,
|
||||
same_file_line_distance: Option<u32>,
|
||||
path_import_match_count: usize,
|
||||
wildcard_path_import_match_count: usize,
|
||||
}
|
||||
|
||||
fn declaration_path_matches_import(
|
||||
declaration_path: &CachedDeclarationPath,
|
||||
import_path: &Arc<Path>,
|
||||
) -> bool {
|
||||
if import_path.is_absolute() {
|
||||
declaration_path.equals_absolute_path(import_path)
|
||||
} else {
|
||||
declaration_path.ends_with_posix_path(import_path)
|
||||
}
|
||||
}
|
||||
|
||||
fn range_intersection<T: Ord + Clone>(a: &Range<T>, b: &Range<T>) -> Option<Range<T>> {
|
||||
@@ -173,17 +375,23 @@ fn range_intersection<T: Ord + Clone>(a: &Range<T>, b: &Range<T>) -> Option<Rang
|
||||
fn score_declaration(
|
||||
identifier: &Identifier,
|
||||
references: &[Reference],
|
||||
declaration: Declaration,
|
||||
is_same_file: bool,
|
||||
declaration_line_distance: u32,
|
||||
declaration_line_distance_rank: usize,
|
||||
checked_declaration: CheckedDeclaration,
|
||||
same_file_declaration_count: usize,
|
||||
declaration_count: usize,
|
||||
excerpt_occurrences: &Occurrences,
|
||||
adjacent_occurrences: &Occurrences,
|
||||
import_occurrences: &[Occurrences],
|
||||
wildcard_import_occurrences: &[Occurrences],
|
||||
cursor: Point,
|
||||
current_buffer: &BufferSnapshot,
|
||||
) -> Option<ScoredDeclaration> {
|
||||
) -> ScoredDeclaration {
|
||||
let CheckedDeclaration {
|
||||
declaration,
|
||||
same_file_line_distance,
|
||||
path_import_match_count,
|
||||
wildcard_path_import_match_count,
|
||||
} = checked_declaration;
|
||||
|
||||
let is_referenced_nearby = references
|
||||
.iter()
|
||||
.any(|r| r.region == ReferenceRegion::Nearby);
|
||||
@@ -200,6 +408,9 @@ fn score_declaration(
|
||||
.min()
|
||||
.unwrap();
|
||||
|
||||
let is_same_file = same_file_line_distance.is_some();
|
||||
let declaration_line_distance = same_file_line_distance.unwrap_or(u32::MAX);
|
||||
|
||||
let item_source_occurrences = Occurrences::within_string(&declaration.item_text().0);
|
||||
let item_signature_occurrences = Occurrences::within_string(&declaration.signature_text().0);
|
||||
let excerpt_vs_item_jaccard = jaccard_similarity(excerpt_occurrences, &item_source_occurrences);
|
||||
@@ -219,6 +430,37 @@ fn score_declaration(
|
||||
let adjacent_vs_signature_weighted_overlap =
|
||||
weighted_overlap_coefficient(adjacent_occurrences, &item_signature_occurrences);
|
||||
|
||||
let mut import_similarity = 0f32;
|
||||
let mut wildcard_import_similarity = 0f32;
|
||||
if !import_occurrences.is_empty() || !wildcard_import_occurrences.is_empty() {
|
||||
let cached_path = declaration.cached_path();
|
||||
let path_occurrences = Occurrences::from_worktree_path(
|
||||
cached_path
|
||||
.worktree_abs_path
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy()),
|
||||
&cached_path.rel_path,
|
||||
);
|
||||
import_similarity = import_occurrences
|
||||
.iter()
|
||||
.map(|namespace_occurrences| {
|
||||
OrderedFloat(jaccard_similarity(namespace_occurrences, &path_occurrences))
|
||||
})
|
||||
.max()
|
||||
.map(|similarity| similarity.into_inner())
|
||||
.unwrap_or_default();
|
||||
|
||||
// TODO: Consider something other than max
|
||||
wildcard_import_similarity = wildcard_import_occurrences
|
||||
.iter()
|
||||
.map(|namespace_occurrences| {
|
||||
OrderedFloat(jaccard_similarity(namespace_occurrences, &path_occurrences))
|
||||
})
|
||||
.max()
|
||||
.map(|similarity| similarity.into_inner())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
// TODO: Consider adding declaration_file_count
|
||||
let score_components = DeclarationScoreComponents {
|
||||
is_same_file,
|
||||
@@ -226,7 +468,6 @@ fn score_declaration(
|
||||
is_referenced_in_breadcrumb,
|
||||
reference_line_distance,
|
||||
declaration_line_distance,
|
||||
declaration_line_distance_rank,
|
||||
reference_count,
|
||||
same_file_declaration_count,
|
||||
declaration_count,
|
||||
@@ -238,52 +479,61 @@ fn score_declaration(
|
||||
excerpt_vs_signature_weighted_overlap,
|
||||
adjacent_vs_item_weighted_overlap,
|
||||
adjacent_vs_signature_weighted_overlap,
|
||||
path_import_match_count,
|
||||
wildcard_path_import_match_count,
|
||||
import_similarity,
|
||||
max_import_similarity: 0.0,
|
||||
normalized_import_similarity: 0.0,
|
||||
wildcard_import_similarity,
|
||||
normalized_wildcard_import_similarity: 0.0,
|
||||
included_by_others: 0,
|
||||
includes_others: 0,
|
||||
};
|
||||
|
||||
Some(ScoredDeclaration {
|
||||
ScoredDeclaration {
|
||||
identifier: identifier.clone(),
|
||||
declaration: declaration,
|
||||
scores: DeclarationScores::score(&score_components),
|
||||
score_components,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct DeclarationScores {
|
||||
pub signature: f32,
|
||||
pub declaration: f32,
|
||||
pub retrieval: f32,
|
||||
}
|
||||
|
||||
impl DeclarationScores {
|
||||
fn score(components: &DeclarationScoreComponents) -> DeclarationScores {
|
||||
// TODO: handle truncation
|
||||
|
||||
// Score related to how likely this is the correct declaration, range 0 to 1
|
||||
let retrieval = if components.is_same_file {
|
||||
// TODO: use declaration_line_distance_rank
|
||||
1.0 / components.same_file_declaration_count as f32
|
||||
} else {
|
||||
1.0 / components.declaration_count as f32
|
||||
};
|
||||
|
||||
// Score related to the distance between the reference and cursor, range 0 to 1
|
||||
let distance_score = if components.is_referenced_nearby {
|
||||
1.0 / (1.0 + components.reference_line_distance as f32 / 10.0).powf(2.0)
|
||||
} else {
|
||||
// same score as ~14 lines away, rationale is to not overly penalize references from parent signatures
|
||||
0.5
|
||||
};
|
||||
|
||||
// For now instead of linear combination, the scores are just multiplied together.
|
||||
let combined_score = 10.0 * retrieval * distance_score;
|
||||
|
||||
DeclarationScores {
|
||||
signature: combined_score * components.excerpt_vs_signature_weighted_overlap,
|
||||
// declaration score gets boosted both by being multiplied by 2 and by there being more
|
||||
// weighted overlap.
|
||||
declaration: 2.0 * combined_score * components.excerpt_vs_item_weighted_overlap,
|
||||
retrieval,
|
||||
}
|
||||
declaration: declaration.clone(),
|
||||
components: score_components,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_declaration_path_matches() {
|
||||
let declaration_path =
|
||||
CachedDeclarationPath::new_for_test("/home/user/project", "src/maths.ts");
|
||||
|
||||
assert!(declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("maths.ts").into()
|
||||
));
|
||||
|
||||
assert!(declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("project/src/maths.ts").into()
|
||||
));
|
||||
|
||||
assert!(declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("user/project/src/maths.ts").into()
|
||||
));
|
||||
|
||||
assert!(declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("/home/user/project/src/maths.ts").into()
|
||||
));
|
||||
|
||||
assert!(!declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("other.ts").into()
|
||||
));
|
||||
|
||||
assert!(!declaration_path_matches_import(
|
||||
&declaration_path,
|
||||
&Path::new("/home/user/project/src/other.ts").into()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
mod declaration;
|
||||
mod declaration_scoring;
|
||||
mod excerpt;
|
||||
mod imports;
|
||||
mod outline;
|
||||
mod reference;
|
||||
mod syntax_index;
|
||||
pub mod text_similarity;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Entity, Task};
|
||||
@@ -16,9 +17,17 @@ use text::{Point, ToOffset as _};
|
||||
pub use declaration::*;
|
||||
pub use declaration_scoring::*;
|
||||
pub use excerpt::*;
|
||||
pub use imports::*;
|
||||
pub use reference::*;
|
||||
pub use syntax_index::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct EditPredictionContextOptions {
|
||||
pub use_imports: bool,
|
||||
pub excerpt: EditPredictionExcerptOptions,
|
||||
pub score: EditPredictionScoreOptions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EditPredictionContext {
|
||||
pub excerpt: EditPredictionExcerpt,
|
||||
@@ -31,21 +40,34 @@ impl EditPredictionContext {
|
||||
pub fn gather_context_in_background(
|
||||
cursor_point: Point,
|
||||
buffer: BufferSnapshot,
|
||||
excerpt_options: EditPredictionExcerptOptions,
|
||||
options: EditPredictionContextOptions,
|
||||
syntax_index: Option<Entity<SyntaxIndex>>,
|
||||
cx: &mut App,
|
||||
) -> Task<Option<Self>> {
|
||||
let parent_abs_path = project::File::from_dyn(buffer.file()).and_then(|f| {
|
||||
let mut path = f.worktree.read(cx).absolutize(&f.path);
|
||||
if path.pop() { Some(path) } else { None }
|
||||
});
|
||||
|
||||
if let Some(syntax_index) = syntax_index {
|
||||
let index_state =
|
||||
syntax_index.read_with(cx, |index, _cx| Arc::downgrade(index.state()));
|
||||
cx.background_spawn(async move {
|
||||
let parent_abs_path = parent_abs_path.as_deref();
|
||||
let index_state = index_state.upgrade()?;
|
||||
let index_state = index_state.lock().await;
|
||||
Self::gather_context(cursor_point, &buffer, &excerpt_options, Some(&index_state))
|
||||
Self::gather_context(
|
||||
cursor_point,
|
||||
&buffer,
|
||||
parent_abs_path,
|
||||
&options,
|
||||
Some(&index_state),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
cx.background_spawn(async move {
|
||||
Self::gather_context(cursor_point, &buffer, &excerpt_options, None)
|
||||
let parent_abs_path = parent_abs_path.as_deref();
|
||||
Self::gather_context(cursor_point, &buffer, parent_abs_path, &options, None)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -53,13 +75,20 @@ impl EditPredictionContext {
|
||||
pub fn gather_context(
|
||||
cursor_point: Point,
|
||||
buffer: &BufferSnapshot,
|
||||
excerpt_options: &EditPredictionExcerptOptions,
|
||||
parent_abs_path: Option<&Path>,
|
||||
options: &EditPredictionContextOptions,
|
||||
index_state: Option<&SyntaxIndexState>,
|
||||
) -> Option<Self> {
|
||||
let imports = if options.use_imports {
|
||||
Imports::gather(&buffer, parent_abs_path)
|
||||
} else {
|
||||
Imports::default()
|
||||
};
|
||||
Self::gather_context_with_references_fn(
|
||||
cursor_point,
|
||||
buffer,
|
||||
excerpt_options,
|
||||
&imports,
|
||||
options,
|
||||
index_state,
|
||||
references_in_excerpt,
|
||||
)
|
||||
@@ -68,7 +97,8 @@ impl EditPredictionContext {
|
||||
pub fn gather_context_with_references_fn(
|
||||
cursor_point: Point,
|
||||
buffer: &BufferSnapshot,
|
||||
excerpt_options: &EditPredictionExcerptOptions,
|
||||
imports: &Imports,
|
||||
options: &EditPredictionContextOptions,
|
||||
index_state: Option<&SyntaxIndexState>,
|
||||
get_references: impl FnOnce(
|
||||
&EditPredictionExcerpt,
|
||||
@@ -79,7 +109,7 @@ impl EditPredictionContext {
|
||||
let excerpt = EditPredictionExcerpt::select_from_buffer(
|
||||
cursor_point,
|
||||
buffer,
|
||||
excerpt_options,
|
||||
&options.excerpt,
|
||||
index_state,
|
||||
)?;
|
||||
let excerpt_text = excerpt.text(buffer);
|
||||
@@ -101,10 +131,12 @@ impl EditPredictionContext {
|
||||
let references = get_references(&excerpt, &excerpt_text, buffer);
|
||||
|
||||
scored_declarations(
|
||||
&options.score,
|
||||
&index_state,
|
||||
&excerpt,
|
||||
&excerpt_occurrences,
|
||||
&adjacent_occurrences,
|
||||
&imports,
|
||||
references,
|
||||
cursor_offset_in_file,
|
||||
buffer,
|
||||
@@ -160,12 +192,18 @@ mod tests {
|
||||
EditPredictionContext::gather_context_in_background(
|
||||
cursor_point,
|
||||
buffer_snapshot,
|
||||
EditPredictionExcerptOptions {
|
||||
max_bytes: 60,
|
||||
min_bytes: 10,
|
||||
target_before_cursor_over_total_bytes: 0.5,
|
||||
EditPredictionContextOptions {
|
||||
use_imports: true,
|
||||
excerpt: EditPredictionExcerptOptions {
|
||||
max_bytes: 60,
|
||||
min_bytes: 10,
|
||||
target_before_cursor_over_total_bytes: 0.5,
|
||||
},
|
||||
score: EditPredictionScoreOptions {
|
||||
omit_excerpt_overlaps: true,
|
||||
},
|
||||
},
|
||||
Some(index),
|
||||
Some(index.clone()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
||||
1319
crates/edit_prediction_context/src/imports.rs
Normal file
1319
crates/edit_prediction_context/src/imports.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ use futures::lock::Mutex;
|
||||
use futures::{FutureExt as _, StreamExt, future};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
|
||||
use itertools::Itertools;
|
||||
|
||||
use language::{Buffer, BufferEvent};
|
||||
use postage::stream::Stream as _;
|
||||
use project::buffer_store::{BufferStore, BufferStoreEvent};
|
||||
@@ -17,6 +18,7 @@ use std::sync::Arc;
|
||||
use text::BufferId;
|
||||
use util::{RangeExt as _, debug_panic, some_or_debug_panic};
|
||||
|
||||
use crate::CachedDeclarationPath;
|
||||
use crate::declaration::{
|
||||
BufferDeclaration, Declaration, DeclarationId, FileDeclaration, Identifier,
|
||||
};
|
||||
@@ -28,6 +30,8 @@ use crate::outline::declarations_in_buffer;
|
||||
// `buffer_declarations_containing_range` assumes that the index is always immediately up to date.
|
||||
//
|
||||
// * Add a per language configuration for skipping indexing.
|
||||
//
|
||||
// * Handle tsx / ts / js referencing each-other
|
||||
|
||||
// Potential future improvements:
|
||||
//
|
||||
@@ -61,6 +65,7 @@ pub struct SyntaxIndex {
|
||||
state: Arc<Mutex<SyntaxIndexState>>,
|
||||
project: WeakEntity<Project>,
|
||||
initial_file_indexing_done_rx: postage::watch::Receiver<bool>,
|
||||
_file_indexing_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
pub struct SyntaxIndexState {
|
||||
@@ -70,7 +75,6 @@ pub struct SyntaxIndexState {
|
||||
buffers: HashMap<BufferId, BufferState>,
|
||||
dirty_files: HashMap<ProjectEntryId, ProjectPath>,
|
||||
dirty_files_tx: mpsc::Sender<()>,
|
||||
_file_indexing_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -102,12 +106,12 @@ impl SyntaxIndex {
|
||||
buffers: HashMap::default(),
|
||||
dirty_files: HashMap::default(),
|
||||
dirty_files_tx,
|
||||
_file_indexing_task: None,
|
||||
};
|
||||
let this = Self {
|
||||
let mut this = Self {
|
||||
project: project.downgrade(),
|
||||
state: Arc::new(Mutex::new(initial_state)),
|
||||
initial_file_indexing_done_rx,
|
||||
_file_indexing_task: None,
|
||||
};
|
||||
|
||||
let worktree_store = project.read(cx).worktree_store();
|
||||
@@ -116,75 +120,77 @@ impl SyntaxIndex {
|
||||
.worktrees()
|
||||
.map(|w| w.read(cx).snapshot())
|
||||
.collect::<Vec<_>>();
|
||||
if !initial_worktree_snapshots.is_empty() {
|
||||
this.state.try_lock().unwrap()._file_indexing_task =
|
||||
Some(cx.spawn(async move |this, cx| {
|
||||
let snapshots_file_count = initial_worktree_snapshots
|
||||
.iter()
|
||||
.map(|worktree| worktree.file_count())
|
||||
.sum::<usize>();
|
||||
let chunk_size = snapshots_file_count.div_ceil(file_indexing_parallelism);
|
||||
let chunk_count = snapshots_file_count.div_ceil(chunk_size);
|
||||
let file_chunks = initial_worktree_snapshots
|
||||
.iter()
|
||||
.flat_map(|worktree| {
|
||||
let worktree_id = worktree.id();
|
||||
worktree.files(false, 0).map(move |entry| {
|
||||
(
|
||||
entry.id,
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: entry.path.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
this._file_indexing_task = Some(cx.spawn(async move |this, cx| {
|
||||
let snapshots_file_count = initial_worktree_snapshots
|
||||
.iter()
|
||||
.map(|worktree| worktree.file_count())
|
||||
.sum::<usize>();
|
||||
if snapshots_file_count > 0 {
|
||||
let chunk_size = snapshots_file_count.div_ceil(file_indexing_parallelism);
|
||||
let chunk_count = snapshots_file_count.div_ceil(chunk_size);
|
||||
let file_chunks = initial_worktree_snapshots
|
||||
.iter()
|
||||
.flat_map(|worktree| {
|
||||
let worktree_id = worktree.id();
|
||||
worktree.files(false, 0).map(move |entry| {
|
||||
(
|
||||
entry.id,
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: entry.path.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.chunks(chunk_size);
|
||||
})
|
||||
.chunks(chunk_size);
|
||||
|
||||
let mut tasks = Vec::with_capacity(chunk_count);
|
||||
for chunk in file_chunks.into_iter() {
|
||||
tasks.push(Self::update_dirty_files(
|
||||
&this,
|
||||
chunk.into_iter().collect(),
|
||||
cx.clone(),
|
||||
));
|
||||
}
|
||||
futures::future::join_all(tasks).await;
|
||||
let mut tasks = Vec::with_capacity(chunk_count);
|
||||
for chunk in file_chunks.into_iter() {
|
||||
tasks.push(Self::update_dirty_files(
|
||||
&this,
|
||||
chunk.into_iter().collect(),
|
||||
cx.clone(),
|
||||
));
|
||||
}
|
||||
futures::future::join_all(tasks).await;
|
||||
log::info!("Finished initial file indexing");
|
||||
}
|
||||
|
||||
log::info!("Finished initial file indexing");
|
||||
*initial_file_indexing_done_tx.borrow_mut() = true;
|
||||
*initial_file_indexing_done_tx.borrow_mut() = true;
|
||||
|
||||
let Ok(state) = this.read_with(cx, |this, _cx| this.state.clone()) else {
|
||||
return;
|
||||
};
|
||||
while dirty_files_rx.next().await.is_some() {
|
||||
let mut state = state.lock().await;
|
||||
let was_underused = state.dirty_files.capacity() > 255
|
||||
&& state.dirty_files.len() * 8 < state.dirty_files.capacity();
|
||||
let dirty_files = state.dirty_files.drain().collect::<Vec<_>>();
|
||||
if was_underused {
|
||||
state.dirty_files.shrink_to_fit();
|
||||
}
|
||||
drop(state);
|
||||
if dirty_files.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Ok(state) = this.read_with(cx, |this, _cx| Arc::downgrade(&this.state)) else {
|
||||
return;
|
||||
};
|
||||
while dirty_files_rx.next().await.is_some() {
|
||||
let Some(state) = state.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let mut state = state.lock().await;
|
||||
let was_underused = state.dirty_files.capacity() > 255
|
||||
&& state.dirty_files.len() * 8 < state.dirty_files.capacity();
|
||||
let dirty_files = state.dirty_files.drain().collect::<Vec<_>>();
|
||||
if was_underused {
|
||||
state.dirty_files.shrink_to_fit();
|
||||
}
|
||||
drop(state);
|
||||
if dirty_files.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let chunk_size = dirty_files.len().div_ceil(file_indexing_parallelism);
|
||||
let chunk_count = dirty_files.len().div_ceil(chunk_size);
|
||||
let mut tasks = Vec::with_capacity(chunk_count);
|
||||
let chunks = dirty_files.into_iter().chunks(chunk_size);
|
||||
for chunk in chunks.into_iter() {
|
||||
tasks.push(Self::update_dirty_files(
|
||||
&this,
|
||||
chunk.into_iter().collect(),
|
||||
cx.clone(),
|
||||
));
|
||||
}
|
||||
futures::future::join_all(tasks).await;
|
||||
}
|
||||
}));
|
||||
}
|
||||
let chunk_size = dirty_files.len().div_ceil(file_indexing_parallelism);
|
||||
let chunk_count = dirty_files.len().div_ceil(chunk_size);
|
||||
let mut tasks = Vec::with_capacity(chunk_count);
|
||||
let chunks = dirty_files.into_iter().chunks(chunk_size);
|
||||
for chunk in chunks.into_iter() {
|
||||
tasks.push(Self::update_dirty_files(
|
||||
&this,
|
||||
chunk.into_iter().collect(),
|
||||
cx.clone(),
|
||||
));
|
||||
}
|
||||
futures::future::join_all(tasks).await;
|
||||
}
|
||||
}));
|
||||
|
||||
cx.subscribe(&worktree_store, Self::handle_worktree_store_event)
|
||||
.detach();
|
||||
@@ -364,7 +370,9 @@ impl SyntaxIndex {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
BufferEvent::Edited => self.update_buffer(buffer, cx),
|
||||
BufferEvent::Edited |
|
||||
// paths are cached and so should be updated
|
||||
BufferEvent::FileHandleChanged => self.update_buffer(buffer, cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -375,8 +383,16 @@ impl SyntaxIndex {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(project_entry_id) =
|
||||
project::File::from_dyn(buffer.file()).and_then(|f| f.project_entry_id(cx))
|
||||
let Some((project_entry_id, cached_path)) = project::File::from_dyn(buffer.file())
|
||||
.and_then(|f| {
|
||||
let project_entry_id = f.project_entry_id()?;
|
||||
let cached_path = CachedDeclarationPath::new(
|
||||
f.worktree.read(cx).abs_path(),
|
||||
&f.path,
|
||||
buffer.language(),
|
||||
);
|
||||
Some((project_entry_id, cached_path))
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -440,6 +456,7 @@ impl SyntaxIndex {
|
||||
buffer_id,
|
||||
declaration,
|
||||
project_entry_id,
|
||||
cached_path: cached_path.clone(),
|
||||
});
|
||||
new_ids.push(declaration_id);
|
||||
|
||||
@@ -507,13 +524,14 @@ impl SyntaxIndex {
|
||||
|
||||
let snapshot_task = worktree.update(cx, |worktree, cx| {
|
||||
let load_task = worktree.load_file(&project_path.path, cx);
|
||||
let worktree_abs_path = worktree.abs_path();
|
||||
cx.spawn(async move |_this, cx| {
|
||||
let loaded_file = load_task.await?;
|
||||
let language = language.await?;
|
||||
|
||||
let buffer = cx.new(|cx| {
|
||||
let mut buffer = Buffer::local(loaded_file.text, cx);
|
||||
buffer.set_language(Some(language), cx);
|
||||
buffer.set_language(Some(language.clone()), cx);
|
||||
buffer
|
||||
})?;
|
||||
|
||||
@@ -522,14 +540,22 @@ impl SyntaxIndex {
|
||||
parse_status.changed().await?;
|
||||
}
|
||||
|
||||
buffer.read_with(cx, |buffer, _cx| buffer.snapshot())
|
||||
let cached_path = CachedDeclarationPath::new(
|
||||
worktree_abs_path,
|
||||
&project_path.path,
|
||||
Some(&language),
|
||||
);
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
anyhow::Ok((snapshot, cached_path))
|
||||
})
|
||||
});
|
||||
|
||||
let state = Arc::downgrade(&self.state);
|
||||
cx.background_spawn(async move {
|
||||
// TODO: How to handle errors?
|
||||
let Ok(snapshot) = snapshot_task.await else {
|
||||
let Ok((snapshot, cached_path)) = snapshot_task.await else {
|
||||
return;
|
||||
};
|
||||
let rope = snapshot.as_rope();
|
||||
@@ -567,6 +593,7 @@ impl SyntaxIndex {
|
||||
let declaration_id = state.declarations.insert(Declaration::File {
|
||||
project_entry_id: entry_id,
|
||||
declaration,
|
||||
cached_path: cached_path.clone(),
|
||||
});
|
||||
new_ids.push(declaration_id);
|
||||
|
||||
@@ -921,6 +948,7 @@ mod tests {
|
||||
if let Declaration::File {
|
||||
declaration,
|
||||
project_entry_id: file,
|
||||
..
|
||||
} = declaration
|
||||
{
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use hashbrown::HashTable;
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
hash::{Hash, Hasher as _},
|
||||
path::Path,
|
||||
sync::LazyLock,
|
||||
};
|
||||
use util::rel_path::RelPath;
|
||||
|
||||
use crate::reference::Reference;
|
||||
|
||||
@@ -45,19 +48,34 @@ impl Occurrences {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_identifiers<'a>(identifiers: impl IntoIterator<Item = &'a str>) -> Self {
|
||||
pub fn from_identifiers(identifiers: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
|
||||
let mut this = Self::default();
|
||||
// TODO: Score matches that match case higher?
|
||||
//
|
||||
// TODO: Also include unsplit identifier?
|
||||
for identifier in identifiers {
|
||||
for identifier_part in split_identifier(identifier) {
|
||||
for identifier_part in split_identifier(identifier.as_ref()) {
|
||||
this.add_hash(fx_hash(&identifier_part.to_lowercase()));
|
||||
}
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
pub fn from_worktree_path(worktree_name: Option<Cow<'_, str>>, rel_path: &RelPath) -> Self {
|
||||
if let Some(worktree_name) = worktree_name {
|
||||
Self::from_identifiers(
|
||||
std::iter::once(worktree_name)
|
||||
.chain(iter_path_without_extension(rel_path.as_std_path())),
|
||||
)
|
||||
} else {
|
||||
Self::from_path(rel_path.as_std_path())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_path(path: &Path) -> Self {
|
||||
Self::from_identifiers(iter_path_without_extension(path))
|
||||
}
|
||||
|
||||
fn add_hash(&mut self, hash: u64) {
|
||||
self.table
|
||||
.entry(
|
||||
@@ -82,6 +100,15 @@ impl Occurrences {
|
||||
}
|
||||
}
|
||||
|
||||
fn iter_path_without_extension(path: &Path) -> impl Iterator<Item = Cow<'_, str>> {
|
||||
let last_component: Option<Cow<'_, str>> = path.file_stem().map(|stem| stem.to_string_lossy());
|
||||
let mut path_components = path.components();
|
||||
path_components.next_back();
|
||||
path_components
|
||||
.map(|component| component.as_os_str().to_string_lossy())
|
||||
.chain(last_component)
|
||||
}
|
||||
|
||||
pub fn fx_hash<T: Hash + ?Sized>(data: &T) -> u64 {
|
||||
let mut hasher = collections::FxHasher::default();
|
||||
data.hash(&mut hasher);
|
||||
@@ -269,4 +296,19 @@ mod test {
|
||||
// the smaller set, 10.
|
||||
assert_eq!(weighted_overlap_coefficient(&set_a, &set_b), 7.0 / 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_path_without_extension() {
|
||||
let mut iter = iter_path_without_extension(Path::new(""));
|
||||
assert_eq!(iter.next(), None);
|
||||
|
||||
let iter = iter_path_without_extension(Path::new("foo"));
|
||||
assert_eq!(iter.collect::<Vec<_>>(), ["foo"]);
|
||||
|
||||
let iter = iter_path_without_extension(Path::new("foo/bar.txt"));
|
||||
assert_eq!(iter.collect::<Vec<_>>(), ["foo", "bar"]);
|
||||
|
||||
let iter = iter_path_without_extension(Path::new("foo/bar/baz.txt"));
|
||||
assert_eq!(iter.collect::<Vec<_>>(), ["foo", "bar", "baz"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,6 +456,33 @@ actions!(
|
||||
Fold,
|
||||
/// Folds all foldable regions in the editor.
|
||||
FoldAll,
|
||||
/// Folds all code blocks at indentation level 1.
|
||||
#[action(name = "FoldAtLevel_1")]
|
||||
FoldAtLevel1,
|
||||
/// Folds all code blocks at indentation level 2.
|
||||
#[action(name = "FoldAtLevel_2")]
|
||||
FoldAtLevel2,
|
||||
/// Folds all code blocks at indentation level 3.
|
||||
#[action(name = "FoldAtLevel_3")]
|
||||
FoldAtLevel3,
|
||||
/// Folds all code blocks at indentation level 4.
|
||||
#[action(name = "FoldAtLevel_4")]
|
||||
FoldAtLevel4,
|
||||
/// Folds all code blocks at indentation level 5.
|
||||
#[action(name = "FoldAtLevel_5")]
|
||||
FoldAtLevel5,
|
||||
/// Folds all code blocks at indentation level 6.
|
||||
#[action(name = "FoldAtLevel_6")]
|
||||
FoldAtLevel6,
|
||||
/// Folds all code blocks at indentation level 7.
|
||||
#[action(name = "FoldAtLevel_7")]
|
||||
FoldAtLevel7,
|
||||
/// Folds all code blocks at indentation level 8.
|
||||
#[action(name = "FoldAtLevel_8")]
|
||||
FoldAtLevel8,
|
||||
/// Folds all code blocks at indentation level 9.
|
||||
#[action(name = "FoldAtLevel_9")]
|
||||
FoldAtLevel9,
|
||||
/// Folds all function bodies in the editor.
|
||||
FoldFunctionBodies,
|
||||
/// Folds the current code block and all its children.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{App, Context, Entity, Window};
|
||||
use language::Language;
|
||||
use project::lsp_store::lsp_ext_command::SwitchSourceHeaderResult;
|
||||
use rpc::proto;
|
||||
use url::Url;
|
||||
use util::paths::PathStyle;
|
||||
use workspace::{OpenOptions, OpenVisible};
|
||||
|
||||
@@ -77,16 +78,17 @@ pub fn switch_source_header(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let goto = Url::parse(&switch_source_header.0).with_context(|| {
|
||||
format!(
|
||||
"Parsing URL \"{}\" returned from switch source/header failed",
|
||||
switch_source_header.0
|
||||
)
|
||||
})?;
|
||||
let goto = switch_source_header
|
||||
.0
|
||||
.strip_prefix("file://")
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Parsing file url \"{}\" returned from switch source/header failed",
|
||||
switch_source_header.0
|
||||
)
|
||||
})?;
|
||||
|
||||
let path = goto
|
||||
.to_file_path()
|
||||
.map_err(|()| anyhow::anyhow!("URL conversion to file path failed for \"{goto}\""))?;
|
||||
let path = PathBuf::from(goto);
|
||||
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
|
||||
@@ -1518,6 +1518,7 @@ impl CodeActionsMenu {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(task.resolved_label.replace("\n", ""))
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
@@ -1528,6 +1529,7 @@ impl CodeActionsMenu {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child("debug: ")
|
||||
.child(scenario.label.clone())
|
||||
.when(selected, |this| {
|
||||
|
||||
@@ -170,20 +170,15 @@ impl DisplayMap {
|
||||
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
|
||||
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
|
||||
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot, edits);
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
|
||||
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot, edits, tab_size);
|
||||
let (wrap_snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
|
||||
let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits).snapshot;
|
||||
.update(cx, |map, cx| map.sync(tab_snapshot, edits, cx));
|
||||
let block_snapshot = self.block_map.read(wrap_snapshot, edits).snapshot;
|
||||
|
||||
DisplaySnapshot {
|
||||
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
|
||||
fold_snapshot,
|
||||
inlay_snapshot,
|
||||
tab_snapshot,
|
||||
wrap_snapshot,
|
||||
block_snapshot,
|
||||
diagnostics_max_severity: self.diagnostics_max_severity,
|
||||
crease_snapshot: self.crease_map.snapshot(),
|
||||
@@ -198,10 +193,10 @@ impl DisplayMap {
|
||||
pub fn set_state(&mut self, other: &DisplaySnapshot, cx: &mut Context<Self>) {
|
||||
self.fold(
|
||||
other
|
||||
.folds_in_range(0..other.buffer_snapshot.len())
|
||||
.folds_in_range(0..other.buffer_snapshot().len())
|
||||
.map(|fold| {
|
||||
Crease::simple(
|
||||
fold.range.to_offset(&other.buffer_snapshot),
|
||||
fold.range.to_offset(other.buffer_snapshot()),
|
||||
fold.placeholder.clone(),
|
||||
)
|
||||
})
|
||||
@@ -762,12 +757,7 @@ impl<'a> HighlightedChunk<'a> {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DisplaySnapshot {
|
||||
pub buffer_snapshot: MultiBufferSnapshot,
|
||||
pub fold_snapshot: FoldSnapshot,
|
||||
pub crease_snapshot: CreaseSnapshot,
|
||||
inlay_snapshot: InlaySnapshot,
|
||||
tab_snapshot: TabSnapshot,
|
||||
wrap_snapshot: WrapSnapshot,
|
||||
block_snapshot: BlockSnapshot,
|
||||
text_highlights: TextHighlights,
|
||||
inlay_highlights: InlayHighlights,
|
||||
@@ -776,15 +766,44 @@ pub struct DisplaySnapshot {
|
||||
diagnostics_max_severity: DiagnosticSeverity,
|
||||
pub(crate) fold_placeholder: FoldPlaceholder,
|
||||
}
|
||||
|
||||
impl DisplaySnapshot {
|
||||
pub fn wrap_snapshot(&self) -> &WrapSnapshot {
|
||||
&self.block_snapshot.wrap_snapshot
|
||||
}
|
||||
pub fn tab_snapshot(&self) -> &TabSnapshot {
|
||||
&self.block_snapshot.wrap_snapshot.tab_snapshot
|
||||
}
|
||||
|
||||
pub fn fold_snapshot(&self) -> &FoldSnapshot {
|
||||
&self.block_snapshot.wrap_snapshot.tab_snapshot.fold_snapshot
|
||||
}
|
||||
|
||||
pub fn inlay_snapshot(&self) -> &InlaySnapshot {
|
||||
&self
|
||||
.block_snapshot
|
||||
.wrap_snapshot
|
||||
.tab_snapshot
|
||||
.fold_snapshot
|
||||
.inlay_snapshot
|
||||
}
|
||||
|
||||
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
|
||||
&self
|
||||
.block_snapshot
|
||||
.wrap_snapshot
|
||||
.tab_snapshot
|
||||
.fold_snapshot
|
||||
.inlay_snapshot
|
||||
.buffer
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn fold_count(&self) -> usize {
|
||||
self.fold_snapshot.fold_count()
|
||||
self.fold_snapshot().fold_count()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.buffer_snapshot.len() == 0
|
||||
self.buffer_snapshot().len() == 0
|
||||
}
|
||||
|
||||
pub fn row_infos(&self, start_row: DisplayRow) -> impl Iterator<Item = RowInfo> + '_ {
|
||||
@@ -792,16 +811,16 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
pub fn widest_line_number(&self) -> u32 {
|
||||
self.buffer_snapshot.widest_line_number()
|
||||
self.buffer_snapshot().widest_line_number()
|
||||
}
|
||||
|
||||
pub fn prev_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
|
||||
loop {
|
||||
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
|
||||
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left);
|
||||
let mut inlay_point = self.inlay_snapshot().to_inlay_point(point);
|
||||
let mut fold_point = self.fold_snapshot().to_fold_point(inlay_point, Bias::Left);
|
||||
fold_point.0.column = 0;
|
||||
inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
|
||||
point = self.inlay_snapshot.to_buffer_point(inlay_point);
|
||||
inlay_point = fold_point.to_inlay_point(self.fold_snapshot());
|
||||
point = self.inlay_snapshot().to_buffer_point(inlay_point);
|
||||
|
||||
let mut display_point = self.point_to_display_point(point, Bias::Left);
|
||||
*display_point.column_mut() = 0;
|
||||
@@ -819,11 +838,11 @@ impl DisplaySnapshot {
|
||||
) -> (MultiBufferPoint, DisplayPoint) {
|
||||
let original_point = point;
|
||||
loop {
|
||||
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
|
||||
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
|
||||
fold_point.0.column = self.fold_snapshot.line_len(fold_point.row());
|
||||
inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
|
||||
point = self.inlay_snapshot.to_buffer_point(inlay_point);
|
||||
let mut inlay_point = self.inlay_snapshot().to_inlay_point(point);
|
||||
let mut fold_point = self.fold_snapshot().to_fold_point(inlay_point, Bias::Right);
|
||||
fold_point.0.column = self.fold_snapshot().line_len(fold_point.row());
|
||||
inlay_point = fold_point.to_inlay_point(self.fold_snapshot());
|
||||
point = self.inlay_snapshot().to_buffer_point(inlay_point);
|
||||
|
||||
let mut display_point = self.point_to_display_point(point, Bias::Right);
|
||||
*display_point.column_mut() = self.line_len(display_point.row());
|
||||
@@ -841,7 +860,8 @@ impl DisplaySnapshot {
|
||||
let new_end = if range.end.column > 0 {
|
||||
MultiBufferPoint::new(
|
||||
range.end.row,
|
||||
self.buffer_snapshot.line_len(MultiBufferRow(range.end.row)),
|
||||
self.buffer_snapshot()
|
||||
.line_len(MultiBufferRow(range.end.row)),
|
||||
)
|
||||
} else {
|
||||
range.end
|
||||
@@ -851,52 +871,52 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
pub fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
|
||||
let inlay_point = self.inlay_snapshot.to_inlay_point(point);
|
||||
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
|
||||
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
|
||||
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
|
||||
let inlay_point = self.inlay_snapshot().to_inlay_point(point);
|
||||
let fold_point = self.fold_snapshot().to_fold_point(inlay_point, bias);
|
||||
let tab_point = self.tab_snapshot().to_tab_point(fold_point);
|
||||
let wrap_point = self.wrap_snapshot().tab_point_to_wrap_point(tab_point);
|
||||
let block_point = self.block_snapshot.to_block_point(wrap_point);
|
||||
DisplayPoint(block_point)
|
||||
}
|
||||
|
||||
pub fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
|
||||
self.inlay_snapshot
|
||||
self.inlay_snapshot()
|
||||
.to_buffer_point(self.display_point_to_inlay_point(point, bias))
|
||||
}
|
||||
|
||||
pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset {
|
||||
self.inlay_snapshot
|
||||
self.inlay_snapshot()
|
||||
.to_offset(self.display_point_to_inlay_point(point, bias))
|
||||
}
|
||||
|
||||
pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset {
|
||||
self.inlay_snapshot
|
||||
.to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
|
||||
self.inlay_snapshot()
|
||||
.to_inlay_offset(anchor.to_offset(self.buffer_snapshot()))
|
||||
}
|
||||
|
||||
pub fn display_point_to_anchor(&self, point: DisplayPoint, bias: Bias) -> Anchor {
|
||||
self.buffer_snapshot
|
||||
self.buffer_snapshot()
|
||||
.anchor_at(point.to_offset(self, bias), bias)
|
||||
}
|
||||
|
||||
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
|
||||
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
|
||||
let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
|
||||
fold_point.to_inlay_point(&self.fold_snapshot)
|
||||
let tab_point = self.wrap_snapshot().to_tab_point(wrap_point);
|
||||
let fold_point = self.tab_snapshot().to_fold_point(tab_point, bias).0;
|
||||
fold_point.to_inlay_point(self.fold_snapshot())
|
||||
}
|
||||
|
||||
pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
|
||||
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
|
||||
self.tab_snapshot.to_fold_point(tab_point, bias).0
|
||||
let tab_point = self.wrap_snapshot().to_tab_point(wrap_point);
|
||||
self.tab_snapshot().to_fold_point(tab_point, bias).0
|
||||
}
|
||||
|
||||
pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint {
|
||||
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
|
||||
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
|
||||
let tab_point = self.tab_snapshot().to_tab_point(fold_point);
|
||||
let wrap_point = self.wrap_snapshot().tab_point_to_wrap_point(tab_point);
|
||||
let block_point = self.block_snapshot.to_block_point(wrap_point);
|
||||
DisplayPoint(block_point)
|
||||
}
|
||||
@@ -1118,7 +1138,7 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
pub fn buffer_chars_at(&self, mut offset: usize) -> impl Iterator<Item = (char, usize)> + '_ {
|
||||
self.buffer_snapshot.chars_at(offset).map(move |ch| {
|
||||
self.buffer_snapshot().chars_at(offset).map(move |ch| {
|
||||
let ret = (ch, offset);
|
||||
offset += ch.len_utf8();
|
||||
ret
|
||||
@@ -1129,7 +1149,7 @@ impl DisplaySnapshot {
|
||||
&self,
|
||||
mut offset: usize,
|
||||
) -> impl Iterator<Item = (char, usize)> + '_ {
|
||||
self.buffer_snapshot
|
||||
self.buffer_snapshot()
|
||||
.reversed_chars_at(offset)
|
||||
.map(move |ch| {
|
||||
offset -= ch.len_utf8();
|
||||
@@ -1152,11 +1172,11 @@ impl DisplaySnapshot {
|
||||
pub fn clip_at_line_end(&self, display_point: DisplayPoint) -> DisplayPoint {
|
||||
let mut point = self.display_point_to_point(display_point, Bias::Left);
|
||||
|
||||
if point.column != self.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
|
||||
if point.column != self.buffer_snapshot().line_len(MultiBufferRow(point.row)) {
|
||||
return display_point;
|
||||
}
|
||||
point.column = point.column.saturating_sub(1);
|
||||
point = self.buffer_snapshot.clip_point(point, Bias::Left);
|
||||
point = self.buffer_snapshot().clip_point(point, Bias::Left);
|
||||
self.point_to_display_point(point, Bias::Left)
|
||||
}
|
||||
|
||||
@@ -1164,7 +1184,7 @@ impl DisplaySnapshot {
|
||||
where
|
||||
T: ToOffset,
|
||||
{
|
||||
self.fold_snapshot.folds_in_range(range)
|
||||
self.fold_snapshot().folds_in_range(range)
|
||||
}
|
||||
|
||||
pub fn blocks_in_range(
|
||||
@@ -1185,12 +1205,12 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
|
||||
self.fold_snapshot.intersects_fold(offset)
|
||||
self.fold_snapshot().intersects_fold(offset)
|
||||
}
|
||||
|
||||
pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
|
||||
self.block_snapshot.is_line_replaced(buffer_row)
|
||||
|| self.fold_snapshot.is_line_folded(buffer_row)
|
||||
|| self.fold_snapshot().is_line_folded(buffer_row)
|
||||
}
|
||||
|
||||
pub fn is_block_line(&self, display_row: DisplayRow) -> bool {
|
||||
@@ -1207,7 +1227,7 @@ impl DisplaySnapshot {
|
||||
.block_snapshot
|
||||
.to_wrap_point(BlockPoint::new(display_row.0, 0), Bias::Left)
|
||||
.row();
|
||||
self.wrap_snapshot.soft_wrap_indent(wrap_row)
|
||||
self.wrap_snapshot().soft_wrap_indent(wrap_row)
|
||||
}
|
||||
|
||||
pub fn text(&self) -> String {
|
||||
@@ -1228,7 +1248,7 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
pub fn line_indent_for_buffer_row(&self, buffer_row: MultiBufferRow) -> LineIndent {
|
||||
self.buffer_snapshot.line_indent_for_row(buffer_row)
|
||||
self.buffer_snapshot().line_indent_for_row(buffer_row)
|
||||
}
|
||||
|
||||
pub fn line_len(&self, row: DisplayRow) -> u32 {
|
||||
@@ -1246,7 +1266,7 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool {
|
||||
let max_row = self.buffer_snapshot.max_row();
|
||||
let max_row = self.buffer_snapshot().max_row();
|
||||
if buffer_row >= max_row {
|
||||
return false;
|
||||
}
|
||||
@@ -1271,10 +1291,11 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
pub fn crease_for_buffer_row(&self, buffer_row: MultiBufferRow) -> Option<Crease<Point>> {
|
||||
let start = MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot.line_len(buffer_row));
|
||||
let start =
|
||||
MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot().line_len(buffer_row));
|
||||
if let Some(crease) = self
|
||||
.crease_snapshot
|
||||
.query_row(buffer_row, &self.buffer_snapshot)
|
||||
.query_row(buffer_row, self.buffer_snapshot())
|
||||
{
|
||||
match crease {
|
||||
Crease::Inline {
|
||||
@@ -1284,7 +1305,7 @@ impl DisplaySnapshot {
|
||||
render_trailer,
|
||||
metadata,
|
||||
} => Some(Crease::Inline {
|
||||
range: range.to_point(&self.buffer_snapshot),
|
||||
range: range.to_point(self.buffer_snapshot()),
|
||||
placeholder: placeholder.clone(),
|
||||
render_toggle: render_toggle.clone(),
|
||||
render_trailer: render_trailer.clone(),
|
||||
@@ -1298,7 +1319,7 @@ impl DisplaySnapshot {
|
||||
block_priority,
|
||||
render_toggle,
|
||||
} => Some(Crease::Block {
|
||||
range: range.to_point(&self.buffer_snapshot),
|
||||
range: range.to_point(self.buffer_snapshot()),
|
||||
block_height: *block_height,
|
||||
block_style: *block_style,
|
||||
render_block: render_block.clone(),
|
||||
@@ -1310,7 +1331,7 @@ impl DisplaySnapshot {
|
||||
&& !self.is_line_folded(MultiBufferRow(start.row))
|
||||
{
|
||||
let start_line_indent = self.line_indent_for_buffer_row(buffer_row);
|
||||
let max_point = self.buffer_snapshot.max_point();
|
||||
let max_point = self.buffer_snapshot().max_point();
|
||||
let mut end = None;
|
||||
|
||||
for row in (buffer_row.0 + 1)..=max_point.row {
|
||||
@@ -1321,7 +1342,7 @@ impl DisplaySnapshot {
|
||||
let prev_row = row - 1;
|
||||
end = Some(Point::new(
|
||||
prev_row,
|
||||
self.buffer_snapshot.line_len(MultiBufferRow(prev_row)),
|
||||
self.buffer_snapshot().line_len(MultiBufferRow(prev_row)),
|
||||
));
|
||||
break;
|
||||
}
|
||||
@@ -1330,7 +1351,7 @@ impl DisplaySnapshot {
|
||||
let mut row_before_line_breaks = end.unwrap_or(max_point);
|
||||
while row_before_line_breaks.row > start.row
|
||||
&& self
|
||||
.buffer_snapshot
|
||||
.buffer_snapshot()
|
||||
.is_line_blank(MultiBufferRow(row_before_line_breaks.row))
|
||||
{
|
||||
row_before_line_breaks.row -= 1;
|
||||
@@ -1338,7 +1359,7 @@ impl DisplaySnapshot {
|
||||
|
||||
row_before_line_breaks = Point::new(
|
||||
row_before_line_breaks.row,
|
||||
self.buffer_snapshot
|
||||
self.buffer_snapshot()
|
||||
.line_len(MultiBufferRow(row_before_line_breaks.row)),
|
||||
);
|
||||
|
||||
@@ -1482,23 +1503,23 @@ impl DisplayPoint {
|
||||
|
||||
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
|
||||
let wrap_point = map.block_snapshot.to_wrap_point(self.0, bias);
|
||||
let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
|
||||
let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
|
||||
let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
|
||||
map.inlay_snapshot
|
||||
.to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
|
||||
let tab_point = map.wrap_snapshot().to_tab_point(wrap_point);
|
||||
let fold_point = map.tab_snapshot().to_fold_point(tab_point, bias).0;
|
||||
let inlay_point = fold_point.to_inlay_point(map.fold_snapshot());
|
||||
map.inlay_snapshot()
|
||||
.to_buffer_offset(map.inlay_snapshot().to_offset(inlay_point))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToDisplayPoint for usize {
|
||||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
|
||||
map.point_to_display_point(self.to_point(&map.buffer_snapshot), Bias::Left)
|
||||
map.point_to_display_point(self.to_point(map.buffer_snapshot()), Bias::Left)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToDisplayPoint for OffsetUtf16 {
|
||||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
|
||||
self.to_offset(&map.buffer_snapshot).to_display_point(map)
|
||||
self.to_offset(map.buffer_snapshot()).to_display_point(map)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1510,7 +1531,7 @@ impl ToDisplayPoint for Point {
|
||||
|
||||
impl ToDisplayPoint for Anchor {
|
||||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
|
||||
self.to_point(&map.buffer_snapshot).to_display_point(map)
|
||||
self.to_point(map.buffer_snapshot()).to_display_point(map)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1599,10 +1620,10 @@ pub mod tests {
|
||||
let mut blocks = Vec::new();
|
||||
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
|
||||
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
|
||||
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
|
||||
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
|
||||
log::info!("buffer text: {:?}", snapshot.buffer_snapshot().text());
|
||||
log::info!("fold text: {:?}", snapshot.fold_snapshot().text());
|
||||
log::info!("tab text: {:?}", snapshot.tab_snapshot().text());
|
||||
log::info!("wrap text: {:?}", snapshot.wrap_snapshot().text());
|
||||
log::info!("block text: {:?}", snapshot.block_snapshot.text());
|
||||
log::info!("display text: {:?}", snapshot.text());
|
||||
|
||||
@@ -1634,7 +1655,8 @@ pub mod tests {
|
||||
30..=44 => {
|
||||
map.update(cx, |map, cx| {
|
||||
if rng.random() || blocks.is_empty() {
|
||||
let buffer = map.snapshot(cx).buffer_snapshot;
|
||||
let snapshot = map.snapshot(cx);
|
||||
let buffer = snapshot.buffer_snapshot();
|
||||
let block_properties = (0..rng.random_range(1..=1))
|
||||
.map(|_| {
|
||||
let position = buffer.anchor_after(buffer.clip_offset(
|
||||
@@ -1715,15 +1737,15 @@ pub mod tests {
|
||||
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
fold_count = snapshot.fold_count();
|
||||
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
|
||||
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
|
||||
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
|
||||
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
|
||||
log::info!("buffer text: {:?}", snapshot.buffer_snapshot().text());
|
||||
log::info!("fold text: {:?}", snapshot.fold_snapshot().text());
|
||||
log::info!("tab text: {:?}", snapshot.tab_snapshot().text());
|
||||
log::info!("wrap text: {:?}", snapshot.wrap_snapshot().text());
|
||||
log::info!("block text: {:?}", snapshot.block_snapshot.text());
|
||||
log::info!("display text: {:?}", snapshot.text());
|
||||
|
||||
// Line boundaries
|
||||
let buffer = &snapshot.buffer_snapshot;
|
||||
let buffer = snapshot.buffer_snapshot();
|
||||
for _ in 0..5 {
|
||||
let row = rng.random_range(0..=buffer.max_point().row);
|
||||
let column = rng.random_range(0..=buffer.line_len(MultiBufferRow(row)));
|
||||
@@ -1907,7 +1929,7 @@ pub mod tests {
|
||||
)
|
||||
);
|
||||
|
||||
let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
|
||||
let ix = snapshot.buffer_snapshot().text().find("seven").unwrap();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(ix..ix, "and ")], None, cx);
|
||||
});
|
||||
|
||||
@@ -33,8 +33,8 @@ const BULLETS: &[u8; u128::BITS as usize] = &[b'*'; _];
|
||||
///
|
||||
/// See the [`display_map` module documentation](crate::display_map) for more information.
|
||||
pub struct BlockMap {
|
||||
pub(super) wrap_snapshot: RefCell<WrapSnapshot>,
|
||||
next_block_id: AtomicUsize,
|
||||
wrap_snapshot: RefCell<WrapSnapshot>,
|
||||
custom_blocks: Vec<Arc<CustomBlock>>,
|
||||
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
|
||||
transforms: RefCell<SumTree<Transform>>,
|
||||
@@ -53,7 +53,7 @@ pub struct BlockMapWriter<'a>(&'a mut BlockMap);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BlockSnapshot {
|
||||
wrap_snapshot: WrapSnapshot,
|
||||
pub(super) wrap_snapshot: WrapSnapshot,
|
||||
transforms: SumTree<Transform>,
|
||||
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
|
||||
pub(super) buffer_header_height: u32,
|
||||
@@ -689,6 +689,7 @@ impl BlockMap {
|
||||
|
||||
// For each of these blocks, insert a new isomorphic transform preceding the block,
|
||||
// and then insert the block itself.
|
||||
let mut just_processed_folded_buffer = false;
|
||||
for (block_placement, block) in blocks_in_edit.drain(..) {
|
||||
let mut summary = TransformSummary {
|
||||
input_rows: 0,
|
||||
@@ -701,8 +702,12 @@ impl BlockMap {
|
||||
match block_placement {
|
||||
BlockPlacement::Above(position) => {
|
||||
rows_before_block = position.0 - new_transforms.summary().input_rows;
|
||||
just_processed_folded_buffer = false;
|
||||
}
|
||||
BlockPlacement::Near(position) | BlockPlacement::Below(position) => {
|
||||
if just_processed_folded_buffer {
|
||||
continue;
|
||||
}
|
||||
if position.0 + 1 < new_transforms.summary().input_rows {
|
||||
continue;
|
||||
}
|
||||
@@ -711,6 +716,7 @@ impl BlockMap {
|
||||
BlockPlacement::Replace(range) => {
|
||||
rows_before_block = range.start().0 - new_transforms.summary().input_rows;
|
||||
summary.input_rows = range.end().0 - range.start().0 + 1;
|
||||
just_processed_folded_buffer = matches!(block, Block::FoldedBuffer { .. });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3566,6 +3572,96 @@ mod tests {
|
||||
assert_eq!(blocks_snapshot.text(), "abc\n\ndef\nghi\njkl\nmno");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_folded_buffer_with_near_blocks(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(init_test);
|
||||
|
||||
let text = "line 1\nline 2\nline 3";
|
||||
let buffer = cx.update(|cx| {
|
||||
MultiBuffer::build_multi([(text, vec![Point::new(0, 0)..Point::new(2, 6)])], cx)
|
||||
});
|
||||
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
|
||||
let buffer_ids = buffer_snapshot
|
||||
.excerpts()
|
||||
.map(|(_, buffer_snapshot, _)| buffer_snapshot.remote_id())
|
||||
.dedup()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(buffer_ids.len(), 1);
|
||||
let buffer_id = buffer_ids[0];
|
||||
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, wrap_snapshot) =
|
||||
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
|
||||
let mut block_map = BlockMap::new(wrap_snapshot.clone(), 1, 1);
|
||||
|
||||
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
|
||||
writer.insert(vec![BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
placement: BlockPlacement::Near(buffer_snapshot.anchor_after(Point::new(0, 0))),
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
}]);
|
||||
|
||||
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
|
||||
assert_eq!(blocks_snapshot.text(), "\nline 1\n\nline 2\nline 3");
|
||||
|
||||
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
|
||||
buffer.read_with(cx, |buffer, cx| {
|
||||
writer.fold_buffers([buffer_id], buffer, cx);
|
||||
});
|
||||
|
||||
let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default());
|
||||
assert_eq!(blocks_snapshot.text(), "");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_folded_buffer_with_near_blocks_on_last_line(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(init_test);
|
||||
|
||||
let text = "line 1\nline 2\nline 3\nline 4";
|
||||
let buffer = cx.update(|cx| {
|
||||
MultiBuffer::build_multi([(text, vec![Point::new(0, 0)..Point::new(3, 6)])], cx)
|
||||
});
|
||||
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
|
||||
let buffer_ids = buffer_snapshot
|
||||
.excerpts()
|
||||
.map(|(_, buffer_snapshot, _)| buffer_snapshot.remote_id())
|
||||
.dedup()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(buffer_ids.len(), 1);
|
||||
let buffer_id = buffer_ids[0];
|
||||
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, wrap_snapshot) =
|
||||
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
|
||||
let mut block_map = BlockMap::new(wrap_snapshot.clone(), 1, 1);
|
||||
|
||||
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
|
||||
writer.insert(vec![BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
placement: BlockPlacement::Near(buffer_snapshot.anchor_after(Point::new(3, 6))),
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
}]);
|
||||
|
||||
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
|
||||
assert_eq!(blocks_snapshot.text(), "\nline 1\nline 2\nline 3\nline 4\n");
|
||||
|
||||
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
|
||||
buffer.read_with(cx, |buffer, cx| {
|
||||
writer.fold_buffers([buffer_id], buffer, cx);
|
||||
});
|
||||
|
||||
let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default());
|
||||
assert_eq!(blocks_snapshot.text(), "");
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut gpui::App) {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
|
||||
@@ -624,10 +624,10 @@ impl FoldMap {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FoldSnapshot {
|
||||
pub inlay_snapshot: InlaySnapshot,
|
||||
transforms: SumTree<Transform>,
|
||||
folds: SumTree<Fold>,
|
||||
fold_metadata_by_id: TreeMap<FoldId, FoldMetadata>,
|
||||
pub inlay_snapshot: InlaySnapshot,
|
||||
pub version: usize,
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user