Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc996a87c | ||
|
|
82cee9e9a4 | ||
|
|
ecf70db7f0 | ||
|
|
848f29831e | ||
|
|
78a8c1a68a | ||
|
|
db65ee0add | ||
|
|
8ed2a81962 | ||
|
|
01b57b10e7 | ||
|
|
a1077c6fff | ||
|
|
51fcb710d7 | ||
|
|
aad04e078a | ||
|
|
3c0d05e362 | ||
|
|
55d99e0259 | ||
|
|
08b3c03241 | ||
|
|
2c2a3ef13d | ||
|
|
6aab82c180 | ||
|
|
a811979894 | ||
|
|
514d9b4161 | ||
|
|
f7703973d2 | ||
|
|
09fe1e7f66 | ||
|
|
9f87145af9 | ||
|
|
5930b552b9 |
34
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
Normal file
34
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
Normal 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
|
||||
24
.github/ISSUE_TEMPLATE/config.yml
vendored
24
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
38
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 |
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
197
crates/assistant/src/assistant_configuration.rs
Normal file
197
crates/assistant/src/assistant_configuration.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
173
crates/assistant2/src/assistant_configuration.rs
Normal file
173
crates/assistant2/src/assistant_configuration.rs
Normal 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)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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('-') {
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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!({
|
||||
|
||||
@@ -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!({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>(¶ms).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>(¶ms).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>(¶ms).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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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>(
|
||||
|
||||
243
crates/project/src/project_tree.rs
Normal file
243
crates/project/src/project_tree.rs
Normal 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));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
241
crates/project/src/project_tree/path_trie.rs
Normal file
241
crates/project/src/project_tree/path_trie.rs
Normal 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)(¤t.worktree_relative_path, ¤t.labels).is_break() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
current = match current.children.get(key) {
|
||||
Some(child) => child,
|
||||
None => return,
|
||||
};
|
||||
}
|
||||
if !current.labels.is_empty() {
|
||||
(callback)(¤t.worktree_relative_path, ¤t.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/")
|
||||
);
|
||||
}
|
||||
}
|
||||
428
crates/project/src/project_tree/server_tree.rs
Normal file
428
crates/project/src/project_tree/server_tree.rs
Normal 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(())
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)?;
|
||||
|
||||
@@ -765,6 +765,7 @@ message Symbol {
|
||||
PointUtf16 start = 7;
|
||||
PointUtf16 end = 8;
|
||||
bytes signature = 9;
|
||||
uint64 language_server_id = 10;
|
||||
}
|
||||
|
||||
message OpenBufferForSymbol {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1094,6 +1094,7 @@ impl Render for TerminalView {
|
||||
let focused = self.focus_handle.is_focused(cx);
|
||||
|
||||
div()
|
||||
.occlude()
|
||||
.id("terminal-view")
|
||||
.size_full()
|
||||
.relative()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,6 @@ pub enum IconName {
|
||||
Ai,
|
||||
AiAnthropic,
|
||||
AiAnthropicHosted,
|
||||
AiDeepSeek,
|
||||
AiGoogle,
|
||||
AiLmStudio,
|
||||
AiOllama,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
stable
|
||||
dev
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user