Compare commits

..

22 Commits

Author SHA1 Message Date
Conrad Irwin
fdc996a87c Some of :norm in vim mode 2025-01-23 10:42:29 -07:00
Marshall Bowers
82cee9e9a4 assistant2: Don't suggest thread context for inline assist without a ThreadStore (#23506)
This PR makes it so we don't suggest threads as context in the inline
assist when we don't have a `ThreadStore` to pull from.

Release Notes:

- N/A
2025-01-22 23:47:56 +00:00
renovate[bot]
ecf70db7f0 Update Rust crate serde_json to v1.0.137 (#23498)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://redirect.github.com/serde-rs/json) | dependencies
| patch | `1.0.135` -> `1.0.137` |
| [serde_json](https://redirect.github.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.135` -> `1.0.137` |

---

### Release Notes

<details>
<summary>serde-rs/json (serde_json)</summary>

###
[`v1.0.137`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.137)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/v1.0.136...v1.0.137)

- Turn on "float_roundtrip" and "unbounded_depth" features for
serde_json in play.rust-lang.org
([#&#8203;1231](https://redirect.github.com/serde-rs/json/issues/1231))

###
[`v1.0.136`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.136)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/v1.0.135...v1.0.136)

- Optimize serde_json::value::Serializer::serialize_map by using
Map::with_capacity
([#&#8203;1230](https://redirect.github.com/serde-rs/json/issues/1230),
thanks [@&#8203;goffrie](https://redirect.github.com/goffrie))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 18:12:27 -05:00
renovate[bot]
848f29831e Update Rust crate semver to v1.0.25 (#23497)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [semver](https://redirect.github.com/dtolnay/semver) |
workspace.dependencies | patch | `1.0.24` -> `1.0.25` |

---

### Release Notes

<details>
<summary>dtolnay/semver (semver)</summary>

###
[`v1.0.25`](https://redirect.github.com/dtolnay/semver/releases/tag/1.0.25)

[Compare
Source](https://redirect.github.com/dtolnay/semver/compare/1.0.24...1.0.25)

- Enable serde impls on play.rust-lang.org
([#&#8203;330](https://redirect.github.com/dtolnay/semver/issues/330),
thanks [@&#8203;jofas](https://redirect.github.com/jofas))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 18:11:47 -05:00
Cole Miller
78a8c1a68a git: No-op when trying to commit with nothing staged or no commit message (#23491)
The buttons are disabled in this case, but users can still trigger a
commit via the actions directly, and an error toast seems a bit loud for
this.

cc @iamnbutler 

Release Notes:

- N/A
2025-01-22 18:04:14 -05:00
renovate[bot]
db65ee0add Update Rust crate etagere to v0.2.15 (#23493)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [etagere](https://redirect.github.com/nical/etagere) | dependencies |
patch | `0.2.13` -> `0.2.15` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 18:03:51 -05:00
Marshall Bowers
8ed2a81962 feature_flags: Enable assistant2 for all staff (#23502)
This PR makes it so the `assistant2` feature flag is automatically
enabled for all staff.

Previously all the staff members had been opted in to the feature flag
manually (to allow for opting out), but I think we're ready to
unconditionally ship it to all staff.

Release Notes:

- N/A
2025-01-22 23:02:53 +00:00
Marshall Bowers
01b57b10e7 assistant2: Only show thread context in picker when we have a ThreadStore (#23501)
This PR fixes an issue with the context picker where it would show
thread context as an option even if there was no `ThreadStore`
available.

Release Notes:

- N/A
2025-01-22 23:00:43 +00:00
Marshall Bowers
a1077c6fff assistant2: Add button to open the prompt library (#23500)
This PR adds a button to open the prompt library from the configuration
view in Assistant2.

<img width="1309" alt="Screenshot 2025-01-22 at 5 38 08 PM"
src="https://github.com/user-attachments/assets/d514abca-53bc-4cde-bead-ab68a1994fb5"
/>

Release Notes:

- N/A
2025-01-22 22:53:54 +00:00
Marshall Bowers
51fcb710d7 Dedupe PromptBuilder construction (#23496)
This PR dedupes the construction of the `PromptBuilder`.

Previously this was constructed by both `assistant` and `assistant2`,
but now we construct it outside and pass it in.

Release Notes:

- N/A
2025-01-22 22:17:36 +00:00
renovate[bot]
aad04e078a Update Rust crate indexmap to v2.7.1 (#23494)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [indexmap](https://redirect.github.com/indexmap-rs/indexmap) |
workspace.dependencies | patch | `2.7.0` -> `2.7.1` |

---

### Release Notes

<details>
<summary>indexmap-rs/indexmap (indexmap)</summary>

###
[`v2.7.1`](https://redirect.github.com/indexmap-rs/indexmap/blob/HEAD/RELEASES.md#271-2025-01-19)

[Compare
Source](https://redirect.github.com/indexmap-rs/indexmap/compare/2.7.0...2.7.1)

-   Added `#[track_caller]` to functions that may panic.
-   Improved memory reservation for `insert_entry`.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMDcuMCIsInVwZGF0ZWRJblZlciI6IjM5LjEwNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 16:19:23 -05:00
Piotr Osiewicz
3c0d05e362 lsp: Use replace edits for completions (#23490)
(Late) follow up to #9634.
Fixes #23395

Release Notes:

- Accepting completions while the cursor is in the middle of suggested
completion will now result in smaller edits being applied.
2025-01-22 21:00:04 +00:00
Cole Miller
55d99e0259 git: Handle git status output for deleted-in-index state (#23483)
When a file exists in HEAD, is deleted in the index, and exists again in
the working copy, git produces two lines for it, one reading `D `
(deleted in index, unmodified in working copy), and the other reading
`??` (untracked). Merge these two into the equivalent of `DA`.

Release Notes:

- Improved handling of files that are deleted in the git index but exist
in HEAD and the working copy
2025-01-22 15:28:25 -05:00
Piotr Osiewicz
08b3c03241 project: Allow running multiple instances of a single language server within a single worktree (#23473)
This PR introduces a new entity called Project Tree which is responsible
for finding subprojects within a worktree;
a subproject is a language-specific subset of a worktree which should be
accurately tracked on the language server side. We'll have an ability to
set multiple disjoint workspaceFolders on language server side OR spawn
multiple instances of a single language server (which will be the case
with e.g. Python language servers, as they need to interact with
multiple disjoint virtual environments).
Project Tree assumes that projects of the same LspAdapter kind cannot
overlap. Additionally project nesting is not allowed within the scope of
a single LspAdapter.

Closes https://github.com/zed-industries/zed/issues/5108
Re-lands #22182 which I had to revert due to merging it into todays
Preview.

Release Notes:

- Language servers now track their working directory more accurately.

---------

Co-authored-by: João <joao@zed.dev>
2025-01-22 21:19:02 +01:00
Marshall Bowers
2c2a3ef13d assistant2: Handle LLM providers that do not emit StartMessage events (#23485)
This PR updates Assistant2's response streaming to work with LLM
providers that do not emit `StartMessage` events.

Now if we get a `Text` event without having received a `StartMessage`
event we will still insert an Assistant message so we can stream in the
response from the model.

Release Notes:

- N/A
2025-01-22 20:15:16 +00:00
Peter Tripp
6aab82c180 docs: Mention PopOS 24.04 in Linux FAQ for /etc/prime-discrete (#23482) 2025-01-22 19:37:04 +00:00
Cole Miller
a811979894 git: Fix the panel's update debouncing (#23462)
This PR replaces the update debouncing code in the git panel with a more
correct and conventional structure (holding a `Task` field instead of
spawning a task that runs a loop). I wrote the code that this replaces
without realizing that it doesn't implement debouncing properly.

Release Notes:

- N/A
2025-01-22 14:33:17 -05:00
Marshall Bowers
514d9b4161 assistant2: Add configuration (#23481)
This PR wires up the ability to configure Assistant2.

<img width="1309" alt="Screenshot 2025-01-22 at 1 52 56 PM"
src="https://github.com/user-attachments/assets/3de47797-7959-47af-bd93-51f105e87c28"
/>

Release Notes:

- N/A
2025-01-22 19:07:48 +00:00
Marshall Bowers
f7703973d2 assistant: Extract ConfigurationView to its own module (#23480)
This PR is a small refactoring that extracts the Assistant's
`ConfigurationView` into its own module.

Release Notes:

- N/A
2025-01-22 18:37:00 +00:00
Michael Sloan
09fe1e7f66 Scroll completions menu to new selection on filter, fix corner case (#23478)
In the future if `filter` was used more this would fix other issues. In
the current code paths, this just fixes the particular corner case of
edit prediction arriving async while `y_flipped = true` (in this case it
needs to be scrolled down to show item with index 0).

Release Notes:

- N/A
2025-01-22 17:54:10 +00:00
Marshall Bowers
9f87145af9 title_bar: Simplify git-ui feature flag check (#23475)
This PR is a follow-up to
https://github.com/zed-industries/zed/pull/23470 that simplifies the way
we check the `git-ui` feature flag in the title bar.

Release Notes:

- N/A
2025-01-22 17:28:47 +00:00
Peter Tripp
5930b552b9 Bump Zed to v0.172 (#23474) 2025-01-22 11:57:24 -05:00
100 changed files with 3241 additions and 2672 deletions

View File

@@ -0,0 +1,34 @@
name: Feature Request
description: "Tip: open this issue template from within Zed with the `request feature` command palette action"
labels: ["admin read", "triage", "enhancement"]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: textarea
attributes:
label: Describe the feature
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: Zed version, release channel, architecture (x86_64 or aarch64), OS (macOS version / Linux distro and version) and RAM amount.
placeholder: |
<!-- In Zed run `copy system specs into clipboard` from the Zed command palette and paste here. -->
<!-- Alternatively spawn `request feature` and this field will be autopopulated -->
validations:
required: true
- type: textarea
attributes:
label: |
If applicable, add mockups / screenshots to help present your vision of the feature
description: Drag images into the text input below
validations:
required: false

View File

@@ -1,12 +1,18 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
blank_issues_enabled: false
contact_links:
- name: Feature Request
url: https://github.com/zed-industries/zed/discussions/new/choose
about: To request a feature, open a new Discussion in one of the appropriate Discussion categories
- name: Zed Discussion Forum
url: https://github.com/zed-industries/zed/discussions
about: A community discussion forum
- name: "Zed Discord: #Support Channel"
url: https://zed.dev/community-links
about: Real-time discussion and user support
- name: Language Request
url: https://github.com/zed-industries/extensions/issues/new?assignees=&labels=language&projects=&template=1_language_request.yml&title=%3Cname_of_language%3E
about: Request a language in the extensions repository
- name: Theme Request
url: https://github.com/zed-industries/extensions/issues/new?assignees=&labels=theme&projects=&template=0_theme_request.yml&title=%3Cname_of_theme%3E+theme
about: Request a theme in the extensions repository
- name: Top-Ranking Issues
url: https://github.com/zed-industries/zed/issues/5393
about: See an overview of the most popular Zed issues
- name: Platform Support
url: https://github.com/zed-industries/zed/issues/5391
about: A quick note on platform support
- name: Positive Feedback
url: https://github.com/zed-industries/zed/discussions/5397
about: A central location for kind words about Zed

View File

@@ -28,8 +28,7 @@ jobs:
maxLength: 2000
truncationSymbol: "..."
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4 # v6.0.0
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ steps.get-content.outputs.string }}
flags: 4 # suppress embeds - https://discord.com/developers/docs/resources/message#message-object-message-flags

38
Cargo.lock generated
View File

@@ -559,7 +559,6 @@ version = "0.1.0"
dependencies = [
"anthropic",
"anyhow",
"deepseek",
"feature_flags",
"fs",
"gpui",
@@ -3685,18 +3684,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "deepseek"
version = "0.1.0"
dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "deflate64"
version = "0.1.9"
@@ -4236,9 +4223,9 @@ dependencies = [
[[package]]
name = "etagere"
version = "0.2.13"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e2f1e3be19fb10f549be8c1bf013e8675b4066c445e36eb76d2ebb2f54ee495"
checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342"
dependencies = [
"euclid",
"svg_fmt",
@@ -5255,7 +5242,6 @@ dependencies = [
"collections",
"db",
"editor",
"feature_flags",
"futures 0.3.31",
"git",
"gpui",
@@ -5267,7 +5253,6 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
"smol",
"theme",
"ui",
"util",
@@ -6323,9 +6308,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.7.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@@ -6804,7 +6789,6 @@ dependencies = [
"async-trait",
"collections",
"extension",
"fs",
"futures 0.3.31",
"gpui",
"language",
@@ -6822,7 +6806,6 @@ dependencies = [
"anyhow",
"base64 0.22.1",
"collections",
"deepseek",
"futures 0.3.31",
"google_ai",
"gpui",
@@ -6866,7 +6849,6 @@ dependencies = [
"client",
"collections",
"copilot",
"deepseek",
"editor",
"feature_flags",
"fs",
@@ -7414,6 +7396,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
"text",
"util",
]
@@ -9850,6 +9833,7 @@ dependencies = [
"log",
"lsp",
"node_runtime",
"once_cell",
"parking_lot",
"pathdiff",
"paths",
@@ -11596,9 +11580,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.24"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
dependencies = [
"serde",
]
@@ -11645,9 +11629,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.135"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
dependencies = [
"indexmap",
"itoa",
@@ -16285,7 +16269,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.171.5"
version = "0.172.0"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -32,7 +32,6 @@ members = [
"crates/copilot",
"crates/db",
"crates/diagnostics",
"crates/deepseek",
"crates/docs_preprocessor",
"crates/editor",
"crates/evals",
@@ -230,7 +229,6 @@ context_server = { path = "crates/context_server" }
context_server_settings = { path = "crates/context_server_settings" }
copilot = { path = "crates/copilot" }
db = { path = "crates/db" }
deepseek = { path = "crates/deepseek" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
extension = { path = "crates/extension" }
@@ -374,7 +372,7 @@ async-tungstenite = "0.28"
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
base64 = "0.22"
bitflags = "2.6.0"
bitflags = "2.8.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
@@ -422,12 +420,13 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev="060964da10574cd9bf06463a53bf6e0769c5c45e", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
log = { version = "0.4.25", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nbformat = { version = "0.10.0" }
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.20"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
@@ -510,7 +509,7 @@ tree-sitter = { version = "0.23", features = ["wasm"] }
tree-sitter-bash = "0.23"
tree-sitter-c = "0.23"
tree-sitter-cpp = "0.23"
tree-sitter-css = "0.23"
tree-sitter-css = "0.23.2"
tree-sitter-elixir = "0.3"
tree-sitter-embedded-template = "0.23.0"
tree-sitter-go = "0.23"

View File

@@ -1 +0,0 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>DeepSeek</title><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="black"></path></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -13,7 +13,7 @@
"cmd-b": "editor::GoToDefinition",
"cmd-j": "editor::ScrollCursorCenter",
"cmd-enter": "editor::NewlineBelow",
"cmd-alt-enter": "editor::NewlineAbove",
"cmd-alt-enter": "editor::NewLineAbove",
"cmd-shift-l": "editor::SelectLine",
"cmd-shift-t": "outline::Toggle",
"alt-backspace": "editor::DeleteToPreviousWordStart",
@@ -70,7 +70,7 @@
"bindings": {
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
"cmd-d": "project_panel::Duplicate",
"cmd-n": "project_panel::NewDirectory",
"cmd-n": "project_panel::NewFolder",
"return": "project_panel::Rename",
"cmd-c": "project_panel::Copy",
"cmd-v": "project_panel::Paste",

View File

@@ -773,14 +773,7 @@
"load_direnv": "direct",
"inline_completions": {
// A list of globs representing files that inline completions should be disabled for.
"disabled_globs": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
]
"disabled_globs": [".env"]
},
// Settings specific to journaling
"journal": {
@@ -1173,9 +1166,6 @@
},
"lmstudio": {
"api_url": "http://localhost:1234/api/v0"
},
"deepseek": {
"api_url": "https://api.deepseek.com"
}
},
// Zed's Prettier integration settings.

View File

@@ -1,5 +1,6 @@
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
mod assistant_configuration;
pub mod assistant_panel;
mod inline_assistant;
pub mod slash_command_settings;
@@ -18,11 +19,10 @@ use gpui::{actions, AppContext, Global, UpdateGlobal};
use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
use prompt_library::{PromptBuilder, PromptLoadingParams};
use prompt_library::PromptBuilder;
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
use serde::Deserialize;
use settings::{Settings, SettingsStore};
use util::ResultExt;
pub use crate::assistant_panel::{AssistantPanel, AssistantPanelEvent};
pub(crate) use crate::inline_assistant::*;
@@ -33,7 +33,6 @@ actions!(
[
InsertActivePrompt,
DeployHistory,
DeployPromptLibrary,
NewContext,
CycleNextInlineAssist,
CyclePreviousInlineAssist
@@ -92,9 +91,9 @@ impl Assistant {
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
stdout_is_a_pty: bool,
prompt_builder: Arc<PromptBuilder>,
cx: &mut AppContext,
) -> Arc<PromptBuilder> {
) {
cx.set_global(Assistant::default());
AssistantSettings::register(cx);
SlashCommandSettings::register(cx);
@@ -134,16 +133,6 @@ pub fn init(
assistant_panel::init(cx);
context_server::init(cx);
let prompt_builder = PromptBuilder::new(Some(PromptLoadingParams {
fs: fs.clone(),
repo_path: stdout_is_a_pty
.then(|| std::env::current_dir().log_err())
.flatten(),
cx,
}))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(PromptBuilder::new(None).unwrap()));
register_slash_commands(Some(prompt_builder.clone()), cx);
inline_assistant::init(
fs.clone(),
@@ -174,8 +163,6 @@ pub fn init(
});
})
.detach();
prompt_builder
}
fn init_language_model_settings(cx: &mut AppContext) {

View File

@@ -0,0 +1,197 @@
use std::sync::Arc;
use collections::HashMap;
use gpui::{canvas, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use ui::{prelude::*, ElevationIndex};
use workspace::Item;
pub struct ConfigurationView {
focus_handle: FocusHandle,
configuration_views: HashMap<LanguageModelProviderId, AnyView>,
_registry_subscription: Subscription,
}
impl ConfigurationView {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let focus_handle = cx.focus_handle();
let registry_subscription = cx.subscribe(
&LanguageModelRegistry::global(cx),
|this, _, event: &language_model::Event, cx| match event {
language_model::Event::AddedProvider(provider_id) => {
let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
if let Some(provider) = provider {
this.add_configuration_view(&provider, cx);
}
}
language_model::Event::RemovedProvider(provider_id) => {
this.remove_configuration_view(provider_id);
}
_ => {}
},
);
let mut this = Self {
focus_handle,
configuration_views: HashMap::default(),
_registry_subscription: registry_subscription,
};
this.build_configuration_views(cx);
this
}
fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
let providers = LanguageModelRegistry::read_global(cx).providers();
for provider in providers {
self.add_configuration_view(&provider, cx);
}
}
fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
self.configuration_views.remove(provider_id);
}
fn add_configuration_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut ViewContext<Self>,
) {
let configuration_view = provider.configuration_view(cx);
self.configuration_views
.insert(provider.id(), configuration_view);
}
fn render_provider_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut ViewContext<Self>,
) -> Div {
let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone();
let configuration_view = self.configuration_views.get(&provider.id()).cloned();
let open_new_context = cx.listener({
let provider = provider.clone();
move |_, _, cx| {
cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
provider.clone(),
))
}
});
v_flex()
.gap_2()
.child(
h_flex()
.justify_between()
.child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
.when(provider.is_authenticated(cx), move |this| {
this.child(
h_flex().justify_end().child(
Button::new(
SharedString::from(format!("new-context-{provider_id}")),
"Open New Chat",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.on_click(open_new_context),
),
)
}),
)
.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().surface_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.when(configuration_view.is_none(), |this| {
this.child(div().child(Label::new(format!(
"No configuration view for {}",
provider_name
))))
})
.when_some(configuration_view, |this, configuration_view| {
this.child(configuration_view)
}),
)
}
}
impl Render for ConfigurationView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let provider_views = providers
.into_iter()
.map(|provider| self.render_provider_view(&provider, cx))
.collect::<Vec<_>>();
let mut element = v_flex()
.id("assistant-configuration-view")
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().editor_background)
.size_full()
.overflow_y_scroll()
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.gap_1()
.child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
.child(
Label::new(
"At least one LLM provider must be configured to use the Assistant.",
)
.color(Color::Muted),
),
)
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.mt_1()
.gap_6()
.flex_1()
.children(provider_views),
)
.into_any();
// We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
// because we couldn't the element to take up the size of the parent.
canvas(
move |bounds, cx| {
element.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
element
},
|_, mut element, cx| {
element.paint(cx);
},
)
.flex_1()
.w_full()
}
}
pub enum ConfigurationViewEvent {
NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
}
impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
impl FocusableView for ConfigurationView {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for ConfigurationView {
type Event = ConfigurationViewEvent;
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
Some("Configuration".into())
}
}

View File

@@ -1,6 +1,6 @@
use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
use crate::{
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, DeployPromptLibrary,
InlineAssistant, NewContext,
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewContext,
};
use anyhow::{anyhow, Result};
use assistant_context_editor::{
@@ -13,19 +13,15 @@ use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::{proto, Client, Status};
use collections::HashMap;
use editor::{Editor, EditorEvent};
use fs::Fs;
use gpui::{
canvas, div, prelude::*, Action, AnyView, AppContext, AsyncWindowContext, EventEmitter,
ExternalPaths, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model,
ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
Task, UpdateGlobal, View, WeakView,
prelude::*, Action, AppContext, AsyncWindowContext, EventEmitter, ExternalPaths, FocusHandle,
FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render, Styled,
Subscription, Task, UpdateGlobal, View, WeakView,
};
use language::LanguageRegistry;
use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
use language_model::{LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use language_model_selector::LanguageModelSelector;
use project::Project;
use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary};
@@ -34,15 +30,14 @@ use settings::{update_settings_file, Settings};
use smol::stream::StreamExt;
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use ui::{prelude::*, ContextMenu, ElevationIndex, PopoverMenu, PopoverMenuHandle, Tooltip};
use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
use util::{maybe, ResultExt};
use workspace::DraggedTab;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
item::Item,
pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
};
use zed_actions::assistant::{InlineAssist, ToggleFocus};
use zed_actions::assistant::{DeployPromptLibrary, InlineAssist, ToggleFocus};
pub fn init(cx: &mut AppContext) {
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
@@ -1371,193 +1366,3 @@ pub enum WorkflowAssistStatus {
Done,
Idle,
}
pub struct ConfigurationView {
focus_handle: FocusHandle,
configuration_views: HashMap<LanguageModelProviderId, AnyView>,
_registry_subscription: Subscription,
}
impl ConfigurationView {
fn new(cx: &mut ViewContext<Self>) -> Self {
let focus_handle = cx.focus_handle();
let registry_subscription = cx.subscribe(
&LanguageModelRegistry::global(cx),
|this, _, event: &language_model::Event, cx| match event {
language_model::Event::AddedProvider(provider_id) => {
let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
if let Some(provider) = provider {
this.add_configuration_view(&provider, cx);
}
}
language_model::Event::RemovedProvider(provider_id) => {
this.remove_configuration_view(provider_id);
}
_ => {}
},
);
let mut this = Self {
focus_handle,
configuration_views: HashMap::default(),
_registry_subscription: registry_subscription,
};
this.build_configuration_views(cx);
this
}
fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
let providers = LanguageModelRegistry::read_global(cx).providers();
for provider in providers {
self.add_configuration_view(&provider, cx);
}
}
fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
self.configuration_views.remove(provider_id);
}
fn add_configuration_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut ViewContext<Self>,
) {
let configuration_view = provider.configuration_view(cx);
self.configuration_views
.insert(provider.id(), configuration_view);
}
fn render_provider_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut ViewContext<Self>,
) -> Div {
let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone();
let configuration_view = self.configuration_views.get(&provider.id()).cloned();
let open_new_context = cx.listener({
let provider = provider.clone();
move |_, _, cx| {
cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
provider.clone(),
))
}
});
v_flex()
.gap_2()
.child(
h_flex()
.justify_between()
.child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
.when(provider.is_authenticated(cx), move |this| {
this.child(
h_flex().justify_end().child(
Button::new(
SharedString::from(format!("new-context-{provider_id}")),
"Open New Chat",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.on_click(open_new_context),
),
)
}),
)
.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().surface_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.when(configuration_view.is_none(), |this| {
this.child(div().child(Label::new(format!(
"No configuration view for {}",
provider_name
))))
})
.when_some(configuration_view, |this, configuration_view| {
this.child(configuration_view)
}),
)
}
}
impl Render for ConfigurationView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let provider_views = providers
.into_iter()
.map(|provider| self.render_provider_view(&provider, cx))
.collect::<Vec<_>>();
let mut element = v_flex()
.id("assistant-configuration-view")
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().editor_background)
.size_full()
.overflow_y_scroll()
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.gap_1()
.child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
.child(
Label::new(
"At least one LLM provider must be configured to use the Assistant.",
)
.color(Color::Muted),
),
)
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.mt_1()
.gap_6()
.flex_1()
.children(provider_views),
)
.into_any();
// We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
// because we couldn't the element to take up the size of the parent.
canvas(
move |bounds, cx| {
element.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
element
},
|_, mut element, cx| {
element.paint(cx);
},
)
.flex_1()
.w_full()
}
}
pub enum ConfigurationViewEvent {
NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
}
impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
impl FocusableView for ConfigurationView {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for ConfigurationView {
type Event = ConfigurationViewEvent;
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
Some("Configuration".into())
}
}

View File

@@ -259,7 +259,7 @@ impl ActiveThread {
h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
context
.into_iter()
.map(|context| ContextPill::new_added(context, false, false, None)),
.map(|context| ContextPill::added(context, false, false, None)),
),
)
} else {

View File

@@ -1,4 +1,5 @@
mod active_thread;
mod assistant_configuration;
mod assistant_model_selector;
mod assistant_panel;
mod buffer_codegen;
@@ -24,9 +25,8 @@ use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs;
use gpui::{actions, AppContext};
use prompt_library::{PromptBuilder, PromptLoadingParams};
use prompt_library::PromptBuilder;
use settings::Settings as _;
use util::ResultExt;
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant;
@@ -41,6 +41,7 @@ actions!(
RemoveAllContext,
OpenHistory,
OpenPromptEditorHistory,
OpenConfiguration,
RemoveSelectedThread,
Chat,
ChatMode,
@@ -58,20 +59,15 @@ actions!(
const NAMESPACE: &str = "assistant2";
/// Initializes the `assistant2` crate.
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mut AppContext) {
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
prompt_builder: Arc<PromptBuilder>,
cx: &mut AppContext,
) {
AssistantSettings::register(cx);
assistant_panel::init(cx);
let prompt_builder = PromptBuilder::new(Some(PromptLoadingParams {
fs: fs.clone(),
repo_path: stdout_is_a_pty
.then(|| std::env::current_dir().log_err())
.flatten(),
cx,
}))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(PromptBuilder::new(None).unwrap()));
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),

View File

@@ -0,0 +1,173 @@
use std::sync::Arc;
use collections::HashMap;
use gpui::{Action, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use ui::{prelude::*, ElevationIndex};
use zed_actions::assistant::DeployPromptLibrary;
pub struct AssistantConfiguration {
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
_registry_subscription: Subscription,
}
impl AssistantConfiguration {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let focus_handle = cx.focus_handle();
let registry_subscription = cx.subscribe(
&LanguageModelRegistry::global(cx),
|this, _, event: &language_model::Event, cx| match event {
language_model::Event::AddedProvider(provider_id) => {
let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
if let Some(provider) = provider {
this.add_provider_configuration_view(&provider, cx);
}
}
language_model::Event::RemovedProvider(provider_id) => {
this.remove_provider_configuration_view(provider_id);
}
_ => {}
},
);
let mut this = Self {
focus_handle,
configuration_views_by_provider: HashMap::default(),
_registry_subscription: registry_subscription,
};
this.build_provider_configuration_views(cx);
this
}
fn build_provider_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
let providers = LanguageModelRegistry::read_global(cx).providers();
for provider in providers {
self.add_provider_configuration_view(&provider, cx);
}
}
fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
self.configuration_views_by_provider.remove(provider_id);
}
fn add_provider_configuration_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut ViewContext<Self>,
) {
let configuration_view = provider.configuration_view(cx);
self.configuration_views_by_provider
.insert(provider.id(), configuration_view);
}
}
impl FocusableView for AssistantConfiguration {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
pub enum AssistantConfigurationEvent {
NewThread(Arc<dyn LanguageModelProvider>),
}
impl EventEmitter<AssistantConfigurationEvent> for AssistantConfiguration {}
impl AssistantConfiguration {
fn render_provider_configuration(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone();
let configuration_view = self
.configuration_views_by_provider
.get(&provider.id())
.cloned();
v_flex()
.gap_2()
.child(
h_flex()
.justify_between()
.child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
.when(provider.is_authenticated(cx), |parent| {
parent.child(
h_flex().justify_end().child(
Button::new(
SharedString::from(format!("new-thread-{provider_id}")),
"Open New Thread",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.on_click(cx.listener({
let provider = provider.clone();
move |_this, _event, cx| {
cx.emit(AssistantConfigurationEvent::NewThread(
provider.clone(),
))
}
})),
),
)
}),
)
.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().surface_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.map(|parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
None => parent.child(div().child(Label::new(format!(
"No configuration view for {provider_name}",
)))),
}),
)
}
}
impl Render for AssistantConfiguration {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
v_flex()
.id("assistant-configuration")
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().editor_background)
.size_full()
.overflow_y_scroll()
.child(
h_flex().p(DynamicSpacing::Base16.rems(cx)).child(
Button::new("open-prompt-library", "Open Prompt Library")
.style(ButtonStyle::Filled)
.full_width()
.icon(IconName::Book)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.on_click(|_event, cx| {
cx.dispatch_action(DeployPromptLibrary.boxed_clone())
}),
),
)
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.mt_1()
.gap_6()
.flex_1()
.children(
providers
.into_iter()
.map(|provider| self.render_provider_configuration(&provider, cx)),
),
)
}
}

View File

@@ -4,34 +4,41 @@ use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_context_editor::{
make_lsp_adapter_delegate, AssistantPanelDelegate, ContextEditor, ContextHistory,
SlashCommandCompletionProvider,
};
use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use editor::Editor;
use fs::Fs;
use gpui::{
prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, Corner, EventEmitter,
FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
WindowContext,
FocusHandle, FocusableView, FontWeight, Model, Pixels, Subscription, Task, UpdateGlobal, View,
ViewContext, WeakView, WindowContext,
};
use language::LanguageRegistry;
use language_model::LanguageModelRegistry;
use project::Project;
use prompt_library::PromptBuilder;
use settings::Settings;
use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary};
use settings::{update_settings_file, Settings};
use time::UtcOffset;
use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip};
use util::ResultExt as _;
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use zed_actions::assistant::ToggleFocus;
use zed_actions::assistant::{DeployPromptLibrary, ToggleFocus};
use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
use crate::message_editor::MessageEditor;
use crate::thread::{Thread, ThreadError, ThreadId};
use crate::thread_history::{PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{NewPromptEditor, NewThread, OpenHistory, OpenPromptEditorHistory};
use crate::{
InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory,
OpenPromptEditorHistory,
};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
@@ -60,6 +67,12 @@ pub fn init(cx: &mut AppContext) {
workspace.focus_panel::<AssistantPanel>(cx);
panel.update(cx, |panel, cx| panel.open_prompt_editor_history(cx));
}
})
.register_action(|workspace, _: &OpenConfiguration, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(cx);
panel.update(cx, |panel, cx| panel.open_configuration(cx));
}
});
},
)
@@ -71,6 +84,7 @@ enum ActiveView {
PromptEditor,
History,
PromptEditorHistory,
Configuration,
}
pub struct AssistantPanel {
@@ -84,6 +98,8 @@ pub struct AssistantPanel {
context_store: Model<assistant_context_editor::ContextStore>,
context_editor: Option<View<ContextEditor>>,
context_history: Option<View<ContextHistory>>,
configuration: Option<View<AssistantConfiguration>>,
configuration_subscription: Option<Subscription>,
tools: Arc<ToolWorkingSet>,
local_timezone: UtcOffset,
active_view: ActiveView,
@@ -173,6 +189,8 @@ impl AssistantPanel {
context_store,
context_editor: None,
context_history: None,
configuration: None,
configuration_subscription: None,
tools,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
@@ -267,6 +285,22 @@ impl AssistantPanel {
}
}
fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext<Self>) {
open_prompt_library(
self.language_registry.clone(),
Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
Arc::new(|| {
Box::new(SlashCommandCompletionProvider::new(
Arc::new(SlashCommandWorkingSet::default()),
None,
None,
))
}),
cx,
)
.detach_and_log_err(cx);
}
fn open_history(&mut self, cx: &mut ViewContext<Self>) {
self.active_view = ActiveView::History;
self.history.focus_handle(cx).focus(cx);
@@ -357,6 +391,46 @@ impl AssistantPanel {
self.message_editor.focus_handle(cx).focus(cx);
}
pub(crate) fn open_configuration(&mut self, cx: &mut ViewContext<Self>) {
self.active_view = ActiveView::Configuration;
self.configuration = Some(cx.new_view(AssistantConfiguration::new));
if let Some(configuration) = self.configuration.as_ref() {
self.configuration_subscription =
Some(cx.subscribe(configuration, Self::handle_assistant_configuration_event));
configuration.focus_handle(cx).focus(cx);
}
}
fn handle_assistant_configuration_event(
&mut self,
_view: View<AssistantConfiguration>,
event: &AssistantConfigurationEvent,
cx: &mut ViewContext<Self>,
) {
match event {
AssistantConfigurationEvent::NewThread(provider) => {
if LanguageModelRegistry::read_global(cx)
.active_provider()
.map_or(true, |active_provider| {
active_provider.id() != provider.id()
})
{
if let Some(model) = provider.provided_models(cx).first().cloned() {
update_settings_file::<AssistantSettings>(
self.fs.clone(),
cx,
move |settings, _| settings.set_model(model),
);
}
}
self.new_thread(cx);
}
}
}
pub(crate) fn active_thread(&self, cx: &AppContext) -> Model<Thread> {
self.thread.read(cx).thread.clone()
}
@@ -386,6 +460,13 @@ impl FocusableView for AssistantPanel {
cx.focus_handle()
}
}
ActiveView::Configuration => {
if let Some(configuration) = self.configuration.as_ref() {
configuration.focus_handle(cx)
} else {
cx.focus_handle()
}
}
}
}
}
@@ -493,6 +574,7 @@ impl AssistantPanel {
.unwrap_or_else(|| SharedString::from("Loading Summary…")),
ActiveView::History => "History / Thread".into(),
ActiveView::PromptEditorHistory => "History / Prompt Editor".into(),
ActiveView::Configuration => "Configuration".into(),
};
h_flex()
@@ -555,8 +637,8 @@ impl AssistantPanel {
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
.on_click(move |_event, _cx| {
println!("Configure Assistant");
.on_click(move |_event, cx| {
cx.dispatch_action(OpenConfiguration.boxed_clone());
}),
),
)
@@ -796,6 +878,7 @@ impl Render for AssistantPanel {
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
this.open_history(cx);
}))
.on_action(cx.listener(Self::deploy_prompt_library))
.child(self.render_toolbar(cx))
.map(|parent| match self.active_view {
ActiveView::Thread => parent
@@ -810,10 +893,42 @@ impl Render for AssistantPanel {
ActiveView::History => parent.child(self.history.clone()),
ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
ActiveView::PromptEditorHistory => parent.children(self.context_history.clone()),
ActiveView::Configuration => parent.children(self.configuration.clone()),
})
}
}
struct PromptLibraryInlineAssist {
workspace: WeakView<Workspace>,
}
impl PromptLibraryInlineAssist {
pub fn new(workspace: WeakView<Workspace>) -> Self {
Self { workspace }
}
}
impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
fn assist(
&self,
prompt_editor: &View<Editor>,
_initial_prompt: Option<String>,
cx: &mut ViewContext<PromptLibrary>,
) {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&prompt_editor, self.workspace.clone(), None, cx)
})
}
fn focus_assistant_panel(
&self,
workspace: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) -> bool {
workspace.focus_panel::<AssistantPanel>(cx).is_some()
}
}
pub struct ConcreteAssistantPanelDelegate;
impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {

View File

@@ -43,15 +43,6 @@ pub enum ContextKind {
}
impl ContextKind {
pub fn all() -> &'static [ContextKind] {
&[
ContextKind::File,
ContextKind::Directory,
ContextKind::FetchedUrl,
ContextKind::Thread,
]
}
pub fn label(&self) -> &'static str {
match self {
ContextKind::File => "File",

View File

@@ -77,16 +77,6 @@ impl ContextPicker {
let context_picker = cx.view().clone();
let menu = ContextMenu::build(cx, move |menu, cx| {
let kind_entry = |kind: &'static ContextKind| {
let context_picker = context_picker.clone();
ContextMenuEntry::new(kind.label())
.icon(kind.icon())
.handler(move |cx| {
context_picker.update(cx, |this, cx| this.select_kind(*kind, cx))
})
};
let recent = self.recent_entries(cx);
let has_recent = !recent.is_empty();
let recent_entries = recent
@@ -94,6 +84,15 @@ impl ContextPicker {
.enumerate()
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
let mut context_kinds = vec![
ContextKind::File,
ContextKind::Directory,
ContextKind::FetchedUrl,
];
if self.allow_threads() {
context_kinds.push(ContextKind::Thread);
}
let menu = menu
.when(has_recent, |menu| {
menu.custom_row(|_| {
@@ -109,7 +108,15 @@ impl ContextPicker {
})
.extend(recent_entries)
.when(has_recent, |menu| menu.separator())
.extend(ContextKind::all().into_iter().map(kind_entry));
.extend(context_kinds.into_iter().map(|kind| {
let context_picker = context_picker.clone();
ContextMenuEntry::new(kind.label())
.icon(kind.icon())
.handler(move |cx| {
context_picker.update(cx, |this, cx| this.select_kind(kind, cx))
})
}));
match self.confirm_behavior {
ConfirmBehavior::KeepOpen => menu.keep_open_on_confirm(),
@@ -125,6 +132,11 @@ impl ContextPicker {
menu
}
/// Whether threads are allowed as context.
pub fn allow_threads(&self) -> bool {
self.thread_store.is_some()
}
fn select_kind(&mut self, kind: ContextKind, cx: &mut ViewContext<Self>) {
let context_picker = cx.view().downgrade();

View File

@@ -118,6 +118,10 @@ impl ContextStrip {
}
fn suggested_thread(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
if !self.context_picker.read(cx).allow_threads() {
return None;
}
let workspace = self.workspace.upgrade()?;
let active_thread = workspace
.read(cx)
@@ -432,7 +436,7 @@ impl Render for ContextStrip {
}
})
.children(context.iter().enumerate().map(|(i, context)| {
ContextPill::new_added(
ContextPill::added(
context.clone(),
dupe_names.contains(&context.name),
self.focused_index == Some(i),
@@ -454,7 +458,7 @@ impl Render for ContextStrip {
}))
.when_some(suggested_context, |el, suggested| {
el.child(
ContextPill::new_suggested(
ContextPill::suggested(
suggested.name().clone(),
suggested.icon_path(),
suggested.kind(),

View File

@@ -308,6 +308,13 @@ impl Thread {
last_message.id,
chunk,
));
} else {
// If we won't have an Assistant message yet, assume this chunk marks the beginning
// of a new Assistant response.
//
// Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it
// will result in duplicating the text of the chunk in the rendered Markdown.
thread.insert_message(Role::Assistant, chunk, cx);
}
}
}

View File

@@ -24,7 +24,7 @@ pub enum ContextPill {
}
impl ContextPill {
pub fn new_added(
pub fn added(
context: ContextSnapshot,
dupe_name: bool,
focused: bool,
@@ -39,7 +39,7 @@ impl ContextPill {
}
}
pub fn new_suggested(
pub fn suggested(
name: SharedString,
icon_path: Option<SharedString>,
kind: ContextKind,

View File

@@ -14,7 +14,6 @@ path = "src/assistant_settings.rs"
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
deepseek = { workspace = true, features = ["schemars"] }
feature_flags.workspace = true
gpui.workspace = true
language_model.workspace = true

View File

@@ -2,7 +2,6 @@ use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use deepseek::Model as DeepseekModel;
use feature_flags::FeatureFlagAppExt;
use gpui::{AppContext, Pixels};
use language_model::{CloudModel, LanguageModel};
@@ -47,11 +46,6 @@ pub enum AssistantProviderContentV1 {
default_model: Option<LmStudioModel>,
api_url: Option<String>,
},
#[serde(rename = "deepseek")]
DeepSeek {
default_model: Option<DeepseekModel>,
api_url: Option<String>,
},
}
#[derive(Debug, Default)]
@@ -155,12 +149,6 @@ impl AssistantSettingsContent {
model: model.id().to_string(),
})
}
AssistantProviderContentV1::DeepSeek { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "deepseek".to_string(),
model: model.id().to_string(),
})
}
}),
inline_alternatives: None,
enable_experimental_live_diffs: None,
@@ -265,18 +253,6 @@ impl AssistantSettingsContent {
available_models,
});
}
"deepseek" => {
let api_url = match &settings.provider {
Some(AssistantProviderContentV1::DeepSeek { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AssistantProviderContentV1::DeepSeek {
default_model: DeepseekModel::from_id(&model).ok(),
api_url,
});
}
_ => {}
},
VersionedAssistantSettingsContent::V2(settings) => {
@@ -365,7 +341,6 @@ fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema:
"openai".into(),
"zed.dev".into(),
"copilot_chat".into(),
"deepseek".into(),
]),
..Default::default()
}
@@ -405,7 +380,7 @@ pub struct AssistantSettingsContentV1 {
default_height: Option<f32>,
/// The provider of the assistant service.
///
/// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
/// This can be "openai", "anthropic", "ollama", "lmstudio", "zed.dev"
/// each with their respective default models and configurations.
provider: Option<AssistantProviderContentV1>,
}

View File

@@ -53,7 +53,7 @@ reqwest_client.workspace = true
rpc.workspace = true
rustc-demangle.workspace = true
scrypt = "0.11"
sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
sea-orm = { version = "1.1.4", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
semantic_version.workspace = true
semver.workspace = true
serde.workspace = true
@@ -116,7 +116,7 @@ release_channel.workspace = true
remote = { workspace = true, features = ["test-support"] }
remote_server.workspace = true
rpc = { workspace = true, features = ["test-support"] }
sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-sqlite"] }
sea-orm = { version = "1.1.4", features = ["sqlx-sqlite"] }
serde_json.workspace = true
session = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }

View File

@@ -461,12 +461,14 @@ impl Copilot {
.on_notification::<StatusNotification, _>(|_, _| { /* Silence the notification */ })
.detach();
let initialize_params = None;
let configuration = lsp::DidChangeConfigurationParams {
settings: Default::default(),
};
let server = cx
.update(|cx| server.initialize(initialize_params, configuration.into(), cx))?
.update(|cx| {
let params = server.default_initialize_params(cx);
server.initialize(params, configuration.into(), cx)
})?
.await?;
let status = server

View File

@@ -1,24 +0,0 @@
[package]
name = "deepseek"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/deepseek.rs"
[features]
default = []
schemars = ["dep:schemars"]
[dependencies]
anyhow.workspace = true
futures.workspace = true
http_client.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true

View File

@@ -1 +0,0 @@
../../LICENSE-GPL

View File

@@ -1,301 +0,0 @@
use anyhow::{anyhow, Result};
use futures::{
io::BufReader,
stream::{BoxStream, StreamExt},
AsyncBufReadExt, AsyncReadExt,
};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::convert::TryFrom;
pub const DEEPSEEK_API_URL: &str = "https://api.deepseek.com";
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
System,
Tool,
}
impl TryFrom<String> for Role {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self> {
match value.as_str() {
"user" => Ok(Self::User),
"assistant" => Ok(Self::Assistant),
"system" => Ok(Self::System),
"tool" => Ok(Self::Tool),
_ => Err(anyhow!("invalid role '{value}'")),
}
}
}
impl From<Role> for String {
fn from(val: Role) -> Self {
match val {
Role::User => "user".to_owned(),
Role::Assistant => "assistant".to_owned(),
Role::System => "system".to_owned(),
Role::Tool => "tool".to_owned(),
}
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub enum Model {
#[serde(rename = "deepseek-chat")]
#[default]
Chat,
#[serde(rename = "deepseek-reasoner")]
Reasoner,
#[serde(rename = "custom")]
Custom {
name: String,
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
display_name: Option<String>,
max_tokens: usize,
max_output_tokens: Option<u32>,
},
}
impl Model {
pub fn from_id(id: &str) -> Result<Self> {
match id {
"deepseek-chat" => Ok(Self::Chat),
"deepseek-reasoner" => Ok(Self::Reasoner),
_ => Err(anyhow!("invalid model id")),
}
}
pub fn id(&self) -> &str {
match self {
Self::Chat => "deepseek-chat",
Self::Reasoner => "deepseek-reasoner",
Self::Custom { name, .. } => name,
}
}
pub fn display_name(&self) -> &str {
match self {
Self::Chat => "DeepSeek Chat",
Self::Reasoner => "DeepSeek Reasoner",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name).as_str(),
}
}
pub fn max_token_count(&self) -> usize {
match self {
Self::Chat | Self::Reasoner => 64_000,
Self::Custom { max_tokens, .. } => *max_tokens,
}
}
pub fn max_output_tokens(&self) -> Option<u32> {
match self {
Self::Chat => Some(8_192),
Self::Reasoner => Some(8_192),
Self::Custom {
max_output_tokens, ..
} => *max_output_tokens,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Request {
pub model: String,
pub messages: Vec<RequestMessage>,
pub stream: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_format: Option<ResponseFormat>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<ToolDefinition>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseFormat {
Text,
#[serde(rename = "json_object")]
JsonObject,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolDefinition {
Function { function: FunctionDefinition },
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FunctionDefinition {
pub name: String,
pub description: Option<String>,
pub parameters: Option<Value>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(tag = "role", rename_all = "lowercase")]
pub enum RequestMessage {
Assistant {
content: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
tool_calls: Vec<ToolCall>,
},
User {
content: String,
},
System {
content: String,
},
Tool {
content: String,
tool_call_id: String,
},
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ToolCall {
pub id: String,
#[serde(flatten)]
pub content: ToolCallContent,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ToolCallContent {
Function { function: FunctionContent },
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct FunctionContent {
pub name: String,
pub arguments: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Response {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<Choice>,
pub usage: Usage,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
#[serde(default)]
pub prompt_cache_hit_tokens: u32,
#[serde(default)]
pub prompt_cache_miss_tokens: u32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Choice {
pub index: u32,
pub message: RequestMessage,
pub finish_reason: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct StreamResponse {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<StreamChoice>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct StreamChoice {
pub index: u32,
pub delta: StreamDelta,
pub finish_reason: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct StreamDelta {
pub role: Option<Role>,
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallChunk>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ToolCallChunk {
pub index: usize,
pub id: Option<String>,
pub function: Option<FunctionChunk>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct FunctionChunk {
pub name: Option<String>,
pub arguments: Option<String>,
}
pub async fn stream_completion(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
request: Request,
) -> Result<BoxStream<'static, Result<StreamResponse>>> {
let uri = format!("{api_url}/v1/chat/completions");
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key));
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;
if response.status().is_success() {
let reader = BufReader::new(response.into_body());
Ok(reader
.lines()
.filter_map(|line| async move {
match line {
Ok(line) => {
let line = line.strip_prefix("data: ")?;
if line == "[DONE]" {
None
} else {
match serde_json::from_str(line) {
Ok(response) => Some(Ok(response)),
Err(error) => Some(Err(anyhow!(error))),
}
}
}
Err(error) => Some(Err(anyhow!(error))),
}
})
.boxed())
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
Err(anyhow!(
"Failed to connect to DeepSeek API: {} {}",
response.status(),
body,
))
}
}

View File

@@ -865,18 +865,22 @@ impl CompletionsMenu {
drop(completions);
let mut entries = self.entries.borrow_mut();
if let Some(CompletionEntry::InlineCompletionHint(_)) = entries.first() {
let new_selection = if let Some(CompletionEntry::InlineCompletionHint(_)) = entries.first()
{
entries.truncate(1);
if inline_completion_was_selected || matches.is_empty() {
self.selected_item = 0;
0
} else {
self.selected_item = 1;
1
}
} else {
entries.truncate(0);
self.selected_item = 0;
}
0
};
entries.extend(matches.into_iter().map(CompletionEntry::Match));
self.selected_item = new_selection;
self.scroll_handle
.scroll_to_item(new_selection, ScrollStrategy::Top);
}
}

View File

@@ -12476,28 +12476,27 @@ impl Editor {
cx.emit(SearchEvent::MatchesInvalidated);
if *singleton_buffer_edited {
if let Some(project) = &self.project {
let project = project.read(cx);
#[allow(clippy::mutable_key_type)]
let languages_affected = multibuffer
.read(cx)
.all_buffers()
.into_iter()
.filter_map(|buffer| {
let buffer = buffer.read(cx);
let language = buffer.language()?;
if project.is_local()
&& project
.language_servers_for_local_buffer(buffer, cx)
.count()
== 0
{
None
} else {
Some(language)
}
})
.cloned()
.collect::<HashSet<_>>();
let languages_affected = multibuffer.update(cx, |multibuffer, cx| {
multibuffer
.all_buffers()
.into_iter()
.filter_map(|buffer| {
buffer.update(cx, |buffer, cx| {
let language = buffer.language()?;
let should_discard = project.update(cx, |project, cx| {
project.is_local()
&& project.for_language_servers_for_local_buffer(
buffer,
|it| it.count() == 0,
cx,
)
});
should_discard.not().then_some(language.clone())
})
})
.collect::<HashSet<_>>()
});
if !languages_affected.is_empty() {
self.refresh_inlay_hints(
InlayHintRefreshReason::BufferEdited(languages_affected),
@@ -13051,15 +13050,18 @@ impl Editor {
self.handle_input(text, cx);
}
pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool {
pub fn supports_inlay_hints(&self, cx: &mut AppContext) -> bool {
let Some(provider) = self.semantics_provider.as_ref() else {
return false;
};
let mut supports = false;
self.buffer().read(cx).for_each_buffer(|buffer| {
supports |= provider.supports_inlay_hints(buffer, cx);
self.buffer().update(cx, |this, cx| {
this.for_each_buffer(|buffer| {
supports |= provider.supports_inlay_hints(buffer, cx);
})
});
supports
}
@@ -13671,7 +13673,7 @@ pub trait SemanticsProvider {
cx: &mut AppContext,
) -> Option<Task<anyhow::Result<InlayHint>>>;
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool;
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &mut AppContext) -> bool;
fn document_highlights(
&self,
@@ -14056,17 +14058,25 @@ impl SemanticsProvider for Model<Project> {
}))
}
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &mut AppContext) -> bool {
// TODO: make this work for remote projects
self.read(cx)
.language_servers_for_local_buffer(buffer.read(cx), cx)
.any(
|(_, server)| match server.capabilities().inlay_hint_provider {
Some(lsp::OneOf::Left(enabled)) => enabled,
Some(lsp::OneOf::Right(_)) => true,
None => false,
},
)
buffer.update(cx, |buffer, cx| {
self.update(cx, |this, cx| {
this.for_language_servers_for_local_buffer(
buffer,
|mut it| {
it.any(
|(_, server)| match server.capabilities().inlay_hint_provider {
Some(lsp::OneOf::Left(enabled)) => enabled,
Some(lsp::OneOf::Right(_)) => true,
None => false,
},
)
},
cx,
)
})
})
}
fn inlay_hints(

View File

@@ -6839,7 +6839,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor());
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let project = Project::test(fs, ["/".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
@@ -7193,7 +7193,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor());
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let project = Project::test(fs, ["/".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
@@ -7327,7 +7327,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor());
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let project = Project::test(fs, ["/".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(Language::new(
@@ -10742,7 +10742,6 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
0,
"Should not restart LSP server on an unrelated LSP settings change"
);
update_test_project_settings(cx, |project_settings| {
project_settings.lsp.insert(
language_server_name.into(),

View File

@@ -11,7 +11,7 @@ use multi_buffer::Anchor;
pub(crate) fn find_specific_language_server_in_selection<F>(
editor: &Editor,
cx: &WindowContext,
cx: &mut WindowContext,
filter_language: F,
language_server_name: &str,
) -> Option<(Anchor, Arc<Language>, LanguageServerId, Model<Buffer>)>
@@ -21,7 +21,6 @@ where
let Some(project) = &editor.project else {
return None;
};
let multibuffer = editor.buffer().read(cx);
let mut language_servers_for = HashMap::default();
editor
.selections
@@ -29,29 +28,33 @@ where
.iter()
.filter(|selection| selection.start == selection.end)
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
.filter_map(|(buffer_id, trigger_anchor)| {
let buffer = multibuffer.buffer(buffer_id)?;
.find_map(|(buffer_id, trigger_anchor)| {
let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
let server_id = *match language_servers_for.entry(buffer_id) {
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
Entry::Vacant(vacant_entry) => {
let language_server_id = project
.read(cx)
.language_servers_for_local_buffer(buffer.read(cx), cx)
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == language_server_name {
Some(server.server_id())
} else {
None
}
});
let language_server_id = buffer.update(cx, |buffer, cx| {
project.update(cx, |project, cx| {
project.for_language_servers_for_local_buffer(
buffer,
|mut it| {
it.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == language_server_name {
Some(server.server_id())
} else {
None
}
})
},
cx,
)
})
});
vacant_entry.insert(language_server_id)
}
}
.as_ref()?;
Some((buffer, trigger_anchor, server_id))
})
.find_map(|(buffer, trigger_anchor, server_id)| {
let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
if !filter_language(&language) {
return None;

View File

@@ -455,7 +455,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
}
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &mut AppContext) -> bool {
if let Some(buffer) = self.to_base(&buffer, &[], cx) {
self.0.supports_inlay_hints(&buffer, cx)
} else {

View File

@@ -43,10 +43,6 @@ pub struct Assistant2FeatureFlag;
impl FeatureFlag for Assistant2FeatureFlag {
const NAME: &'static str = "assistant2";
fn enabled_for_staff() -> bool {
false
}
}
pub struct ToolUseFeatureFlag;

View File

@@ -21,18 +21,25 @@ const fn zed_repo_url() -> &'static str {
"https://github.com/zed-industries/zed"
}
fn request_feature_url() -> String {
"https://github.com/zed-industries/zed/discussions/new/choose".to_string()
fn request_feature_url(specs: &SystemSpecs) -> String {
format!(
concat!(
"https://github.com/zed-industries/zed/issues/new",
"?labels=admin+read%2Ctriage%2Cenhancement",
"&template=0_feature_request.yml",
"&environment={}"
),
urlencoding::encode(&specs.to_string())
)
}
fn file_bug_report_url(specs: &SystemSpecs) -> String {
format!(
concat!(
"https://github.com/zed-industries/zed/issues/new",
"?",
"template=1_bug_report.yml",
"&",
"environment={}"
"?labels=admin+read%2Ctriage%2Cbug",
"&template=1_bug_report.yml",
"&environment={}"
),
urlencoding::encode(&specs.to_string())
)
@@ -63,9 +70,11 @@ pub fn init(cx: &mut AppContext) {
.detach();
})
.register_action(|_, _: &RequestFeature, cx| {
let specs = SystemSpecs::new(cx);
cx.spawn(|_, mut cx| async move {
let specs = specs.await;
cx.update(|cx| {
cx.open_url(&request_feature_url());
cx.open_url(&request_feature_url(&specs));
})
.log_err();
})

View File

@@ -62,6 +62,13 @@ impl FileStatus {
})
}
pub const fn index(index_status: StatusCode) -> Self {
FileStatus::Tracked(TrackedStatus {
worktree_status: StatusCode::Unmodified,
index_status,
})
}
/// Generate a FileStatus Code from a byte pair, as described in
/// https://git-scm.com/docs/git-status#_output
///
@@ -454,6 +461,26 @@ impl GitStatus {
})
.collect::<Vec<_>>();
entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
// When a file exists in HEAD, is deleted in the index, and exists again in the working copy,
// git produces two lines for it, one reading `D ` (deleted in index, unmodified in working copy)
// and the other reading `??` (untracked). Merge these two into the equivalent of `DA`.
entries.dedup_by(|(a, a_status), (b, b_status)| {
const INDEX_DELETED: FileStatus = FileStatus::index(StatusCode::Deleted);
if a.ne(&b) {
return false;
}
match (*a_status, *b_status) {
(INDEX_DELETED, FileStatus::Untracked) | (FileStatus::Untracked, INDEX_DELETED) => {
*b_status = TrackedStatus {
index_status: StatusCode::Deleted,
worktree_status: StatusCode::Added,
}
.into();
}
_ => panic!("Unexpected duplicated status entries: {a_status:?} and {b_status:?}"),
}
true
});
Ok(Self {
entries: entries.into(),
})

View File

@@ -32,8 +32,6 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
picker.workspace = true
feature_flags.workspace = true
smol.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -16,7 +16,6 @@ use project::git::RepositoryHandle;
use project::{Fs, Project, ProjectPath};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{collections::HashSet, ops::Range, path::PathBuf, sync::Arc, time::Duration, usize};
use theme::ThemeSettings;
use ui::{
@@ -91,7 +90,7 @@ pub struct GitPanel {
scrollbar_state: ScrollbarState,
selected_entry: Option<usize>,
show_scrollbar: bool,
rebuild_requested: Arc<AtomicBool>,
update_visible_entries_task: Task<()>,
commit_editor: View<Editor>,
visible_entries: Vec<GitListEntry>,
all_staged: Option<bool>,
@@ -167,33 +166,11 @@ impl GitPanel {
let scroll_handle = UniformListScrollHandle::new();
let rebuild_requested = Arc::new(AtomicBool::new(false));
let flag = rebuild_requested.clone();
let handle = cx.view().downgrade();
cx.spawn(|_, mut cx| async move {
loop {
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
if flag.load(Ordering::Relaxed) {
if let Some(this) = handle.upgrade() {
this.update(&mut cx, |this, cx| {
this.update_visible_entries(cx);
let active_repository = this.active_repository.as_ref();
this.commit_editor =
cx.new_view(|cx| commit_message_editor(active_repository, cx));
})
.ok();
}
flag.store(false, Ordering::Relaxed);
}
}
})
.detach();
if let Some(git_state) = git_state {
cx.subscribe(&git_state, move |this, git_state, event, cx| match event {
project::git::Event::RepositoriesUpdated => {
this.active_repository = git_state.read(cx).active_repository();
this.schedule_update();
this.schedule_update(cx);
}
})
.detach();
@@ -210,16 +187,16 @@ impl GitPanel {
selected_entry: None,
show_scrollbar: false,
hide_scrollbar_task: None,
update_visible_entries_task: Task::ready(()),
active_repository,
scroll_handle,
fs,
rebuild_requested,
commit_editor,
project,
err_sender,
workspace,
};
git_panel.schedule_update();
git_panel.schedule_update(cx);
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
git_panel
});
@@ -572,11 +549,10 @@ impl GitPanel {
let Some(active_repository) = self.active_repository.as_ref() else {
return;
};
if let Err(e) = active_repository.commit(self.err_sender.clone(), cx) {
self.show_err_toast("commit error", e, cx);
};
self.commit_editor
.update(cx, |editor, cx| editor.set_text("", cx));
if !active_repository.can_commit(false, cx) {
return;
}
active_repository.commit(self.err_sender.clone(), cx);
}
/// Commit all changes, regardless of whether they are staged or not
@@ -584,11 +560,10 @@ impl GitPanel {
let Some(active_repository) = self.active_repository.as_ref() else {
return;
};
if let Err(e) = active_repository.commit_all(self.err_sender.clone(), cx) {
self.show_err_toast("commit all error", e, cx);
};
self.commit_editor
.update(cx, |editor, cx| editor.set_text("", cx));
if !active_repository.can_commit(true, cx) {
return;
}
active_repository.commit_all(self.err_sender.clone(), cx);
}
fn fill_co_authors(&mut self, _: &FillCoAuthors, cx: &mut ViewContext<Self>) {
@@ -689,8 +664,20 @@ impl GitPanel {
}
}
fn schedule_update(&mut self) {
self.rebuild_requested.store(true, Ordering::Relaxed);
fn schedule_update(&mut self, cx: &mut ViewContext<Self>) {
let handle = cx.view().downgrade();
self.update_visible_entries_task = cx.spawn(|_, mut cx| async move {
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
if let Some(this) = handle.upgrade() {
this.update(&mut cx, |this, cx| {
this.update_visible_entries(cx);
let active_repository = this.active_repository.as_ref();
this.commit_editor =
cx.new_view(|cx| commit_message_editor(active_repository, cx));
})
.ok();
}
});
}
fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) {
@@ -917,15 +904,15 @@ impl GitPanel {
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
let editor = self.commit_editor.clone();
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
let (can_commit, can_commit_all) = self.active_repository.as_ref().map_or_else(
|| (false, false),
|active_repository| {
(
active_repository.can_commit(false, cx),
active_repository.can_commit(true, cx),
)
},
);
let (can_commit, can_commit_all) =
self.active_repository
.as_ref()
.map_or((false, false), |active_repository| {
(
active_repository.can_commit(false, cx),
active_repository.can_commit(true, cx),
)
});
let focus_handle_1 = self.focus_handle(cx).clone();
let focus_handle_2 = self.focus_handle(cx).clone();

View File

@@ -1,6 +1,4 @@
use ::settings::Settings;
use feature_flags::WaitForFlag;
use futures::{select_biased, FutureExt};
use git::status::FileStatus;
use git_panel_settings::GitPanelSettings;
use gpui::{AppContext, Hsla};
@@ -14,17 +12,6 @@ pub fn init(cx: &mut AppContext) {
GitPanelSettings::register(cx);
}
// TODO: Remove this before launching Git UI
pub async fn git_ui_enabled(flag: WaitForFlag) -> bool {
let mut git_ui_feature_flag = flag.fuse();
let mut timeout = FutureExt::fuse(smol::Timer::after(std::time::Duration::from_secs(5)));
select_biased! {
is_git_ui_enabled = git_ui_feature_flag => is_git_ui_enabled,
_ = timeout => false,
}
}
const ADDED_COLOR: Hsla = Hsla {
h: 142. / 360.,
s: 0.68,

View File

@@ -94,7 +94,7 @@ impl Keystroke {
"alt" => alt = true,
"shift" => shift = true,
"fn" => function = true,
"cmd" | "super" | "win" => platform = true,
"cmd" | "super" | "win" | "" => platform = true,
_ => {
if let Some(next) = components.peek() {
if next.is_empty() && source.ends_with('-') {

View File

@@ -290,13 +290,9 @@ impl MacPlatform {
action,
os_action,
} => {
// Note that this is not the standard logic for selecting which keybinding to
// display. Typically the last binding takes precedence for display. However, in
// this case the menus are not updated on context changes. To make these bindings
// more likely to be correct, the first binding instead takes precedence (typically
// from the base keymap).
let keystrokes = keymap
.bindings_for_action(action.as_ref())
.rev()
.next()
.map(|binding| binding.keystrokes());

View File

@@ -14,6 +14,6 @@ proc-macro = true
doctest = false
[dependencies]
proc-macro2 = "1.0.66"
proc-macro2 = "1.0.93"
quote = "1.0.9"
syn = { version = "1.0.72", features = ["full", "extra-traits"] }

View File

@@ -25,7 +25,6 @@ use crate::language_settings::SoftWrap;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use collections::{HashMap, HashSet};
use fs::Fs;
use futures::Future;
use gpui::{AppContext, AsyncAppContext, Model, SharedString, Task};
pub use highlight_map::HighlightMap;
@@ -46,7 +45,6 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use settings::WorktreeId;
use smol::future::FutureExt as _;
use std::num::NonZeroU32;
use std::{
any::Any,
ffi::OsStr,
@@ -62,6 +60,7 @@ use std::{
Arc, LazyLock,
},
};
use std::{num::NonZeroU32, sync::OnceLock};
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
use task::RunnableTag;
pub use task_context::{ContextProvider, RunnableRange};
@@ -164,6 +163,7 @@ pub struct CachedLspAdapter {
pub adapter: Arc<dyn LspAdapter>,
pub reinstall_attempt_count: AtomicU64,
cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
attach_kind: OnceLock<Attach>,
}
impl Debug for CachedLspAdapter {
@@ -199,6 +199,7 @@ impl CachedLspAdapter {
adapter,
cached_binary: Default::default(),
reinstall_attempt_count: AtomicU64::new(0),
attach_kind: Default::default(),
})
}
@@ -260,6 +261,38 @@ impl CachedLspAdapter {
.cloned()
.unwrap_or_else(|| language_name.lsp_id())
}
pub fn find_project_root(
&self,
path: &Path,
ancestor_depth: usize,
delegate: &Arc<dyn LspAdapterDelegate>,
) -> Option<Arc<Path>> {
self.adapter
.find_project_root(path, ancestor_depth, delegate)
}
pub fn attach_kind(&self) -> Attach {
*self.attach_kind.get_or_init(|| self.adapter.attach_kind())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Attach {
/// Create a single language server instance per subproject root.
InstancePerRoot,
/// Use one shared language server instance for all subprojects within a project.
Shared,
}
impl Attach {
pub fn root_path(
&self,
root_subproject_path: (WorktreeId, Arc<Path>),
) -> (WorktreeId, Arc<Path>) {
match self {
Attach::InstancePerRoot => root_subproject_path,
Attach::Shared => (root_subproject_path.0, Arc::from(Path::new(""))),
}
}
}
/// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application
@@ -465,7 +498,6 @@ pub trait LspAdapter: 'static + Send + Sync {
/// Returns initialization options that are going to be sent to a LSP server as a part of [`lsp::InitializeParams`]
async fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<Value>> {
Ok(None)
@@ -473,7 +505,6 @@ pub trait LspAdapter: 'static + Send + Sync {
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
_cx: &mut AsyncAppContext,
@@ -508,6 +539,19 @@ pub trait LspAdapter: 'static + Send + Sync {
fn prepare_initialize_params(&self, original: InitializeParams) -> Result<InitializeParams> {
Ok(original)
}
fn attach_kind(&self) -> Attach {
Attach::Shared
}
fn find_project_root(
&self,
_path: &Path,
_ancestor_depth: usize,
_: &Arc<dyn LspAdapterDelegate>,
) -> Option<Arc<Path>> {
// By default all language servers are rooted at the root of the worktree.
Some(Arc::from("".as_ref()))
}
}
async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>(
@@ -1857,7 +1901,6 @@ impl LspAdapter for FakeLspAdapter {
async fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<Value>> {
Ok(self.initialization_options.clone())

View File

@@ -96,6 +96,7 @@ struct LanguageRegistryState {
available_languages: Vec<AvailableLanguage>,
grammars: HashMap<Arc<str>, AvailableGrammar>,
lsp_adapters: HashMap<LanguageName, Vec<Arc<CachedLspAdapter>>>,
all_lsp_adapters: HashMap<LanguageServerName, Arc<CachedLspAdapter>>,
available_lsp_adapters:
HashMap<LanguageServerName, Arc<dyn Fn() -> Arc<CachedLspAdapter> + 'static + Send + Sync>>,
loading_languages: HashMap<LanguageId, Vec<oneshot::Sender<Result<Arc<Language>>>>>,
@@ -222,6 +223,7 @@ impl LanguageRegistry {
language_settings: Default::default(),
loading_languages: Default::default(),
lsp_adapters: Default::default(),
all_lsp_adapters: Default::default(),
available_lsp_adapters: HashMap::default(),
subscription: watch::channel(),
theme: Default::default(),
@@ -344,12 +346,16 @@ impl LanguageRegistry {
adapter: Arc<dyn LspAdapter>,
) -> Arc<CachedLspAdapter> {
let cached = CachedLspAdapter::new(adapter);
self.state
.write()
let mut state = self.state.write();
state
.lsp_adapters
.entry(language_name)
.or_default()
.push(cached.clone());
state
.all_lsp_adapters
.insert(cached.name.clone(), cached.clone());
cached
}
@@ -389,12 +395,17 @@ impl LanguageRegistry {
let adapter_name = LanguageServerName(adapter.name.into());
let capabilities = adapter.capabilities.clone();
let initializer = adapter.initializer.take();
self.state
.write()
.lsp_adapters
.entry(language_name.clone())
.or_default()
.push(CachedLspAdapter::new(Arc::new(adapter)));
let adapter = CachedLspAdapter::new(Arc::new(adapter));
{
let mut state = self.state.write();
state
.lsp_adapters
.entry(language_name.clone())
.or_default()
.push(adapter.clone());
state.all_lsp_adapters.insert(adapter.name(), adapter);
}
self.register_fake_language_server(adapter_name, capabilities, initializer)
}
@@ -407,12 +418,16 @@ impl LanguageRegistry {
adapter: crate::FakeLspAdapter,
) {
let language_name = language_name.into();
self.state
.write()
let mut state = self.state.write();
let cached_adapter = CachedLspAdapter::new(Arc::new(adapter));
state
.lsp_adapters
.entry(language_name.clone())
.or_default()
.push(CachedLspAdapter::new(Arc::new(adapter)));
.push(cached_adapter.clone());
state
.all_lsp_adapters
.insert(cached_adapter.name(), cached_adapter);
}
/// Register a fake language server (without the adapter)
@@ -880,6 +895,10 @@ impl LanguageRegistry {
.unwrap_or_default()
}
pub fn adapter_for_name(&self, name: &LanguageServerName) -> Option<Arc<CachedLspAdapter>> {
self.state.read().all_lsp_adapters.get(name).cloned()
}
pub fn update_lsp_status(
&self,
server_name: LanguageServerName,

View File

@@ -17,7 +17,6 @@ async-trait.workspace = true
collections.workspace = true
extension.workspace = true
futures.workspace = true
fs.workspace = true
gpui.workspace = true
language.workspace = true
lsp.workspace = true

View File

@@ -8,7 +8,6 @@ use anyhow::{Context, Result};
use async_trait::async_trait;
use collections::HashMap;
use extension::{Extension, ExtensionLanguageServerProxy, WorktreeDelegate};
use fs::Fs;
use futures::{Future, FutureExt};
use gpui::AsyncAppContext;
use language::{
@@ -225,7 +224,6 @@ impl LspAdapter for ExtensionLspAdapter {
async fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
@@ -248,7 +246,6 @@ impl LspAdapter for ExtensionLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
_cx: &mut AsyncAppContext,

View File

@@ -29,7 +29,6 @@ log.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
lmstudio = { workspace = true, features = ["schemars"] }
deepseek = { workspace = true, features = ["schemars"] }
parking_lot.workspace = true
proto.workspace = true
schemars.workspace = true

View File

@@ -80,9 +80,7 @@ impl CloudModel {
| open_ai::Model::FourOmni
| open_ai::Model::FourOmniMini
| open_ai::Model::O1Mini
| open_ai::Model::O1Preview
| open_ai::Model::O1
| open_ai::Model::O3Mini
| open_ai::Model::Custom { .. } => {
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
}

View File

@@ -410,84 +410,6 @@ impl LanguageModelRequest {
top_p: None,
}
}
pub fn into_deepseek(self, model: String, max_output_tokens: Option<u32>) -> deepseek::Request {
let is_reasoner = model == "deepseek-reasoner";
let len = self.messages.len();
let merged_messages =
self.messages
.into_iter()
.fold(Vec::with_capacity(len), |mut acc, msg| {
let role = msg.role;
let content = msg.string_contents();
if is_reasoner {
if let Some(last_msg) = acc.last_mut() {
match (last_msg, role) {
(deepseek::RequestMessage::User { content: last }, Role::User) => {
last.push(' ');
last.push_str(&content);
return acc;
}
(
deepseek::RequestMessage::Assistant {
content: last_content,
..
},
Role::Assistant,
) => {
*last_content = last_content
.take()
.map(|c| {
let mut s =
String::with_capacity(c.len() + content.len() + 1);
s.push_str(&c);
s.push(' ');
s.push_str(&content);
s
})
.or(Some(content));
return acc;
}
_ => {}
}
}
}
acc.push(match role {
Role::User => deepseek::RequestMessage::User { content },
Role::Assistant => deepseek::RequestMessage::Assistant {
content: Some(content),
tool_calls: Vec::new(),
},
Role::System => deepseek::RequestMessage::System { content },
});
acc
});
deepseek::Request {
model,
messages: merged_messages,
stream: true,
max_tokens: max_output_tokens,
temperature: if is_reasoner { None } else { self.temperature },
response_format: None,
tools: self
.tools
.into_iter()
.map(|tool| deepseek::ToolDefinition::Function {
function: deepseek::FunctionDefinition {
name: tool.name,
description: Some(tool.description),
parameters: Some(tool.input_schema),
},
})
.collect(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]

View File

@@ -66,16 +66,6 @@ impl From<Role> for open_ai::Role {
}
}
impl From<Role> for deepseek::Role {
fn from(val: Role) -> Self {
match val {
Role::User => deepseek::Role::User,
Role::Assistant => deepseek::Role::Assistant,
Role::System => deepseek::Role::System,
}
}
}
impl From<Role> for lmstudio::Role {
fn from(val: Role) -> Self {
match val {

View File

@@ -29,7 +29,6 @@ menu.workspace = true
ollama = { workspace = true, features = ["schemars"] }
lmstudio = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
deepseek = { workspace = true, features = ["schemars"] }
project.workspace = true
proto.workspace = true
schemars.workspace = true

View File

@@ -4,7 +4,6 @@ use client::{Client, UserStore};
use fs::Fs;
use gpui::{AppContext, Model, ModelContext};
use language_model::{LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use provider::deepseek::DeepSeekLanguageModelProvider;
mod logging;
pub mod provider;
@@ -61,10 +60,6 @@ fn register_language_model_providers(
LmStudioLanguageModelProvider::new(client.http_client(), cx),
cx,
);
registry.register_provider(
DeepSeekLanguageModelProvider::new(client.http_client(), cx),
cx,
);
registry.register_provider(
GoogleLanguageModelProvider::new(client.http_client(), cx),
cx,

View File

@@ -1,7 +1,6 @@
pub mod anthropic;
pub mod cloud;
pub mod copilot_chat;
pub mod deepseek;
pub mod google;
pub mod lmstudio;
pub mod ollama;

View File

@@ -1,558 +0,0 @@
use anyhow::{anyhow, Result};
use collections::BTreeMap;
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{
AnyView, AppContext, AsyncAppContext, FontStyle, ModelContext, Subscription, Task, TextStyle,
View, WhiteSpace,
};
use http_client::HttpClient;
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use std::sync::Arc;
use theme::ThemeSettings;
use ui::{prelude::*, Icon, IconName};
use util::ResultExt;
use crate::AllLanguageModelSettings;
const PROVIDER_ID: &str = "deepseek";
const PROVIDER_NAME: &str = "DeepSeek";
const DEEPSEEK_API_KEY_VAR: &str = "DEEPSEEK_API_KEY";
#[derive(Default, Clone, Debug, PartialEq)]
pub struct DeepSeekSettings {
pub api_url: String,
pub available_models: Vec<AvailableModel>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct AvailableModel {
pub name: String,
pub display_name: Option<String>,
pub max_tokens: usize,
pub max_output_tokens: Option<u32>,
}
pub struct DeepSeekLanguageModelProvider {
http_client: Arc<dyn HttpClient>,
state: gpui::Model<State>,
}
pub struct State {
api_key: Option<String>,
api_key_from_env: bool,
_subscription: Subscription,
}
impl State {
fn is_authenticated(&self) -> bool {
self.api_key.is_some()
}
fn reset_api_key(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).deepseek;
let delete_credentials = cx.delete_credentials(&settings.api_url);
cx.spawn(|this, mut cx| async move {
delete_credentials.await.log_err();
this.update(&mut cx, |this, cx| {
this.api_key = None;
this.api_key_from_env = false;
cx.notify();
})
})
}
fn set_api_key(&mut self, api_key: String, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).deepseek;
let write_credentials =
cx.write_credentials(&settings.api_url, "Bearer", api_key.as_bytes());
cx.spawn(|this, mut cx| async move {
write_credentials.await?;
this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key);
cx.notify();
})
})
}
fn authenticate(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.is_authenticated() {
Task::ready(Ok(()))
} else {
let api_url = AllLanguageModelSettings::get_global(cx)
.deepseek
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(DEEPSEEK_API_KEY_VAR) {
(api_key, true)
} else {
let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))?
.await?
.ok_or_else(|| anyhow!("credentials not found"))?;
(String::from_utf8(api_key)?, false)
};
this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key);
this.api_key_from_env = from_env;
cx.notify();
})
})
}
}
}
impl DeepSeekLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State {
api_key: None,
api_key_from_env: false,
_subscription: cx.observe_global::<SettingsStore>(|_this: &mut State, cx| {
cx.notify();
}),
});
Self { http_client, state }
}
}
impl LanguageModelProviderState for DeepSeekLanguageModelProvider {
type ObservableEntity = State;
fn observable_entity(&self) -> Option<gpui::Model<Self::ObservableEntity>> {
Some(self.state.clone())
}
}
impl LanguageModelProvider for DeepSeekLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
LanguageModelProviderId(PROVIDER_ID.into())
}
fn name(&self) -> LanguageModelProviderName {
LanguageModelProviderName(PROVIDER_NAME.into())
}
fn icon(&self) -> IconName {
IconName::AiDeepSeek
}
fn provided_models(&self, cx: &AppContext) -> Vec<Arc<dyn LanguageModel>> {
let mut models = BTreeMap::default();
models.insert("deepseek-chat", deepseek::Model::Chat);
models.insert("deepseek-reasoner", deepseek::Model::Reasoner);
for available_model in AllLanguageModelSettings::get_global(cx)
.deepseek
.available_models
.iter()
{
models.insert(
&available_model.name,
deepseek::Model::Custom {
name: available_model.name.clone(),
display_name: available_model.display_name.clone(),
max_tokens: available_model.max_tokens,
max_output_tokens: available_model.max_output_tokens,
},
);
}
models
.into_values()
.map(|model| {
Arc::new(DeepSeekLanguageModel {
id: LanguageModelId::from(model.id().to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
}) as Arc<dyn LanguageModel>
})
.collect()
}
fn is_authenticated(&self, cx: &AppContext) -> bool {
self.state.read(cx).is_authenticated()
}
fn authenticate(&self, cx: &mut AppContext) -> Task<Result<()>> {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
fn configuration_view(&self, cx: &mut WindowContext) -> AnyView {
cx.new_view(|cx| ConfigurationView::new(self.state.clone(), cx))
.into()
}
fn reset_credentials(&self, cx: &mut AppContext) -> Task<Result<()>> {
self.state.update(cx, |state, cx| state.reset_api_key(cx))
}
}
pub struct DeepSeekLanguageModel {
id: LanguageModelId,
model: deepseek::Model,
state: gpui::Model<State>,
http_client: Arc<dyn HttpClient>,
request_limiter: RateLimiter,
}
impl DeepSeekLanguageModel {
fn stream_completion(
&self,
request: deepseek::Request,
cx: &AsyncAppContext,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<deepseek::StreamResponse>>>> {
let http_client = self.http_client.clone();
let Ok((api_key, api_url)) = cx.read_model(&self.state, |state, cx| {
let settings = &AllLanguageModelSettings::get_global(cx).deepseek;
(state.api_key.clone(), settings.api_url.clone())
}) else {
return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
};
let future = self.request_limiter.stream(async move {
let api_key = api_key.ok_or_else(|| anyhow!("Missing DeepSeek API Key"))?;
let request =
deepseek::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
let response = request.await?;
Ok(response)
});
async move { Ok(future.await?.boxed()) }.boxed()
}
}
impl LanguageModel for DeepSeekLanguageModel {
fn id(&self) -> LanguageModelId {
self.id.clone()
}
fn name(&self) -> LanguageModelName {
LanguageModelName::from(self.model.display_name().to_string())
}
fn provider_id(&self) -> LanguageModelProviderId {
LanguageModelProviderId(PROVIDER_ID.into())
}
fn provider_name(&self) -> LanguageModelProviderName {
LanguageModelProviderName(PROVIDER_NAME.into())
}
fn telemetry_id(&self) -> String {
format!("deepseek/{}", self.model.id())
}
fn max_token_count(&self) -> usize {
self.model.max_token_count()
}
fn max_output_tokens(&self) -> Option<u32> {
self.model.max_output_tokens()
}
fn count_tokens(
&self,
request: LanguageModelRequest,
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
cx.background_executor()
.spawn(async move {
let messages = request
.messages
.into_iter()
.map(|message| tiktoken_rs::ChatCompletionRequestMessage {
role: match message.role {
Role::User => "user".into(),
Role::Assistant => "assistant".into(),
Role::System => "system".into(),
},
content: Some(message.string_contents()),
name: None,
function_call: None,
})
.collect::<Vec<_>>();
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
})
.boxed()
}
fn stream_completion(
&self,
request: LanguageModelRequest,
cx: &AsyncAppContext,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {
let request = request.into_deepseek(self.model.id().to_string(), self.max_output_tokens());
let stream = self.stream_completion(request, cx);
async move {
let stream = stream.await?;
Ok(stream
.map(|result| {
result.and_then(|response| {
response
.choices
.first()
.ok_or_else(|| anyhow!("Empty response"))
.map(|choice| {
choice
.delta
.content
.clone()
.unwrap_or_default()
.map(LanguageModelCompletionEvent::Text)
})
})
})
.boxed())
}
.boxed()
}
fn use_any_tool(
&self,
request: LanguageModelRequest,
name: String,
description: String,
schema: serde_json::Value,
cx: &AsyncAppContext,
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<String>>>> {
let mut deepseek_request =
request.into_deepseek(self.model.id().to_string(), self.max_output_tokens());
deepseek_request.tools = vec![deepseek::ToolDefinition::Function {
function: deepseek::FunctionDefinition {
name: name.clone(),
description: Some(description),
parameters: Some(schema),
},
}];
let response_stream = self.stream_completion(deepseek_request, cx);
self.request_limiter
.run(async move {
let stream = response_stream.await?;
let tool_args_stream = stream
.filter_map(move |response| async move {
match response {
Ok(response) => {
for choice in response.choices {
if let Some(tool_calls) = choice.delta.tool_calls {
for tool_call in tool_calls {
if let Some(function) = tool_call.function {
if let Some(args) = function.arguments {
return Some(Ok(args));
}
}
}
}
}
None
}
Err(e) => Some(Err(e)),
}
})
.boxed();
Ok(tool_args_stream)
})
.boxed()
}
}
struct ConfigurationView {
api_key_editor: View<Editor>,
state: gpui::Model<State>,
load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
fn new(state: gpui::Model<State>, cx: &mut ViewContext<Self>) -> Self {
let api_key_editor = cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("sk-00000000000000000000000000000000", cx);
editor
});
cx.observe(&state, |_, _, cx| {
cx.notify();
})
.detach();
let load_credentials_task = Some(cx.spawn({
let state = state.clone();
|this, mut cx| async move {
if let Some(task) = state
.update(&mut cx, |state, cx| state.authenticate(cx))
.log_err()
{
let _ = task.await;
}
this.update(&mut cx, |this, cx| {
this.load_credentials_task = None;
cx.notify();
})
.log_err();
}
}));
Self {
api_key_editor,
state,
load_credentials_task,
}
}
fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let api_key = self.api_key_editor.read(cx).text(cx);
if api_key.is_empty() {
return;
}
let state = self.state.clone();
cx.spawn(|_, mut cx| async move {
state
.update(&mut cx, |state, cx| state.set_api_key(api_key, cx))?
.await
})
.detach_and_log_err(cx);
cx.notify();
}
fn reset_api_key(&mut self, cx: &mut ViewContext<Self>) {
self.api_key_editor
.update(cx, |editor, cx| editor.set_text("", cx));
let state = self.state.clone();
cx.spawn(|_, mut cx| async move {
state
.update(&mut cx, |state, cx| state.reset_api_key(cx))?
.await
})
.detach_and_log_err(cx);
cx.notify();
}
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_fallbacks: settings.ui_font.fallbacks.clone(),
font_size: rems(0.875).into(),
font_weight: settings.ui_font.weight,
font_style: FontStyle::Normal,
line_height: relative(1.3),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
truncate: None,
};
EditorElement::new(
&self.api_key_editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
fn should_render_editor(&self, cx: &mut ViewContext<Self>) -> bool {
!self.state.read(cx).is_authenticated()
}
}
impl Render for ConfigurationView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
const DEEPSEEK_CONSOLE_URL: &str = "https://platform.deepseek.com/api_keys";
const INSTRUCTIONS: [&str; 3] = [
"To use DeepSeek in Zed, you need an API key:",
"- Get your API key from:",
"- Paste it below and press enter:",
];
let env_var_set = self.state.read(cx).api_key_from_env;
if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any()
} else if self.should_render_editor(cx) {
v_flex()
.size_full()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new(INSTRUCTIONS[0]))
.child(
h_flex().child(Label::new(INSTRUCTIONS[1])).child(
Button::new("deepseek_console", DEEPSEEK_CONSOLE_URL)
.style(ButtonStyle::Subtle)
.icon(IconName::ExternalLink)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, cx| cx.open_url(DEEPSEEK_CONSOLE_URL)),
),
)
.child(Label::new(INSTRUCTIONS[2]))
.child(
h_flex()
.w_full()
.my_2()
.px_2()
.py_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(self.render_api_key_editor(cx)),
)
.child(
Label::new(format!(
"Or set {} environment variable",
DEEPSEEK_API_KEY_VAR
))
.size(LabelSize::Small),
)
.into_any()
} else {
h_flex()
.size_full()
.justify_between()
.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new(if env_var_set {
format!("API key set in {}", DEEPSEEK_API_KEY_VAR)
} else {
"API key configured".to_string()
})),
)
.child(
Button::new("reset-key", "Reset")
.icon(IconName::Trash)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))),
)
.into_any()
}
}
}

View File

@@ -13,7 +13,6 @@ use crate::provider::{
anthropic::AnthropicSettings,
cloud::{self, ZedDotDevSettings},
copilot_chat::CopilotChatSettings,
deepseek::DeepSeekSettings,
google::GoogleSettings,
lmstudio::LmStudioSettings,
ollama::OllamaSettings,
@@ -62,7 +61,6 @@ pub struct AllLanguageModelSettings {
pub google: GoogleSettings,
pub copilot_chat: CopilotChatSettings,
pub lmstudio: LmStudioSettings,
pub deepseek: DeepSeekSettings,
}
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
@@ -74,7 +72,6 @@ pub struct AllLanguageModelSettingsContent {
#[serde(rename = "zed.dev")]
pub zed_dot_dev: Option<ZedDotDevSettingsContent>,
pub google: Option<GoogleSettingsContent>,
pub deepseek: Option<DeepseekSettingsContent>,
pub copilot_chat: Option<CopilotChatSettingsContent>,
}
@@ -165,12 +162,6 @@ pub struct LmStudioSettingsContent {
pub available_models: Option<Vec<provider::lmstudio::AvailableModel>>,
}
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct DeepseekSettingsContent {
pub api_url: Option<String>,
pub available_models: Option<Vec<provider::deepseek::AvailableModel>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(untagged)]
pub enum OpenAiSettingsContent {
@@ -308,18 +299,6 @@ impl settings::Settings for AllLanguageModelSettings {
lmstudio.as_ref().and_then(|s| s.available_models.clone()),
);
// DeepSeek
let deepseek = value.deepseek.clone();
merge(
&mut settings.deepseek.api_url,
value.deepseek.as_ref().and_then(|s| s.api_url.clone()),
);
merge(
&mut settings.deepseek.available_models,
deepseek.as_ref().and_then(|s| s.available_models.clone()),
);
// OpenAI
let (openai, upgraded) = match value.openai.clone().map(|s| s.upgrade()) {
Some((content, upgraded)) => (Some(content), upgraded),

View File

@@ -730,7 +730,8 @@ impl LspLogView {
* Binary: {BINARY:#?}
* Running in project: {PATH:?}
* Registered workspace folders:
{WORKSPACE_FOLDERS}
* Capabilities: {CAPABILITIES}
@@ -738,7 +739,15 @@ impl LspLogView {
NAME = server.name(),
ID = server.server_id(),
BINARY = server.binary(),
PATH = server.root_path(),
WORKSPACE_FOLDERS = server
.workspace_folders()
.iter()
.filter_map(|path| path
.to_file_path()
.ok()
.map(|path| path.to_string_lossy().into_owned()))
.collect::<Vec<_>>()
.join(", "),
CAPABILITIES = serde_json::to_string_pretty(&server.capabilities())
.unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
CONFIGURATION = serde_json::to_string_pretty(server.configuration())

View File

@@ -4,7 +4,6 @@ use futures::StreamExt;
use language::{LspAdapter, LspAdapterDelegate};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::Fs;
use serde_json::json;
use smol::fs;
use std::{
@@ -108,7 +107,6 @@ impl LspAdapter for CssLspAdapter {
async fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
Ok(Some(json!({

View File

@@ -6,7 +6,6 @@ use gpui::{AppContext, AsyncAppContext, Task};
use http_client::github::latest_github_release;
pub use language::*;
use lsp::{LanguageServerBinary, LanguageServerName};
use project::Fs;
use regex::Regex;
use serde_json::json;
use smol::fs;
@@ -198,7 +197,6 @@ impl super::LspAdapter for GoLspAdapter {
async fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
Ok(Some(json!({

View File

@@ -9,7 +9,7 @@ use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
use language::{LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::{lsp_store::language_server_settings, ContextProviderWithTasks, Fs};
use project::{lsp_store::language_server_settings, ContextProviderWithTasks};
use serde_json::{json, Value};
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
use smol::{
@@ -208,7 +208,6 @@ impl LspAdapter for JsonLspAdapter {
async fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
Ok(Some(json!({
@@ -218,7 +217,6 @@ impl LspAdapter for JsonLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,

View File

@@ -18,7 +18,6 @@ use pet_core::os_environment::Environment;
use pet_core::python_environment::PythonEnvironmentKind;
use pet_core::Configuration;
use project::lsp_store::language_server_settings;
use project::Fs;
use serde_json::{json, Value};
use smol::lock::OnceCell;
use std::cmp::Ordering;
@@ -251,7 +250,6 @@ impl LspAdapter for PythonLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
adapter: &Arc<dyn LspAdapterDelegate>,
toolchains: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
@@ -933,7 +931,6 @@ impl LspAdapter for PyLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
adapter: &Arc<dyn LspAdapterDelegate>,
toolchains: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,

View File

@@ -74,6 +74,22 @@ impl LspAdapter for RustLspAdapter {
Self::SERVER_NAME.clone()
}
fn find_project_root(
&self,
path: &Path,
ancestor_depth: usize,
delegate: &Arc<dyn LspAdapterDelegate>,
) -> Option<Arc<Path>> {
let mut outermost_cargo_toml = None;
for path in path.ancestors().take(ancestor_depth) {
let p = path.join("Cargo.toml").to_path_buf();
if smol::block_on(delegate.read_text_file(p)).is_ok() {
outermost_cargo_toml = Some(Arc::from(path));
}
}
outermost_cargo_toml
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,

View File

@@ -6,7 +6,7 @@ use gpui::AsyncAppContext;
use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::{lsp_store::language_server_settings, Fs};
use project::lsp_store::language_server_settings;
use serde_json::{json, Value};
use smol::fs;
use std::{
@@ -116,7 +116,6 @@ impl LspAdapter for TailwindLspAdapter {
async fn initialization_options(
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
Ok(Some(json!({
@@ -132,7 +131,6 @@ impl LspAdapter for TailwindLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,

View File

@@ -8,8 +8,8 @@ use http_client::github::{build_asset_url, AssetKind, GitHubLspBinaryVersion};
use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::lsp_store::language_server_settings;
use project::ContextProviderWithTasks;
use project::{lsp_store::language_server_settings, Fs};
use serde_json::{json, Value};
use smol::{fs, io::BufReader, stream::StreamExt};
use std::{
@@ -77,25 +77,16 @@ impl TypeScriptLspAdapter {
pub fn new(node: NodeRuntime) -> Self {
TypeScriptLspAdapter { node }
}
async fn tsdk_path(fs: &dyn Fs, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
let is_yarn = adapter
.read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
.await
.is_ok();
let tsdk_path = if is_yarn {
if is_yarn {
".yarn/sdks/typescript/lib"
} else {
"node_modules/typescript/lib"
};
if fs
.is_dir(&adapter.worktree_root_path().join(tsdk_path))
.await
{
Some(tsdk_path)
} else {
None
}
}
}
@@ -242,10 +233,9 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn initialization_options(
self: Arc<Self>,
fs: &dyn Fs,
adapter: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
let tsdk_path = Self::tsdk_path(fs, adapter).await;
let tsdk_path = Self::tsdk_path(adapter).await;
Ok(Some(json!({
"provideFormatter": true,
"hostInfo": "zed",
@@ -267,7 +257,6 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
@@ -364,7 +353,6 @@ impl LspAdapter for EsLintLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,

View File

@@ -5,7 +5,7 @@ use gpui::AsyncAppContext;
use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::{lsp_store::language_server_settings, Fs};
use project::lsp_store::language_server_settings;
use serde_json::Value;
use std::{
any::Any,
@@ -34,25 +34,16 @@ impl VtslsLspAdapter {
VtslsLspAdapter { node }
}
async fn tsdk_path(fs: &dyn Fs, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
let is_yarn = adapter
.read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
.await
.is_ok();
let tsdk_path = if is_yarn {
if is_yarn {
".yarn/sdks/typescript/lib"
} else {
Self::TYPESCRIPT_TSDK_PATH
};
if fs
.is_dir(&adapter.worktree_root_path().join(tsdk_path))
.await
{
Some(tsdk_path)
} else {
None
}
}
}
@@ -205,12 +196,11 @@ impl LspAdapter for VtslsLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
fs: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let tsdk_path = Self::tsdk_path(fs, delegate).await;
let tsdk_path = Self::tsdk_path(delegate).await;
let config = serde_json::json!({
"tsdk": tsdk_path,
"suggest": {

View File

@@ -7,7 +7,7 @@ use language::{
};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::{lsp_store::language_server_settings, Fs};
use project::lsp_store::language_server_settings;
use serde_json::Value;
use settings::{Settings, SettingsLocation};
use smol::fs;
@@ -128,7 +128,6 @@ impl LspAdapter for YamlLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,

View File

@@ -192,7 +192,7 @@ pub struct ModelEntry {
pub publisher: String,
pub arch: Option<String>,
pub compatibility_type: CompatibilityType,
pub quantization: Option<String>,
pub quantization: String,
pub state: ModelState,
pub max_context_length: Option<u32>,
pub loaded_context_length: Option<u32>,

View File

@@ -29,6 +29,7 @@ serde.workspace = true
serde_json.workspace = true
schemars.workspace = true
smol.workspace = true
text.workspace = true
util.workspace = true
release_channel.workspace = true

View File

@@ -7,6 +7,7 @@ use anyhow::{anyhow, Context, Result};
use collections::HashMap;
use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, Future, FutureExt};
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, SharedString, Task};
use notification::DidChangeWorkspaceFolders;
use parking_lot::{Mutex, RwLock};
use postage::{barrier, prelude::Stream};
use schemars::{
@@ -21,12 +22,14 @@ use smol::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
process::Child,
};
use text::BufferId;
use std::{
collections::BTreeSet,
ffi::{OsStr, OsString},
fmt,
io::Write,
ops::DerefMut,
ops::{Deref, DerefMut},
path::PathBuf,
pin::Pin,
sync::{
@@ -96,9 +99,9 @@ pub struct LanguageServer {
#[allow(clippy::type_complexity)]
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
output_done_rx: Mutex<Option<barrier::Receiver>>,
root_path: PathBuf,
working_dir: PathBuf,
server: Arc<Mutex<Option<Child>>>,
workspace_folders: Arc<Mutex<BTreeSet<Url>>>,
registered_buffers: Arc<Mutex<HashMap<BufferId, Url>>>,
}
/// Identifies a running language server.
@@ -376,8 +379,6 @@ impl LanguageServer {
Some(stderr),
stderr_capture,
Some(server),
root_path,
working_dir,
code_action_kinds,
binary,
cx,
@@ -403,8 +404,6 @@ impl LanguageServer {
stderr: Option<Stderr>,
stderr_capture: Arc<Mutex<Option<String>>>,
server: Option<Child>,
root_path: &Path,
working_dir: &Path,
code_action_kinds: Option<Vec<CodeActionKind>>,
binary: LanguageServerBinary,
cx: AsyncAppContext,
@@ -488,9 +487,9 @@ impl LanguageServer {
executor: cx.background_executor().clone(),
io_tasks: Mutex::new(Some((input_task, output_task))),
output_done_rx: Mutex::new(Some(output_done_rx)),
root_path: root_path.to_path_buf(),
working_dir: working_dir.to_path_buf(),
server: Arc::new(Mutex::new(server)),
workspace_folders: Default::default(),
registered_buffers: Default::default(),
}
}
@@ -615,12 +614,11 @@ impl LanguageServer {
}
pub fn default_initialize_params(&self, cx: &AppContext) -> InitializeParams {
let root_uri = Url::from_file_path(&self.working_dir).unwrap();
#[allow(deprecated)]
InitializeParams {
process_id: None,
root_path: None,
root_uri: Some(root_uri.clone()),
root_uri: None,
initialization_options: None,
capabilities: ClientCapabilities {
general: Some(GeneralClientCapabilities {
@@ -787,10 +785,7 @@ impl LanguageServer {
}),
},
trace: None,
workspace_folders: Some(vec![WorkspaceFolder {
uri: root_uri,
name: Default::default(),
}]),
workspace_folders: None,
client_info: release_channel::ReleaseChannel::try_global(cx).map(|release_channel| {
ClientInfo {
name: release_channel.display_name().to_string(),
@@ -809,16 +804,10 @@ impl LanguageServer {
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)
pub fn initialize(
mut self,
initialize_params: Option<InitializeParams>,
params: InitializeParams,
configuration: Arc<DidChangeConfigurationParams>,
cx: &AppContext,
) -> Task<Result<Arc<Self>>> {
let params = if let Some(params) = initialize_params {
params
} else {
self.default_initialize_params(cx)
};
cx.spawn(|_| async move {
let response = self.request::<request::Initialize>(params).await?;
if let Some(info) = response.server_info {
@@ -1070,16 +1059,10 @@ impl LanguageServer {
self.server_id
}
/// Get the root path of the project the language server is running against.
pub fn root_path(&self) -> &PathBuf {
&self.root_path
}
/// Language server's binary information.
pub fn binary(&self) -> &LanguageServerBinary {
&self.binary
}
/// Sends a RPC request to the language server.
///
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
@@ -1207,6 +1190,129 @@ impl LanguageServer {
outbound_tx.try_send(message)?;
Ok(())
}
/// Add new workspace folder to the list.
pub fn add_workspace_folder(&self, uri: Url) {
if self
.capabilities()
.workspace
.and_then(|ws| {
ws.workspace_folders.and_then(|folders| {
folders
.change_notifications
.map(|caps| matches!(caps, OneOf::Left(false)))
})
})
.unwrap_or(true)
{
return;
}
let is_new_folder = self.workspace_folders.lock().insert(uri.clone());
if is_new_folder {
let params = DidChangeWorkspaceFoldersParams {
event: WorkspaceFoldersChangeEvent {
added: vec![WorkspaceFolder {
uri,
name: String::default(),
}],
removed: vec![],
},
};
self.notify::<DidChangeWorkspaceFolders>(&params).log_err();
}
}
/// Add new workspace folder to the list.
pub fn remove_workspace_folder(&self, uri: Url) {
if self
.capabilities()
.workspace
.and_then(|ws| {
ws.workspace_folders.and_then(|folders| {
folders
.change_notifications
.map(|caps| !matches!(caps, OneOf::Left(false)))
})
})
.unwrap_or(true)
{
return;
}
let was_removed = self.workspace_folders.lock().remove(&uri);
if was_removed {
let params = DidChangeWorkspaceFoldersParams {
event: WorkspaceFoldersChangeEvent {
added: vec![],
removed: vec![WorkspaceFolder {
uri,
name: String::default(),
}],
},
};
self.notify::<DidChangeWorkspaceFolders>(&params).log_err();
}
}
pub fn set_workspace_folders(&self, folders: BTreeSet<Url>) {
let mut workspace_folders = self.workspace_folders.lock();
let added: Vec<_> = folders
.iter()
.map(|uri| WorkspaceFolder {
uri: uri.clone(),
name: String::default(),
})
.collect();
let removed: Vec<_> = std::mem::replace(&mut *workspace_folders, folders)
.into_iter()
.map(|uri| WorkspaceFolder {
uri: uri.clone(),
name: String::default(),
})
.collect();
let should_notify = !added.is_empty() || !removed.is_empty();
if should_notify {
let params = DidChangeWorkspaceFoldersParams {
event: WorkspaceFoldersChangeEvent { added, removed },
};
self.notify::<DidChangeWorkspaceFolders>(&params).log_err();
}
}
pub fn workspace_folders(&self) -> impl Deref<Target = BTreeSet<Url>> + '_ {
self.workspace_folders.lock()
}
pub fn register_buffer(
&self,
buffer_id: BufferId,
uri: Url,
language_id: String,
version: i32,
initial_text: String,
) {
let previous_value = self
.registered_buffers
.lock()
.insert(buffer_id, uri.clone());
if previous_value.is_none() {
self.notify::<notification::DidOpenTextDocument>(&DidOpenTextDocumentParams {
text_document: TextDocumentItem::new(uri, language_id, version, initial_text),
})
.log_err();
} else {
debug_assert_eq!(previous_value, Some(uri));
}
}
pub fn unregister_buffer(&self, buffer_id: BufferId) {
if let Some(path) = self.registered_buffers.lock().remove(&buffer_id) {
self.notify::<notification::DidCloseTextDocument>(&DidCloseTextDocumentParams {
text_document: TextDocumentIdentifier::new(path),
})
.log_err();
}
}
}
impl Drop for LanguageServer {
@@ -1288,8 +1394,6 @@ impl FakeLanguageServer {
let (stdout_writer, stdout_reader) = async_pipe::pipe();
let (notifications_tx, notifications_rx) = channel::unbounded();
let root = Self::root_path();
let server_name = LanguageServerName(name.clone().into());
let process_name = Arc::from(name.as_str());
let mut server = LanguageServer::new_internal(
@@ -1300,8 +1404,6 @@ impl FakeLanguageServer {
None::<async_pipe::PipeReader>,
Arc::new(Mutex::new(None)),
None,
root,
root,
None,
binary.clone(),
cx.clone(),
@@ -1319,8 +1421,6 @@ impl FakeLanguageServer {
None::<async_pipe::PipeReader>,
Arc::new(Mutex::new(None)),
None,
root,
root,
None,
binary,
cx.clone(),
@@ -1357,16 +1457,6 @@ impl FakeLanguageServer {
(server, fake)
}
#[cfg(target_os = "windows")]
fn root_path() -> &'static Path {
Path::new("C:\\")
}
#[cfg(not(target_os = "windows"))]
fn root_path() -> &'static Path {
Path::new("/")
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -1554,12 +1644,14 @@ mod tests {
})
.detach();
let initialize_params = None;
let configuration = DidChangeConfigurationParams {
settings: Default::default(),
};
let server = cx
.update(|cx| server.initialize(initialize_params, configuration.into(), cx))
.update(|cx| {
let params = server.default_initialize_params(cx);
let configuration = DidChangeConfigurationParams {
settings: Default::default(),
};
server.initialize(params, configuration.into(), cx)
})
.await
.unwrap();
server

View File

@@ -72,14 +72,10 @@ pub enum Model {
FourOmni,
#[serde(rename = "gpt-4o-mini", alias = "gpt-4o-mini")]
FourOmniMini,
#[serde(rename = "o1", alias = "o1")]
#[serde(rename = "o1", alias = "o1-preview")]
O1,
#[serde(rename = "o1-preview", alias = "o1-preview")]
O1Preview,
#[serde(rename = "o1-mini", alias = "o1-mini")]
O1Mini,
#[serde(rename = "o3-mini", alias = "o3-mini")]
O3Mini,
#[serde(rename = "custom")]
Custom {
@@ -101,7 +97,6 @@ impl Model {
"gpt-4o" => Ok(Self::FourOmni),
"gpt-4o-mini" => Ok(Self::FourOmniMini),
"o1" => Ok(Self::O1),
"o1-preview" => Ok(Self::O1Preview),
"o1-mini" => Ok(Self::O1Mini),
_ => Err(anyhow!("invalid model id")),
}
@@ -115,9 +110,7 @@ impl Model {
Self::FourOmni => "gpt-4o",
Self::FourOmniMini => "gpt-4o-mini",
Self::O1 => "o1",
Self::O1Preview => "o1-preview",
Self::O1Mini => "o1-mini",
Self::O3Mini => "o3-mini",
Self::Custom { name, .. } => name,
}
}
@@ -130,9 +123,7 @@ impl Model {
Self::FourOmni => "gpt-4o",
Self::FourOmniMini => "gpt-4o-mini",
Self::O1 => "o1",
Self::O1Preview => "o1-preview",
Self::O1Mini => "o1-mini",
Self::O3Mini => "o3-mini",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
@@ -146,10 +137,8 @@ impl Model {
Self::FourTurbo => 128000,
Self::FourOmni => 128000,
Self::FourOmniMini => 128000,
Self::O1 => 200000,
Self::O1Preview => 128000,
Self::O1 => 128000,
Self::O1Mini => 128000,
Self::O3Mini => 200000,
Self::Custom { max_tokens, .. } => *max_tokens,
}
}

View File

@@ -283,13 +283,13 @@ impl Prettier {
)
.context("prettier server creation")?;
let initialize_params = None;
let configuration = lsp::DidChangeConfigurationParams {
settings: Default::default(),
};
let server = cx
.update(|cx| {
executor.spawn(server.initialize(initialize_params, configuration.into(), cx))
let params = server.default_initialize_params(cx);
let configuration = lsp::DidChangeConfigurationParams {
settings: Default::default(),
};
executor.spawn(server.initialize(params, configuration.into(), cx))
})?
.await
.context("prettier server initialization")?;

View File

@@ -43,6 +43,7 @@ log.workspace = true
lsp.workspace = true
node_runtime.workspace = true
image.workspace = true
once_cell.workspace = true
parking_lot.workspace = true
pathdiff.workspace = true
paths.workspace = true

View File

@@ -314,32 +314,28 @@ impl RepositoryHandle {
&& (commit_all || self.have_staged_changes());
}
pub fn commit(
&self,
err_sender: mpsc::Sender<anyhow::Error>,
cx: &mut AppContext,
) -> anyhow::Result<()> {
if !self.can_commit(false, cx) {
return Err(anyhow!("Unable to commit"));
}
pub fn commit(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut AppContext) {
let message = self.commit_message.read(cx).as_rope().clone();
self.update_sender
.unbounded_send((Message::Commit(self.git_repo.clone(), message), err_sender))
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
let result = self.update_sender.unbounded_send((
Message::Commit(self.git_repo.clone(), message),
err_sender.clone(),
));
if result.is_err() {
cx.spawn(|_| async move {
err_sender
.send(anyhow!("Failed to submit commit operation"))
.await
.ok();
})
.detach();
return;
}
self.commit_message.update(cx, |commit_message, cx| {
commit_message.set_text("", cx);
});
Ok(())
}
pub fn commit_all(
&self,
err_sender: mpsc::Sender<anyhow::Error>,
cx: &mut AppContext,
) -> anyhow::Result<()> {
if !self.can_commit(true, cx) {
return Err(anyhow!("Unable to commit"));
}
pub fn commit_all(&self, mut err_sender: mpsc::Sender<anyhow::Error>, cx: &mut AppContext) {
let to_stage = self
.repository_entry
.status()
@@ -347,15 +343,22 @@ impl RepositoryHandle {
.map(|entry| entry.repo_path.clone())
.collect::<Vec<_>>();
let message = self.commit_message.read(cx).as_rope().clone();
self.update_sender
.unbounded_send((
Message::StageAndCommit(self.git_repo.clone(), message, to_stage),
err_sender,
))
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
let result = self.update_sender.unbounded_send((
Message::StageAndCommit(self.git_repo.clone(), message, to_stage),
err_sender.clone(),
));
if result.is_err() {
cx.spawn(|_| async move {
err_sender
.send(anyhow!("Failed to submit commit all operation"))
.await
.ok();
})
.detach();
return;
}
self.commit_message.update(cx, |commit_message, cx| {
commit_message.set_text("", cx);
});
Ok(())
}
}

View File

@@ -942,9 +942,11 @@ fn language_server_for_buffer(
) -> Result<(Arc<CachedLspAdapter>, Arc<LanguageServer>)> {
lsp_store
.update(cx, |lsp_store, cx| {
lsp_store
.language_server_for_local_buffer(buffer.read(cx), server_id, cx)
.map(|(adapter, server)| (adapter.clone(), server.clone()))
buffer.update(cx, |buffer, cx| {
lsp_store
.language_server_for_local_buffer(buffer, server_id, cx)
.map(|(adapter, server)| (adapter.clone(), server.clone()))
})
})?
.ok_or_else(|| anyhow!("no language server found for buffer"))
}
@@ -2121,7 +2123,7 @@ pub(crate) fn parse_completion_text_edit(
}
lsp::CompletionTextEdit::InsertAndReplace(edit) => {
let range = range_from_lsp(edit.insert);
let range = range_from_lsp(edit.replace);
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,7 @@ pub struct PrettierStore {
prettier_instances: HashMap<PathBuf, PrettierInstance>,
}
pub enum PrettierStoreEvent {
pub(crate) enum PrettierStoreEvent {
LanguageServerRemoved(LanguageServerId),
LanguageServerAdded {
new_server_id: LanguageServerId,

View File

@@ -9,6 +9,7 @@ pub mod lsp_ext_command;
pub mod lsp_store;
pub mod prettier_store;
pub mod project_settings;
mod project_tree;
pub mod search;
mod task_inventory;
pub mod task_store;
@@ -474,6 +475,7 @@ pub struct DocumentHighlight {
pub struct Symbol {
pub language_server_name: LanguageServerName,
pub source_worktree_id: WorktreeId,
pub source_language_server_id: LanguageServerId,
pub path: ProjectPath,
pub label: CodeLabel,
pub name: String,
@@ -808,7 +810,6 @@ impl Project {
languages.clone(),
ssh_proto.clone(),
SSH_PROJECT_ID,
fs.clone(),
cx,
)
});
@@ -982,7 +983,6 @@ impl Project {
languages.clone(),
client.clone().into(),
remote_id,
fs.clone(),
cx,
);
lsp_store.set_language_server_statuses_from_proto(response.payload.language_servers);
@@ -1892,7 +1892,7 @@ impl Project {
pub fn open_buffer(
&mut self,
path: impl Into<ProjectPath>,
cx: &mut ModelContext<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<Buffer>>> {
if self.is_disconnected(cx) {
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
@@ -1907,11 +1907,11 @@ impl Project {
pub fn open_buffer_with_lsp(
&mut self,
path: impl Into<ProjectPath>,
cx: &mut ModelContext<Self>,
cx: &mut AppContext,
) -> Task<Result<(Model<Buffer>, lsp_store::OpenLspBufferHandle)>> {
let buffer = self.open_buffer(path, cx);
let lsp_store = self.lsp_store().clone();
cx.spawn(|_, mut cx| async move {
cx.spawn(|mut cx| async move {
let buffer = buffer.await?;
let handle = lsp_store.update(&mut cx, |lsp_store, cx| {
lsp_store.register_buffer_with_language_servers(&buffer, cx)
@@ -4147,14 +4147,25 @@ impl Project {
self.lsp_store.read(cx).supplementary_language_servers()
}
pub fn language_servers_for_local_buffer<'a>(
&'a self,
buffer: &'a Buffer,
cx: &'a AppContext,
) -> impl Iterator<Item = (&'a Arc<CachedLspAdapter>, &'a Arc<LanguageServer>)> {
self.lsp_store
.read(cx)
.language_servers_for_local_buffer(buffer, cx)
pub fn language_server_for_id(
&self,
id: LanguageServerId,
cx: &AppContext,
) -> Option<Arc<LanguageServer>> {
self.lsp_store.read(cx).language_server_for_id(id)
}
pub fn for_language_servers_for_local_buffer<R: 'static>(
&self,
buffer: &Buffer,
callback: impl FnOnce(
Box<dyn Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> + '_>,
) -> R,
cx: &mut AppContext,
) -> R {
self.lsp_store.update(cx, |this, cx| {
callback(Box::new(this.language_servers_for_local_buffer(buffer, cx)))
})
}
pub fn buffer_store(&self) -> &Model<BufferStore> {

View File

@@ -1749,6 +1749,12 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
});
})
});
let _rs_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
})
.await
.unwrap();
let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
assert_eq!(
fake_rust_server_2
@@ -2573,25 +2579,28 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
fs.insert_tree(
"/dir",
json!({
"a.rs": "const fn a() { A }",
"b.rs": "const y: i32 = crate::a()",
}),
)
.await;
fs.insert_tree(
"/another_dir",
json!({
"a.rs": "const fn a() { A }"}),
)
.await;
let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/b.rs", cx)
})
.await
.unwrap();
let fake_server = fake_servers.next().await.unwrap();
fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
let params = params.text_document_position_params;
@@ -2603,12 +2612,11 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path("/dir/a.rs").unwrap(),
lsp::Url::from_file_path("/another_dir/a.rs").unwrap(),
lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
),
)))
});
let mut definitions = project
.update(cx, |project, cx| project.definition(&buffer, 22, cx))
.await
@@ -2629,18 +2637,21 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
.as_local()
.unwrap()
.abs_path(cx),
Path::new("/dir/a.rs"),
Path::new("/another_dir/a.rs"),
);
assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
assert_eq!(
list_worktrees(&project, cx),
[("/dir/a.rs".as_ref(), false), ("/dir/b.rs".as_ref(), true)],
[
("/another_dir/a.rs".as_ref(), false),
("/dir".as_ref(), true)
],
);
drop(definition);
});
cx.update(|cx| {
assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
assert_eq!(list_worktrees(&project, cx), [("/dir".as_ref(), true)]);
});
fn list_worktrees<'a>(

View File

@@ -0,0 +1,243 @@
//! This module defines a Project Tree.
//!
//! A Project Tree is responsible for determining where the roots of subprojects are located in a project.
mod path_trie;
mod server_tree;
use std::{
borrow::Borrow,
collections::{hash_map::Entry, BTreeMap},
ops::ControlFlow,
sync::Arc,
};
use collections::HashMap;
use gpui::{AppContext, Context as _, EventEmitter, Model, ModelContext, Subscription};
use language::{CachedLspAdapter, LspAdapterDelegate};
use lsp::LanguageServerName;
use path_trie::{LabelPresence, RootPathTrie, TriePath};
use settings::{SettingsStore, WorktreeId};
use worktree::{Event as WorktreeEvent, Worktree};
use crate::{
worktree_store::{WorktreeStore, WorktreeStoreEvent},
ProjectPath,
};
pub(crate) use server_tree::{LanguageServerTree, LaunchDisposition};
struct WorktreeRoots {
roots: RootPathTrie<LanguageServerName>,
worktree_store: Model<WorktreeStore>,
_worktree_subscription: Subscription,
}
impl WorktreeRoots {
fn new(
worktree_store: Model<WorktreeStore>,
worktree: Model<Worktree>,
cx: &mut AppContext,
) -> Model<Self> {
cx.new_model(|cx| Self {
roots: RootPathTrie::new(),
worktree_store,
_worktree_subscription: cx.subscribe(&worktree, |this: &mut Self, _, event, cx| {
match event {
WorktreeEvent::UpdatedEntries(changes) => {
for (path, _, kind) in changes.iter() {
match kind {
worktree::PathChange::Removed => {
let path = TriePath::from(path.as_ref());
this.roots.remove(&path);
}
_ => {}
}
}
}
WorktreeEvent::UpdatedGitRepositories(_) => {}
WorktreeEvent::DeletedEntry(entry_id) => {
let Some(entry) = this.worktree_store.read(cx).entry_for_id(*entry_id, cx)
else {
return;
};
let path = TriePath::from(entry.path.as_ref());
this.roots.remove(&path);
}
}
}),
})
}
}
pub struct ProjectTree {
root_points: HashMap<WorktreeId, Model<WorktreeRoots>>,
worktree_store: Model<WorktreeStore>,
_subscriptions: [Subscription; 2],
}
#[derive(Debug, Clone)]
struct AdapterWrapper(Arc<CachedLspAdapter>);
impl PartialEq for AdapterWrapper {
fn eq(&self, other: &Self) -> bool {
self.0.name.eq(&other.0.name)
}
}
impl Eq for AdapterWrapper {}
impl std::hash::Hash for AdapterWrapper {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.name.hash(state);
}
}
impl PartialOrd for AdapterWrapper {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.0.name.cmp(&other.0.name))
}
}
impl Ord for AdapterWrapper {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.name.cmp(&other.0.name)
}
}
impl Borrow<LanguageServerName> for AdapterWrapper {
fn borrow(&self) -> &LanguageServerName {
&self.0.name
}
}
#[derive(PartialEq)]
pub(crate) enum ProjectTreeEvent {
WorktreeRemoved(WorktreeId),
Cleared,
}
impl EventEmitter<ProjectTreeEvent> for ProjectTree {}
impl ProjectTree {
pub(crate) fn new(worktree_store: Model<WorktreeStore>, cx: &mut AppContext) -> Model<Self> {
cx.new_model(|cx| Self {
root_points: Default::default(),
_subscriptions: [
cx.subscribe(&worktree_store, Self::on_worktree_store_event),
cx.observe_global::<SettingsStore>(|this, cx| {
for (_, roots) in &mut this.root_points {
roots.update(cx, |worktree_roots, _| {
worktree_roots.roots = RootPathTrie::new();
})
}
cx.emit(ProjectTreeEvent::Cleared);
}),
],
worktree_store,
})
}
#[allow(clippy::mutable_key_type)]
fn root_for_path(
&mut self,
ProjectPath { worktree_id, path }: ProjectPath,
adapters: Vec<Arc<CachedLspAdapter>>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AppContext,
) -> BTreeMap<AdapterWrapper, ProjectPath> {
debug_assert_eq!(delegate.worktree_id(), worktree_id);
#[allow(clippy::mutable_key_type)]
let mut roots = BTreeMap::from_iter(
adapters
.into_iter()
.map(|adapter| (AdapterWrapper(adapter), (None, LabelPresence::KnownAbsent))),
);
let worktree_roots = match self.root_points.entry(worktree_id) {
Entry::Occupied(occupied_entry) => occupied_entry.get().clone(),
Entry::Vacant(vacant_entry) => {
let Some(worktree) = self
.worktree_store
.read(cx)
.worktree_for_id(worktree_id, cx)
else {
return Default::default();
};
let roots = WorktreeRoots::new(self.worktree_store.clone(), worktree, cx);
vacant_entry.insert(roots).clone()
}
};
let key = TriePath::from(&*path);
worktree_roots.update(cx, |this, _| {
this.roots.walk(&key, &mut |path, labels| {
for (label, presence) in labels {
if let Some((marked_path, current_presence)) = roots.get_mut(label) {
if *current_presence > *presence {
debug_assert!(false, "RootPathTrie precondition violation; while walking the tree label presence is only allowed to increase");
}
*marked_path = Some(ProjectPath {worktree_id, path: path.clone()});
*current_presence = *presence;
}
}
ControlFlow::Continue(())
});
});
for (adapter, (root_path, presence)) in &mut roots {
if *presence == LabelPresence::Present {
continue;
}
let depth = root_path
.as_ref()
.map(|root_path| {
path.strip_prefix(&root_path.path)
.unwrap()
.components()
.count()
})
.unwrap_or_else(|| path.components().count() + 1);
if depth > 0 {
let root = adapter.0.find_project_root(&path, depth, &delegate);
match root {
Some(known_root) => worktree_roots.update(cx, |this, _| {
let root = TriePath::from(&*known_root);
this.roots
.insert(&root, adapter.0.name(), LabelPresence::Present);
*presence = LabelPresence::Present;
*root_path = Some(ProjectPath {
worktree_id,
path: known_root,
});
}),
None => worktree_roots.update(cx, |this, _| {
this.roots
.insert(&key, adapter.0.name(), LabelPresence::KnownAbsent);
}),
}
}
}
roots
.into_iter()
.filter_map(|(k, (path, presence))| {
let path = path?;
presence.eq(&LabelPresence::Present).then(|| (k, path))
})
.collect()
}
fn on_worktree_store_event(
&mut self,
_: Model<WorktreeStore>,
evt: &WorktreeStoreEvent,
cx: &mut ModelContext<Self>,
) {
match evt {
WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
self.root_points.remove(&worktree_id);
cx.emit(ProjectTreeEvent::WorktreeRemoved(*worktree_id));
}
_ => {}
}
}
}

View File

@@ -0,0 +1,241 @@
use std::{
collections::{btree_map::Entry, BTreeMap},
ffi::OsStr,
ops::ControlFlow,
path::{Path, PathBuf},
sync::Arc,
};
/// [RootPathTrie] is a workhorse of [super::ProjectTree]. It is responsible for determining the closest known project root for a given path.
/// It also determines how much of a given path is unexplored, thus letting callers fill in that gap if needed.
/// Conceptually, it allows one to annotate Worktree entries with arbitrary extra metadata and run closest-ancestor searches.
///
/// A path is unexplored when the closest ancestor of a path is not the path itself; that means that we have not yet ran the scan on that path.
/// For example, if there's a project root at path `python/project` and we query for a path `python/project/subdir/another_subdir/file.py`, there is
/// a known root at `python/project` and the unexplored part is `subdir/another_subdir` - we need to run a scan on these 2 directories.
pub(super) struct RootPathTrie<Label> {
worktree_relative_path: Arc<Path>,
labels: BTreeMap<Label, LabelPresence>,
children: BTreeMap<Arc<OsStr>, RootPathTrie<Label>>,
}
/// Label presence is a marker that allows to optimize searches within [RootPathTrie]; node label can be:
/// - Present; we know there's definitely a project root at this node and it is the only label of that kind on the path to the root of a worktree
/// (none of it's ancestors or descendants can contain the same present label)
/// - Known Absent - we know there's definitely no project root at this node and none of it's ancestors are Present (descendants can be present though!).
/// - Forbidden - we know there's definitely no project root at this node and none of it's ancestors or descendants can be Present.
/// The distinction is there to optimize searching; when we encounter a node with unknown status, we don't need to look at it's full path
/// to the root of the worktree; it's sufficient to explore only the path between last node with a KnownAbsent state and the directory of a path, since we run searches
/// from the leaf up to the root of the worktree. When any of the ancestors is forbidden, we don't need to look at the node or its ancestors.
/// When there's a present labeled node on the path to the root, we don't need to ask the adapter to run the search at all.
///
/// In practical terms, it means that by storing label presence we don't need to do a project discovery on a given folder more than once
/// (unless the node is invalidated, which can happen when FS entries are renamed/removed).
///
/// Storing project absence allows us to recognize which paths have already been scanned for a project root unsuccessfully. This way we don't need to run
/// such scan more than once.
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Ord, Eq)]
pub(super) enum LabelPresence {
KnownAbsent,
Present,
}
impl<Label: Ord + Clone> RootPathTrie<Label> {
pub(super) fn new() -> Self {
Self::new_with_key(Arc::from(Path::new("")))
}
fn new_with_key(worktree_relative_path: Arc<Path>) -> Self {
RootPathTrie {
worktree_relative_path,
labels: Default::default(),
children: Default::default(),
}
}
// Internal implementation of inner that allows one to visit descendants of insertion point for a node.
fn insert_inner(
&mut self,
path: &TriePath,
value: Label,
presence: LabelPresence,
) -> &mut Self {
let mut current = self;
let mut path_so_far = PathBuf::new();
for key in path.0.iter() {
path_so_far.push(Path::new(key));
current = match current.children.entry(key.clone()) {
Entry::Vacant(vacant_entry) => vacant_entry
.insert(RootPathTrie::new_with_key(Arc::from(path_so_far.as_path()))),
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
};
}
let _previous_value = current.labels.insert(value, presence);
debug_assert_eq!(_previous_value, None);
current
}
pub(super) fn insert(&mut self, path: &TriePath, value: Label, presence: LabelPresence) {
self.insert_inner(path, value, presence);
}
pub(super) fn walk<'a>(
&'a self,
path: &TriePath,
callback: &mut dyn for<'b> FnMut(
&'b Arc<Path>,
&'a BTreeMap<Label, LabelPresence>,
) -> ControlFlow<()>,
) {
let mut current = self;
for key in path.0.iter() {
if !current.labels.is_empty() {
if (callback)(&current.worktree_relative_path, &current.labels).is_break() {
return;
};
}
current = match current.children.get(key) {
Some(child) => child,
None => return,
};
}
if !current.labels.is_empty() {
(callback)(&current.worktree_relative_path, &current.labels);
}
}
pub(super) fn remove(&mut self, path: &TriePath) {
debug_assert_ne!(path.0.len(), 0);
let mut current = self;
for path in path.0.iter().take(path.0.len().saturating_sub(1)) {
current = match current.children.get_mut(path) {
Some(child) => child,
None => return,
};
}
if let Some(final_entry_name) = path.0.last() {
current.children.remove(final_entry_name);
}
}
}
/// [TriePath] is a [Path] preprocessed for amortizing the cost of doing multiple lookups in distinct [RootPathTrie]s.
#[derive(Clone)]
pub(super) struct TriePath(Arc<[Arc<OsStr>]>);
impl From<&Path> for TriePath {
fn from(value: &Path) -> Self {
TriePath(value.components().map(|c| c.as_os_str().into()).collect())
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use super::*;
#[test]
fn test_insert_and_lookup() {
let mut trie = RootPathTrie::<()>::new();
trie.insert(
&TriePath::from(Path::new("a/b/c")),
(),
LabelPresence::Present,
);
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
assert_eq!(path.as_ref(), Path::new("a/b/c"));
ControlFlow::Continue(())
});
// Now let's annotate a parent with "Known missing" node.
trie.insert(
&TriePath::from(Path::new("a")),
(),
LabelPresence::KnownAbsent,
);
// Ensure that we walk from the root to the leaf.
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
if path.as_ref() == Path::new("a/b/c") {
assert_eq!(
visited_paths,
BTreeSet::from_iter([Arc::from(Path::new("a/"))])
);
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
} else if path.as_ref() == Path::new("a/") {
assert!(visited_paths.is_empty());
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
} else {
panic!("Unknown path");
}
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Continue(())
});
// One can also pass a path whose prefix is in the tree, but not that path itself.
let mut visited_paths = BTreeSet::new();
trie.walk(
&TriePath::from(Path::new("a/b/c/d/e/f/g")),
&mut |path, nodes| {
if path.as_ref() == Path::new("a/b/c") {
assert_eq!(
visited_paths,
BTreeSet::from_iter([Arc::from(Path::new("a/"))])
);
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
} else if path.as_ref() == Path::new("a/") {
assert!(visited_paths.is_empty());
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
} else {
panic!("Unknown path");
}
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Continue(())
},
);
// Test breaking from the tree-walk.
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
if path.as_ref() == Path::new("a/") {
assert!(visited_paths.is_empty());
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
} else {
panic!("Unknown path");
}
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Break(())
});
assert_eq!(visited_paths.len(), 1);
// Entry removal.
trie.insert(
&TriePath::from(Path::new("a/b")),
(),
LabelPresence::KnownAbsent,
);
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, _nodes| {
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Continue(())
});
assert_eq!(visited_paths.len(), 3);
trie.remove(&TriePath::from(Path::new("a/b/")));
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, _nodes| {
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Continue(())
});
assert_eq!(visited_paths.len(), 1);
assert_eq!(
visited_paths.into_iter().next().unwrap().as_ref(),
Path::new("a/")
);
}
}

View File

@@ -0,0 +1,428 @@
//! This module defines an LSP Tree.
//!
//! An LSP Tree is responsible for determining which language servers apply to a given project path.
//!
//! ## RPC
//! LSP Tree is transparent to RPC peers; when clients ask host to spawn a new language server, the host will perform LSP Tree lookup for provided path; it may decide
//! to reuse existing language server. The client maintains it's own LSP Tree that is a subset of host LSP Tree. Done this way, the client does not need to
//! ask about suitable language server for each path it interacts with; it can resolve most of the queries locally.
//! This module defines a Project Tree.
use std::{
collections::{BTreeMap, BTreeSet},
path::Path,
sync::{Arc, Weak},
};
use collections::{HashMap, IndexMap};
use gpui::{AppContext, Context as _, Model, Subscription};
use language::{
language_settings::AllLanguageSettings, Attach, LanguageName, LanguageRegistry,
LspAdapterDelegate,
};
use lsp::LanguageServerName;
use once_cell::sync::OnceCell;
use settings::{Settings, SettingsLocation, WorktreeId};
use util::maybe;
use crate::{project_settings::LspSettings, LanguageServerId, ProjectPath};
use super::{AdapterWrapper, ProjectTree, ProjectTreeEvent};
#[derive(Debug, Default)]
struct ServersForWorktree {
roots: BTreeMap<
Arc<Path>,
BTreeMap<LanguageServerName, (Arc<InnerTreeNode>, BTreeSet<LanguageName>)>,
>,
}
pub struct LanguageServerTree {
project_tree: Model<ProjectTree>,
instances: BTreeMap<WorktreeId, ServersForWorktree>,
attach_kind_cache: HashMap<LanguageServerName, Attach>,
languages: Arc<LanguageRegistry>,
_subscriptions: Subscription,
}
/// A node in language server tree represents either:
/// - A language server that has already been initialized/updated for a given project
/// - A soon-to-be-initialized language server.
#[derive(Clone)]
pub(crate) struct LanguageServerTreeNode(Weak<InnerTreeNode>);
/// Describes a request to launch a language server.
#[derive(Debug)]
pub(crate) struct LaunchDisposition<'a> {
pub(crate) server_name: &'a LanguageServerName,
pub(crate) attach: Attach,
pub(crate) path: ProjectPath,
pub(crate) settings: Arc<LspSettings>,
}
impl<'a> From<&'a InnerTreeNode> for LaunchDisposition<'a> {
fn from(value: &'a InnerTreeNode) -> Self {
LaunchDisposition {
server_name: &value.name,
attach: value.attach,
path: value.path.clone(),
settings: value.settings.clone(),
}
}
}
impl LanguageServerTreeNode {
/// Returns a language server ID for this node if there is one.
/// Returns None if this node has not been initialized yet or it is no longer in the tree.
pub(crate) fn server_id(&self) -> Option<LanguageServerId> {
self.0.upgrade()?.id.get().copied()
}
/// Returns a language server ID for this node if it has already been initialized; otherwise runs the provided closure to initialize the language server node in a tree.
/// May return None if the node no longer belongs to the server tree it was created in.
pub(crate) fn server_id_or_init(
&self,
init: impl FnOnce(LaunchDisposition) -> LanguageServerId,
) -> Option<LanguageServerId> {
self.server_id_or_try_init(|disposition| Ok(init(disposition)))
}
fn server_id_or_try_init(
&self,
init: impl FnOnce(LaunchDisposition) -> Result<LanguageServerId, ()>,
) -> Option<LanguageServerId> {
let this = self.0.upgrade()?;
this.id
.get_or_try_init(|| init(LaunchDisposition::from(&*this)))
.ok()
.copied()
}
}
impl From<Weak<InnerTreeNode>> for LanguageServerTreeNode {
fn from(weak: Weak<InnerTreeNode>) -> Self {
LanguageServerTreeNode(weak)
}
}
#[derive(Debug)]
struct InnerTreeNode {
id: OnceCell<LanguageServerId>,
name: LanguageServerName,
attach: Attach,
path: ProjectPath,
settings: Arc<LspSettings>,
}
impl InnerTreeNode {
fn new(
name: LanguageServerName,
attach: Attach,
path: ProjectPath,
settings: impl Into<Arc<LspSettings>>,
) -> Self {
InnerTreeNode {
id: Default::default(),
name,
attach,
path,
settings: settings.into(),
}
}
}
impl LanguageServerTree {
pub(crate) fn new(
project_tree: Model<ProjectTree>,
languages: Arc<LanguageRegistry>,
cx: &mut AppContext,
) -> Model<Self> {
cx.new_model(|cx| Self {
_subscriptions: cx.subscribe(
&project_tree,
|_: &mut Self, _, event, _| {
if event == &ProjectTreeEvent::Cleared {}
},
),
project_tree,
instances: Default::default(),
attach_kind_cache: Default::default(),
languages,
})
}
/// Memoize calls to attach_kind on LspAdapter (which might be a WASM extension, thus ~expensive to call).
fn attach_kind(&mut self, adapter: &AdapterWrapper) -> Attach {
*self
.attach_kind_cache
.entry(adapter.0.name.clone())
.or_insert_with(|| adapter.0.attach_kind())
}
/// Get all language server root points for a given path and language; the language servers might already be initialized at a given path.
pub(crate) fn get<'a>(
&'a mut self,
path: ProjectPath,
language_name: &LanguageName,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AppContext,
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
let settings_location = SettingsLocation {
worktree_id: path.worktree_id,
path: &path.path,
};
let adapters = self.adapters_for_language(settings_location, language_name, cx);
self.get_with_adapters(path, adapters, delegate, cx)
}
fn get_with_adapters<'a>(
&'a mut self,
path: ProjectPath,
adapters: IndexMap<AdapterWrapper, (LspSettings, BTreeSet<LanguageName>)>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AppContext,
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
let worktree_id = path.worktree_id;
#[allow(clippy::mutable_key_type)]
let mut roots = self.project_tree.update(cx, |this, cx| {
this.root_for_path(
path,
adapters
.iter()
.map(|(adapter, _)| adapter.0.clone())
.collect(),
delegate,
cx,
)
});
let mut root_path = None;
// Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree.
for (adapter, _) in adapters.iter() {
roots.entry(adapter.clone()).or_insert_with(|| {
root_path
.get_or_insert_with(|| ProjectPath {
worktree_id,
path: Arc::from("".as_ref()),
})
.clone()
});
}
roots.into_iter().filter_map(move |(adapter, root_path)| {
let attach = self.attach_kind(&adapter);
let (settings, new_languages) = adapters.get(&adapter).cloned()?;
let inner_node = self
.instances
.entry(root_path.worktree_id)
.or_default()
.roots
.entry(root_path.path.clone())
.or_default()
.entry(adapter.0.name.clone());
let (node, languages) = inner_node.or_insert_with(move || {
(
Arc::new(InnerTreeNode::new(
adapter.0.name(),
attach,
root_path,
settings,
)),
Default::default(),
)
});
languages.extend(new_languages);
Some(Arc::downgrade(&node).into())
})
}
fn adapters_for_language(
&self,
settings_location: SettingsLocation,
language_name: &LanguageName,
cx: &AppContext,
) -> IndexMap<AdapterWrapper, (LspSettings, BTreeSet<LanguageName>)> {
let settings = AllLanguageSettings::get(Some(settings_location), cx).language(
Some(settings_location),
Some(language_name),
cx,
);
if !settings.enable_language_server {
return Default::default();
}
let available_lsp_adapters = self.languages.lsp_adapters(&language_name);
let available_language_servers = available_lsp_adapters
.iter()
.map(|lsp_adapter| lsp_adapter.name.clone())
.collect::<Vec<_>>();
let desired_language_servers =
settings.customized_language_servers(&available_language_servers);
let adapters_with_settings = desired_language_servers
.into_iter()
.filter_map(|desired_adapter| {
let adapter = if let Some(adapter) = available_lsp_adapters
.iter()
.find(|adapter| adapter.name == desired_adapter)
{
Some(adapter.clone())
} else if let Some(adapter) =
self.languages.load_available_lsp_adapter(&desired_adapter)
{
self.languages
.register_lsp_adapter(language_name.clone(), adapter.adapter.clone());
Some(adapter)
} else {
None
}?;
let adapter_settings = crate::lsp_store::language_server_settings_for(
settings_location,
&adapter.name,
cx,
)
.cloned()
.unwrap_or_default();
Some((
AdapterWrapper(adapter),
(
adapter_settings,
BTreeSet::from_iter([language_name.clone()]),
),
))
})
.collect::<IndexMap<_, _>>();
adapters_with_settings
}
pub(crate) fn on_settings_changed(
&mut self,
get_delegate: &mut dyn FnMut(
WorktreeId,
&mut AppContext,
) -> Option<Arc<dyn LspAdapterDelegate>>,
spawn_language_server: &mut dyn FnMut(
LaunchDisposition,
&mut AppContext,
) -> LanguageServerId,
on_language_server_removed: &mut dyn FnMut(LanguageServerId),
cx: &mut AppContext,
) {
// Settings are checked at query time. Thus, to avoid messing with inference of applicable settings, we're just going to clear ourselves and let the next query repopulate.
// We're going to optimistically re-run the queries and re-assign the same language server id when a language server still exists at a given tree node.
let old_instances = std::mem::take(&mut self.instances);
let old_attach_kinds = std::mem::take(&mut self.attach_kind_cache);
let mut referenced_instances = BTreeSet::new();
// Re-map the old tree onto a new one. In the process we'll get a list of servers we have to shut down.
let mut all_instances = BTreeSet::new();
for (worktree_id, servers) in &old_instances {
// Record all initialized node ids.
all_instances.extend(servers.roots.values().flat_map(|servers_at_node| {
servers_at_node
.values()
.filter_map(|(server_node, _)| server_node.id.get().copied())
}));
let Some(delegate) = get_delegate(*worktree_id, cx) else {
// If worktree is no longer around, we're just going to shut down all of the language servers (since they've been added to all_instances).
continue;
};
for (path, servers_for_path) in &servers.roots {
for (server_name, (_, languages)) in servers_for_path {
let settings_location = SettingsLocation {
worktree_id: *worktree_id,
path: &path,
};
// Verify which of the previous languages still have this server enabled.
let mut adapter_with_settings = IndexMap::default();
for language_name in languages {
self.adapters_for_language(settings_location, language_name, cx)
.into_iter()
.for_each(|(lsp_adapter, lsp_settings)| {
if &lsp_adapter.0.name() != server_name {
return;
}
adapter_with_settings
.entry(lsp_adapter)
.and_modify(|x: &mut (_, BTreeSet<LanguageName>)| {
x.1.extend(lsp_settings.1.clone())
})
.or_insert(lsp_settings);
});
}
if adapter_with_settings.is_empty() {
// Since all languages that have had this server enabled are now disabled, we can remove the server entirely.
continue;
};
for new_node in self.get_with_adapters(
ProjectPath {
path: path.clone(),
worktree_id: *worktree_id,
},
adapter_with_settings,
delegate.clone(),
cx,
) {
new_node.server_id_or_try_init(|disposition| {
let Some((existing_node, _)) = servers
.roots
.get(&disposition.path.path)
.and_then(|roots| roots.get(disposition.server_name))
.filter(|(old_node, _)| {
old_attach_kinds.get(disposition.server_name).map_or(
false,
|old_attach| {
disposition.attach == *old_attach
&& disposition.settings == old_node.settings
},
)
})
else {
return Ok(spawn_language_server(disposition, cx));
};
if let Some(id) = existing_node.id.get().copied() {
// If we have a node with ID assigned (and it's parameters match `disposition`), reuse the id.
referenced_instances.insert(id);
Ok(id)
} else {
// Otherwise, if we do have a node but it does not have an ID assigned, keep it that way.
Err(())
}
});
}
}
}
}
for server_to_remove in all_instances.difference(&referenced_instances) {
on_language_server_removed(*server_to_remove);
}
}
/// Updates nodes in language server tree in place, changing the ID of initialized nodes.
pub(crate) fn restart_language_servers(
&mut self,
worktree_id: WorktreeId,
ids: BTreeSet<LanguageServerId>,
restart_callback: &mut dyn FnMut(LanguageServerId, LaunchDisposition) -> LanguageServerId,
) {
maybe! {{
for (_, nodes) in &mut self.instances.get_mut(&worktree_id)?.roots {
for (_, (node, _)) in nodes {
let Some(old_server_id) = node.id.get().copied() else {
continue;
};
if !ids.contains(&old_server_id) {
continue;
}
let new_id = restart_callback(old_server_id, LaunchDisposition::from(&**node));
*node = Arc::new(InnerTreeNode::new(node.name.clone(), node.attach, node.path.clone(), node.settings.clone()));
node.id.set(new_id).expect("The id to be unset after clearing the node.");
}
}
Some(())
}
};
}
}

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use assets::Assets;
use fs::Fs;
use futures::StreamExt;
use gpui::AssetSource;
use gpui::{AppContext, AssetSource};
use handlebars::{Handlebars, RenderError};
use language::{BufferSnapshot, LanguageName, Point};
use parking_lot::Mutex;
@@ -56,6 +56,19 @@ pub struct PromptBuilder {
}
impl PromptBuilder {
pub fn load(fs: Arc<dyn Fs>, stdout_is_a_pty: bool, cx: &mut AppContext) -> Arc<Self> {
Self::new(Some(PromptLoadingParams {
fs: fs.clone(),
repo_path: stdout_is_a_pty
.then(|| std::env::current_dir().log_err())
.flatten(),
cx,
}))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(Self::new(None).unwrap()))
}
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
let mut handlebars = Handlebars::new();
Self::register_built_in_templates(&mut handlebars)?;

View File

@@ -765,6 +765,7 @@ message Symbol {
PointUtf16 start = 7;
PointUtf16 end = 8;
bytes signature = 9;
uint64 language_server_id = 10;
}
message OpenBufferForSymbol {

View File

@@ -16,4 +16,4 @@ doctest = false
[dependencies]
syn = "1.0.72"
quote = "1.0.9"
proc-macro2 = "1.0.66"
proc-macro2 = "1.0.93"

View File

@@ -35,10 +35,10 @@ use workspace::{
item::SerializableItem,
move_active_item, move_item, pane,
ui::IconName,
ActivateNextPane, ActivatePane, ActivatePaneInDirection, ActivatePreviousPane,
DraggedSelection, DraggedTab, ItemId, MoveItemToPane, MoveItemToPaneInDirection, NewTerminal,
Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp,
SwapPaneInDirection, ToggleZoom, Workspace,
ActivateNextPane, ActivatePane, ActivatePaneInDirection, ActivatePreviousPane, DraggedTab,
ItemId, MoveItemToPane, MoveItemToPaneInDirection, NewTerminal, Pane, PaneGroup,
SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp, SwapPaneInDirection, ToggleZoom,
Workspace,
};
use anyhow::{anyhow, Context, Result};
@@ -1000,17 +1000,6 @@ pub fn new_terminal_pane(
}
}
}
} else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>() {
let project = project.read(cx);
let paths_to_add = selection
.items()
.map(|selected_entry| selected_entry.entry_id)
.filter_map(|entry_id| project.path_for_entry(entry_id, cx))
.filter_map(|project_path| project.absolute_path(&project_path, cx))
.collect::<Vec<_>>();
if !paths_to_add.is_empty() {
add_paths_to_terminal(pane, &paths_to_add, cx);
}
} else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
if let Some(entry_path) = project
.read(cx)

View File

@@ -1094,6 +1094,7 @@ impl Render for TerminalView {
let focused = self.focus_handle.is_focused(cx);
div()
.occlude()
.id("terminal-view")
.size_full()
.relative()

View File

@@ -15,7 +15,7 @@ use crate::platforms::{platform_linux, platform_mac, platform_windows};
use auto_update::AutoUpdateStatus;
use call::ActiveCall;
use client::{Client, UserStore};
use feature_flags::{FeatureFlagAppExt, ZedPro};
use feature_flags::{FeatureFlagAppExt, GitUiFeatureFlag, ZedPro};
use git_ui::repository_selector::RepositorySelector;
use git_ui::repository_selector::RepositorySelectorPopoverMenu;
use gpui::{
@@ -27,6 +27,7 @@ use project::Project;
use rpc::proto;
use settings::Settings as _;
use smallvec::SmallVec;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use theme::ActiveTheme;
use ui::{
@@ -108,7 +109,7 @@ pub struct TitleBar {
should_move: bool,
application_menu: Option<View<ApplicationMenu>>,
_subscriptions: Vec<Subscription>,
git_ui_enabled: bool,
git_ui_enabled: Arc<AtomicBool>,
}
impl Render for TitleBar {
@@ -290,7 +291,15 @@ impl TitleBar {
subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
let title_bar = Self {
let is_git_ui_enabled = Arc::new(AtomicBool::new(false));
subscriptions.push(cx.observe_flag::<GitUiFeatureFlag, _>({
let is_git_ui_enabled = is_git_ui_enabled.clone();
move |enabled, _cx| {
is_git_ui_enabled.store(enabled, Ordering::SeqCst);
}
}));
Self {
platform_style,
content: div().id(id.into()),
children: SmallVec::new(),
@@ -302,29 +311,8 @@ impl TitleBar {
user_store,
client,
_subscriptions: subscriptions,
git_ui_enabled: false,
};
title_bar.check_git_ui_enabled(cx);
title_bar
}
fn check_git_ui_enabled(&self, cx: &mut ViewContext<Self>) {
let git_ui_feature_flag = cx.wait_for_flag::<feature_flags::GitUiFeatureFlag>();
let weak_self = cx.view().downgrade();
cx.spawn(|_, mut cx| async move {
let enabled = git_ui::git_ui_enabled(git_ui_feature_flag).await;
if let Some(this) = weak_self.upgrade() {
this.update(&mut cx, |this, cx| {
this.git_ui_enabled = enabled;
cx.notify();
})
.ok();
}
})
.detach();
git_ui_enabled: is_git_ui_enabled,
}
}
#[cfg(not(target_os = "windows"))]
@@ -507,7 +495,7 @@ impl TitleBar {
&self,
cx: &mut ViewContext<Self>,
) -> Option<impl IntoElement> {
if !self.git_ui_enabled {
if !self.git_ui_enabled.load(Ordering::SeqCst) {
return None;
}

View File

@@ -128,7 +128,6 @@ pub enum IconName {
Ai,
AiAnthropic,
AiAnthropicHosted,
AiDeepSeek,
AiGoogle,
AiLmStudio,
AiOllama,

View File

@@ -13,7 +13,7 @@ path = "src/ui_macros.rs"
proc-macro = true
[dependencies]
proc-macro2 = "1.0.66"
proc-macro2 = "1.0.93"
quote = "1.0.9"
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
convert_case.workspace = true

View File

@@ -8,8 +8,10 @@ use editor::{
Bias, Editor, ToPoint,
};
use gpui::{
actions, impl_internal_actions, Action, AppContext, Global, ViewContext, WindowContext,
actions, impl_internal_actions, Action, AppContext, Global, Keystroke, ViewContext,
WindowContext,
};
use itertools::Itertools;
use language::Point;
use multi_buffer::MultiBufferRow;
use regex::Regex;
@@ -78,6 +80,7 @@ impl_internal_actions!(
WithRange,
WithCount,
OnMatchingLines,
NormalCommand,
ShellExec
]
);
@@ -238,6 +241,10 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, action: &ShellExec, cx| {
action.run(vim, cx)
});
Vim::action(editor, cx, |vim, action: &NormalCommand, cx| {
action.run(vim, cx)
})
}
@@ -846,6 +853,20 @@ pub fn command_interceptor(mut input: &str, cx: &AppContext) -> Option<CommandIn
} else {
None
}
} else if query.starts_with("norm") {
let mut normal = "normal".chars().peekable();
let mut query = query.chars().peekable();
while normal.peek().is_some_and(|char| Some(char) == query.peek()) {
normal.next();
query.next();
}
if query.next() == Some(' ') {
let remainder = query.collect::<String>();
NormalCommand::parse(&remainder, range.clone())
} else {
None
}
} else if query.contains('!') {
ShellExec::parse(query, range.clone())
} else {
@@ -1072,14 +1093,37 @@ impl OnMatchingLines {
editor.change_selections(None, cx, |s| {
s.replace_cursors_with(|_| new_selections);
});
cx.dispatch_action(action);
cx.defer(move |editor, cx| {
let newest = editor.selections.newest::<Point>(cx).clone();
editor.change_selections(None, cx, |s| {
s.select(vec![newest]);
if let Some(NormalCommand { keystrokes, .. }) =
action.as_any().downcast_ref::<NormalCommand>()
{
let Some(workspace) = editor.workspace() else {
return;
};
let task = workspace.update(cx, |workspace, cx| {
workspace.send_keystrokes_impl(keystrokes.clone(), cx)
});
editor.end_transaction_at(Instant::now(), cx);
})
cx.spawn(|editor, mut cx| async move {
task.await?;
editor.update(&mut cx, move |editor, cx| {
let newest = editor.selections.newest::<Point>(cx).clone();
editor.change_selections(None, cx, |s| {
s.select(vec![newest]);
});
editor.end_transaction_at(Instant::now(), cx);
})
})
.detach_and_log_err(cx);
} else {
cx.dispatch_action(action);
cx.defer(move |editor, cx| {
let newest = editor.selections.newest::<Point>(cx).clone();
editor.change_selections(None, cx, |s| {
s.select(vec![newest]);
});
editor.end_transaction_at(Instant::now(), cx);
})
}
})
.ok();
})
@@ -1088,6 +1132,111 @@ impl OnMatchingLines {
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct NormalCommand {
range: Option<CommandRange>,
keystrokes: Vec<Keystroke>,
}
fn preprocess_keystroke(input: &str) -> String {
let parts = input.split("-").collect::<Vec<&str>>();
parts
.into_iter()
.enumerate()
.map(|(i, part)| {
if i + 1 < parts.len() {
match &part.to_ascii_lowercase() {
"s" => "shift",
"c" => "ctrl",
"d" => "command",
"a" | "m" | "t" => "alt",
part => part,
}
} else {
match &part.to_ascii_lowercase() {
"bs" => "backspace",
"lt" => "<",
"bar" => "|",
"bslash" => "\\",
"cr" | "return" => "enter",
"esc" => "escape",
part => part,
}
}
})
.join("-")
}
impl NormalCommand {
fn parse(query: &str, range: Option<CommandRange>) -> Option<Box<dyn Action>> {
let mut keystrokes: Vec<Keystroke> = Vec::default();
let mut chars = query.chars();
let mut in_angled = false;
let mut angled = "".to_string();
while let Some(char) = chars.next() {
if in_angled {
if char == '>' {
keystrokes.push(Keystroke::parse(&preprocess_keystrke(&angled)).log_err()?);
in_angled = false;
} else {
angled.push(char);
}
} else if char.to_ascii_lowercase() != char {
keystrokes.push(
Keystroke::parse(&format!("shift-{}", char.to_ascii_lowercase())).log_err()?,
)
} else if char == '<' {
in_angled = true;
angled = "".to_string();
} else {
keystrokes.push(Keystroke::parse(&format!("{}", char)).log_err()?)
}
}
if in_angled {
return None;
}
let keystrokes: Result<Vec<Keystroke>> = query
.chars()
.map(|char| {
let input = if char.to_ascii_lowercase() != char {
format!("shift-{}", char.to_ascii_lowercase())
} else {
char.to_string()
};
Ok(Keystroke::parse(&input)?)
})
.collect();
if let Ok(keystrokes) = keystrokes {
Some(Self { range, keystrokes }.boxed_clone())
// let zed_keystrokes = keystrokes.into_iter().map(|k| k.unparse()).join(" ");
// let mut action = workspace::SendKeystrokes(zed_keystrokes).boxed_clone();
// if let Some(range) = range.clone() {
// action = WithRange {
// restore_selection: false,
// range,
// action: WrappedAction(action),
// }
// .boxed_clone()
// }
// Some(action)
} else {
None
}
}
pub fn run(&self, vim: &mut Vim, cx: &mut ViewContext<Vim>) {
let zed_keystrokes = self.keystrokes.iter().map(|k| k.unparse()).join(" ");
let mut action = workspace::SendKeystrokes(zed_keystrokes).boxed_clone();
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShellExec {
command: String,
@@ -1101,7 +1250,8 @@ impl Vim {
self.update_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, _| {
editor.clear_row_highlights::<ShellExec>();
})
});
editor.workspace()
});
}
}

View File

@@ -1653,7 +1653,6 @@ pub(crate) fn next_subword_end(
if need_backtrack {
*new_point.column_mut() -= 1;
}
let new_point = map.clip_point(new_point, Bias::Left);
if point == new_point {
break;
}

View File

@@ -1256,10 +1256,7 @@ impl Workspace {
.unwrap_or_default();
window
.update(&mut cx, |_, cx| {
cx.activate_window();
cx.activate(true);
})
.update(&mut cx, |_, cx| cx.activate_window())
.log_err();
Ok((window, opened_items))
})
@@ -1849,49 +1846,59 @@ impl Workspace {
}
fn send_keystrokes(&mut self, action: &SendKeystrokes, cx: &mut ViewContext<Self>) {
let mut state = self.dispatching_keystrokes.borrow_mut();
if !state.0.insert(action.0.clone()) {
cx.propagate();
return;
}
let mut keystrokes: Vec<Keystroke> = action
let keystrokes: Vec<Keystroke> = action
.0
.split(' ')
.flat_map(|k| Keystroke::parse(k).log_err())
.collect();
self.send_keystrokes_impl(keystrokes, cx)
.detach_and_log_err(cx);
}
pub fn send_keystrokes_impl(
&mut self,
mut keystrokes: Vec<Keystroke>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
keystrokes.reverse();
let key = keystrokes.iter().map(|k| k.unparse()).join(" ");
let mut state = self.dispatching_keystrokes.borrow_mut();
if !state.0.insert(key) {
cx.propagate();
return Task::ready(Ok(()));
}
state.1.append(&mut keystrokes);
drop(state);
let keystrokes = self.dispatching_keystrokes.clone();
cx.window_context()
.spawn(|mut cx| async move {
// limit to 100 keystrokes to avoid infinite recursion.
for _ in 0..100 {
let Some(keystroke) = keystrokes.borrow_mut().1.pop() else {
keystrokes.borrow_mut().0.clear();
return Ok(());
};
cx.update(|cx| {
let focused = cx.focused();
cx.dispatch_keystroke(keystroke.clone());
if cx.focused() != focused {
// dispatch_keystroke may cause the focus to change.
// draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
// And we need that to happen before the next keystroke to keep vim mode happy...
// (Note that the tests always do this implicitly, so you must manually test with something like:
// "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
// )
cx.draw();
}
})?;
}
cx.window_context().spawn(|mut cx| async move {
// limit to 100 keystrokes to avoid infinite recursion.
for _ in 0..100 {
let Some(keystroke) = keystrokes.borrow_mut().1.pop() else {
keystrokes.borrow_mut().0.clear();
return Ok(());
};
cx.update(|cx| {
let focused = cx.focused();
cx.dispatch_keystroke(keystroke.clone());
if cx.focused() != focused {
// dispatch_keystroke may cause the focus to change.
// draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
// And we need that to happen before the next keystroke to keep vim mode happy...
// (Note that the tests always do this implicitly, so you must manually test with something like:
// "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
// )
cx.draw();
}
})?;
}
*keystrokes.borrow_mut() = Default::default();
Err(anyhow!("over 100 keystrokes passed to send_keystrokes"))
})
.detach_and_log_err(cx);
*keystrokes.borrow_mut() = Default::default();
Err(anyhow!("over 100 keystrokes passed to send_keystrokes"))
})
}
fn save_all_internal(

View File

@@ -5725,8 +5725,7 @@ impl<'a> GitTraversal<'a> {
} else if entry.is_file() {
// For a file entry, park the cursor on the corresponding status
if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &()) {
// TODO: Investigate statuses.item() being None here.
self.current_entry_summary = statuses.item().map(|item| item.status.into());
self.current_entry_summary = Some(statuses.item().unwrap().status.into());
} else {
self.current_entry_summary = Some(GitSummary::UNCHANGED);
}

View File

@@ -2639,6 +2639,62 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_git_status_postprocessing(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
let root = temp_tree(json!({
"project": {
"sub": {},
"a.txt": "",
},
}));
let work_dir = root.path().join("project");
let repo = git_init(work_dir.as_path());
// a.txt exists in HEAD and the working copy but is deleted in the index.
git_add("a.txt", &repo);
git_commit("Initial commit", &repo);
git_remove_index("a.txt".as_ref(), &repo);
// `sub` is a nested git repository.
let _sub = git_init(&work_dir.join("sub"));
let tree = Worktree::local(
root.path(),
true,
Arc::new(RealFs::default()),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
tree.flush_fs_events(cx).await;
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.executor().run_until_parked();
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
let repo = snapshot.repositories().iter().next().unwrap();
let entries = repo.status().collect::<Vec<_>>();
// `sub` doesn't appear in our computed statuses.
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
// a.txt appears with a combined `DA` status.
assert_eq!(
entries[0].status,
TrackedStatus {
index_status: StatusCode::Deleted,
worktree_status: StatusCode::Added
}
.into()
);
});
}
#[gpui::test]
async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
init_test(cx);

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.171.5"
version = "0.172.0"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
stable
dev

View File

@@ -25,6 +25,7 @@ use gpui::{
use http_client::{read_proxy_from_env, Uri};
use language::LanguageRegistry;
use log::LevelFilter;
use prompt_library::PromptBuilder;
use reqwest_client::ReqwestClient;
use assets::Assets;
@@ -443,16 +444,17 @@ fn main() {
app_state.user_store.clone(),
cx,
);
let prompt_builder = assistant::init(
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx);
assistant::init(
app_state.fs.clone(),
app_state.client.clone(),
stdout_is_a_pty(),
prompt_builder.clone(),
cx,
);
assistant2::init(
app_state.fs.clone(),
app_state.client.clone(),
stdout_is_a_pty(),
prompt_builder.clone(),
cx,
);
assistant_tools::init(cx);

View File

@@ -389,7 +389,16 @@ fn initialize_panels(prompt_builder: Arc<PromptBuilder>, cx: &mut ViewContext<Wo
workspace.add_panel(notification_panel, cx);
})?;
let git_ui_enabled = git_ui::git_ui_enabled(git_ui_feature_flag).await;
let git_ui_enabled = {
let mut git_ui_feature_flag = git_ui_feature_flag.fuse();
let mut timeout =
FutureExt::fuse(smol::Timer::after(std::time::Duration::from_secs(5)));
select_biased! {
is_git_ui_enabled = git_ui_feature_flag => is_git_ui_enabled,
_ = timeout => false,
}
};
let git_panel = if git_ui_enabled {
Some(git_ui::git_panel::GitPanel::load(workspace_handle.clone(), cx.clone()).await?)
@@ -3824,8 +3833,13 @@ mod tests {
app_state.fs.clone(),
cx,
);
let prompt_builder =
assistant::init(app_state.fs.clone(), app_state.client.clone(), false, cx);
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
assistant::init(
app_state.fs.clone(),
app_state.client.clone(),
prompt_builder.clone(),
cx,
);
repl::init(app_state.fs.clone(), cx);
repl::notebook::init(cx);
tasks_ui::init(cx);

View File

@@ -95,26 +95,17 @@ impl Render for QuickActionBar {
show_git_blame_gutter,
auto_signature_help_enabled,
inline_completions_enabled,
) = {
let editor = editor.read(cx);
let selection_menu_enabled = editor.selection_menu_enabled(cx);
let inlay_hints_enabled = editor.inlay_hints_enabled();
let supports_inlay_hints = editor.supports_inlay_hints(cx);
let git_blame_inline_enabled = editor.git_blame_inline_enabled();
let show_git_blame_gutter = editor.show_git_blame_gutter();
let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
let inline_completions_enabled = editor.inline_completions_enabled(cx);
) = editor.update(cx, |editor, cx| {
(
selection_menu_enabled,
inlay_hints_enabled,
supports_inlay_hints,
git_blame_inline_enabled,
show_git_blame_gutter,
auto_signature_help_enabled,
inline_completions_enabled,
editor.selection_menu_enabled(cx),
editor.inlay_hints_enabled(),
editor.supports_inlay_hints(cx),
editor.git_blame_inline_enabled(),
editor.show_git_blame_gutter(),
editor.auto_signature_help_enabled(cx),
editor.inline_completions_enabled(cx),
)
};
});
let focus_handle = editor.read(cx).focus_handle(cx);
@@ -450,16 +441,19 @@ impl ToolbarItemView for QuickActionBar {
if let Some(editor) = active_item.downcast::<Editor>() {
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
let mut supports_inlay_hints =
editor.update(cx, |this, cx| this.supports_inlay_hints(cx));
self._inlay_hints_enabled_subscription =
Some(cx.observe(&editor, move |_, editor, cx| {
let editor = editor.read(cx);
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
|| supports_inlay_hints != new_supports_inlay_hints;
inlay_hints_enabled = new_inlay_hints_enabled;
supports_inlay_hints = new_supports_inlay_hints;
let mut should_notify = false;
editor.update(cx, |editor, cx| {
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
should_notify = inlay_hints_enabled != new_inlay_hints_enabled
|| supports_inlay_hints != new_supports_inlay_hints;
inlay_hints_enabled = new_inlay_hints_enabled;
supports_inlay_hints = new_supports_inlay_hints;
});
if should_notify {
cx.notify()
}

View File

@@ -82,7 +82,7 @@ pub mod assistant {
use schemars::JsonSchema;
use serde::Deserialize;
actions!(assistant, [ToggleFocus]);
actions!(assistant, [ToggleFocus, DeployPromptLibrary]);
#[derive(Clone, Default, Deserialize, PartialEq, JsonSchema)]
pub struct InlineAssist {

View File

@@ -382,16 +382,11 @@ There are two options to choose from:
- Default:
```json
"inline_completions": {
"disabled_globs": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
]
}
"inline_completions": {
"disabled_globs": [
".env"
]
}
```
**Options**

View File

@@ -115,7 +115,7 @@ If you are using Mesa, and want more control over which GPU is selected you can
If you are using `amdvlk` you may find that zed only opens when run with `sudo $(which zed)`. To fix this, remove the `amdvlk` and `lib32-amdvlk` packages and install mesa/vulkan instead. ([#14141](https://github.com/zed-industries/zed/issues/14141).
If you have a discrete GPU and you are using [PRIME](https://wiki.archlinux.org/title/PRIME) you may be able to configure Zed to work by setting `/etc/prime-discrete` to 'on'.
If you have a discrete GPU and you are using [PRIME](https://wiki.archlinux.org/title/PRIME) (e.g. Pop_OS 24.04, ArchLinux, etc) you may be able to configure Zed to work by switching `/etc/prime-discrete` from 'off' to 'on' (or the reverse).
For more information, the [Arch guide to Vulkan](https://wiki.archlinux.org/title/Vulkan) has some good steps that translate well to most distributions.