Compare commits
25 Commits
stateful-r
...
cole/diff-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e1b077915 | ||
|
|
74b7e8ca32 | ||
|
|
ba2760544a | ||
|
|
20e24dca68 | ||
|
|
59afc27f03 | ||
|
|
611abcadc0 | ||
|
|
fff12ec1e5 | ||
|
|
13a81e454a | ||
|
|
de89f8cf83 | ||
|
|
c594ccb0af | ||
|
|
937186da12 | ||
|
|
b3ffbea376 | ||
|
|
124e63d07c | ||
|
|
dd66a20d78 | ||
|
|
e8c72d91c3 | ||
|
|
dfe455b054 | ||
|
|
db7e38464a | ||
|
|
f8b6d71670 | ||
|
|
ae351298b4 | ||
|
|
664468d468 | ||
|
|
714f183ede | ||
|
|
b36dcf3b92 | ||
|
|
63e1bf01a4 | ||
|
|
62a6a755ec | ||
|
|
28faba12a2 |
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,3 +1,4 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Language Request
|
||||
|
||||
1194
Cargo.lock
generated
@@ -142,6 +142,7 @@ members = [
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zeta",
|
||||
"crates/git_ui",
|
||||
|
||||
#
|
||||
# Extensions
|
||||
@@ -227,6 +228,7 @@ fs = { path = "crates/fs" }
|
||||
fsevent = { path = "crates/fsevent" }
|
||||
fuzzy = { path = "crates/fuzzy" }
|
||||
git = { path = "crates/git" }
|
||||
git_ui = { path = "crates/git_ui" }
|
||||
git_hosting_providers = { path = "crates/git_hosting_providers" }
|
||||
go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
|
||||
1
assets/icons/file_diff.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-diff"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M9 10h6"/><path d="M12 13V7"/><path d="M9 17h6"/></svg>
|
||||
|
After Width: | Height: | Size: 348 B |
1
assets/icons/git_branch.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-branch"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
|
||||
|
After Width: | Height: | Size: 348 B |
1
assets/icons/panel_left.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>
|
||||
|
After Width: | Height: | Size: 289 B |
1
assets/icons/panel_right.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>
|
||||
|
After Width: | Height: | Size: 291 B |
1
assets/icons/square_dot.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dot"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="12" cy="12" r="1"/></svg>
|
||||
|
After Width: | Height: | Size: 301 B |
1
assets/icons/square_minus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-minus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/></svg>
|
||||
|
After Width: | Height: | Size: 291 B |
1
assets/icons/square_plus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-plus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>
|
||||
|
After Width: | Height: | Size: 309 B |
1
assets/icons/thumbs_down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-down"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>
|
||||
|
After Width: | Height: | Size: 405 B |
1
assets/icons/thumbs_up.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>
|
||||
|
After Width: | Height: | Size: 404 B |
@@ -468,13 +468,21 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"context": "Editor && inline_completion",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"cmd-v": "editor::Paste",
|
||||
"cmd-z": "editor::Undo",
|
||||
"cmd-shift-z": "editor::Redo",
|
||||
"ctrl-shift-z": "zeta::RateCompletions",
|
||||
"up": "editor::MoveUp",
|
||||
"ctrl-up": "editor::MoveToStartOfParagraph",
|
||||
"pageup": "editor::MovePageUp",
|
||||
@@ -540,12 +541,18 @@
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"enter": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"context": "Editor && inline_completion",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
@@ -788,5 +795,24 @@
|
||||
"ctrl-k left": "pane::SplitLeft",
|
||||
"ctrl-k right": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RateCompletionModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "zeta::ThumbsUp",
|
||||
"shift-down": "zeta::NextEdit",
|
||||
"shift-up": "zeta::PreviousEdit",
|
||||
"right": "zeta::PreviewCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RateCompletionModal > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "zeta::FocusCompletions",
|
||||
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
||||
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5113,9 +5113,11 @@ fn make_lsp_adapter_delegate(
|
||||
return Ok(None::<Arc<dyn LspAdapterDelegate>>);
|
||||
};
|
||||
let http_client = project.client().http_client().clone();
|
||||
project.lsp_store().update(cx, |lsp_store, cx| {
|
||||
project.lsp_store().update(cx, |_, cx| {
|
||||
Ok(Some(LocalLspAdapterDelegate::new(
|
||||
lsp_store,
|
||||
project.languages().clone(),
|
||||
project.environment(),
|
||||
cx.weak_model(),
|
||||
&worktree,
|
||||
http_client,
|
||||
project.fs().clone(),
|
||||
|
||||
@@ -17,7 +17,7 @@ use futures::{
|
||||
channel::mpsc,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use parking_lot::Mutex;
|
||||
@@ -35,7 +35,7 @@ use std::{
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
|
||||
use ui::{Context as _, IconName, WindowContext};
|
||||
use ui::{IconName, WindowContext};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
|
||||
@@ -13,30 +13,51 @@ path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
async-watch.workspace = true
|
||||
client.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
context_server.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
handlebars.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
ordered-float.workspace = true
|
||||
paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
similar.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
@@ -45,3 +66,8 @@ unindent.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rand.workspace = true
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
mod active_thread;
|
||||
mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod context;
|
||||
mod context_picker;
|
||||
mod inline_assistant;
|
||||
mod message_editor;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
mod thread;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod ui;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
use gpui::{actions, AppContext};
|
||||
use prompts::PromptLoadingParams;
|
||||
use settings::Settings as _;
|
||||
use util::ResultExt;
|
||||
|
||||
pub use crate::assistant_panel::AssistantPanel;
|
||||
|
||||
@@ -21,15 +33,37 @@ actions!(
|
||||
NewThread,
|
||||
ToggleModelSelector,
|
||||
OpenHistory,
|
||||
Chat
|
||||
Chat,
|
||||
ToggleInlineAssist,
|
||||
CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist
|
||||
]
|
||||
);
|
||||
|
||||
const NAMESPACE: &str = "assistant2";
|
||||
|
||||
/// Initializes the `assistant2` crate.
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mut AppContext) {
|
||||
AssistantSettings::register(cx);
|
||||
assistant_panel::init(cx);
|
||||
|
||||
let prompt_builder = prompts::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(prompts::PromptBuilder::new(None).unwrap()));
|
||||
inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
feature_gate_assistant2_actions(cx);
|
||||
}
|
||||
|
||||
|
||||
485
crates/assistant2/src/assistant_settings.rs
Normal file
@@ -0,0 +1,485 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use gpui::Pixels;
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
#[default]
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContentV1 {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev { default_model: Option<CloudModel> },
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
api_url: Option<String>,
|
||||
available_models: Option<Vec<OpenAiModel>>,
|
||||
},
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic {
|
||||
default_model: Option<AnthropicModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
#[serde(rename = "ollama")]
|
||||
Ollama {
|
||||
default_model: Option<OllamaModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AssistantSettings {
|
||||
pub enabled: bool,
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AssistantSettingsContent {
|
||||
Versioned(VersionedAssistantSettingsContent),
|
||||
Legacy(LegacyAssistantSettingsContent),
|
||||
}
|
||||
|
||||
impl JsonSchema for AssistantSettingsContent {
|
||||
fn schema_name() -> String {
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
VersionedAssistantSettingsContent::is_referenceable()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::Versioned(VersionedAssistantSettingsContent::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantSettingsContent {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(_) => true,
|
||||
VersionedAssistantSettingsContent::V2(_) => false,
|
||||
},
|
||||
AssistantSettingsContent::Legacy(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_width,
|
||||
default_model: settings
|
||||
.provider
|
||||
.clone()
|
||||
.and_then(|provider| match provider {
|
||||
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "zed.dev".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "openai".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "anthropic".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "ollama".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "openai".to_string(),
|
||||
model: settings
|
||||
.default_open_ai_model
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.id()
|
||||
.to_string(),
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
|
||||
let model = language_model.id().0.to_string();
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(tag = "version")]
|
||||
pub enum VersionedAssistantSettingsContent {
|
||||
#[serde(rename = "1")]
|
||||
V1(AssistantSettingsContentV1),
|
||||
#[serde(rename = "2")]
|
||||
V2(AssistantSettingsContentV2),
|
||||
}
|
||||
|
||||
impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
default_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV2 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The default model to use when creating new chats.
|
||||
default_model: Option<LanguageModelSelection>,
|
||||
/// Additional models with which to generate alternatives when performing inline assists.
|
||||
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
||||
/// Enable experimental live diffs in the assistant panel.
|
||||
///
|
||||
/// Default: false
|
||||
enable_experimental_live_diffs: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct LanguageModelSelection {
|
||||
#[schemars(schema_with = "providers_schema")]
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"google".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
impl Default for LanguageModelSelection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: "openai".to_string(),
|
||||
model: "gpt-4".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV1 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The provider of the assistant service.
|
||||
///
|
||||
/// This can be "openai", "anthropic", "ollama", "zed.dev"
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AssistantProviderContentV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct LegacyAssistantSettingsContent {
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
pub default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
pub default_height: Option<f32>,
|
||||
/// The default OpenAI model to use when creating new chats.
|
||||
///
|
||||
/// Default: gpt-4-1106-preview
|
||||
pub default_open_ai_model: Option<OpenAiModel>,
|
||||
/// OpenAI API base URL to use when creating new chats.
|
||||
///
|
||||
/// Default: https://api.openai.com/v1
|
||||
pub openai_api_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut settings = AssistantSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.is_version_outdated() {
|
||||
settings.using_outdated_settings_version = true;
|
||||
}
|
||||
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
merge(
|
||||
&mut settings.default_width,
|
||||
value.default_width.map(Into::into),
|
||||
);
|
||||
merge(
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model);
|
||||
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||
merge(
|
||||
&mut settings.enable_experimental_live_diffs,
|
||||
value.enable_experimental_live_diffs,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::Fs;
|
||||
use gpui::{ReadGlobal, TestAppContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let test_settings = settings::SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
AssistantSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: "claude-3-5-sonnet".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AssistantSettingsContent::Versioned(
|
||||
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
}),
|
||||
)
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
|
||||
assert!(raw_settings_value.contains(r#""version": "2""#));
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AssistantSettingsTest {
|
||||
assistant: AssistantSettingsContent,
|
||||
}
|
||||
|
||||
let assistant_settings: AssistantSettingsTest =
|
||||
serde_json_lenient::from_str(&raw_settings_value).unwrap();
|
||||
|
||||
assert!(!assistant_settings.assistant.is_version_outdated());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,20 @@
|
||||
use gpui::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::post_inc;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct ContextId(pub(crate) usize);
|
||||
|
||||
impl ContextId {
|
||||
pub fn post_inc(&mut self) -> Self {
|
||||
Self(post_inc(&mut self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Some context attached to a message in a thread.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub id: ContextId,
|
||||
pub name: SharedString,
|
||||
pub kind: ContextKind,
|
||||
pub text: SharedString,
|
||||
|
||||
3851
crates/assistant2/src/inline_assistant.rs
Normal file
@@ -1,3 +1,5 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
||||
@@ -10,7 +12,7 @@ use ui::{
|
||||
PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
|
||||
use crate::context::{Context, ContextKind};
|
||||
use crate::context::{Context, ContextId, ContextKind};
|
||||
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::ui::ContextPill;
|
||||
@@ -20,19 +22,14 @@ pub struct MessageEditor {
|
||||
thread: Model<Thread>,
|
||||
editor: View<Editor>,
|
||||
context: Vec<Context>,
|
||||
next_context_id: ContextId,
|
||||
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
|
||||
use_tools: bool,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let mocked_context = vec![Context {
|
||||
name: "shape.rs".into(),
|
||||
kind: ContextKind::File,
|
||||
text: "```rs\npub enum Shape {\n Circle,\n Square,\n Triangle,\n}".into(),
|
||||
}];
|
||||
|
||||
Self {
|
||||
let mut this = Self {
|
||||
thread,
|
||||
editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
@@ -40,10 +37,20 @@ impl MessageEditor {
|
||||
|
||||
editor
|
||||
}),
|
||||
context: mocked_context,
|
||||
context: Vec::new(),
|
||||
next_context_id: ContextId(0),
|
||||
context_picker_handle: PopoverMenuHandle::default(),
|
||||
use_tools: false,
|
||||
}
|
||||
};
|
||||
|
||||
this.context.push(Context {
|
||||
id: this.next_context_id.post_inc(),
|
||||
name: "shape.rs".into(),
|
||||
kind: ContextKind::File,
|
||||
text: "```rs\npub enum Shape {\n Circle,\n Square,\n Triangle,\n}".into(),
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
|
||||
@@ -178,11 +185,15 @@ impl Render for MessageEditor {
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small),
|
||||
))
|
||||
.children(
|
||||
self.context
|
||||
.iter()
|
||||
.map(|context| ContextPill::new(context.clone())),
|
||||
)
|
||||
.children(self.context.iter().map(|context| {
|
||||
ContextPill::new(context.clone()).on_remove({
|
||||
let context = context.clone();
|
||||
Rc::new(cx.listener(move |this, _event, cx| {
|
||||
this.context.retain(|other| other.id != context.id);
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
}))
|
||||
.when(!self.context.is_empty(), |parent| {
|
||||
parent.child(
|
||||
IconButton::new("remove-all-context", IconName::Eraser)
|
||||
|
||||
291
crates/assistant2/src/prompts.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
use anyhow::Result;
|
||||
use assets::Assets;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::AssetSource;
|
||||
use handlebars::{Handlebars, RenderError};
|
||||
use language::{BufferSnapshot, LanguageName, Point};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use text::LineEnding;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContentPromptDiagnosticContext {
|
||||
pub line_number: usize,
|
||||
pub error_message: String,
|
||||
pub code_content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContentPromptContext {
|
||||
pub content_type: String,
|
||||
pub language_name: Option<String>,
|
||||
pub is_insert: bool,
|
||||
pub is_truncated: bool,
|
||||
pub document_content: String,
|
||||
pub user_prompt: String,
|
||||
pub rewrite_section: Option<String>,
|
||||
pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TerminalAssistantPromptContext {
|
||||
pub os: String,
|
||||
pub arch: String,
|
||||
pub shell: Option<String>,
|
||||
pub working_directory: Option<String>,
|
||||
pub latest_output: Vec<String>,
|
||||
pub user_prompt: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProjectSlashCommandPromptContext {
|
||||
pub context_buffer: String,
|
||||
}
|
||||
|
||||
pub struct PromptLoadingParams<'a> {
|
||||
pub fs: Arc<dyn Fs>,
|
||||
pub repo_path: Option<PathBuf>,
|
||||
pub cx: &'a gpui::AppContext,
|
||||
}
|
||||
|
||||
pub struct PromptBuilder {
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
}
|
||||
|
||||
impl PromptBuilder {
|
||||
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
|
||||
let mut handlebars = Handlebars::new();
|
||||
Self::register_built_in_templates(&mut handlebars)?;
|
||||
|
||||
let handlebars = Arc::new(Mutex::new(handlebars));
|
||||
|
||||
if let Some(params) = loading_params {
|
||||
Self::watch_fs_for_template_overrides(params, handlebars.clone());
|
||||
}
|
||||
|
||||
Ok(Self { handlebars })
|
||||
}
|
||||
|
||||
/// Watches the filesystem for changes to prompt template overrides.
|
||||
///
|
||||
/// This function sets up a file watcher on the prompt templates directory. It performs
|
||||
/// an initial scan of the directory and registers any existing template overrides.
|
||||
/// Then it continuously monitors for changes, reloading templates as they are
|
||||
/// modified or added.
|
||||
///
|
||||
/// If the templates directory doesn't exist initially, it waits for it to be created.
|
||||
/// If the directory is removed, it restores the built-in templates and waits for the
|
||||
/// directory to be recreated.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
|
||||
/// and application context.
|
||||
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
|
||||
fn watch_fs_for_template_overrides(
|
||||
params: PromptLoadingParams,
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
) {
|
||||
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
|
||||
params.cx.background_executor()
|
||||
.spawn(async move {
|
||||
let Some(parent_dir) = templates_dir.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut found_dir_once = false;
|
||||
loop {
|
||||
// Check if the templates directory exists and handle its status
|
||||
// If it exists, log its presence and check if it's a symlink
|
||||
// If it doesn't exist:
|
||||
// - Log that we're using built-in prompts
|
||||
// - Check if it's a broken symlink and log if so
|
||||
// - Set up a watcher to detect when it's created
|
||||
// After the first check, set the `found_dir_once` flag
|
||||
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
|
||||
let dir_status = params.fs.is_dir(&templates_dir).await;
|
||||
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
|
||||
if dir_status {
|
||||
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
} else {
|
||||
if !found_dir_once {
|
||||
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
|
||||
}
|
||||
}
|
||||
|
||||
if params.fs.is_dir(parent_dir).await {
|
||||
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
while let Some(changed_paths) = changes.next().await {
|
||||
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
|
||||
if let Ok(target) = params.fs.read_link(&templates_dir).await {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
found_dir_once = true;
|
||||
|
||||
// Initial scan of the prompt overrides directory
|
||||
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
|
||||
while let Some(Ok(file_path)) = entries.next().await {
|
||||
if file_path.to_string_lossy().ends_with(".hbs") {
|
||||
if let Ok(content) = params.fs.load(&file_path).await {
|
||||
let file_name = file_path.file_stem().unwrap().to_string_lossy();
|
||||
log::debug!("Registering prompt template override: {}", file_name);
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch both the parent directory and the template overrides directory:
|
||||
// - Monitor the parent directory to detect if the template overrides directory is deleted.
|
||||
// - Monitor the template overrides directory to re-register templates when they change.
|
||||
// Combine both watch streams into a single stream.
|
||||
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
|
||||
let mut combined_changes = futures::stream::select(changes, parent_changes);
|
||||
|
||||
while let Some(changed_paths) = combined_changes.next().await {
|
||||
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||
if !params.fs.is_dir(&templates_dir).await {
|
||||
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
|
||||
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
|
||||
break;
|
||||
}
|
||||
}
|
||||
for event in changed_paths {
|
||||
if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") {
|
||||
log::info!("Reloading prompt template override: {}", event.path.display());
|
||||
if let Some(content) = params.fs.load(&event.path).await.log_err() {
|
||||
let file_name = event.path.file_stem().unwrap().to_string_lossy();
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(watcher);
|
||||
drop(parent_watcher);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
|
||||
for path in Assets.list("prompts")? {
|
||||
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
|
||||
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
|
||||
log::debug!("Registering built-in prompt template: {}", id);
|
||||
let prompt = String::from_utf8_lossy(prompt.as_ref());
|
||||
handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_inline_transformation_prompt(
|
||||
&self,
|
||||
user_prompt: String,
|
||||
language_name: Option<&LanguageName>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
) -> Result<String, RenderError> {
|
||||
let content_type = match language_name.as_ref().map(|l| l.0.as_ref()) {
|
||||
None | Some("Markdown" | "Plain Text") => "text",
|
||||
Some(_) => "code",
|
||||
};
|
||||
|
||||
const MAX_CTX: usize = 50000;
|
||||
let is_insert = range.is_empty();
|
||||
let mut is_truncated = false;
|
||||
|
||||
let before_range = 0..range.start;
|
||||
let truncated_before = if before_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right);
|
||||
start..range.start
|
||||
} else {
|
||||
before_range
|
||||
};
|
||||
|
||||
let after_range = range.end..buffer.len();
|
||||
let truncated_after = if after_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left);
|
||||
range.end..end
|
||||
} else {
|
||||
after_range
|
||||
};
|
||||
|
||||
let mut document_content = String::new();
|
||||
for chunk in buffer.text_for_range(truncated_before) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
if is_insert {
|
||||
document_content.push_str("<insert_here></insert_here>");
|
||||
} else {
|
||||
document_content.push_str("<rewrite_this>\n");
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
document_content.push_str("\n</rewrite_this>");
|
||||
}
|
||||
for chunk in buffer.text_for_range(truncated_after) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
|
||||
let rewrite_section = if !is_insert {
|
||||
let mut section = String::new();
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
section.push_str(chunk);
|
||||
}
|
||||
Some(section)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
|
||||
let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
|
||||
.map(|entry| {
|
||||
let start = entry.range.start;
|
||||
ContentPromptDiagnosticContext {
|
||||
line_number: (start.row + 1) as usize,
|
||||
error_message: entry.diagnostic.message.clone(),
|
||||
code_content: buffer.text_for_range(entry.range.clone()).collect(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let context = ContentPromptContext {
|
||||
content_type: content_type.to_string(),
|
||||
language_name: language_name.map(|s| s.to_string()),
|
||||
is_insert,
|
||||
is_truncated,
|
||||
document_content,
|
||||
user_prompt,
|
||||
rewrite_section,
|
||||
diagnostic_errors,
|
||||
};
|
||||
self.handlebars.lock().render("content_prompt", &context)
|
||||
}
|
||||
}
|
||||
1102
crates/assistant2/src/streaming_diff.rs
Normal file
@@ -1,25 +1,49 @@
|
||||
use ui::prelude::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::ClickEvent;
|
||||
use ui::{prelude::*, IconButtonShape};
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ContextPill {
|
||||
context: Context,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||
}
|
||||
|
||||
impl ContextPill {
|
||||
pub fn new(context: Context) -> Self {
|
||||
Self { context }
|
||||
Self {
|
||||
context,
|
||||
on_remove: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
|
||||
self.on_remove = Some(on_remove);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ContextPill {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
|
||||
.when_some(self.on_remove, |parent, on_remove| {
|
||||
parent.child(
|
||||
IconButton::new("remove", IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, cx| on_remove(event, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +310,9 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetStagedText>)
|
||||
.add_request_handler(
|
||||
forward_mutating_project_request::<proto::RegisterBufferWithLanguageServers>,
|
||||
)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::UpdateGitBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
|
||||
.add_request_handler(
|
||||
|
||||
@@ -994,10 +994,12 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
|
||||
|
||||
let _buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_local_buffer_with_lsp("/dir/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1587,7 +1589,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
@@ -1597,6 +1598,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
|
||||
// Set up the language server to return an additional inlay hint on each request.
|
||||
let edits_made = Arc::new(AtomicUsize::new(0));
|
||||
let closure_edits_made = Arc::clone(&edits_made);
|
||||
|
||||
@@ -3891,13 +3891,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
// Cause the language server to start.
|
||||
let _buffer = project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.open_buffer(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Path::new("other.rs").into(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
project.open_local_buffer_with_lsp("/a/other.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4176,7 +4170,9 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
// Join the project as client B and open all three files.
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
|
||||
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
|
||||
project_b.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, file_name), cx)
|
||||
})
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4230,7 +4226,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
cx.subscribe(&project_b, move |_, _, event, cx| {
|
||||
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
|
||||
disk_based_diagnostics_finished.store(true, SeqCst);
|
||||
for buffer in &guest_buffers {
|
||||
for (buffer, _) in &guest_buffers {
|
||||
assert_eq!(
|
||||
buffer
|
||||
.read(cx)
|
||||
@@ -4351,7 +4347,6 @@ async fn test_formatting_buffer(
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
executor.allow_parking();
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
@@ -4379,10 +4374,16 @@ async fn test_formatting_buffer(
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let lsp_store_b = project_b.update(cx_b, |p, _| p.lsp_store());
|
||||
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _handle = lsp_store_b.update(cx_b, |lsp_store, cx| {
|
||||
lsp_store.register_buffer_with_language_servers(&buffer_b, cx)
|
||||
});
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
|
||||
Ok(Some(vec![
|
||||
@@ -4431,6 +4432,8 @@ async fn test_formatting_buffer(
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
executor.allow_parking();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.format(
|
||||
@@ -4503,8 +4506,12 @@ async fn test_prettier_formatting_buffer(
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
@@ -4620,8 +4627,12 @@ async fn test_definition(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Request the definition of a symbol as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -4765,8 +4776,12 @@ async fn test_references(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Request references to a symbol as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -5012,8 +5027,12 @@ async fn test_document_highlights(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Request document highlights as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -5130,8 +5149,12 @@ async fn test_lsp_hover(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file as the guest
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut servers_with_hover_requests = HashMap::default();
|
||||
for i in 0..language_server_names.len() {
|
||||
@@ -5306,9 +5329,12 @@ async fn test_project_symbols(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Cause the language server to start.
|
||||
let open_buffer_task =
|
||||
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
|
||||
let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
|
||||
let _buffer = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
|
||||
@@ -5400,8 +5426,12 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
|
||||
let (buffer_b1, _lsp) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
|
||||
@@ -5417,13 +5447,22 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
let buffer_b2;
|
||||
if rng.gen() {
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
|
||||
buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
|
||||
(buffer_b2, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
|
||||
(buffer_b2, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
|
||||
}
|
||||
|
||||
let buffer_b2 = buffer_b2.await.unwrap();
|
||||
let definitions = definitions.await.unwrap();
|
||||
assert_eq!(definitions.len(), 1);
|
||||
assert_eq!(definitions[0].target.buffer, buffer_b2);
|
||||
|
||||
@@ -426,8 +426,10 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
executor.run_until_parked();
|
||||
|
||||
// Opens the buffer and formats it
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
|
||||
})
|
||||
.await
|
||||
.expect("user B opens buffer for formatting");
|
||||
|
||||
|
||||
@@ -6,13 +6,12 @@ use anyhow::{anyhow, Result};
|
||||
use chrono::DateTime;
|
||||
use fs::Fs;
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Global};
|
||||
use gpui::{prelude::*, AppContext, AsyncAppContext, Global};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use paths::home_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::watch_config_file;
|
||||
use strum::EnumIter;
|
||||
use ui::Context;
|
||||
|
||||
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
|
||||
pub const COPILOT_CHAT_AUTH_URL: &str = "https://api.github.com/copilot_internal/v2/token";
|
||||
|
||||
@@ -296,7 +296,6 @@ mod tests {
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
});
|
||||
|
||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
@@ -323,8 +322,9 @@ mod tests {
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
// We want to show both: the inline completion and the completion menu
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
|
||||
// Confirming a completion inserts it and hides the context menu, without showing
|
||||
// the copilot suggestion afterwards.
|
||||
@@ -338,40 +338,7 @@ mod tests {
|
||||
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Ensure Copilot suggestions are shown right away if no autocompletion is available.
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
two
|
||||
three
|
||||
"},
|
||||
vec![],
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
|
||||
// Reset editor and test that accepting completions works
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
@@ -399,17 +366,12 @@ mod tests {
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
|
||||
// When hiding the context menu, the Copilot suggestion becomes visible.
|
||||
editor.cancel(&Default::default(), cx);
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Ensure existing completion is interpolated when inserting again.
|
||||
// Ensure existing inline completion is interpolated when inserting again.
|
||||
cx.simulate_keystroke("c");
|
||||
executor.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
@@ -880,7 +842,7 @@ mod tests {
|
||||
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible(), "Even there are some completions available, those are not triggered when active copilot suggestion is present");
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||||
@@ -934,15 +896,9 @@ mod tests {
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(
|
||||
editor.context_menu_visible(),
|
||||
"On completion trigger input, the completions should be fetched and visible"
|
||||
);
|
||||
assert!(
|
||||
!editor.has_active_inline_completion(),
|
||||
"On completion trigger input, copilot suggestion should be dismissed"
|
||||
);
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion(),);
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,10 +129,10 @@ use multi_buffer::{
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use project::{
|
||||
lsp_store::{FormatTarget, FormatTrigger},
|
||||
lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle},
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
|
||||
Project, ProjectItem, ProjectTransaction, TaskSourceKind,
|
||||
LspStore, Project, ProjectItem, ProjectTransaction, TaskSourceKind,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rpc::{proto::*, ErrorExt};
|
||||
@@ -663,6 +663,7 @@ pub struct Editor {
|
||||
focused_block: Option<FocusedBlock>,
|
||||
next_scroll_position: NextScrollCursorCenterTopBottom,
|
||||
addons: HashMap<TypeId, Box<dyn Addon>>,
|
||||
registered_buffers: HashMap<BufferId, OpenLspBufferHandle>,
|
||||
_scroll_cursor_center_top_bottom_task: Task<()>,
|
||||
}
|
||||
|
||||
@@ -1308,6 +1309,7 @@ impl Editor {
|
||||
focused_block: None,
|
||||
next_scroll_position: NextScrollCursorCenterTopBottom::default(),
|
||||
addons: HashMap::default(),
|
||||
registered_buffers: HashMap::default(),
|
||||
_scroll_cursor_center_top_bottom_task: Task::ready(()),
|
||||
text_style_refinement: None,
|
||||
};
|
||||
@@ -1325,6 +1327,17 @@ impl Editor {
|
||||
this.git_blame_inline_enabled = true;
|
||||
this.start_git_blame_inline(false, cx);
|
||||
}
|
||||
|
||||
if let Some(buffer) = buffer.read(cx).as_singleton() {
|
||||
if let Some(project) = this.project.as_ref() {
|
||||
let lsp_store = project.read(cx).lsp_store();
|
||||
let handle = lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.register_buffer_with_language_servers(&buffer, cx)
|
||||
});
|
||||
this.registered_buffers
|
||||
.insert(buffer.read(cx).remote_id(), handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.report_editor_event("open", None, cx);
|
||||
@@ -1635,6 +1648,22 @@ impl Editor {
|
||||
self.collapse_matches = collapse_matches;
|
||||
}
|
||||
|
||||
pub fn register_buffers_with_language_servers(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let buffers = self.buffer.read(cx).all_buffers();
|
||||
let Some(lsp_store) = self.lsp_store(cx) else {
|
||||
return;
|
||||
};
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
for buffer in buffers {
|
||||
self.registered_buffers
|
||||
.entry(buffer.read(cx).remote_id())
|
||||
.or_insert_with(|| {
|
||||
lsp_store.register_buffer_with_language_servers(&buffer, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
|
||||
if self.collapse_matches {
|
||||
return range.start..range.start;
|
||||
@@ -3687,16 +3716,13 @@ impl Editor {
|
||||
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
|
||||
*context_menu = Some(CodeContextMenu::Completions(menu));
|
||||
drop(context_menu);
|
||||
editor.discard_inline_completion(false, cx);
|
||||
cx.notify();
|
||||
} else if editor.completion_tasks.len() <= 1 {
|
||||
// If there are no more completion tasks and the last menu was
|
||||
// empty, we should hide it. If it was already hidden, we should
|
||||
// also show the copilot completion when available.
|
||||
drop(context_menu);
|
||||
if editor.hide_context_menu(cx).is_none() {
|
||||
editor.update_visible_inline_completion(cx);
|
||||
}
|
||||
editor.hide_context_menu(cx);
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -3732,6 +3758,7 @@ impl Editor {
|
||||
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
|
||||
use language::ToOffset as _;
|
||||
|
||||
self.discard_inline_completion(true, cx);
|
||||
let completions_menu =
|
||||
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
|
||||
menu
|
||||
@@ -4475,6 +4502,8 @@ impl Editor {
|
||||
_: &AcceptInlineCompletion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.hide_context_menu(cx);
|
||||
|
||||
let Some(active_inline_completion) = self.active_inline_completion.as_ref() else {
|
||||
return;
|
||||
};
|
||||
@@ -4629,9 +4658,7 @@ impl Editor {
|
||||
let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
|
||||
let excerpt_id = cursor.excerpt_id;
|
||||
|
||||
if self.context_menu.read().is_some()
|
||||
|| (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())
|
||||
|| !offset_selection.is_empty()
|
||||
if !offset_selection.is_empty()
|
||||
|| self
|
||||
.active_inline_completion
|
||||
.as_ref()
|
||||
@@ -4978,11 +5005,7 @@ impl Editor {
|
||||
fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<CodeContextMenu> {
|
||||
cx.notify();
|
||||
self.completion_tasks.clear();
|
||||
let context_menu = self.context_menu.write().take();
|
||||
if context_menu.is_some() {
|
||||
self.update_visible_inline_completion(cx);
|
||||
}
|
||||
context_menu
|
||||
self.context_menu.write().take()
|
||||
}
|
||||
|
||||
fn show_snippet_choices(
|
||||
@@ -9648,6 +9671,7 @@ impl Editor {
|
||||
|theme| theme.editor_highlighted_line_background,
|
||||
cx,
|
||||
);
|
||||
editor.register_buffers_with_language_servers(cx);
|
||||
});
|
||||
|
||||
let item = Box::new(editor);
|
||||
@@ -11844,6 +11868,12 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn lsp_store(&self, cx: &AppContext) -> Option<Model<LspStore>> {
|
||||
self.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).lsp_store())
|
||||
}
|
||||
|
||||
fn on_buffer_changed(&mut self, _: Model<MultiBuffer>, cx: &mut ViewContext<Self>) {
|
||||
cx.notify();
|
||||
}
|
||||
@@ -11857,6 +11887,7 @@ impl Editor {
|
||||
match event {
|
||||
multi_buffer::Event::Edited {
|
||||
singleton_buffer_edited,
|
||||
edited_buffer: buffer_edited,
|
||||
} => {
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
self.active_indent_guides_state.dirty = true;
|
||||
@@ -11865,6 +11896,19 @@ impl Editor {
|
||||
if self.has_active_inline_completion() {
|
||||
self.update_visible_inline_completion(cx);
|
||||
}
|
||||
if let Some(buffer) = buffer_edited {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
if !self.registered_buffers.contains_key(&buffer_id) {
|
||||
if let Some(lsp_store) = self.lsp_store(cx) {
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
self.registered_buffers.insert(
|
||||
buffer_id,
|
||||
lsp_store.register_buffer_with_language_servers(&buffer, cx),
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.emit(EditorEvent::BufferEdited);
|
||||
cx.emit(SearchEvent::MatchesInvalidated);
|
||||
if *singleton_buffer_edited {
|
||||
@@ -11931,6 +11975,9 @@ impl Editor {
|
||||
}
|
||||
multi_buffer::Event::ExcerptsRemoved { ids } => {
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
|
||||
let buffer = self.buffer.read(cx);
|
||||
self.registered_buffers
|
||||
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
|
||||
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
|
||||
}
|
||||
multi_buffer::Event::ExcerptsEdited { ids } => {
|
||||
|
||||
@@ -32,9 +32,12 @@ use project::{
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use std::sync::atomic::{self, AtomicUsize};
|
||||
use std::{cell::RefCell, future::Future, iter, rc::Rc, time::Instant};
|
||||
use test::editor_lsp_test_context::rust_lang;
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use std::{
|
||||
iter,
|
||||
sync::atomic::{self, AtomicUsize},
|
||||
};
|
||||
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
@@ -6836,14 +6839,15 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let (editor, cx) =
|
||||
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let save = editor
|
||||
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
|
||||
.unwrap();
|
||||
@@ -7117,6 +7121,7 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
assert!(!buffer.is_dirty());
|
||||
assert_eq!(buffer.text(), sample_text_3,)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let save = multi_buffer_editor
|
||||
@@ -7188,14 +7193,15 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let (editor, cx) =
|
||||
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let save = editor
|
||||
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
|
||||
.unwrap();
|
||||
@@ -7339,13 +7345,14 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) =
|
||||
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
|
||||
let format = editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.perform_format(
|
||||
@@ -10332,9 +10339,6 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
let editor_handle = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
@@ -10345,6 +10349,9 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
@@ -10434,7 +10441,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
|
||||
let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let _buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/a/main.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/a/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -50,7 +50,7 @@ use language::{
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
MultiBufferSnapshot,
|
||||
MultiBufferSnapshot, ToOffset,
|
||||
};
|
||||
use project::{
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
@@ -1696,16 +1696,23 @@ impl EditorElement {
|
||||
None
|
||||
};
|
||||
|
||||
let offset_range_start = snapshot
|
||||
.display_point_to_anchor(DisplayPoint::new(range.start, 0), Bias::Left)
|
||||
.to_offset(&snapshot.buffer_snapshot);
|
||||
let offset_range_end = snapshot
|
||||
.display_point_to_anchor(DisplayPoint::new(range.end, 0), Bias::Right)
|
||||
.to_offset(&snapshot.buffer_snapshot);
|
||||
|
||||
editor
|
||||
.tasks
|
||||
.iter()
|
||||
.filter_map(|(_, tasks)| {
|
||||
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
|
||||
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
|
||||
let display_row = multibuffer_point.to_display_point(snapshot).row();
|
||||
if range.start > display_row || range.end < display_row {
|
||||
if tasks.offset.0 < offset_range_start || tasks.offset.0 >= offset_range_end {
|
||||
return None;
|
||||
}
|
||||
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
|
||||
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
|
||||
|
||||
if snapshot.is_line_folded(multibuffer_row) {
|
||||
// Skip folded indicators, unless it's the starting line of a fold.
|
||||
if multibuffer_row
|
||||
@@ -1718,6 +1725,7 @@ impl EditorElement {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let display_row = multibuffer_point.to_display_point(snapshot).row();
|
||||
let button = editor.render_run_indicator(
|
||||
&self.style,
|
||||
Some(display_row) == active_task_indicator_row,
|
||||
@@ -6653,7 +6661,6 @@ mod tests {
|
||||
use language::language_settings;
|
||||
use log::info;
|
||||
use std::num::NonZeroU32;
|
||||
use ui::Context;
|
||||
use util::test::sample_text;
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -22,10 +22,7 @@ use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer}
|
||||
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
|
||||
use text::{OffsetRangeExt, ToPoint};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
div, h_flex, Color, Context, FluentBuilder, Icon, IconName, IntoElement, Label, LabelCommon,
|
||||
ParentElement, SharedString, Styled, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::{paths::compare_paths, ResultExt};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
@@ -1099,136 +1096,290 @@ impl Render for ProjectDiffEditor {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use futures::future::join_all;
|
||||
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
||||
use project::buffer_store::BufferChangeSet;
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, DistString},
|
||||
prelude::*,
|
||||
};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
ops::Deref as _,
|
||||
ffi::OsString,
|
||||
ops::Not,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use text::{Edit, Patch, Rope};
|
||||
|
||||
use super::*;
|
||||
|
||||
// TODO finish
|
||||
// #[gpui::test]
|
||||
// async fn randomized_tests(cx: &mut TestAppContext) {
|
||||
// // Create a new project (how?? temp fs?),
|
||||
// let fs = FakeFs::new(cx.executor());
|
||||
// let project = Project::test(fs, [], cx).await;
|
||||
struct TestFile {
|
||||
name: OsString,
|
||||
staged_text: String,
|
||||
buffer: text::BufferSnapshot,
|
||||
editor: View<Editor>,
|
||||
patch: Patch<usize>,
|
||||
}
|
||||
|
||||
// // create random files with random content
|
||||
|
||||
// // Commit it into git somehow (technically can do with "real" fs in a temp dir)
|
||||
// //
|
||||
// // Apply randomized changes to the project: select a random file, random change and apply to buffers
|
||||
// }
|
||||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn simple_edit_test(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn random_edits(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
let rng = &mut rng;
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
".git": {},
|
||||
"file_a": "This is file_a",
|
||||
"file_b": "This is file_b",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [Path::new("/root")], cx).await;
|
||||
let names = ["file0", "file1", "file2", "file3", "file4"].map(str::to_owned);
|
||||
let fs = {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
let mut files = json!(names
|
||||
.clone()
|
||||
.into_iter()
|
||||
.zip(std::iter::repeat_with(|| gen_file(rng).into()))
|
||||
.collect::<serde_json::Map<_, _>>());
|
||||
files
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert(".git".to_owned(), json!({}));
|
||||
fs.insert_tree("/project", files).await;
|
||||
fs
|
||||
};
|
||||
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let file_a_editor = workspace
|
||||
let (file_editors, project_diff_editor) = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let file_a_editor =
|
||||
workspace.open_abs_path(PathBuf::from("/root/file_a"), true, cx);
|
||||
let file_editors = names.clone().map(|name| {
|
||||
workspace.open_abs_path(PathBuf::from(format!("/project/{}", name)), true, cx)
|
||||
});
|
||||
ProjectDiffEditor::deploy(workspace, &Deploy, cx);
|
||||
file_a_editor
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.expect("did not open an item at all")
|
||||
.downcast::<Editor>()
|
||||
.expect("did not open an editor for file_a");
|
||||
let project_diff_editor = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
let project_diff_editor = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| item.downcast::<ProjectDiffEditor>())
|
||||
.expect("Didn't open project diff editor");
|
||||
(file_editors, project_diff_editor)
|
||||
})
|
||||
.unwrap()
|
||||
.expect("did not find a ProjectDiffEditor");
|
||||
project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
assert!(
|
||||
project_diff_editor.editor.read(cx).text(cx).is_empty(),
|
||||
"Should have no changes after opening the diff on no git changes"
|
||||
);
|
||||
});
|
||||
|
||||
let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx));
|
||||
let change = "an edit after git add";
|
||||
file_a_editor
|
||||
.update(cx, |file_a_editor, cx| {
|
||||
file_a_editor.insert(change, cx);
|
||||
file_a_editor.save(false, project.clone(), cx)
|
||||
})
|
||||
.unwrap();
|
||||
let file_editors = join_all(file_editors)
|
||||
.await
|
||||
.expect("failed to save a file");
|
||||
file_a_editor.update(cx, |file_a_editor, cx| {
|
||||
let change_set = cx.new_model(|cx| {
|
||||
BufferChangeSet::new_with_base_text(
|
||||
old_text.clone(),
|
||||
file_a_editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.text_snapshot(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
file_a_editor
|
||||
.diff_map
|
||||
.add_change_set(change_set.clone(), cx);
|
||||
project.update(cx, |project, cx| {
|
||||
project.buffer_store().update(cx, |buffer_store, cx| {
|
||||
buffer_store.set_change_set(
|
||||
file_a_editor
|
||||
.into_iter()
|
||||
.map(|result| {
|
||||
result
|
||||
.expect("Failed to open file editor")
|
||||
.downcast::<Editor>()
|
||||
.expect("Unexpected non-editor")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for file_editor in &file_editors {
|
||||
file_editor
|
||||
.update(cx, |editor, cx| editor.save(false, project.clone(), cx))
|
||||
.await
|
||||
.expect("Failed to save file");
|
||||
}
|
||||
let buffers = file_editors.clone().into_iter().map(|file_editor| {
|
||||
file_editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.text_snapshot()
|
||||
})
|
||||
});
|
||||
let mut files = names
|
||||
.into_iter()
|
||||
.zip(file_editors)
|
||||
.zip(buffers)
|
||||
.map(|((name, editor), buffer)| {
|
||||
let staged_text = buffer.text();
|
||||
TestFile {
|
||||
name: name.into(),
|
||||
editor,
|
||||
buffer,
|
||||
staged_text,
|
||||
patch: Patch::new(Vec::new()),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
check(project_diff_editor.clone(), &files, cx);
|
||||
for _ in 0..10 {
|
||||
let file = files.choose_mut(rng).unwrap();
|
||||
match rng.gen_range(0..5) {
|
||||
0..3 => {
|
||||
let old_text = file.buffer.as_rope().clone();
|
||||
let new_edits = gen_edits(rng, &old_text);
|
||||
file.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.edit(
|
||||
new_edits
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(old, _, content)| (old, content)),
|
||||
cx,
|
||||
);
|
||||
editor.save(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to save file");
|
||||
let snapshot = file.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.remote_id(),
|
||||
change_set,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/.git"),
|
||||
&[(Path::new("file_a"), GitFileStatus::Modified)],
|
||||
);
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
.text_snapshot()
|
||||
});
|
||||
let diff = file
|
||||
.editor
|
||||
.update(cx, |editor, cx| {
|
||||
let change_set = cx.new_model(|cx| {
|
||||
BufferChangeSet::new_with_base_text(
|
||||
old_text.to_string(),
|
||||
snapshot.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
editor.diff_map.add_change_set_with_project(
|
||||
project.clone(),
|
||||
change_set,
|
||||
cx,
|
||||
);
|
||||
let diff = BufferDiff::build(&old_text, &snapshot);
|
||||
diff
|
||||
})
|
||||
.await;
|
||||
let patch = std::mem::take(&mut file.patch);
|
||||
file.patch = patch.compose(diff.hunks(&snapshot).map(|hunk| {
|
||||
let new = hunk.buffer_range.to_offset(&snapshot);
|
||||
let old = if hunk.diff_base_byte_range == (0..0) {
|
||||
new_edits
|
||||
.iter()
|
||||
.find_map(|(old, edit_new, _)| (edit_new == &new).then_some(old))
|
||||
.cloned()
|
||||
.unwrap()
|
||||
} else {
|
||||
hunk.diff_base_byte_range
|
||||
};
|
||||
Edit { old, new }
|
||||
}));
|
||||
file.buffer = snapshot;
|
||||
}
|
||||
3 => {
|
||||
file.staged_text = file.buffer.text();
|
||||
file.patch = Patch::new(Vec::new());
|
||||
}
|
||||
4 => {
|
||||
file.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.set_text(file.staged_text.as_str(), cx);
|
||||
editor.save(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to save file");
|
||||
file.buffer = file.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.text_snapshot()
|
||||
});
|
||||
file.patch = Patch::new(Vec::new());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
assert_eq!(
|
||||
// TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added)
|
||||
project_diff_editor.editor.read(cx).text(cx),
|
||||
format!("{change}{old_text}"),
|
||||
"Should have a new change shown in the beginning, and the old text shown as deleted text afterwards"
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/project/.git"),
|
||||
&files
|
||||
.iter()
|
||||
.filter_map(|file| {
|
||||
file.patch
|
||||
.is_empty()
|
||||
.not()
|
||||
.then_some((file.name.as_ref(), GitFileStatus::Modified))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
check(project_diff_editor.clone(), &files, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn check(
|
||||
project_diff_editor: View<ProjectDiffEditor>,
|
||||
files: &[TestFile],
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
for file in files {
|
||||
assert_eq!(
|
||||
file.patch.is_empty(),
|
||||
file.staged_text == file.buffer.text(),
|
||||
)
|
||||
}
|
||||
project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
let snapshot = project_diff_editor
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
let hunks = snapshot
|
||||
.diff_map
|
||||
.diff_hunks(&snapshot.buffer_snapshot)
|
||||
.map(|hunk| {
|
||||
let buffer_snapshot = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_for_excerpt(hunk.excerpt_id)
|
||||
.unwrap();
|
||||
(
|
||||
hunk.buffer_id,
|
||||
hunk.diff_base_byte_range,
|
||||
hunk.buffer_range.to_offset(buffer_snapshot),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let edit_hunks = files
|
||||
.iter()
|
||||
.flat_map(|file| {
|
||||
file.patch.edits().iter().map(|Edit { old, new }| {
|
||||
(file.buffer.remote_id(), old.clone(), new.clone())
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(hunks.len(), edit_hunks.len());
|
||||
for edit_hunk in edit_hunks {
|
||||
assert!(hunks.iter().any(|hunk| hunk == &edit_hunk));
|
||||
}
|
||||
|
||||
let mut hunks = hunks.into_iter().peekable();
|
||||
for excerpt in snapshot.buffer_snapshot.all_excerpts() {
|
||||
let Some((buffer_id, _, new)) = hunks.next() else {
|
||||
panic!("Excerpt without a hunk")
|
||||
};
|
||||
let excerpt_buffer_id = excerpt.buffer().remote_id();
|
||||
let excerpt_range = excerpt.buffer_range().to_offset(excerpt.buffer());
|
||||
assert!(excerpt_buffer_id == buffer_id);
|
||||
assert!(excerpt_range.start <= new.start);
|
||||
assert!(excerpt_range.end >= new.end);
|
||||
while let Some((_, _, new)) = hunks.peek().map_or(None, |hunk @ (id, _, new)| {
|
||||
(id == &excerpt_buffer_id
|
||||
&& (excerpt_range.end > new.start
|
||||
|| (new.is_empty() && excerpt_range.end == new.start)))
|
||||
.then_some(hunk)
|
||||
}) {
|
||||
assert!(excerpt_range.start <= new.start);
|
||||
assert!(excerpt_range.end >= new.end);
|
||||
hunks.next();
|
||||
}
|
||||
}
|
||||
if let Some(hunk) = hunks.next() {
|
||||
panic!("Hunk without an excerpt: {hunk:?}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1251,4 +1402,71 @@ mod tests {
|
||||
cx.set_staff(true);
|
||||
});
|
||||
}
|
||||
|
||||
fn gen_line(rng: &mut StdRng) -> String {
|
||||
let len = rng.gen_range(0..20);
|
||||
let mut s = Alphanumeric.sample_string(rng, len);
|
||||
s.push('\n');
|
||||
s
|
||||
}
|
||||
|
||||
fn gen_file(rng: &mut StdRng) -> String {
|
||||
let line_count = rng.gen_range(0..10);
|
||||
(0..line_count).map(|_| gen_line(rng)).collect()
|
||||
}
|
||||
|
||||
fn gen_edits(rng: &mut StdRng, old: &Rope) -> Vec<(Range<usize>, Range<usize>, String)> {
|
||||
let mut old_lines = {
|
||||
let mut old_lines = Vec::new();
|
||||
let mut old_lines_iter = old.chunks().lines();
|
||||
while let Some(line) = old_lines_iter.next() {
|
||||
assert!(!line.ends_with("\n"));
|
||||
old_lines.push(line.to_owned());
|
||||
}
|
||||
if old_lines.last().is_some_and(|line| line.is_empty()) {
|
||||
old_lines.pop();
|
||||
}
|
||||
old_lines.into_iter()
|
||||
};
|
||||
let mut edits = Vec::new();
|
||||
let unchanged_count = rng.gen_range(0..=old_lines.len());
|
||||
let mut old_offset = old_lines
|
||||
.by_ref()
|
||||
.take(unchanged_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
let mut new_offset = old_offset;
|
||||
while old_lines.len() > 0 {
|
||||
let deleted_count = rng.gen_range(0..=old_lines.len());
|
||||
let advance = old_lines
|
||||
.by_ref()
|
||||
.take(deleted_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
let deleted_range = old_offset..old_offset + advance;
|
||||
old_offset += advance;
|
||||
let minimum_added = if deleted_count == 0 { 1 } else { 0 };
|
||||
let added_count = rng.gen_range(minimum_added..=5);
|
||||
let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
|
||||
let added_range = new_offset..new_offset + addition.len();
|
||||
new_offset += addition.len();
|
||||
edits.push((deleted_range, added_range, addition));
|
||||
|
||||
if old_lines.len() > 0 {
|
||||
let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
|
||||
if blank_lines == old_lines.len() {
|
||||
break;
|
||||
};
|
||||
let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
|
||||
let advance = old_lines
|
||||
.by_ref()
|
||||
.take(unchanged_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
old_offset += advance;
|
||||
new_offset += advance;
|
||||
}
|
||||
}
|
||||
edits
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,40 @@ impl DiffMap {
|
||||
self.snapshot.clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn add_change_set_with_project(
|
||||
&mut self,
|
||||
project: Model<project::Project>,
|
||||
change_set: Model<BufferChangeSet>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let buffer_id = change_set.read(cx).buffer_id;
|
||||
self.snapshot
|
||||
.0
|
||||
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
|
||||
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
|
||||
self.diff_bases.insert(
|
||||
buffer_id,
|
||||
DiffBaseState {
|
||||
last_version: None,
|
||||
_subscription: cx.observe(&change_set, move |editor, change_set, cx| {
|
||||
editor
|
||||
.diff_map
|
||||
.snapshot
|
||||
.0
|
||||
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
|
||||
Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, cx);
|
||||
}),
|
||||
change_set: change_set.clone(),
|
||||
},
|
||||
);
|
||||
project.update(cx, |project, cx| {
|
||||
project.buffer_store().update(cx, |buffer_store, _cx| {
|
||||
buffer_store.set_change_set(buffer_id, change_set);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_change_set(
|
||||
&mut self,
|
||||
change_set: Model<BufferChangeSet>,
|
||||
@@ -149,6 +183,7 @@ impl DiffMapSnapshot {
|
||||
let end =
|
||||
excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0));
|
||||
MultiBufferDiffHunk {
|
||||
excerpt_id: excerpt.id(),
|
||||
row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row),
|
||||
buffer_id,
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
@@ -185,6 +220,7 @@ impl DiffMapSnapshot {
|
||||
.map_point_from_buffer(Point::new(hunk.row_range.end, 0))
|
||||
.row;
|
||||
MultiBufferDiffHunk {
|
||||
excerpt_id: excerpt.id(),
|
||||
row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row),
|
||||
buffer_id,
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
@@ -1112,6 +1148,7 @@ pub(crate) fn to_diff_hunk(
|
||||
.multi_buffer_range
|
||||
.to_point(multi_buffer_snapshot);
|
||||
Some(MultiBufferDiffHunk {
|
||||
excerpt_id: hovered_hunk.multi_buffer_range.start.excerpt_id,
|
||||
row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
|
||||
buffer_id,
|
||||
buffer_range,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use gpui::Model;
|
||||
use gpui::{prelude::*, Model};
|
||||
use indoc::indoc;
|
||||
use inline_completion::InlineCompletionProvider;
|
||||
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
|
||||
use std::ops::Range;
|
||||
use text::{Point, ToOffset};
|
||||
use ui::Context;
|
||||
|
||||
use crate::{
|
||||
editor_tests::init_test, test::editor_test_context::EditorTestContext, InlineCompletion,
|
||||
|
||||
@@ -6,8 +6,8 @@ use collections::BTreeMap;
|
||||
use futures::Future;
|
||||
use git::diff::DiffHunkStatus;
|
||||
use gpui::{
|
||||
AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View, ViewContext,
|
||||
VisualTestContext, WindowHandle,
|
||||
prelude::*, AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View,
|
||||
ViewContext, VisualTestContext, WindowHandle,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry};
|
||||
@@ -23,8 +23,6 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use ui::Context;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
|
||||
@@ -623,9 +623,9 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
None,
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(project_dir.join("test.gleam"), cx)
|
||||
project.open_local_buffer_with_lsp(project_dir.join("test.gleam"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -64,6 +64,11 @@ impl FeatureFlag for ZetaFeatureFlag {
|
||||
const NAME: &'static str = "zeta";
|
||||
}
|
||||
|
||||
pub struct GitUiFeatureFlag;
|
||||
impl FeatureFlag for GitUiFeatureFlag {
|
||||
const NAME: &'static str = "git-ui";
|
||||
}
|
||||
|
||||
pub struct Remoting {}
|
||||
impl FeatureFlag for Remoting {
|
||||
const NAME: &'static str = "remoting";
|
||||
|
||||
@@ -74,16 +74,23 @@ impl BufferDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self {
|
||||
pub async fn build(diff_base: &Rope, buffer: &text::BufferSnapshot) -> Self {
|
||||
let mut tree = SumTree::new(buffer);
|
||||
|
||||
let buffer_text = buffer.as_rope().to_string();
|
||||
let patch = Self::diff(diff_base, &buffer_text);
|
||||
let base_text = diff_base.to_string();
|
||||
let buffer_text = buffer.text();
|
||||
let patch = Self::diff(&base_text, &buffer_text);
|
||||
|
||||
if let Some(patch) = patch {
|
||||
let mut divergence = 0;
|
||||
for hunk_index in 0..patch.num_hunks() {
|
||||
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
|
||||
let hunk = Self::process_patch_hunk(
|
||||
&patch,
|
||||
hunk_index,
|
||||
diff_base,
|
||||
buffer,
|
||||
&mut divergence,
|
||||
);
|
||||
tree.push(hunk, buffer);
|
||||
}
|
||||
}
|
||||
@@ -187,11 +194,11 @@ impl BufferDiff {
|
||||
}
|
||||
|
||||
pub async fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
|
||||
*self = Self::build(&diff_base.to_string(), buffer).await;
|
||||
*self = Self::build(diff_base, buffer).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let start = text.anchor_before(Point::new(0, 0));
|
||||
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
|
||||
self.hunks_intersecting_range(start..end, text)
|
||||
@@ -222,6 +229,7 @@ impl BufferDiff {
|
||||
fn process_patch_hunk(
|
||||
patch: &GitPatch<'_>,
|
||||
hunk_index: usize,
|
||||
diff_base: &Rope,
|
||||
buffer: &text::BufferSnapshot,
|
||||
buffer_row_divergence: &mut i64,
|
||||
) -> InternalDiffHunk {
|
||||
@@ -231,51 +239,59 @@ impl BufferDiff {
|
||||
let mut first_deletion_buffer_row: Option<u32> = None;
|
||||
let mut buffer_row_range: Option<Range<u32>> = None;
|
||||
let mut diff_base_byte_range: Option<Range<usize>> = None;
|
||||
let mut first_addition_old_row: Option<u32> = None;
|
||||
|
||||
for line_index in 0..line_item_count {
|
||||
let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
|
||||
let kind = line.origin_value();
|
||||
let content_offset = line.content_offset() as isize;
|
||||
let content_len = line.content().len() as isize;
|
||||
match kind {
|
||||
GitDiffLineType::Addition => {
|
||||
if first_addition_old_row.is_none() {
|
||||
first_addition_old_row = Some(
|
||||
(line.new_lineno().unwrap() as i64 - *buffer_row_divergence - 1) as u32,
|
||||
);
|
||||
}
|
||||
*buffer_row_divergence += 1;
|
||||
let row = line.new_lineno().unwrap().saturating_sub(1);
|
||||
|
||||
if kind == GitDiffLineType::Addition {
|
||||
*buffer_row_divergence += 1;
|
||||
let row = line.new_lineno().unwrap().saturating_sub(1);
|
||||
|
||||
match &mut buffer_row_range {
|
||||
Some(buffer_row_range) => buffer_row_range.end = row + 1,
|
||||
None => buffer_row_range = Some(row..row + 1),
|
||||
match &mut buffer_row_range {
|
||||
Some(Range { end, .. }) => *end = row + 1,
|
||||
None => buffer_row_range = Some(row..row + 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
GitDiffLineType::Deletion => {
|
||||
let end = content_offset + content_len;
|
||||
|
||||
if kind == GitDiffLineType::Deletion {
|
||||
let end = content_offset + content_len;
|
||||
match &mut diff_base_byte_range {
|
||||
Some(head_byte_range) => head_byte_range.end = end as usize,
|
||||
None => diff_base_byte_range = Some(content_offset as usize..end as usize),
|
||||
}
|
||||
|
||||
match &mut diff_base_byte_range {
|
||||
Some(head_byte_range) => head_byte_range.end = end as usize,
|
||||
None => diff_base_byte_range = Some(content_offset as usize..end as usize),
|
||||
if first_deletion_buffer_row.is_none() {
|
||||
let old_row = line.old_lineno().unwrap().saturating_sub(1);
|
||||
let row = old_row as i64 + *buffer_row_divergence;
|
||||
first_deletion_buffer_row = Some(row as u32);
|
||||
}
|
||||
|
||||
*buffer_row_divergence -= 1;
|
||||
}
|
||||
|
||||
if first_deletion_buffer_row.is_none() {
|
||||
let old_row = line.old_lineno().unwrap().saturating_sub(1);
|
||||
let row = old_row as i64 + *buffer_row_divergence;
|
||||
first_deletion_buffer_row = Some(row as u32);
|
||||
}
|
||||
|
||||
*buffer_row_divergence -= 1;
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
//unwrap_or deletion without addition
|
||||
let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
|
||||
//we cannot have an addition-less hunk without deletion(s) or else there would be no hunk
|
||||
// Pure deletion hunk without addition.
|
||||
let row = first_deletion_buffer_row.unwrap();
|
||||
row..row
|
||||
});
|
||||
|
||||
//unwrap_or addition without deletion
|
||||
let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0);
|
||||
|
||||
let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
|
||||
// Pure addition hunk without deletion.
|
||||
let row = first_addition_old_row.unwrap();
|
||||
let offset = diff_base.point_to_offset(Point::new(row, 0));
|
||||
offset..offset
|
||||
});
|
||||
let start = Point::new(buffer_row_range.start, 0);
|
||||
let end = Point::new(buffer_row_range.end, 0);
|
||||
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
|
||||
|
||||
25
crates/git_ui/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "git_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
name = "git_ui"
|
||||
path = "src/git_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
serde.workspace = true
|
||||
workspace.workspace = true
|
||||
ui.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
1
crates/git_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
181
crates/git_ui/src/git_panel.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use gpui::*;
|
||||
use ui::{prelude::*, Checkbox, Divider, DividerColor, ElevationIndex};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<GitPanel>(cx);
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
actions!(git_panel, [Deploy, ToggleFocus]);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GitPanel {
|
||||
_workspace: WeakView<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
width: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl GitPanel {
|
||||
pub fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
|
||||
cx.new_view(|cx| Self::new(workspace_handle, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(workspace: WeakView<Workspace>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
_workspace: workspace,
|
||||
focus_handle: cx.focus_handle(),
|
||||
width: Some(px(360.)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.h(px(32.))
|
||||
.items_center()
|
||||
.px_3()
|
||||
.bg(ElevationIndex::Surface.bg(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Checkbox::new("all-changes", true.into()).disabled(true))
|
||||
.child(div().text_buffer(cx).text_ui_sm(cx).child("0 changes")),
|
||||
)
|
||||
.child(div().flex_grow())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("discard-changes", IconName::Undo)
|
||||
.icon_size(IconSize::Small)
|
||||
.disabled(true),
|
||||
)
|
||||
.child(
|
||||
Button::new("stage-all", "Stage All")
|
||||
.label_size(LabelSize::Small)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
|
||||
v_flex()
|
||||
.h_full()
|
||||
.py_2p5()
|
||||
.px_3()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.font_buffer(cx)
|
||||
.text_ui_sm(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child("Add a message")
|
||||
.gap_1()
|
||||
.child(div().flex_grow())
|
||||
.child(
|
||||
h_flex().child(div().gap_1().flex_grow()).child(
|
||||
Button::new("commit", "Commit")
|
||||
.label_size(LabelSize::Small)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(true),
|
||||
),
|
||||
)
|
||||
.cursor(CursorStyle::OperationNotAllowed)
|
||||
.opacity(0.5),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GitPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("GitPanel")
|
||||
.font_buffer(cx)
|
||||
.py_1()
|
||||
.id("git_panel")
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.bg(ElevationIndex::Surface.bg(cx))
|
||||
.child(self.render_panel_header(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.h(px(8.))
|
||||
.child(Divider::horizontal_dashed().color(DividerColor::Border)),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.h(px(8.))
|
||||
.child(Divider::horizontal_dashed().color(DividerColor::Border)),
|
||||
)
|
||||
.child(self.render_commit_editor(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for GitPanel {
|
||||
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for GitPanel {}
|
||||
|
||||
impl Panel for GitPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"GitPanel"
|
||||
}
|
||||
|
||||
fn position(&self, _cx: &gpui::WindowContext) -> DockPosition {
|
||||
DockPosition::Left
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
matches!(position, DockPosition::Left | DockPosition::Right)
|
||||
}
|
||||
|
||||
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn size(&self, _cx: &gpui::WindowContext) -> Pixels {
|
||||
self.width.unwrap_or(px(360.))
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
|
||||
self.width = size;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &gpui::WindowContext) -> Option<ui::IconName> {
|
||||
Some(ui::IconName::GitBranch)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
Some("Git Panel")
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
}
|
||||
1
crates/git_ui/src/git_ui.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod git_panel;
|
||||
254
crates/gpui/examples/gradient.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use gpui::{
|
||||
canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, size, App, AppContext,
|
||||
Bounds, ColorSpace, Half, Render, ViewContext, WindowOptions,
|
||||
};
|
||||
|
||||
struct GradientViewer {
|
||||
color_space: ColorSpace,
|
||||
}
|
||||
|
||||
impl GradientViewer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
color_space: ColorSpace::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GradientViewer {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let color_space = self.color_space;
|
||||
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child("Gradient Examples")
|
||||
.child(
|
||||
div().flex().gap_2().items_center().child(
|
||||
div()
|
||||
.id("method")
|
||||
.flex()
|
||||
.px_3()
|
||||
.py_1()
|
||||
.text_sm()
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.child(format!("{}", color_space))
|
||||
.active(|this| this.opacity(0.8))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.color_space = match this.color_space {
|
||||
ColorSpace::Oklab => ColorSpace::Srgb,
|
||||
ColorSpace::Srgb => ColorSpace::Oklab,
|
||||
};
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.rounded_xl()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(gpui::red())
|
||||
.text_color(gpui::white())
|
||||
.child("Solid Color"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.rounded_xl()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(gpui::blue())
|
||||
.text_color(gpui::white())
|
||||
.child("Solid Color"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.h_24()
|
||||
.text_color(gpui::white())
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
45.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
135.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::green(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
225.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
315.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::yellow(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.h_24()
|
||||
.text_color(gpui::white())
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
360.,
|
||||
linear_color_stop(gpui::yellow(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(gpui::green(), 0.05),
|
||||
linear_color_stop(gpui::yellow(), 0.95),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.05),
|
||||
linear_color_stop(gpui::red(), 0.95),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.child(
|
||||
div().flex().flex_1().gap_3().child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.5),
|
||||
linear_color_stop(gpui::red(), 0.5),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::blue(), 0.5),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(div().h_24().child(canvas(
|
||||
move |_, _| {},
|
||||
move |bounds, _, cx| {
|
||||
let size = size(bounds.size.width * 0.8, px(80.));
|
||||
let square_bounds = Bounds {
|
||||
origin: point(
|
||||
bounds.size.width.half() - size.width.half(),
|
||||
bounds.origin.y,
|
||||
),
|
||||
size,
|
||||
};
|
||||
let height = square_bounds.size.height;
|
||||
let horizontal_offset = height;
|
||||
let vertical_offset = px(30.);
|
||||
let mut path = gpui::Path::new(square_bounds.lower_left());
|
||||
path.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset));
|
||||
path.line_to(
|
||||
square_bounds.upper_right() + point(-horizontal_offset, vertical_offset),
|
||||
);
|
||||
path.line_to(square_bounds.lower_right());
|
||||
path.line_to(square_bounds.lower_left());
|
||||
cx.paint_path(
|
||||
path,
|
||||
linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space),
|
||||
);
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|_| GradientViewer::new()),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
@@ -548,6 +548,164 @@ impl<'de> Deserialize<'de> for Hsla {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) enum BackgroundTag {
|
||||
Solid = 0,
|
||||
LinearGradient = 1,
|
||||
}
|
||||
|
||||
/// A color space for color interpolation.
|
||||
///
|
||||
/// References:
|
||||
/// - https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method
|
||||
/// - https://www.w3.org/TR/css-color-4/#typedef-color-space
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
#[repr(C)]
|
||||
pub enum ColorSpace {
|
||||
#[default]
|
||||
/// The sRGB color space.
|
||||
Srgb = 0,
|
||||
/// The Oklab color space.
|
||||
Oklab = 1,
|
||||
}
|
||||
|
||||
impl Display for ColorSpace {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ColorSpace::Srgb => write!(f, "sRGB"),
|
||||
ColorSpace::Oklab => write!(f, "Oklab"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A background color, which can be either a solid color or a linear gradient.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct Background {
|
||||
pub(crate) tag: BackgroundTag,
|
||||
pub(crate) color_space: ColorSpace,
|
||||
pub(crate) solid: Hsla,
|
||||
pub(crate) angle: f32,
|
||||
pub(crate) colors: [LinearColorStop; 2],
|
||||
/// Padding for alignment for repr(C) layout.
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
impl Eq for Background {}
|
||||
impl Default for Background {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tag: BackgroundTag::Solid,
|
||||
solid: Hsla::default(),
|
||||
color_space: ColorSpace::default(),
|
||||
angle: 0.0,
|
||||
colors: [LinearColorStop::default(), LinearColorStop::default()],
|
||||
pad: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a LinearGradient background color.
|
||||
///
|
||||
/// The gradient line's angle of direction. A value of `0.` is equivalent to to top; increasing values rotate clockwise from there.
|
||||
///
|
||||
/// The `angle` is in degrees value in the range 0.0 to 360.0.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient
|
||||
pub fn linear_gradient(
|
||||
angle: f32,
|
||||
from: impl Into<LinearColorStop>,
|
||||
to: impl Into<LinearColorStop>,
|
||||
) -> Background {
|
||||
Background {
|
||||
tag: BackgroundTag::LinearGradient,
|
||||
angle,
|
||||
colors: [from.into(), to.into()],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A color stop in a linear gradient.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#linear-color-stop
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct LinearColorStop {
|
||||
/// The color of the color stop.
|
||||
pub color: Hsla,
|
||||
/// The percentage of the gradient, in the range 0.0 to 1.0.
|
||||
pub percentage: f32,
|
||||
}
|
||||
|
||||
/// Creates a new linear color stop.
|
||||
///
|
||||
/// The percentage of the gradient, in the range 0.0 to 1.0.
|
||||
pub fn linear_color_stop(color: impl Into<Hsla>, percentage: f32) -> LinearColorStop {
|
||||
LinearColorStop {
|
||||
color: color.into(),
|
||||
percentage,
|
||||
}
|
||||
}
|
||||
|
||||
impl LinearColorStop {
|
||||
/// Returns a new color stop with the same color, but with a modified alpha value.
|
||||
pub fn opacity(&self, factor: f32) -> Self {
|
||||
Self {
|
||||
percentage: self.percentage,
|
||||
color: self.color.opacity(factor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Background {
|
||||
/// Use specified color space for color interpolation.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method
|
||||
pub fn color_space(mut self, color_space: ColorSpace) -> Self {
|
||||
self.color_space = color_space;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value.
|
||||
pub fn opacity(&self, factor: f32) -> Self {
|
||||
let mut background = *self;
|
||||
background.solid = background.solid.opacity(factor);
|
||||
background.colors = [
|
||||
self.colors[0].opacity(factor),
|
||||
self.colors[1].opacity(factor),
|
||||
];
|
||||
background
|
||||
}
|
||||
|
||||
/// Returns whether the background color is transparent.
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
match self.tag {
|
||||
BackgroundTag::Solid => self.solid.is_transparent(),
|
||||
BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsla> for Background {
|
||||
fn from(value: Hsla) -> Self {
|
||||
Background {
|
||||
tag: BackgroundTag::Solid,
|
||||
solid: value,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<Rgba> for Background {
|
||||
fn from(value: Rgba) -> Self {
|
||||
Background {
|
||||
tag: BackgroundTag::Solid,
|
||||
solid: Hsla::from(value),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
@@ -595,4 +753,32 @@ mod tests {
|
||||
|
||||
assert_eq!(actual, rgba(0xdeadbeef))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_background_solid() {
|
||||
let color = Hsla::from(rgba(0xff0099ff));
|
||||
let mut background = Background::from(color);
|
||||
assert_eq!(background.tag, BackgroundTag::Solid);
|
||||
assert_eq!(background.solid, color);
|
||||
|
||||
assert_eq!(background.opacity(0.5).solid, color.opacity(0.5));
|
||||
assert_eq!(background.is_transparent(), false);
|
||||
background.solid = hsla(0.0, 0.0, 0.0, 0.0);
|
||||
assert_eq!(background.is_transparent(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_background_linear_gradient() {
|
||||
let from = linear_color_stop(rgba(0xff0099ff), 0.0);
|
||||
let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
|
||||
let background = linear_gradient(90.0, from, to);
|
||||
assert_eq!(background.tag, BackgroundTag::LinearGradient);
|
||||
assert_eq!(background.colors[0], from);
|
||||
assert_eq!(background.colors[1], to);
|
||||
|
||||
assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
|
||||
assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
|
||||
assert_eq!(background.is_transparent(), false);
|
||||
assert_eq!(background.opacity(0.0).is_transparent(), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use super::{BladeAtlas, PATH_TEXTURE_FORMAT};
|
||||
use crate::{
|
||||
AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, GPUSpecs, Hsla,
|
||||
AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, GPUSpecs,
|
||||
MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
|
||||
ScaledPixels, Scene, Shadow, Size, Underline,
|
||||
};
|
||||
@@ -174,7 +174,7 @@ struct ShaderSurfacesData {
|
||||
#[repr(C)]
|
||||
struct PathSprite {
|
||||
bounds: Bounds<ScaledPixels>,
|
||||
color: Hsla,
|
||||
color: Background,
|
||||
tile: AtlasTile,
|
||||
}
|
||||
|
||||
|
||||
@@ -15,18 +15,21 @@ struct Bounds {
|
||||
origin: vec2<f32>,
|
||||
size: vec2<f32>,
|
||||
}
|
||||
|
||||
struct Corners {
|
||||
top_left: f32,
|
||||
top_right: f32,
|
||||
bottom_right: f32,
|
||||
bottom_left: f32,
|
||||
}
|
||||
|
||||
struct Edges {
|
||||
top: f32,
|
||||
right: f32,
|
||||
bottom: f32,
|
||||
left: f32,
|
||||
}
|
||||
|
||||
struct Hsla {
|
||||
h: f32,
|
||||
s: f32,
|
||||
@@ -34,6 +37,24 @@ struct Hsla {
|
||||
a: f32,
|
||||
}
|
||||
|
||||
struct LinearColorStop {
|
||||
color: Hsla,
|
||||
percentage: f32,
|
||||
}
|
||||
|
||||
struct Background {
|
||||
// 0u is Solid
|
||||
// 1u is LinearGradient
|
||||
tag: u32,
|
||||
// 0u is sRGB linear color
|
||||
// 1u is Oklab color
|
||||
color_space: u32,
|
||||
solid: Hsla,
|
||||
angle: f32,
|
||||
colors: array<LinearColorStop, 2>,
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
struct AtlasTextureId {
|
||||
index: u32,
|
||||
kind: u32,
|
||||
@@ -43,6 +64,7 @@ struct AtlasBounds {
|
||||
origin: vec2<i32>,
|
||||
size: vec2<i32>,
|
||||
}
|
||||
|
||||
struct AtlasTile {
|
||||
texture_id: AtlasTextureId,
|
||||
tile_id: u32,
|
||||
@@ -96,6 +118,24 @@ fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
fn linear_to_srgb(linear: vec3<f32>) -> vec3<f32> {
|
||||
let cutoff = linear < vec3<f32>(0.0031308);
|
||||
let higher = vec3<f32>(1.055) * pow(linear, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
|
||||
let lower = linear * vec3<f32>(12.92);
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
/// Convert a linear color to sRGBA space.
|
||||
fn linear_to_srgba(color: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(linear_to_srgb(color.rgb), color.a);
|
||||
}
|
||||
|
||||
/// Convert a sRGBA color to linear space.
|
||||
fn srgba_to_linear(color: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(srgb_to_linear(color.rgb), color.a);
|
||||
}
|
||||
|
||||
/// Hsla to linear RGBA conversion.
|
||||
fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
|
||||
let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
|
||||
let s = hsla.s;
|
||||
@@ -135,6 +175,43 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
|
||||
return vec4<f32>(linear, a);
|
||||
}
|
||||
|
||||
/// Convert a linear sRGB to Oklab space.
|
||||
/// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
|
||||
fn linear_srgb_to_oklab(color: vec4<f32>) -> vec4<f32> {
|
||||
let l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
|
||||
let m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
|
||||
let s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
|
||||
|
||||
let l_ = pow(l, 1.0 / 3.0);
|
||||
let m_ = pow(m, 1.0 / 3.0);
|
||||
let s_ = pow(s, 1.0 / 3.0);
|
||||
|
||||
return vec4<f32>(
|
||||
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
|
||||
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
|
||||
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
|
||||
color.a
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert an Oklab color to linear sRGB space.
|
||||
fn oklab_to_linear_srgb(color: vec4<f32>) -> vec4<f32> {
|
||||
let l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
|
||||
let m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
|
||||
let s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
|
||||
|
||||
let l = l_ * l_ * l_;
|
||||
let m = m_ * m_ * m_;
|
||||
let s = s_ * s_ * s_;
|
||||
|
||||
return vec4<f32>(
|
||||
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
||||
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
||||
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
|
||||
color.a
|
||||
);
|
||||
}
|
||||
|
||||
fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
|
||||
let alpha = above.a + below.a * (1.0 - above.a);
|
||||
let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
|
||||
@@ -197,6 +274,94 @@ fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
|
||||
return vec4<f32>(color.rgb * multiplier, alpha);
|
||||
}
|
||||
|
||||
|
||||
struct GradientColor {
|
||||
solid: vec4<f32>,
|
||||
color0: vec4<f32>,
|
||||
color1: vec4<f32>,
|
||||
}
|
||||
|
||||
fn prepare_gradient_color(tag: u32, color_space: u32,
|
||||
solid: Hsla, colors: array<LinearColorStop, 2>) -> GradientColor {
|
||||
var result = GradientColor();
|
||||
|
||||
if (tag == 0u) {
|
||||
result.solid = hsla_to_rgba(solid);
|
||||
} else if (tag == 1u) {
|
||||
// The hsla_to_rgba is returns a linear sRGB color
|
||||
result.color0 = hsla_to_rgba(colors[0].color);
|
||||
result.color1 = hsla_to_rgba(colors[1].color);
|
||||
|
||||
// Prepare color space in vertex for avoid conversion
|
||||
// in fragment shader for performance reasons
|
||||
if (color_space == 0u) {
|
||||
// sRGB
|
||||
result.color0 = linear_to_srgba(result.color0);
|
||||
result.color1 = linear_to_srgba(result.color1);
|
||||
} else if (color_space == 1u) {
|
||||
// Oklab
|
||||
result.color0 = linear_srgb_to_oklab(result.color0);
|
||||
result.color1 = linear_srgb_to_oklab(result.color1);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn gradient_color(background: Background, position: vec2<f32>, bounds: Bounds,
|
||||
sold_color: vec4<f32>, color0: vec4<f32>, color1: vec4<f32>) -> vec4<f32> {
|
||||
var background_color = vec4<f32>(0.0);
|
||||
|
||||
switch (background.tag) {
|
||||
default: {
|
||||
return sold_color;
|
||||
}
|
||||
case 1u: {
|
||||
// Linear gradient background.
|
||||
// -90 degrees to match the CSS gradient angle.
|
||||
let radians = (background.angle % 360.0 - 90.0) * M_PI_F / 180.0;
|
||||
var direction = vec2<f32>(cos(radians), sin(radians));
|
||||
let stop0_percentage = background.colors[0].percentage;
|
||||
let stop1_percentage = background.colors[1].percentage;
|
||||
|
||||
// Expand the short side to be the same as the long side
|
||||
if (bounds.size.x > bounds.size.y) {
|
||||
direction.y *= bounds.size.y / bounds.size.x;
|
||||
} else {
|
||||
direction.x *= bounds.size.x / bounds.size.y;
|
||||
}
|
||||
|
||||
// Get the t value for the linear gradient with the color stop percentages.
|
||||
let half_size = bounds.size / 2.0;
|
||||
let center = bounds.origin + half_size;
|
||||
let center_to_point = position - center;
|
||||
var t = dot(center_to_point, direction) / length(direction);
|
||||
// Check the direct to determine the use x or y
|
||||
if (abs(direction.x) > abs(direction.y)) {
|
||||
t = (t + half_size.x) / bounds.size.x;
|
||||
} else {
|
||||
t = (t + half_size.y) / bounds.size.y;
|
||||
}
|
||||
|
||||
// Adjust t based on the stop percentages
|
||||
t = (t - stop0_percentage) / (stop1_percentage - stop0_percentage);
|
||||
t = clamp(t, 0.0, 1.0);
|
||||
|
||||
switch (background.color_space) {
|
||||
default: {
|
||||
background_color = srgba_to_linear(mix(color0, color1, t));
|
||||
}
|
||||
case 1u: {
|
||||
let oklab_color = mix(color0, color1, t);
|
||||
background_color = oklab_to_linear_srgb(oklab_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return background_color;
|
||||
}
|
||||
|
||||
// --- quads --- //
|
||||
|
||||
struct Quad {
|
||||
@@ -204,7 +369,7 @@ struct Quad {
|
||||
pad: u32,
|
||||
bounds: Bounds,
|
||||
content_mask: Bounds,
|
||||
background: Hsla,
|
||||
background: Background,
|
||||
border_color: Hsla,
|
||||
corner_radii: Corners,
|
||||
border_widths: Edges,
|
||||
@@ -213,11 +378,13 @@ var<storage, read> b_quads: array<Quad>;
|
||||
|
||||
struct QuadVarying {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) @interpolate(flat) background_color: vec4<f32>,
|
||||
@location(1) @interpolate(flat) border_color: vec4<f32>,
|
||||
@location(2) @interpolate(flat) quad_id: u32,
|
||||
//TODO: use `clip_distance` once Naga supports it
|
||||
@location(3) clip_distances: vec4<f32>,
|
||||
@location(0) @interpolate(flat) border_color: vec4<f32>,
|
||||
@location(1) @interpolate(flat) quad_id: u32,
|
||||
// TODO: use `clip_distance` once Naga supports it
|
||||
@location(2) clip_distances: vec4<f32>,
|
||||
@location(3) @interpolate(flat) background_solid: vec4<f32>,
|
||||
@location(4) @interpolate(flat) background_color0: vec4<f32>,
|
||||
@location(5) @interpolate(flat) background_color1: vec4<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
@@ -227,7 +394,16 @@ fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
|
||||
|
||||
var out = QuadVarying();
|
||||
out.position = to_device_position(unit_vertex, quad.bounds);
|
||||
out.background_color = hsla_to_rgba(quad.background);
|
||||
|
||||
let gradient = prepare_gradient_color(
|
||||
quad.background.tag,
|
||||
quad.background.color_space,
|
||||
quad.background.solid,
|
||||
quad.background.colors
|
||||
);
|
||||
out.background_solid = gradient.solid;
|
||||
out.background_color0 = gradient.color0;
|
||||
out.background_color1 = gradient.color1;
|
||||
out.border_color = hsla_to_rgba(quad.border_color);
|
||||
out.quad_id = instance_id;
|
||||
out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
|
||||
@@ -242,21 +418,23 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
}
|
||||
|
||||
let quad = b_quads[input.quad_id];
|
||||
let half_size = quad.bounds.size / 2.0;
|
||||
let center = quad.bounds.origin + half_size;
|
||||
let center_to_point = input.position.xy - center;
|
||||
|
||||
let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
|
||||
input.background_solid, input.background_color0, input.background_color1);
|
||||
|
||||
// Fast path when the quad is not rounded and doesn't have any border.
|
||||
if (quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 &&
|
||||
quad.corner_radii.top_right == 0.0 &&
|
||||
quad.corner_radii.bottom_right == 0.0 && quad.border_widths.top == 0.0 &&
|
||||
quad.border_widths.left == 0.0 && quad.border_widths.right == 0.0 &&
|
||||
quad.border_widths.bottom == 0.0) {
|
||||
return blend_color(input.background_color, 1.0);
|
||||
return blend_color(background_color, 1.0);
|
||||
}
|
||||
|
||||
let half_size = quad.bounds.size / 2.0;
|
||||
let center = quad.bounds.origin + half_size;
|
||||
let center_to_point = input.position.xy - center;
|
||||
|
||||
let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
|
||||
|
||||
let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
|
||||
let distance =
|
||||
length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
|
||||
@@ -277,13 +455,13 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
border_width = vertical_border;
|
||||
}
|
||||
|
||||
var color = input.background_color;
|
||||
var color = background_color;
|
||||
if (border_width > 0.0) {
|
||||
let inset_distance = distance + border_width;
|
||||
// Blend the border on top of the background and then linearly interpolate
|
||||
// between the two as we slide inside the background.
|
||||
let blended_border = over(input.background_color, input.border_color);
|
||||
color = mix(blended_border, input.background_color,
|
||||
let blended_border = over(background_color, input.border_color);
|
||||
color = mix(blended_border, background_color,
|
||||
saturate(0.5 - inset_distance));
|
||||
}
|
||||
|
||||
@@ -408,7 +586,7 @@ fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 {
|
||||
|
||||
struct PathSprite {
|
||||
bounds: Bounds,
|
||||
color: Hsla,
|
||||
color: Background,
|
||||
tile: AtlasTile,
|
||||
}
|
||||
var<storage, read> b_path_sprites: array<PathSprite>;
|
||||
@@ -416,7 +594,10 @@ var<storage, read> b_path_sprites: array<PathSprite>;
|
||||
struct PathVarying {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) tile_position: vec2<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
@location(1) @interpolate(flat) instance_id: u32,
|
||||
@location(2) @interpolate(flat) color_solid: vec4<f32>,
|
||||
@location(3) @interpolate(flat) color0: vec4<f32>,
|
||||
@location(4) @interpolate(flat) color1: vec4<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
@@ -428,7 +609,17 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
|
||||
var out = PathVarying();
|
||||
out.position = to_device_position(unit_vertex, sprite.bounds);
|
||||
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
|
||||
out.color = hsla_to_rgba(sprite.color);
|
||||
out.instance_id = instance_id;
|
||||
|
||||
let gradient = prepare_gradient_color(
|
||||
sprite.color.tag,
|
||||
sprite.color.color_space,
|
||||
sprite.color.solid,
|
||||
sprite.color.colors
|
||||
);
|
||||
out.color_solid = gradient.solid;
|
||||
out.color0 = gradient.color0;
|
||||
out.color1 = gradient.color1;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -436,7 +627,11 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
|
||||
fn fs_path(input: PathVarying) -> @location(0) vec4<f32> {
|
||||
let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
|
||||
let mask = 1.0 - abs(1.0 - sample % 2.0);
|
||||
return blend_color(input.color, mask);
|
||||
let sprite = b_path_sprites[input.instance_id];
|
||||
let background = sprite.color;
|
||||
let color = gradient_color(background, input.position.xy, sprite.bounds,
|
||||
input.color_solid, input.color0, input.color1);
|
||||
return blend_color(color, mask);
|
||||
}
|
||||
|
||||
// --- underlines --- //
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::metal_atlas::MetalAtlas;
|
||||
use crate::{
|
||||
point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels,
|
||||
Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
|
||||
point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask,
|
||||
DevicePixels, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
|
||||
PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -1242,7 +1242,7 @@ enum PathRasterizationInputIndex {
|
||||
#[repr(C)]
|
||||
pub struct PathSprite {
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub color: Hsla,
|
||||
pub color: Background,
|
||||
pub tile: AtlasTile,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
using namespace metal;
|
||||
|
||||
float4 hsla_to_rgba(Hsla hsla);
|
||||
float3 srgb_to_linear(float3 color);
|
||||
float3 linear_to_srgb(float3 color);
|
||||
float4 srgb_to_oklab(float4 color);
|
||||
float4 oklab_to_srgb(float4 color);
|
||||
float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
|
||||
constant Size_DevicePixels *viewport_size);
|
||||
float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds,
|
||||
@@ -21,20 +25,34 @@ float2 erf(float2 x);
|
||||
float blur_along_x(float x, float y, float sigma, float corner,
|
||||
float2 half_size);
|
||||
float4 over(float4 below, float4 above);
|
||||
float radians(float degrees);
|
||||
float4 gradient_color(Background background, float2 position, Bounds_ScaledPixels bounds,
|
||||
float4 solid_color, float4 color0, float4 color1);
|
||||
|
||||
struct GradientColor {
|
||||
float4 solid;
|
||||
float4 color0;
|
||||
float4 color1;
|
||||
};
|
||||
GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1);
|
||||
|
||||
struct QuadVertexOutput {
|
||||
float4 position [[position]];
|
||||
float4 background_color [[flat]];
|
||||
float4 border_color [[flat]];
|
||||
uint quad_id [[flat]];
|
||||
float4 position [[position]];
|
||||
float4 border_color [[flat]];
|
||||
float4 background_solid [[flat]];
|
||||
float4 background_color0 [[flat]];
|
||||
float4 background_color1 [[flat]];
|
||||
float clip_distance [[clip_distance]][4];
|
||||
};
|
||||
|
||||
struct QuadFragmentInput {
|
||||
float4 position [[position]];
|
||||
float4 background_color [[flat]];
|
||||
float4 border_color [[flat]];
|
||||
uint quad_id [[flat]];
|
||||
float4 position [[position]];
|
||||
float4 border_color [[flat]];
|
||||
float4 background_solid [[flat]];
|
||||
float4 background_color0 [[flat]];
|
||||
float4 background_color1 [[flat]];
|
||||
};
|
||||
|
||||
vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
|
||||
@@ -51,13 +69,23 @@ vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
|
||||
to_device_position(unit_vertex, quad.bounds, viewport_size);
|
||||
float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds,
|
||||
quad.content_mask.bounds);
|
||||
float4 background_color = hsla_to_rgba(quad.background);
|
||||
float4 border_color = hsla_to_rgba(quad.border_color);
|
||||
|
||||
GradientColor gradient = prepare_gradient_color(
|
||||
quad.background.tag,
|
||||
quad.background.color_space,
|
||||
quad.background.solid,
|
||||
quad.background.colors[0].color,
|
||||
quad.background.colors[1].color
|
||||
);
|
||||
|
||||
return QuadVertexOutput{
|
||||
device_position,
|
||||
background_color,
|
||||
border_color,
|
||||
quad_id,
|
||||
device_position,
|
||||
border_color,
|
||||
gradient.solid,
|
||||
gradient.color0,
|
||||
gradient.color1,
|
||||
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
|
||||
}
|
||||
|
||||
@@ -65,6 +93,11 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
constant Quad *quads
|
||||
[[buffer(QuadInputIndex_Quads)]]) {
|
||||
Quad quad = quads[input.quad_id];
|
||||
float2 half_size = float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
|
||||
float2 center = float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
|
||||
float2 center_to_point = input.position.xy - center;
|
||||
float4 color = gradient_color(quad.background, input.position.xy, quad.bounds,
|
||||
input.background_solid, input.background_color0, input.background_color1);
|
||||
|
||||
// Fast path when the quad is not rounded and doesn't have any border.
|
||||
if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. &&
|
||||
@@ -72,14 +105,9 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. &&
|
||||
quad.border_widths.left == 0. && quad.border_widths.right == 0. &&
|
||||
quad.border_widths.bottom == 0.) {
|
||||
return input.background_color;
|
||||
return color;
|
||||
}
|
||||
|
||||
float2 half_size =
|
||||
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
|
||||
float2 center =
|
||||
float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
|
||||
float2 center_to_point = input.position.xy - center;
|
||||
float corner_radius;
|
||||
if (center_to_point.x < 0.) {
|
||||
if (center_to_point.y < 0.) {
|
||||
@@ -118,15 +146,12 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
border_width = vertical_border;
|
||||
}
|
||||
|
||||
float4 color;
|
||||
if (border_width == 0.) {
|
||||
color = input.background_color;
|
||||
} else {
|
||||
if (border_width != 0.) {
|
||||
float inset_distance = distance + border_width;
|
||||
// Blend the border on top of the background and then linearly interpolate
|
||||
// between the two as we slide inside the background.
|
||||
float4 blended_border = over(input.background_color, input.border_color);
|
||||
color = mix(blended_border, input.background_color,
|
||||
float4 blended_border = over(color, input.border_color);
|
||||
color = mix(blended_border, color,
|
||||
saturate(0.5 - inset_distance));
|
||||
}
|
||||
|
||||
@@ -437,7 +462,10 @@ fragment float4 path_rasterization_fragment(PathRasterizationFragmentInput input
|
||||
struct PathSpriteVertexOutput {
|
||||
float4 position [[position]];
|
||||
float2 tile_position;
|
||||
float4 color [[flat]];
|
||||
uint sprite_id [[flat]];
|
||||
float4 solid_color [[flat]];
|
||||
float4 color0 [[flat]];
|
||||
float4 color1 [[flat]];
|
||||
};
|
||||
|
||||
vertex PathSpriteVertexOutput path_sprite_vertex(
|
||||
@@ -456,8 +484,23 @@ vertex PathSpriteVertexOutput path_sprite_vertex(
|
||||
float4 device_position =
|
||||
to_device_position(unit_vertex, sprite.bounds, viewport_size);
|
||||
float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
|
||||
float4 color = hsla_to_rgba(sprite.color);
|
||||
return PathSpriteVertexOutput{device_position, tile_position, color};
|
||||
|
||||
GradientColor gradient = prepare_gradient_color(
|
||||
sprite.color.tag,
|
||||
sprite.color.color_space,
|
||||
sprite.color.solid,
|
||||
sprite.color.colors[0].color,
|
||||
sprite.color.colors[1].color
|
||||
);
|
||||
|
||||
return PathSpriteVertexOutput{
|
||||
device_position,
|
||||
tile_position,
|
||||
sprite_id,
|
||||
gradient.solid,
|
||||
gradient.color0,
|
||||
gradient.color1
|
||||
};
|
||||
}
|
||||
|
||||
fragment float4 path_sprite_fragment(
|
||||
@@ -469,7 +512,10 @@ fragment float4 path_sprite_fragment(
|
||||
float4 sample =
|
||||
atlas_texture.sample(atlas_texture_sampler, input.tile_position);
|
||||
float mask = 1. - abs(1. - fmod(sample.r, 2.));
|
||||
float4 color = input.color;
|
||||
PathSprite sprite = sprites[input.sprite_id];
|
||||
Background background = sprite.color;
|
||||
float4 color = gradient_color(background, input.position.xy, sprite.bounds,
|
||||
input.solid_color, input.color0, input.color1);
|
||||
color.a *= mask;
|
||||
return color;
|
||||
}
|
||||
@@ -574,6 +620,56 @@ float4 hsla_to_rgba(Hsla hsla) {
|
||||
return rgba;
|
||||
}
|
||||
|
||||
float3 srgb_to_linear(float3 color) {
|
||||
return pow(color, float3(2.2));
|
||||
}
|
||||
|
||||
float3 linear_to_srgb(float3 color) {
|
||||
return pow(color, float3(1.0 / 2.2));
|
||||
}
|
||||
|
||||
// Converts a sRGB color to the Oklab color space.
|
||||
// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
|
||||
float4 srgb_to_oklab(float4 color) {
|
||||
// Convert non-linear sRGB to linear sRGB
|
||||
color = float4(srgb_to_linear(color.rgb), color.a);
|
||||
|
||||
float l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
|
||||
float m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
|
||||
float s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
|
||||
|
||||
float l_ = pow(l, 1.0/3.0);
|
||||
float m_ = pow(m, 1.0/3.0);
|
||||
float s_ = pow(s, 1.0/3.0);
|
||||
|
||||
return float4(
|
||||
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
|
||||
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
|
||||
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
|
||||
color.a
|
||||
);
|
||||
}
|
||||
|
||||
// Converts an Oklab color to the sRGB color space.
|
||||
float4 oklab_to_srgb(float4 color) {
|
||||
float l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
|
||||
float m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
|
||||
float s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
|
||||
|
||||
float l = l_ * l_ * l_;
|
||||
float m = m_ * m_ * m_;
|
||||
float s = s_ * s_ * s_;
|
||||
|
||||
float3 linear_rgb = float3(
|
||||
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
||||
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
||||
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
|
||||
);
|
||||
|
||||
// Convert linear sRGB to non-linear sRGB
|
||||
return float4(linear_to_srgb(linear_rgb), color.a);
|
||||
}
|
||||
|
||||
float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
|
||||
constant Size_DevicePixels *input_viewport_size) {
|
||||
float2 position =
|
||||
@@ -691,3 +787,81 @@ float4 over(float4 below, float4 above) {
|
||||
result.a = alpha;
|
||||
return result;
|
||||
}
|
||||
|
||||
GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid,
|
||||
Hsla color0, Hsla color1) {
|
||||
GradientColor out;
|
||||
if (tag == 0) {
|
||||
out.solid = hsla_to_rgba(solid);
|
||||
} else if (tag == 1) {
|
||||
out.color0 = hsla_to_rgba(color0);
|
||||
out.color1 = hsla_to_rgba(color1);
|
||||
|
||||
// Prepare color space in vertex for avoid conversion
|
||||
// in fragment shader for performance reasons
|
||||
if (color_space == 1) {
|
||||
// Oklab
|
||||
out.color0 = srgb_to_oklab(out.color0);
|
||||
out.color1 = srgb_to_oklab(out.color1);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
float4 gradient_color(Background background,
|
||||
float2 position,
|
||||
Bounds_ScaledPixels bounds,
|
||||
float4 solid_color, float4 color0, float4 color1) {
|
||||
float4 color;
|
||||
|
||||
switch (background.tag) {
|
||||
case 0:
|
||||
color = solid_color;
|
||||
break;
|
||||
case 1: {
|
||||
// -90 degrees to match the CSS gradient angle.
|
||||
float radians = (fmod(background.angle, 360.0) - 90.0) * (M_PI_F / 180.0);
|
||||
float2 direction = float2(cos(radians), sin(radians));
|
||||
|
||||
// Expand the short side to be the same as the long side
|
||||
if (bounds.size.width > bounds.size.height) {
|
||||
direction.y *= bounds.size.height / bounds.size.width;
|
||||
} else {
|
||||
direction.x *= bounds.size.width / bounds.size.height;
|
||||
}
|
||||
|
||||
// Get the t value for the linear gradient with the color stop percentages.
|
||||
float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.;
|
||||
float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
|
||||
float2 center_to_point = position - center;
|
||||
float t = dot(center_to_point, direction) / length(direction);
|
||||
// Check the direct to determine the use x or y
|
||||
if (abs(direction.x) > abs(direction.y)) {
|
||||
t = (t + half_size.x) / bounds.size.width;
|
||||
} else {
|
||||
t = (t + half_size.y) / bounds.size.height;
|
||||
}
|
||||
|
||||
// Adjust t based on the stop percentages
|
||||
t = (t - background.colors[0].percentage)
|
||||
/ (background.colors[1].percentage
|
||||
- background.colors[0].percentage);
|
||||
t = clamp(t, 0.0, 1.0);
|
||||
|
||||
switch (background.color_space) {
|
||||
case 0:
|
||||
color = mix(color0, color1, t);
|
||||
break;
|
||||
case 1: {
|
||||
float4 oklab_color = mix(color0, color1, t);
|
||||
color = oklab_to_srgb(oklab_color);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! application to avoid having to import each trait individually.
|
||||
|
||||
pub use crate::{
|
||||
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
|
||||
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context as _, Element, FocusableElement,
|
||||
InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
|
||||
StatefulInteractiveElement, Styled, StyledImage, VisualContext,
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#![cfg_attr(windows, allow(dead_code))]
|
||||
|
||||
use crate::{
|
||||
bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges,
|
||||
Hsla, Pixels, Point, Radians, ScaledPixels, Size,
|
||||
bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Background, Bounds, ContentMask,
|
||||
Corners, Edges, Hsla, Pixels, Point, Radians, ScaledPixels, Size,
|
||||
};
|
||||
use std::{fmt::Debug, iter::Peekable, ops::Range, slice};
|
||||
|
||||
@@ -458,7 +458,7 @@ pub(crate) struct Quad {
|
||||
pub pad: u32, // align to 8 bytes
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub background: Hsla,
|
||||
pub background: Background,
|
||||
pub border_color: Hsla,
|
||||
pub corner_radii: Corners<ScaledPixels>,
|
||||
pub border_widths: Edges<ScaledPixels>,
|
||||
@@ -671,7 +671,7 @@ pub struct Path<P: Clone + Default + Debug> {
|
||||
pub(crate) bounds: Bounds<P>,
|
||||
pub(crate) content_mask: ContentMask<P>,
|
||||
pub(crate) vertices: Vec<PathVertex<P>>,
|
||||
pub(crate) color: Hsla,
|
||||
pub(crate) color: Background,
|
||||
start: Point<P>,
|
||||
current: Point<P>,
|
||||
contour_count: usize,
|
||||
|
||||
@@ -5,10 +5,11 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
black, phi, point, quad, rems, size, AbsoluteLength, Bounds, ContentMask, Corners,
|
||||
CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font,
|
||||
FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point,
|
||||
PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
|
||||
black, phi, point, quad, rems, size, AbsoluteLength, Background, BackgroundTag, Bounds,
|
||||
ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges,
|
||||
EdgesRefinement, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length,
|
||||
Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun,
|
||||
WindowContext,
|
||||
};
|
||||
use collections::HashSet;
|
||||
use refineable::Refineable;
|
||||
@@ -572,7 +573,17 @@ impl Style {
|
||||
|
||||
let background_color = self.background.as_ref().and_then(Fill::color);
|
||||
if background_color.map_or(false, |color| !color.is_transparent()) {
|
||||
let mut border_color = background_color.unwrap_or_default();
|
||||
let mut border_color = match background_color {
|
||||
Some(color) => match color.tag {
|
||||
BackgroundTag::Solid => color.solid,
|
||||
BackgroundTag::LinearGradient => color
|
||||
.colors
|
||||
.first()
|
||||
.map(|stop| stop.color)
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
None => Hsla::default(),
|
||||
};
|
||||
border_color.a = 0.;
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
@@ -737,12 +748,14 @@ pub struct StrikethroughStyle {
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Fill {
|
||||
/// A solid color fill.
|
||||
Color(Hsla),
|
||||
Color(Background),
|
||||
}
|
||||
|
||||
impl Fill {
|
||||
/// Unwrap this fill into a solid color, if it is one.
|
||||
pub fn color(&self) -> Option<Hsla> {
|
||||
///
|
||||
/// If the fill is not a solid color, this method returns `None`.
|
||||
pub fn color(&self) -> Option<Background> {
|
||||
match self {
|
||||
Fill::Color(color) => Some(*color),
|
||||
}
|
||||
@@ -751,13 +764,13 @@ impl Fill {
|
||||
|
||||
impl Default for Fill {
|
||||
fn default() -> Self {
|
||||
Self::Color(Hsla::default())
|
||||
Self::Color(Background::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsla> for Fill {
|
||||
fn from(color: Hsla) -> Self {
|
||||
Self::Color(color)
|
||||
Self::Color(color.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,6 +780,12 @@ impl From<Rgba> for Fill {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Background> for Fill {
|
||||
fn from(background: Background) -> Self {
|
||||
Self::Color(background)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TextStyle> for HighlightStyle {
|
||||
fn from(other: TextStyle) -> Self {
|
||||
Self::from(&other)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip,
|
||||
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow,
|
||||
Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
|
||||
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Background, Bounds,
|
||||
BoxShadow, Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
|
||||
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
|
||||
FileDropEvent, Flatten, FontId, GPUSpecs, Global, GlobalElementId, GlyphId, Hsla, InputHandler,
|
||||
IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent,
|
||||
@@ -2325,7 +2325,7 @@ impl<'a> WindowContext<'a> {
|
||||
/// Paint the given `Path` into the scene for the next frame at the current z-index.
|
||||
///
|
||||
/// This method should only be called as part of the paint phase of element drawing.
|
||||
pub fn paint_path(&mut self, mut path: Path<Pixels>, color: impl Into<Hsla>) {
|
||||
pub fn paint_path(&mut self, mut path: Path<Pixels>, color: impl Into<Background>) {
|
||||
debug_assert_eq!(
|
||||
self.window.draw_phase,
|
||||
DrawPhase::Paint,
|
||||
@@ -2336,7 +2336,8 @@ impl<'a> WindowContext<'a> {
|
||||
let content_mask = self.content_mask();
|
||||
let opacity = self.element_opacity();
|
||||
path.content_mask = content_mask;
|
||||
path.color = color.into().opacity(opacity);
|
||||
let color: Background = color.into();
|
||||
path.color = color.opacity(opacity);
|
||||
self.window
|
||||
.next_frame
|
||||
.scene
|
||||
@@ -4980,7 +4981,7 @@ pub struct PaintQuad {
|
||||
/// The radii of the quad's corners.
|
||||
pub corner_radii: Corners<Pixels>,
|
||||
/// The background color of the quad.
|
||||
pub background: Hsla,
|
||||
pub background: Background,
|
||||
/// The widths of the quad's borders.
|
||||
pub border_widths: Edges<Pixels>,
|
||||
/// The color of the quad's borders.
|
||||
@@ -5013,7 +5014,7 @@ impl PaintQuad {
|
||||
}
|
||||
|
||||
/// Sets the background color of the quad.
|
||||
pub fn background(self, background: impl Into<Hsla>) -> Self {
|
||||
pub fn background(self, background: impl Into<Background>) -> Self {
|
||||
PaintQuad {
|
||||
background: background.into(),
|
||||
..self
|
||||
@@ -5025,7 +5026,7 @@ impl PaintQuad {
|
||||
pub fn quad(
|
||||
bounds: Bounds<Pixels>,
|
||||
corner_radii: impl Into<Corners<Pixels>>,
|
||||
background: impl Into<Hsla>,
|
||||
background: impl Into<Background>,
|
||||
border_widths: impl Into<Edges<Pixels>>,
|
||||
border_color: impl Into<Hsla>,
|
||||
) -> PaintQuad {
|
||||
@@ -5039,7 +5040,7 @@ pub fn quad(
|
||||
}
|
||||
|
||||
/// Creates a filled quad with the given bounds and background color.
|
||||
pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Hsla>) -> PaintQuad {
|
||||
pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Background>) -> PaintQuad {
|
||||
PaintQuad {
|
||||
bounds: bounds.into(),
|
||||
corner_radii: (0.).into(),
|
||||
@@ -5054,7 +5055,7 @@ pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>)
|
||||
PaintQuad {
|
||||
bounds: bounds.into(),
|
||||
corner_radii: (0.).into(),
|
||||
background: transparent_black(),
|
||||
background: transparent_black().into(),
|
||||
border_widths: (1.).into(),
|
||||
border_color: border_color.into(),
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ impl Render for InlineCompletionButton {
|
||||
}
|
||||
|
||||
div().child(
|
||||
Button::new("zeta", "Zeta")
|
||||
Button::new("zeta", "ζ")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
|
||||
@@ -3,9 +3,8 @@ use crate::{
|
||||
LanguageModelProviderState,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
use gpui::{AppContext, EventEmitter, Global, Model, ModelContext};
|
||||
use gpui::{prelude::*, AppContext, EventEmitter, Global, Model, ModelContext};
|
||||
use std::sync::Arc;
|
||||
use ui::Context;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let registry = cx.new_model(|_cx| LanguageModelRegistry::default());
|
||||
|
||||
@@ -22,11 +22,7 @@ use language_model::{
|
||||
use settings::SettingsStore;
|
||||
use std::time::Duration;
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::{
|
||||
div, h_flex, v_flex, Button, ButtonCommon, Clickable, Color, Context, FixedWidth, Icon,
|
||||
IconName, IconPosition, IconSize, IntoElement, Label, LabelCommon, ParentElement, Styled,
|
||||
ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
|
||||
use super::anthropic::count_anthropic_tokens;
|
||||
use super::open_ai::count_open_ai_tokens;
|
||||
|
||||
@@ -447,7 +447,7 @@ impl Render for ConfigurationView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let is_authenticated = self.state.read(cx).is_authenticated();
|
||||
|
||||
let ollama_intro = "Get up and running with Llama 3.2, Mistral, Gemma 2, and other large language models with Ollama.";
|
||||
let ollama_intro = "Get up and running with Llama 3.3, Mistral, Gemma 2, and other large language models with Ollama.";
|
||||
let ollama_reqs =
|
||||
"Ollama must be running with at least one model installed to use it in the assistant.";
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
|
||||
|
||||
let _rust_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/test.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/test.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name = "C++"
|
||||
grammar = "cpp"
|
||||
path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "cu", "cuh"]
|
||||
path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "cu", "cuh", "C", "H"]
|
||||
line_comments = ["// ", "/// ", "//! "]
|
||||
autoclose_before = ";:.,=}])>"
|
||||
brackets = [
|
||||
|
||||
@@ -89,6 +89,7 @@ pub enum Event {
|
||||
},
|
||||
Edited {
|
||||
singleton_buffer_edited: bool,
|
||||
edited_buffer: Option<Model<Buffer>>,
|
||||
},
|
||||
TransactionUndone {
|
||||
transaction_id: TransactionId,
|
||||
@@ -114,6 +115,8 @@ pub struct MultiBufferDiffHunk {
|
||||
pub row_range: Range<MultiBufferRow>,
|
||||
/// The buffer ID that this hunk belongs to.
|
||||
pub buffer_id: BufferId,
|
||||
/// The ID of the excerpt where this hunk appears.
|
||||
pub excerpt_id: ExcerptId,
|
||||
/// The range of the underlying buffer that this hunk corresponds to.
|
||||
pub buffer_range: Range<text::Anchor>,
|
||||
/// The range within the buffer's diff base that this hunk corresponds to.
|
||||
@@ -250,7 +253,7 @@ struct Excerpt {
|
||||
///
|
||||
/// Contains methods for getting the [`Buffer`] of the excerpt,
|
||||
/// as well as mapping offsets to/from buffer and multibuffer coordinates.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MultiBufferExcerpt<'a> {
|
||||
excerpt: &'a Excerpt,
|
||||
excerpt_offset: usize,
|
||||
@@ -1485,6 +1488,7 @@ impl MultiBuffer {
|
||||
}]);
|
||||
cx.emit(Event::Edited {
|
||||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
});
|
||||
cx.emit(Event::ExcerptsAdded {
|
||||
buffer,
|
||||
@@ -1512,6 +1516,7 @@ impl MultiBuffer {
|
||||
}]);
|
||||
cx.emit(Event::Edited {
|
||||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
});
|
||||
cx.emit(Event::ExcerptsRemoved { ids });
|
||||
cx.notify();
|
||||
@@ -1753,6 +1758,7 @@ impl MultiBuffer {
|
||||
self.subscriptions.publish_mut(edits);
|
||||
cx.emit(Event::Edited {
|
||||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
});
|
||||
cx.emit(Event::ExcerptsRemoved { ids });
|
||||
cx.notify();
|
||||
@@ -1816,6 +1822,7 @@ impl MultiBuffer {
|
||||
cx.emit(match event {
|
||||
language::BufferEvent::Edited => Event::Edited {
|
||||
singleton_buffer_edited: true,
|
||||
edited_buffer: Some(buffer.clone()),
|
||||
},
|
||||
language::BufferEvent::DirtyChanged => Event::DirtyChanged,
|
||||
language::BufferEvent::Saved => Event::Saved,
|
||||
@@ -1979,6 +1986,7 @@ impl MultiBuffer {
|
||||
self.subscriptions.publish_mut(edits);
|
||||
cx.emit(Event::Edited {
|
||||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
});
|
||||
cx.emit(Event::ExcerptsExpanded { ids: vec![id] });
|
||||
cx.notify();
|
||||
@@ -2076,6 +2084,7 @@ impl MultiBuffer {
|
||||
self.subscriptions.publish_mut(edits);
|
||||
cx.emit(Event::Edited {
|
||||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
});
|
||||
cx.emit(Event::ExcerptsExpanded { ids });
|
||||
cx.notify();
|
||||
@@ -3439,17 +3448,13 @@ impl MultiBufferSnapshot {
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<(usize, Point)>(&());
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
cursor.prev(&());
|
||||
|
||||
iter::from_fn(move || {
|
||||
let item = cursor.item()?;
|
||||
let hit =
|
||||
cursor.start().0 < range.end || (range.is_empty() && cursor.start().0 == range.end);
|
||||
let excerpt = hit.then(|| MultiBufferExcerpt::new(item, *cursor.start()));
|
||||
cursor.next(&());
|
||||
if cursor.start().0 < range.end {
|
||||
cursor
|
||||
.item()
|
||||
.map(|item| MultiBufferExcerpt::new(item, *cursor.start()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
excerpt
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5363,13 +5368,16 @@ mod tests {
|
||||
events.read().as_slice(),
|
||||
&[
|
||||
Event::Edited {
|
||||
singleton_buffer_edited: false
|
||||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
},
|
||||
Event::Edited {
|
||||
singleton_buffer_edited: false
|
||||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
},
|
||||
Event::Edited {
|
||||
singleton_buffer_edited: false
|
||||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
lsp_store::OpenLspBufferHandle,
|
||||
search::SearchQuery,
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
ProjectItem as _, ProjectPath,
|
||||
@@ -47,6 +48,7 @@ pub struct BufferStore {
|
||||
struct SharedBuffer {
|
||||
buffer: Model<Buffer>,
|
||||
unstaged_changes: Option<Model<BufferChangeSet>>,
|
||||
lsp_handle: Option<OpenLspBufferHandle>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -1571,6 +1573,21 @@ impl BufferStore {
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn register_shared_lsp_handle(
|
||||
&mut self,
|
||||
peer_id: proto::PeerId,
|
||||
buffer_id: BufferId,
|
||||
handle: OpenLspBufferHandle,
|
||||
) {
|
||||
if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id) {
|
||||
if let Some(buffer) = shared_buffers.get_mut(&buffer_id) {
|
||||
buffer.lsp_handle = Some(handle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
debug_panic!("tried to register shared lsp handle, but buffer was not shared")
|
||||
}
|
||||
|
||||
pub fn handle_synchronize_buffers(
|
||||
&mut self,
|
||||
envelope: TypedEnvelope<proto::SynchronizeBuffers>,
|
||||
@@ -1597,6 +1614,7 @@ impl BufferStore {
|
||||
.or_insert_with(|| SharedBuffer {
|
||||
buffer: buffer.clone(),
|
||||
unstaged_changes: None,
|
||||
lsp_handle: None,
|
||||
});
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
@@ -2017,6 +2035,7 @@ impl BufferStore {
|
||||
SharedBuffer {
|
||||
buffer: buffer.clone(),
|
||||
unstaged_changes: None,
|
||||
lsp_handle: None,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2223,7 +2242,7 @@ impl BufferChangeSet {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> oneshot::Receiver<()> {
|
||||
LineEnding::normalize(&mut base_text);
|
||||
self.recalculate_diff_internal(base_text, buffer_snapshot, true, cx)
|
||||
self.recalculate_diff_internal(Rope::from(base_text), buffer_snapshot, true, cx)
|
||||
}
|
||||
|
||||
pub fn unset_base_text(
|
||||
@@ -2245,8 +2264,13 @@ impl BufferChangeSet {
|
||||
buffer_snapshot: text::BufferSnapshot,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> oneshot::Receiver<()> {
|
||||
if let Some(base_text) = self.base_text.clone() {
|
||||
self.recalculate_diff_internal(base_text.read(cx).text(), buffer_snapshot, false, cx)
|
||||
if let Some(base_text) = self.base_text.as_ref() {
|
||||
self.recalculate_diff_internal(
|
||||
base_text.read(cx).as_rope().clone(),
|
||||
buffer_snapshot,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
oneshot::channel().1
|
||||
}
|
||||
@@ -2254,7 +2278,7 @@ impl BufferChangeSet {
|
||||
|
||||
fn recalculate_diff_internal(
|
||||
&mut self,
|
||||
base_text: String,
|
||||
base_text: Rope,
|
||||
buffer_snapshot: text::BufferSnapshot,
|
||||
base_text_changed: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
@@ -2273,7 +2297,7 @@ impl BufferChangeSet {
|
||||
if base_text_changed {
|
||||
this.base_text_version += 1;
|
||||
this.base_text = Some(cx.new_model(|cx| {
|
||||
Buffer::local_normalized(Rope::from(base_text), LineEnding::default(), cx)
|
||||
Buffer::local_normalized(base_text, LineEnding::default(), cx)
|
||||
}));
|
||||
}
|
||||
this.diff_to_buffer = diff;
|
||||
|
||||
@@ -1254,6 +1254,10 @@ impl Project {
|
||||
self.buffer_store.read(cx).buffers().collect()
|
||||
}
|
||||
|
||||
pub fn environment(&self) -> &Model<ProjectEnvironment> {
|
||||
&self.environment
|
||||
}
|
||||
|
||||
pub fn cli_environment(&self, cx: &AppContext) -> Option<HashMap<String, String>> {
|
||||
self.environment.read(cx).get_cli_environment()
|
||||
}
|
||||
@@ -1843,6 +1847,19 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn open_local_buffer_with_lsp(
|
||||
&mut self,
|
||||
abs_path: impl AsRef<Path>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<(Model<Buffer>, lsp_store::OpenLspBufferHandle)>> {
|
||||
if let Some((worktree, relative_path)) = self.find_worktree(abs_path.as_ref(), cx) {
|
||||
self.open_buffer_with_lsp((worktree.read(cx).id(), relative_path), cx)
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no such path")))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_buffer(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
@@ -1857,6 +1874,23 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn open_buffer_with_lsp(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> 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 {
|
||||
let buffer = buffer.await?;
|
||||
let handle = lsp_store.update(&mut cx, |lsp_store, cx| {
|
||||
lsp_store.register_buffer_with_language_servers(&buffer, cx)
|
||||
})?;
|
||||
Ok((buffer, handle))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_unstaged_changes(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
|
||||
@@ -442,17 +442,17 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
|
||||
// Open a buffer without an associated language server.
|
||||
let toml_buffer = project
|
||||
let (toml_buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/Cargo.toml", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/Cargo.toml", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Open a buffer with an associated language server before the language for it has been loaded.
|
||||
let rust_buffer = project
|
||||
let (rust_buffer, _handle2) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/test.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/test.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -513,9 +513,9 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
|
||||
// Open a third buffer with a different associated language server.
|
||||
let json_buffer = project
|
||||
let (json_buffer, _json_handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/package.json", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/package.json", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -550,9 +550,9 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
// When opening another buffer whose language server is already running,
|
||||
// it is also configured based on the existing language server's capabilities.
|
||||
let rust_buffer2 = project
|
||||
let (rust_buffer2, _handle4) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/test2.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/test2.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -765,7 +765,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
|
||||
// Close notifications are reported only to servers matching the buffer's language.
|
||||
cx.update(|_| drop(json_buffer));
|
||||
cx.update(|_| drop(_json_handle));
|
||||
let close_message = lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(
|
||||
lsp::Url::from_file_path("/the-root/package.json").unwrap(),
|
||||
@@ -827,9 +827,9 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Start the language server by opening a buffer with a compatible file extension.
|
||||
let _buffer = project
|
||||
let _ = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/src/a.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/src/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1239,8 +1239,10 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
|
||||
let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
|
||||
|
||||
// Cause worktree to start the fake language server
|
||||
let _buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
|
||||
let _ = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/b.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1259,6 +1261,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
|
||||
fake_server
|
||||
.start_progress(format!("{}/0", progress_token))
|
||||
.await;
|
||||
assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
|
||||
assert_eq!(
|
||||
events.next().await.unwrap(),
|
||||
Event::DiskBasedDiagnosticsStarted {
|
||||
@@ -1365,8 +1368,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
|
||||
|
||||
let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1390,6 +1395,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
|
||||
Some(worktree_id)
|
||||
)
|
||||
);
|
||||
assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
|
||||
fake_server.start_progress(progress_token).await;
|
||||
assert_eq!(
|
||||
events.next().await.unwrap(),
|
||||
@@ -1438,8 +1444,10 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
|
||||
language_registry.add(rust_lang());
|
||||
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
let (buffer, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1517,8 +1525,10 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
|
||||
language_registry.add(rust_lang());
|
||||
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1565,8 +1575,10 @@ async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1634,11 +1646,15 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
|
||||
language_registry.add(js_lang());
|
||||
|
||||
let _rs_buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let _js_buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx))
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/b.js", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1734,6 +1750,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
fs.insert_tree("/dir", json!({ "a.rs": text })).await;
|
||||
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
|
||||
language_registry.add(rust_lang());
|
||||
@@ -1750,6 +1767,10 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _handle = lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.register_buffer_with_language_servers(&buffer, cx)
|
||||
});
|
||||
|
||||
let mut fake_server = fake_servers.next().await.unwrap();
|
||||
let open_notification = fake_server
|
||||
.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
@@ -2162,8 +2183,10 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
|
||||
language_registry.add(rust_lang());
|
||||
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2533,8 +2556,10 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
|
||||
language_registry.add(rust_lang());
|
||||
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/b.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2638,8 +2663,8 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2730,8 +2755,8 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2793,8 +2818,8 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -3984,7 +4009,7 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let _ = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/one.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/dir/one.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4086,9 +4111,9 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/one.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/dir/one.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4951,8 +4976,8 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
|
||||
),
|
||||
];
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.tsx", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
@@ -5060,8 +5085,8 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
@@ -5130,8 +5155,8 @@ async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
@@ -5251,8 +5276,8 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
|
||||
),
|
||||
];
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.tsx", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
@@ -4262,7 +4262,6 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::path::{Path, PathBuf};
|
||||
use ui::Context;
|
||||
use workspace::{
|
||||
item::{Item, ProjectItem},
|
||||
register_project_item, AppState,
|
||||
|
||||
@@ -292,7 +292,7 @@ mod tests {
|
||||
|
||||
let _buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/test.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/dir/test.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -304,7 +304,9 @@ message Envelope {
|
||||
InstallExtension install_extension = 287;
|
||||
|
||||
GetStagedText get_staged_text = 288;
|
||||
GetStagedTextResponse get_staged_text_response = 289; // current max
|
||||
GetStagedTextResponse get_staged_text_response = 289;
|
||||
|
||||
RegisterBufferWithLanguageServers register_buffer_with_language_servers = 290;
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
@@ -2537,7 +2539,6 @@ message UpdateGitBranch {
|
||||
string branch_name = 2;
|
||||
ProjectPath repository = 3;
|
||||
}
|
||||
|
||||
message GetPanicFiles {
|
||||
}
|
||||
|
||||
@@ -2582,3 +2583,8 @@ message InstallExtension {
|
||||
Extension extension = 1;
|
||||
string tmp_dir = 2;
|
||||
}
|
||||
|
||||
message RegisterBufferWithLanguageServers{
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
}
|
||||
|
||||
@@ -373,6 +373,7 @@ messages!(
|
||||
(SyncExtensions, Background),
|
||||
(SyncExtensionsResponse, Background),
|
||||
(InstallExtension, Background),
|
||||
(RegisterBufferWithLanguageServers, Background),
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
@@ -499,6 +500,7 @@ request_messages!(
|
||||
(CancelLanguageServerWork, Ack),
|
||||
(SyncExtensions, SyncExtensionsResponse),
|
||||
(InstallExtension, Ack),
|
||||
(RegisterBufferWithLanguageServers, Ack),
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
@@ -584,6 +586,7 @@ entity_messages!(
|
||||
ActiveToolchain,
|
||||
GetPathMetadata,
|
||||
CancelLanguageServerWork,
|
||||
RegisterBufferWithLanguageServers,
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
||||
@@ -440,9 +440,9 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
|
||||
// Wait for the settings to synchronize
|
||||
cx.run_until_parked();
|
||||
|
||||
let buffer = project
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
|
||||
project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -616,9 +616,9 @@ async fn test_remote_cancel_language_server_work(
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let buffer = project
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
|
||||
project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2,29 +2,27 @@ use crate::kernels::KernelSpecification;
|
||||
use crate::repl_store::ReplStore;
|
||||
use crate::KERNEL_DOCS_URL;
|
||||
|
||||
use editor::Editor;
|
||||
use gpui::DismissEvent;
|
||||
|
||||
use gpui::FontWeight;
|
||||
use gpui::WeakView;
|
||||
use picker::Picker;
|
||||
use picker::PickerDelegate;
|
||||
use project::WorktreeId;
|
||||
use ui::ButtonLike;
|
||||
use ui::Tooltip;
|
||||
|
||||
use std::sync::Arc;
|
||||
use ui::ListItemSpacing;
|
||||
|
||||
use gpui::SharedString;
|
||||
use gpui::Task;
|
||||
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle};
|
||||
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
|
||||
pub type OnSelect = Box<dyn Fn(KernelSpecification, &mut WindowContext)>;
|
||||
type OnSelect = Box<dyn Fn(KernelSpecification, &mut WindowContext)>;
|
||||
|
||||
pub struct KernelSelector {
|
||||
#[derive(IntoElement)]
|
||||
pub struct KernelSelector<T: PopoverTrigger> {
|
||||
handle: Option<PopoverMenuHandle<Picker<KernelPickerDelegate>>>,
|
||||
editor: WeakView<Editor>,
|
||||
on_select: OnSelect,
|
||||
trigger: T,
|
||||
info_text: Option<SharedString>,
|
||||
worktree_id: WorktreeId,
|
||||
}
|
||||
@@ -34,7 +32,6 @@ pub struct KernelPickerDelegate {
|
||||
filtered_kernels: Vec<KernelSpecification>,
|
||||
selected_kernelspec: Option<KernelSpecification>,
|
||||
on_select: OnSelect,
|
||||
group: Group,
|
||||
}
|
||||
|
||||
// Helper function to truncate long paths
|
||||
@@ -47,15 +44,12 @@ fn truncate_path(path: &SharedString, max_length: usize) -> SharedString {
|
||||
}
|
||||
}
|
||||
|
||||
impl KernelSelector {
|
||||
pub fn new(
|
||||
worktree_id: WorktreeId,
|
||||
editor: WeakView<Editor>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
impl<T: PopoverTrigger> KernelSelector<T> {
|
||||
pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T) -> Self {
|
||||
KernelSelector {
|
||||
editor,
|
||||
on_select,
|
||||
handle: None,
|
||||
trigger,
|
||||
info_text: None,
|
||||
worktree_id,
|
||||
}
|
||||
@@ -72,14 +66,6 @@ impl KernelSelector {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum Group {
|
||||
All,
|
||||
Jupyter,
|
||||
Python,
|
||||
Remote,
|
||||
}
|
||||
|
||||
impl PickerDelegate for KernelPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
@@ -218,75 +204,6 @@ impl PickerDelegate for KernelPickerDelegate {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_header(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
|
||||
let mode = Group::All;
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.child(
|
||||
div()
|
||||
.id("all")
|
||||
.px_2()
|
||||
.py_1()
|
||||
.cursor_pointer()
|
||||
.border_b_2()
|
||||
.when(mode == Group::All, |this| {
|
||||
this.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(Label::new("All"))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.delegate.set_group(Group::All, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("jupyter")
|
||||
.px_2()
|
||||
.py_1()
|
||||
.cursor_pointer()
|
||||
.border_b_2()
|
||||
.when(mode == Group::Jupyter, |this| {
|
||||
this.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(Label::new("Jupyter"))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.delegate.set_group(Group::Jupyter, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("python")
|
||||
.px_2()
|
||||
.py_1()
|
||||
.cursor_pointer()
|
||||
.border_b_2()
|
||||
.when(mode == Group::Python, |this| {
|
||||
this.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(Label::new("Python"))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.delegate.set_group(Group::Python, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("remote")
|
||||
.px_2()
|
||||
.py_1()
|
||||
.cursor_pointer()
|
||||
.border_b_2()
|
||||
.when(mode == Group::Remote, |this| {
|
||||
this.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(Label::new("Remote"))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.delegate.set_group(Group::Remote, cx);
|
||||
})),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
|
||||
Some(
|
||||
h_flex()
|
||||
@@ -308,29 +225,8 @@ impl PickerDelegate for KernelPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
impl KernelPickerDelegate {
|
||||
fn new(
|
||||
on_select: OnSelect,
|
||||
kernels: Vec<KernelSpecification>,
|
||||
selected_kernelspec: Option<KernelSpecification>,
|
||||
) -> Self {
|
||||
Self {
|
||||
on_select,
|
||||
all_kernels: kernels.clone(),
|
||||
filtered_kernels: kernels,
|
||||
group: Group::All,
|
||||
selected_kernelspec,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_group(&mut self, group: Group, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.group = group;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for KernelSelector {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let store = ReplStore::global(cx).read(cx);
|
||||
|
||||
let all_kernels: Vec<KernelSpecification> = store
|
||||
@@ -339,18 +235,13 @@ impl Render for KernelSelector {
|
||||
.collect();
|
||||
|
||||
let selected_kernelspec = store.active_kernelspec(self.worktree_id, None, cx);
|
||||
let current_kernel_name = selected_kernelspec.as_ref().map(|spec| spec.name()).clone();
|
||||
|
||||
let editor = self.editor.clone();
|
||||
let on_select: OnSelect = Box::new(move |kernelspec, cx| {
|
||||
crate::assign_kernelspec(kernelspec, editor.clone(), cx).ok();
|
||||
});
|
||||
|
||||
let menu_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>> =
|
||||
PopoverMenuHandle::default();
|
||||
|
||||
let delegate =
|
||||
KernelPickerDelegate::new(on_select, all_kernels, selected_kernelspec.clone());
|
||||
let delegate = KernelPickerDelegate {
|
||||
on_select: self.on_select,
|
||||
all_kernels: all_kernels.clone(),
|
||||
filtered_kernels: all_kernels,
|
||||
selected_kernelspec,
|
||||
};
|
||||
|
||||
let picker_view = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx)
|
||||
@@ -361,42 +252,8 @@ impl Render for KernelSelector {
|
||||
|
||||
PopoverMenu::new("kernel-switcher")
|
||||
.menu(move |_cx| Some(picker_view.clone()))
|
||||
.trigger(
|
||||
ButtonLike::new("kernel-selector")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(
|
||||
Label::new(if let Some(name) = current_kernel_name {
|
||||
name
|
||||
} else {
|
||||
SharedString::from("Select Kernel")
|
||||
})
|
||||
.size(LabelSize::Small)
|
||||
.color(if selected_kernelspec.is_some() {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Placeholder
|
||||
})
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text("Select Kernel", cx)),
|
||||
)
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::AnchorCorner::BottomLeft)
|
||||
.with_handle(menu_handle)
|
||||
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,21 +73,27 @@ pub fn init(cx: &mut AppContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
let project_path = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).project_path(cx));
|
||||
let buffer = editor.buffer().read(cx).as_singleton();
|
||||
|
||||
let language = buffer
|
||||
.as_ref()
|
||||
.and_then(|buffer| buffer.read(cx).language());
|
||||
|
||||
let project_path = buffer.and_then(|buffer| buffer.read(cx).project_path(cx));
|
||||
|
||||
let editor_handle = cx.view().downgrade();
|
||||
|
||||
if let (Some(project_path), Some(project)) = (project_path, project) {
|
||||
let store = ReplStore::global(cx);
|
||||
store.update(cx, |store, cx| {
|
||||
store
|
||||
.refresh_python_kernelspecs(project_path.worktree_id, &project, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
if let Some(language) = language {
|
||||
if language.name() == "Python".into() {
|
||||
if let (Some(project_path), Some(project)) = (project_path, project) {
|
||||
let store = ReplStore::global(cx);
|
||||
store.update(cx, |store, cx| {
|
||||
store
|
||||
.refresh_python_kernelspecs(project_path.worktree_id, &project, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor
|
||||
|
||||
@@ -173,7 +173,7 @@ impl ReplStore {
|
||||
|
||||
let remote_kernel_specifications = self.get_remote_kernel_specifications(cx);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let all_specs = cx.background_executor().spawn(async move {
|
||||
let mut all_specs = local_kernel_specifications
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -186,10 +186,21 @@ impl ReplStore {
|
||||
}
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.kernel_specifications = all_specs;
|
||||
cx.notify();
|
||||
})
|
||||
anyhow::Ok(all_specs)
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let all_specs = all_specs.await;
|
||||
|
||||
if let Ok(specs) = all_specs {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.kernel_specifications = specs;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -445,7 +445,7 @@ impl ComponentPreview for Button {
|
||||
"A button allows users to take actions, and make choices, with a single tap."
|
||||
}
|
||||
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group_with_title(
|
||||
"Styles",
|
||||
|
||||
@@ -118,7 +118,7 @@ impl ComponentPreview for Checkbox {
|
||||
"A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
|
||||
}
|
||||
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group_with_title(
|
||||
"Default",
|
||||
@@ -214,7 +214,7 @@ impl ComponentPreview for CheckboxWithLabel {
|
||||
"A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
|
||||
}
|
||||
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![example_group(vec![
|
||||
single_example(
|
||||
"Unselected",
|
||||
|
||||
@@ -95,7 +95,7 @@ impl ComponentPreview for ContentGroup {
|
||||
ExampleLabelSide::Bottom
|
||||
}
|
||||
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![example_group(vec![
|
||||
single_example(
|
||||
"Default",
|
||||
|
||||
@@ -3,6 +3,13 @@ use gpui::{Hsla, IntoElement};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum DividerStyle {
|
||||
Solid,
|
||||
Dashed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum DividerDirection {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
@@ -27,6 +34,7 @@ impl DividerColor {
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Divider {
|
||||
style: DividerStyle,
|
||||
direction: DividerDirection,
|
||||
color: DividerColor,
|
||||
inset: bool,
|
||||
@@ -34,22 +42,17 @@ pub struct Divider {
|
||||
|
||||
impl RenderOnce for Divider {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
.map(|this| match self.direction {
|
||||
DividerDirection::Horizontal => {
|
||||
this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
|
||||
}
|
||||
DividerDirection::Vertical => {
|
||||
this.w_px().h_full().when(self.inset, |this| this.my_1p5())
|
||||
}
|
||||
})
|
||||
.bg(self.color.hsla(cx))
|
||||
match self.style {
|
||||
DividerStyle::Solid => self.render_solid(cx).into_any_element(),
|
||||
DividerStyle::Dashed => self.render_dashed(cx).into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Divider {
|
||||
pub fn horizontal() -> Self {
|
||||
Self {
|
||||
style: DividerStyle::Solid,
|
||||
direction: DividerDirection::Horizontal,
|
||||
color: DividerColor::default(),
|
||||
inset: false,
|
||||
@@ -58,6 +61,25 @@ impl Divider {
|
||||
|
||||
pub fn vertical() -> Self {
|
||||
Self {
|
||||
style: DividerStyle::Solid,
|
||||
direction: DividerDirection::Vertical,
|
||||
color: DividerColor::default(),
|
||||
inset: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn horizontal_dashed() -> Self {
|
||||
Self {
|
||||
style: DividerStyle::Dashed,
|
||||
direction: DividerDirection::Horizontal,
|
||||
color: DividerColor::default(),
|
||||
inset: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical_dashed() -> Self {
|
||||
Self {
|
||||
style: DividerStyle::Dashed,
|
||||
direction: DividerDirection::Vertical,
|
||||
color: DividerColor::default(),
|
||||
inset: false,
|
||||
@@ -73,4 +95,49 @@ impl Divider {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render_solid(self, cx: &WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
.map(|this| match self.direction {
|
||||
DividerDirection::Horizontal => {
|
||||
this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
|
||||
}
|
||||
DividerDirection::Vertical => {
|
||||
this.w_px().h_full().when(self.inset, |this| this.my_1p5())
|
||||
}
|
||||
})
|
||||
.bg(self.color.hsla(cx))
|
||||
}
|
||||
|
||||
// TODO: Use canvas or a shader here
|
||||
// This obviously is a short term approach
|
||||
pub fn render_dashed(self, cx: &WindowContext) -> impl IntoElement {
|
||||
let segment_count = 128;
|
||||
let segment_count_f = segment_count as f32;
|
||||
let segment_min_w = 6.;
|
||||
let base = match self.direction {
|
||||
DividerDirection::Horizontal => h_flex(),
|
||||
DividerDirection::Vertical => v_flex(),
|
||||
};
|
||||
let (w, h) = match self.direction {
|
||||
DividerDirection::Horizontal => (px(segment_min_w), px(1.)),
|
||||
DividerDirection::Vertical => (px(1.), px(segment_min_w)),
|
||||
};
|
||||
let color = self.color.hsla(cx);
|
||||
let total_min_w = segment_min_w * segment_count_f * 2.; // * 2 because of the gap
|
||||
|
||||
base.min_w(px(total_min_w))
|
||||
.map(|this| {
|
||||
if self.direction == DividerDirection::Horizontal {
|
||||
this.w_full().h_px()
|
||||
} else {
|
||||
this.w_px().h_full()
|
||||
}
|
||||
})
|
||||
.gap(px(segment_min_w))
|
||||
.overflow_hidden()
|
||||
.children(
|
||||
(0..segment_count).map(|_| div().flex_grow().flex_shrink_0().w(w).h(h).bg(color)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ impl ComponentPreview for Facepile {
|
||||
\n\nFacepiles are used to display a group of people or things,\
|
||||
such as a list of participants in a collaboration session."
|
||||
}
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
let few_faces: [&'static str; 3] = [
|
||||
"https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
|
||||
"https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
|
||||
|
||||
@@ -178,6 +178,7 @@ pub enum IconName {
|
||||
File,
|
||||
FileCode,
|
||||
FileDoc,
|
||||
FileDiff,
|
||||
FileGeneric,
|
||||
FileGit,
|
||||
FileLock,
|
||||
@@ -199,6 +200,7 @@ pub enum IconName {
|
||||
GenericRestore,
|
||||
Github,
|
||||
Globe,
|
||||
GitBranch,
|
||||
Hash,
|
||||
HistoryRerun,
|
||||
Indicator,
|
||||
@@ -223,6 +225,8 @@ pub enum IconName {
|
||||
Option,
|
||||
PageDown,
|
||||
PageUp,
|
||||
PanelLeft,
|
||||
PanelRight,
|
||||
Pencil,
|
||||
Person,
|
||||
PhoneIncoming,
|
||||
@@ -266,6 +270,9 @@ pub enum IconName {
|
||||
SparkleFilled,
|
||||
Spinner,
|
||||
Split,
|
||||
SquareDot,
|
||||
SquareMinus,
|
||||
SquarePlus,
|
||||
Star,
|
||||
StarFilled,
|
||||
Stop,
|
||||
@@ -278,6 +285,8 @@ pub enum IconName {
|
||||
Tab,
|
||||
Terminal,
|
||||
TextSnippet,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
Trash,
|
||||
TrashAlt,
|
||||
Triangle,
|
||||
@@ -494,7 +503,7 @@ impl RenderOnce for IconDecoration {
|
||||
}
|
||||
|
||||
impl ComponentPreview for IconDecoration {
|
||||
fn examples(cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
|
||||
|
||||
let examples = all_kinds
|
||||
@@ -536,7 +545,7 @@ impl RenderOnce for DecoratedIcon {
|
||||
}
|
||||
|
||||
impl ComponentPreview for DecoratedIcon {
|
||||
fn examples(cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
let icon_1 = Icon::new(IconName::FileDoc);
|
||||
let icon_2 = Icon::new(IconName::FileDoc);
|
||||
let icon_3 = Icon::new(IconName::FileDoc);
|
||||
@@ -655,7 +664,7 @@ impl RenderOnce for IconWithIndicator {
|
||||
}
|
||||
|
||||
impl ComponentPreview for Icon {
|
||||
fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
|
||||
fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
|
||||
let arrow_icons = vec![
|
||||
IconName::ArrowDown,
|
||||
IconName::ArrowLeft,
|
||||
|
||||
@@ -89,7 +89,7 @@ impl ComponentPreview for Indicator {
|
||||
"An indicator visually represents a status or state."
|
||||
}
|
||||
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group_with_title(
|
||||
"Types",
|
||||
|
||||
@@ -39,6 +39,7 @@ pub struct ListItem {
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
selectable: bool,
|
||||
overflow_x: bool,
|
||||
focused: Option<bool>,
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
@@ -62,6 +63,7 @@ impl ListItem {
|
||||
children: SmallVec::new(),
|
||||
selectable: true,
|
||||
overflow_x: false,
|
||||
focused: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +142,11 @@ impl ListItem {
|
||||
self.overflow_x = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn focused(mut self, focused: bool) -> Self {
|
||||
self.focused = Some(focused);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for ListItem {
|
||||
@@ -177,9 +184,14 @@ impl RenderOnce for ListItem {
|
||||
this
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border_1()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.when_some(self.focused, |this, focused| {
|
||||
if focused {
|
||||
this.border_1()
|
||||
.border_color(cx.theme().colors().border_focused)
|
||||
} else {
|
||||
this.border_1()
|
||||
}
|
||||
})
|
||||
.when(self.selectable, |this| {
|
||||
this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
@@ -204,10 +216,15 @@ impl RenderOnce for ListItem {
|
||||
.when(self.inset && !self.disabled, |this| {
|
||||
this
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border_1()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
//.when(self.state == InteractionState::Focused, |this| {
|
||||
.when_some(self.focused, |this, focused| {
|
||||
if focused {
|
||||
this.border_1()
|
||||
.border_color(cx.theme().colors().border_focused)
|
||||
} else {
|
||||
this.border_1()
|
||||
}
|
||||
})
|
||||
.when(self.selectable, |this| {
|
||||
this.hover(|style| {
|
||||
style.bg(cx.theme().colors().ghost_element_hover)
|
||||
|
||||
@@ -160,7 +160,7 @@ impl ComponentPreview for Table {
|
||||
ExampleLabelSide::Top
|
||||
}
|
||||
|
||||
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group(vec![
|
||||
single_example(
|
||||
|
||||
@@ -30,20 +30,20 @@ pub trait ComponentPreview: IntoElement {
|
||||
ExampleLabelSide::default()
|
||||
}
|
||||
|
||||
fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>>;
|
||||
fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>>;
|
||||
|
||||
fn custom_example(_cx: &WindowContext) -> impl Into<Option<AnyElement>> {
|
||||
None::<AnyElement>
|
||||
}
|
||||
|
||||
fn component_previews(cx: &WindowContext) -> Vec<AnyElement> {
|
||||
fn component_previews(cx: &mut WindowContext) -> Vec<AnyElement> {
|
||||
Self::examples(cx)
|
||||
.into_iter()
|
||||
.map(|example| Self::render_example_group(example))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_component_previews(cx: &WindowContext) -> AnyElement {
|
||||
fn render_component_previews(cx: &mut WindowContext) -> AnyElement {
|
||||
let title = Self::title();
|
||||
let (source, title) = title
|
||||
.rsplit_once("::")
|
||||
|
||||
@@ -108,147 +108,6 @@ impl ThemePreview {
|
||||
cx.theme().colors().editor_background
|
||||
}
|
||||
|
||||
fn render_avatars(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Headline::new("Avatars")
|
||||
.size(HeadlineSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
.gap_4()
|
||||
.child(Avatar::new(AVATAR_URL).size(px(24.)))
|
||||
.child(Avatar::new(AVATAR_URL).size(px(24.)).grayscale(true))
|
||||
.child(
|
||||
Avatar::new(AVATAR_URL)
|
||||
.size(px(24.))
|
||||
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)),
|
||||
)
|
||||
.child(
|
||||
Avatar::new(AVATAR_URL)
|
||||
.size(px(24.))
|
||||
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
|
||||
)
|
||||
.child(
|
||||
Avatar::new(AVATAR_URL)
|
||||
.size(px(24.))
|
||||
.indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
|
||||
)
|
||||
.child(
|
||||
Avatar::new(AVATAR_URL)
|
||||
.size(px(24.))
|
||||
.indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
|
||||
)
|
||||
.child(
|
||||
Facepile::empty()
|
||||
.child(
|
||||
Avatar::new(AVATAR_URL)
|
||||
.border_color(Self::preview_bg(cx))
|
||||
.size(px(22.))
|
||||
.into_any_element(),
|
||||
)
|
||||
.child(
|
||||
Avatar::new(AVATAR_URL)
|
||||
.border_color(Self::preview_bg(cx))
|
||||
.size(px(22.))
|
||||
.into_any_element(),
|
||||
)
|
||||
.child(
|
||||
Avatar::new(AVATAR_URL)
|
||||
.border_color(Self::preview_bg(cx))
|
||||
.size(px(22.))
|
||||
.into_any_element(),
|
||||
)
|
||||
.child(
|
||||
Avatar::new(AVATAR_URL)
|
||||
.border_color(Self::preview_bg(cx))
|
||||
.size(px(22.))
|
||||
.into_any_element(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_buttons(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Headline::new("Buttons")
|
||||
.size(HeadlineSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
.gap_px()
|
||||
.child(
|
||||
IconButton::new("icon_button_transparent", IconName::Check)
|
||||
.style(ButtonStyle::Transparent),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("icon_button_subtle", IconName::Check)
|
||||
.style(ButtonStyle::Subtle),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("icon_button_filled", IconName::Check)
|
||||
.style(ButtonStyle::Filled),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("icon_button_selected_accent", IconName::Check)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.selected(true),
|
||||
)
|
||||
.child(IconButton::new("icon_button_selected", IconName::Check).selected(true))
|
||||
.child(
|
||||
IconButton::new("icon_button_positive", IconName::Check)
|
||||
.style(ButtonStyle::Tinted(TintColor::Positive)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("icon_button_warning", IconName::Check)
|
||||
.style(ButtonStyle::Tinted(TintColor::Warning)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("icon_button_negative", IconName::Check)
|
||||
.style(ButtonStyle::Tinted(TintColor::Negative)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_px()
|
||||
.child(
|
||||
Button::new("button_transparent", "Transparent")
|
||||
.style(ButtonStyle::Transparent),
|
||||
)
|
||||
.child(Button::new("button_subtle", "Subtle").style(ButtonStyle::Subtle))
|
||||
.child(Button::new("button_filled", "Filled").style(ButtonStyle::Filled))
|
||||
.child(
|
||||
Button::new("button_selected", "Selected")
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.selected(true),
|
||||
)
|
||||
.child(
|
||||
Button::new("button_selected_tinted", "Selected (Tinted)")
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.selected(true),
|
||||
)
|
||||
.child(
|
||||
Button::new("button_positive", "Tint::Positive")
|
||||
.style(ButtonStyle::Tinted(TintColor::Positive)),
|
||||
)
|
||||
.child(
|
||||
Button::new("button_warning", "Tint::Warning")
|
||||
.style(ButtonStyle::Tinted(TintColor::Warning)),
|
||||
)
|
||||
.child(
|
||||
Button::new("button_negative", "Tint::Negative")
|
||||
.style(ButtonStyle::Tinted(TintColor::Negative)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_text(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
let bg = layer.bg(cx);
|
||||
|
||||
@@ -502,7 +361,7 @@ impl ThemePreview {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_components_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_components_page(&self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let layer = ElevationIndex::Surface;
|
||||
|
||||
v_flex()
|
||||
@@ -520,8 +379,6 @@ impl ThemePreview {
|
||||
.child(Indicator::render_component_previews(cx))
|
||||
.child(Icon::render_component_previews(cx))
|
||||
.child(Table::render_component_previews(cx))
|
||||
.child(self.render_avatars(cx))
|
||||
.child(self.render_buttons(layer, cx))
|
||||
}
|
||||
|
||||
fn render_page_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
|
||||
@@ -92,11 +92,7 @@ use task::SpawnInTerminal;
|
||||
use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
|
||||
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
pub use ui;
|
||||
use ui::{
|
||||
div, h_flex, px, BorrowAppContext, Context as _, Div, FluentBuilder, InteractiveElement as _,
|
||||
IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::{paths::SanitizedPath, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
pub use workspace_settings::{
|
||||
@@ -597,7 +593,6 @@ impl AppState {
|
||||
use node_runtime::NodeRuntime;
|
||||
use session::Session;
|
||||
use settings::SettingsStore;
|
||||
use ui::Context as _;
|
||||
|
||||
if !cx.has_global::<SettingsStore>() {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
@@ -7856,7 +7851,7 @@ mod tests {
|
||||
}
|
||||
|
||||
mod register_project_item_tests {
|
||||
use ui::Context as _;
|
||||
use gpui::Context as _;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.166.0"
|
||||
version = "0.167.0"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
@@ -52,6 +52,7 @@ file_icons.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
git.workspace = true
|
||||
git_ui.workspace = true
|
||||
git_hosting_providers.workspace = true
|
||||
go_to_line.workspace = true
|
||||
gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] }
|
||||
@@ -80,6 +81,7 @@ outline.workspace = true
|
||||
outline_panel.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
profiling.workspace = true
|
||||
project.workspace = true
|
||||
project_panel.workspace = true
|
||||
|
||||
@@ -406,7 +406,12 @@ fn main() {
|
||||
stdout_is_a_pty(),
|
||||
cx,
|
||||
);
|
||||
assistant2::init(cx);
|
||||
assistant2::init(
|
||||
app_state.fs.clone(),
|
||||
app_state.client.clone(),
|
||||
stdout_is_a_pty(),
|
||||
cx,
|
||||
);
|
||||
assistant_tools::init(cx);
|
||||
repl::init(
|
||||
app_state.fs.clone(),
|
||||
@@ -442,6 +447,7 @@ fn main() {
|
||||
outline::init(cx);
|
||||
project_symbols::init(cx);
|
||||
project_panel::init(Assets, cx);
|
||||
git_ui::git_panel::init(cx);
|
||||
outline_panel::init(Assets, cx);
|
||||
tasks_ui::init(cx);
|
||||
snippets_ui::init(cx);
|
||||
@@ -463,6 +469,7 @@ fn main() {
|
||||
welcome::init(cx);
|
||||
settings_ui::init(cx);
|
||||
extensions_ui::init(cx);
|
||||
zeta::init(cx);
|
||||
|
||||
cx.observe_global::<SettingsStore>({
|
||||
let languages = app_state.languages.clone();
|
||||
|
||||
@@ -216,7 +216,7 @@ pub fn initialize_workspace(
|
||||
status_bar.add_left_item(activity_indicator, cx);
|
||||
status_bar.add_right_item(inline_completion_button, cx);
|
||||
status_bar.add_right_item(active_buffer_language, cx);
|
||||
status_bar.add_right_item(active_toolchain_language, cx);
|
||||
status_bar.add_right_item(active_toolchain_language, cx);
|
||||
status_bar.add_right_item(vim_mode_indicator, cx);
|
||||
status_bar.add_right_item(cursor_position, cx);
|
||||
});
|
||||
@@ -237,8 +237,11 @@ pub fn initialize_workspace(
|
||||
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
let assistant2_feature_flag = cx.wait_for_flag::<feature_flags::Assistant2FeatureFlag>();
|
||||
let git_ui_feature_flag = cx.wait_for_flag::<feature_flags::GitUiFeatureFlag>();
|
||||
|
||||
let prompt_builder = prompt_builder.clone();
|
||||
let is_staff = cx.is_staff();
|
||||
|
||||
cx.spawn(|workspace_handle, mut cx| async move {
|
||||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
|
||||
@@ -276,13 +279,26 @@ pub fn initialize_workspace(
|
||||
workspace.add_panel(chat_panel, cx);
|
||||
workspace.add_panel(notification_panel, cx);
|
||||
})?;
|
||||
let git_ui_enabled = git_ui_feature_flag.await || is_staff;
|
||||
|
||||
let git_panel = if git_ui_enabled {
|
||||
Some(git_ui::git_panel::GitPanel::load(workspace_handle.clone(), cx.clone()).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||
if let Some(git_panel) = git_panel {
|
||||
workspace.add_panel(git_panel, cx);
|
||||
}
|
||||
})?;
|
||||
|
||||
let is_assistant2_enabled =
|
||||
if cfg!(test) || release_channel != ReleaseChannel::Dev {
|
||||
false
|
||||
} else {
|
||||
assistant2_feature_flag.await
|
||||
}
|
||||
;
|
||||
};
|
||||
|
||||
let (assistant_panel, assistant2_panel) = if is_assistant2_enabled {
|
||||
let assistant2_panel =
|
||||
@@ -303,6 +319,7 @@ pub fn initialize_workspace(
|
||||
if let Some(assistant2_panel) = assistant2_panel {
|
||||
workspace.add_panel(assistant2_panel, cx);
|
||||
}
|
||||
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -165,7 +165,7 @@ fn assign_inline_completion_provider(
|
||||
}
|
||||
}
|
||||
language::language_settings::InlineCompletionProvider::Zeta => {
|
||||
if cx.has_flag::<ZetaFeatureFlag>() {
|
||||
if cx.has_flag::<ZetaFeatureFlag>() || cfg!(debug_assertions) {
|
||||
let zeta = zeta::Zeta::register(client.clone(), cx);
|
||||
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
||||
if buffer.read(cx).file().is_some() {
|
||||
|
||||
@@ -13,8 +13,6 @@ use gpui::{
|
||||
Action, AnchorCorner, ClickEvent, ElementId, EventEmitter, FocusHandle, FocusableView,
|
||||
InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView,
|
||||
};
|
||||
use repl::worktree_id_for_editor;
|
||||
use repl_menu::ReplMenu;
|
||||
use search::{buffer_search, BufferSearchBar};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use ui::{
|
||||
@@ -35,7 +33,6 @@ pub struct QuickActionBar {
|
||||
toggle_selections_handle: PopoverMenuHandle<ContextMenu>,
|
||||
toggle_settings_handle: PopoverMenuHandle<ContextMenu>,
|
||||
workspace: WeakView<Workspace>,
|
||||
repl_menu: Option<View<ReplMenu>>,
|
||||
}
|
||||
|
||||
impl QuickActionBar {
|
||||
@@ -52,7 +49,6 @@ impl QuickActionBar {
|
||||
toggle_selections_handle: Default::default(),
|
||||
toggle_settings_handle: Default::default(),
|
||||
workspace: workspace.weak_handle(),
|
||||
repl_menu: None,
|
||||
};
|
||||
this.apply_settings(cx);
|
||||
cx.observe_global::<SettingsStore>(|this, cx| this.apply_settings(cx))
|
||||
@@ -354,7 +350,7 @@ impl Render for QuickActionBar {
|
||||
h_flex()
|
||||
.id("quick action bar")
|
||||
.gap(DynamicSpacing::Base06.rems(cx))
|
||||
.children(self.repl_menu.clone())
|
||||
.children(self.render_repl_menu(cx))
|
||||
.children(self.render_toggle_markdown_preview(self.workspace.clone(), cx))
|
||||
.children(search_button)
|
||||
.when(
|
||||
@@ -429,15 +425,7 @@ impl ToolbarItemView for QuickActionBar {
|
||||
if let Some(active_item) = active_pane_item {
|
||||
self._inlay_hints_enabled_subscription.take();
|
||||
|
||||
let editor = active_item.downcast::<Editor>();
|
||||
|
||||
let work_tree_id = active_item
|
||||
.downcast::<Editor>()
|
||||
.and_then(|editor| worktree_id_for_editor(editor.downgrade(), cx));
|
||||
|
||||
if let (Some(editor), Some(work_tree_id)) = (editor, work_tree_id) {
|
||||
self.repl_menu =
|
||||
Some(cx.new_view(|cx| ReplMenu::new(work_tree_id, editor.downgrade(), cx)));
|
||||
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);
|
||||
self._inlay_hints_enabled_subscription =
|
||||
@@ -453,8 +441,6 @@ impl ToolbarItemView for QuickActionBar {
|
||||
cx.notify()
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
self.repl_menu = None
|
||||
}
|
||||
}
|
||||
self.get_toolbar_item_location()
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use editor::Editor;
|
||||
use gpui::ElementId;
|
||||
use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View};
|
||||
use gpui::{ElementId, WeakView};
|
||||
use project::WorktreeId;
|
||||
use picker::Picker;
|
||||
use repl::{
|
||||
components::KernelSelector, ExecutionState, JupyterSettings, Kernel, KernelSpecification,
|
||||
components::{KernelPickerDelegate, KernelSelector},
|
||||
worktree_id_for_editor, ExecutionState, JupyterSettings, Kernel, KernelSpecification,
|
||||
KernelStatus, Session, SessionSupport,
|
||||
};
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu,
|
||||
Tooltip,
|
||||
PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
use super::QuickActionBar;
|
||||
|
||||
const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl";
|
||||
|
||||
struct ReplSessionState {
|
||||
struct ReplMenuState {
|
||||
tooltip: SharedString,
|
||||
icon: IconName,
|
||||
icon_color: Color,
|
||||
@@ -29,73 +31,47 @@ struct ReplSessionState {
|
||||
kernel_language: SharedString,
|
||||
}
|
||||
|
||||
pub struct ReplMenu {
|
||||
active_editor: WeakView<Editor>,
|
||||
kernel_menu: View<KernelSelector>,
|
||||
}
|
||||
|
||||
impl ReplMenu {
|
||||
pub fn new(
|
||||
work_tree_id: WorktreeId,
|
||||
editor: WeakView<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
kernel_menu: cx.new_view(|cx| KernelSelector::new(work_tree_id, editor.clone(), cx)),
|
||||
active_editor: editor.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ReplMenu {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
impl QuickActionBar {
|
||||
pub fn render_repl_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return div().into_any_element();
|
||||
return None;
|
||||
}
|
||||
|
||||
let editor = self.active_editor.clone();
|
||||
let editor = self.active_editor()?;
|
||||
|
||||
let is_local_project = editor
|
||||
.upgrade()
|
||||
.as_ref()
|
||||
.map(|editor| {
|
||||
editor
|
||||
.read(cx)
|
||||
.workspace()
|
||||
.map(|workspace| workspace.read(cx).project().read(cx).is_local())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.read(cx)
|
||||
.workspace()
|
||||
.map(|workspace| workspace.read(cx).project().read(cx).is_local())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_local_project {
|
||||
return div().into_any_element();
|
||||
return None;
|
||||
}
|
||||
|
||||
let has_nonempty_selection = {
|
||||
editor
|
||||
.update(cx, |this, cx| {
|
||||
this.selections
|
||||
.count()
|
||||
.ne(&0)
|
||||
.then(|| {
|
||||
let latest = this.selections.newest_display(cx);
|
||||
!latest.is_empty()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
editor.update(cx, |this, cx| {
|
||||
this.selections
|
||||
.count()
|
||||
.ne(&0)
|
||||
.then(|| {
|
||||
let latest = this.selections.newest_display(cx);
|
||||
!latest.is_empty()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
};
|
||||
|
||||
let session = repl::session(editor.clone(), cx);
|
||||
let session = repl::session(editor.downgrade(), cx);
|
||||
let session = match session {
|
||||
SessionSupport::ActiveSession(session) => session,
|
||||
SessionSupport::Inactive(spec) => {
|
||||
return self.render_repl_launch_menu(spec, cx).into_any_element();
|
||||
return self.render_repl_launch_menu(spec, cx);
|
||||
}
|
||||
SessionSupport::RequiresSetup(language) => {
|
||||
return self.render_repl_setup(&language.0, cx).into_any_element();
|
||||
return self.render_repl_setup(&language.0, cx);
|
||||
}
|
||||
SessionSupport::Unsupported => return div().into_any_element(),
|
||||
SessionSupport::Unsupported => return None,
|
||||
};
|
||||
|
||||
let menu_state = session_state(session.clone(), cx);
|
||||
@@ -104,7 +80,7 @@ impl Render for ReplMenu {
|
||||
|
||||
let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into());
|
||||
|
||||
let editor = editor.clone();
|
||||
let editor = editor.downgrade();
|
||||
let dropdown_menu = PopoverMenu::new(element_id("menu"))
|
||||
.menu(move |cx| {
|
||||
let editor = editor.clone();
|
||||
@@ -269,85 +245,138 @@ impl Render for ReplMenu {
|
||||
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
|
||||
.into_any_element();
|
||||
|
||||
h_flex()
|
||||
.child(self.kernel_menu.clone())
|
||||
.child(button)
|
||||
.child(dropdown_menu)
|
||||
.into_any_element()
|
||||
Some(
|
||||
h_flex()
|
||||
.child(self.render_kernel_selector(cx))
|
||||
.child(button)
|
||||
.child(dropdown_menu)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplMenu {
|
||||
pub fn render_repl_launch_menu(
|
||||
&self,
|
||||
kernel_specification: KernelSpecification,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let tooltip: SharedString =
|
||||
SharedString::from(format!("Start REPL for {}", kernel_specification.name()));
|
||||
|
||||
h_flex().child(self.kernel_menu.clone()).child(
|
||||
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {}))),
|
||||
Some(
|
||||
h_flex()
|
||||
.child(self.render_kernel_selector(cx))
|
||||
.child(
|
||||
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {}))),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_repl_setup(&self, language: &str, _cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
pub fn render_kernel_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let editor = if let Some(editor) = self.active_editor() {
|
||||
editor
|
||||
} else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let Some(worktree_id) = worktree_id_for_editor(editor.downgrade(), cx) else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let session = repl::session(editor.downgrade(), cx);
|
||||
|
||||
let current_kernelspec = match session {
|
||||
SessionSupport::ActiveSession(view) => Some(view.read(cx).kernel_specification.clone()),
|
||||
SessionSupport::Inactive(kernel_specification) => Some(kernel_specification),
|
||||
SessionSupport::RequiresSetup(_language_name) => None,
|
||||
SessionSupport::Unsupported => None,
|
||||
};
|
||||
|
||||
let current_kernel_name = current_kernelspec.as_ref().map(|spec| spec.name());
|
||||
|
||||
let menu_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>> =
|
||||
PopoverMenuHandle::default();
|
||||
KernelSelector::new(
|
||||
{
|
||||
Box::new(move |kernelspec, cx| {
|
||||
repl::assign_kernelspec(kernelspec, editor.downgrade(), cx).ok();
|
||||
})
|
||||
},
|
||||
worktree_id,
|
||||
ButtonLike::new("kernel-selector")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(
|
||||
Label::new(if let Some(name) = current_kernel_name {
|
||||
name
|
||||
} else {
|
||||
SharedString::from("Select Kernel")
|
||||
})
|
||||
.size(LabelSize::Small)
|
||||
.color(if current_kernelspec.is_some() {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Placeholder
|
||||
})
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text("Select Kernel", cx)),
|
||||
)
|
||||
.with_handle(menu_handle.clone())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub fn render_repl_setup(
|
||||
&self,
|
||||
language: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language));
|
||||
h_flex()
|
||||
.child(self.kernel_menu.clone())
|
||||
.child(
|
||||
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION))
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
Some(
|
||||
h_flex()
|
||||
.child(self.render_kernel_selector(cx))
|
||||
.child(
|
||||
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION))
|
||||
}),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// struct KernelMenu {
|
||||
// menu_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>>,
|
||||
// editor: WeakView<Editor>,
|
||||
// }
|
||||
|
||||
// impl KernelMenu {
|
||||
// pub fn new(editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Self {
|
||||
|
||||
// Self {
|
||||
// editor,
|
||||
// menu_handle,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// impl Render for KernelMenu {
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// let Some(worktree_id) = worktree_id_for_editor(self.editor.clone(), cx) else {
|
||||
// return div().into_any_element();
|
||||
// };
|
||||
|
||||
// KernelSelector::new(self.editor.clone(), worktree_id)
|
||||
// .with_handle(self.menu_handle.clone())
|
||||
// .into_any_element()
|
||||
// }
|
||||
// }
|
||||
|
||||
fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState {
|
||||
fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
|
||||
let session = session.read(cx);
|
||||
|
||||
let kernel_name = session.kernel_specification.name();
|
||||
let kernel_language: SharedString = session.kernel_specification.language();
|
||||
|
||||
let fill_fields = || {
|
||||
ReplSessionState {
|
||||
ReplMenuState {
|
||||
tooltip: "Nothing running".into(),
|
||||
icon: IconName::ReplNeutral,
|
||||
icon_color: Color::Default,
|
||||
@@ -363,7 +392,7 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState
|
||||
};
|
||||
|
||||
match &session.kernel {
|
||||
Kernel::Restarting => ReplSessionState {
|
||||
Kernel::Restarting => ReplMenuState {
|
||||
tooltip: format!("Restarting {}", kernel_name).into(),
|
||||
icon_is_animating: true,
|
||||
popover_disabled: true,
|
||||
@@ -373,13 +402,13 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState
|
||||
..fill_fields()
|
||||
},
|
||||
Kernel::RunningKernel(kernel) => match &kernel.execution_state() {
|
||||
ExecutionState::Idle => ReplSessionState {
|
||||
ExecutionState::Idle => ReplMenuState {
|
||||
tooltip: format!("Run code on {} ({})", kernel_name, kernel_language).into(),
|
||||
indicator: Some(Indicator::dot().color(Color::Success)),
|
||||
status: session.kernel.status(),
|
||||
..fill_fields()
|
||||
},
|
||||
ExecutionState::Busy => ReplSessionState {
|
||||
ExecutionState::Busy => ReplMenuState {
|
||||
tooltip: format!("Interrupt {} ({})", kernel_name, kernel_language).into(),
|
||||
icon_is_animating: true,
|
||||
popover_disabled: false,
|
||||
@@ -388,7 +417,7 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState
|
||||
..fill_fields()
|
||||
},
|
||||
},
|
||||
Kernel::StartingKernel(_) => ReplSessionState {
|
||||
Kernel::StartingKernel(_) => ReplMenuState {
|
||||
tooltip: format!("{} is starting", kernel_name).into(),
|
||||
icon_is_animating: true,
|
||||
popover_disabled: true,
|
||||
@@ -397,14 +426,14 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState
|
||||
status: session.kernel.status(),
|
||||
..fill_fields()
|
||||
},
|
||||
Kernel::ErroredLaunch(e) => ReplSessionState {
|
||||
Kernel::ErroredLaunch(e) => ReplMenuState {
|
||||
tooltip: format!("Error with kernel {}: {}", kernel_name, e).into(),
|
||||
popover_disabled: false,
|
||||
indicator: Some(Indicator::dot().color(Color::Error)),
|
||||
status: session.kernel.status(),
|
||||
..fill_fields()
|
||||
},
|
||||
Kernel::ShuttingDown => ReplSessionState {
|
||||
Kernel::ShuttingDown => ReplMenuState {
|
||||
tooltip: format!("{} is shutting down", kernel_name).into(),
|
||||
popover_disabled: true,
|
||||
icon_color: Color::Muted,
|
||||
@@ -412,7 +441,7 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState
|
||||
status: session.kernel.status(),
|
||||
..fill_fields()
|
||||
},
|
||||
Kernel::Shutdown => ReplSessionState {
|
||||
Kernel::Shutdown => ReplMenuState {
|
||||
tooltip: "Nothing running".into(),
|
||||
icon: IconName::ReplNeutral,
|
||||
icon_color: Color::Default,
|
||||
|
||||
@@ -13,6 +13,9 @@ workspace = true
|
||||
path = "src/zeta.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
@@ -21,6 +24,7 @@ editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
inline_completion.workspace = true
|
||||
language.workspace = true
|
||||
language_models.workspace = true
|
||||
@@ -32,8 +36,8 @@ settings.workspace = true
|
||||
similar.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
theme.workspace = true
|
||||
util.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
|
||||
@@ -1,18 +1,44 @@
|
||||
use crate::{InlineCompletion, InlineCompletionRating, Zeta};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, HighlightStyle,
|
||||
Model, StyledText, TextStyle, View, ViewContext,
|
||||
actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
HighlightStyle, Model, StyledText, TextStyle, View, ViewContext,
|
||||
};
|
||||
use language::{language_settings, OffsetRangeExt};
|
||||
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, TintColor, Tooltip};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
actions!(
|
||||
zeta,
|
||||
[
|
||||
RateCompletions,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
ThumbsUpActiveCompletion,
|
||||
ThumbsDownActiveCompletion,
|
||||
NextEdit,
|
||||
PreviousEdit,
|
||||
FocusCompletions,
|
||||
PreviewCompletion,
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(move |workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|workspace, _: &RateCompletions, cx| {
|
||||
RateCompletionModal::toggle(workspace, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct RateCompletionModal {
|
||||
zeta: Model<Zeta>,
|
||||
active_completion: Option<ActiveCompletion>,
|
||||
selected_index: usize,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
@@ -33,6 +59,7 @@ impl RateCompletionModal {
|
||||
let subscription = cx.observe(&zeta, |_, _, cx| cx.notify());
|
||||
Self {
|
||||
zeta,
|
||||
selected_index: 0,
|
||||
focus_handle: cx.focus_handle(),
|
||||
active_completion: None,
|
||||
_subscription: subscription,
|
||||
@@ -43,15 +70,194 @@ impl RateCompletionModal {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
|
||||
self.selected_index += 1;
|
||||
self.selected_index = usize::min(
|
||||
self.selected_index,
|
||||
self.zeta.read(cx).recent_completions().count(),
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
self.selected_index = self.selected_index.saturating_sub(1);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next_edit(&mut self, _: &NextEdit, cx: &mut ViewContext<Self>) {
|
||||
let next_index = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.recent_completions()
|
||||
.skip(self.selected_index)
|
||||
.enumerate()
|
||||
.skip(1) // Skip straight to the next item
|
||||
.find(|(_, completion)| !completion.edits.is_empty())
|
||||
.map(|(ix, _)| ix + self.selected_index);
|
||||
|
||||
if let Some(next_index) = next_index {
|
||||
self.selected_index = next_index;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev_edit(&mut self, _: &PreviousEdit, cx: &mut ViewContext<Self>) {
|
||||
let zeta = self.zeta.read(cx);
|
||||
let completions_len = zeta.recent_completions_len();
|
||||
|
||||
let prev_index = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.recent_completions()
|
||||
.rev()
|
||||
.skip((completions_len - 1) - self.selected_index)
|
||||
.enumerate()
|
||||
.skip(1) // Skip straight to the previous item
|
||||
.find(|(_, completion)| !completion.edits.is_empty())
|
||||
.map(|(ix, _)| self.selected_index - ix);
|
||||
|
||||
if let Some(prev_index) = prev_index {
|
||||
self.selected_index = prev_index;
|
||||
cx.notify();
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
|
||||
self.selected_index = 0;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
|
||||
self.selected_index = self.zeta.read(cx).recent_completions_len() - 1;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn thumbs_up(&mut self, _: &ThumbsUp, cx: &mut ViewContext<Self>) {
|
||||
self.zeta.update(cx, |zeta, cx| {
|
||||
let completion = zeta
|
||||
.recent_completions()
|
||||
.skip(self.selected_index)
|
||||
.next()
|
||||
.cloned();
|
||||
|
||||
if let Some(completion) = completion {
|
||||
zeta.rate_completion(
|
||||
&completion,
|
||||
InlineCompletionRating::Positive,
|
||||
"".to_string(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
self.select_next_edit(&Default::default(), cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn thumbs_up_active(&mut self, _: &ThumbsUpActiveCompletion, cx: &mut ViewContext<Self>) {
|
||||
self.zeta.update(cx, |zeta, cx| {
|
||||
if let Some(active) = &self.active_completion {
|
||||
zeta.rate_completion(
|
||||
&active.completion,
|
||||
InlineCompletionRating::Positive,
|
||||
active.feedback_editor.read(cx).text(cx),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let current_completion = self
|
||||
.active_completion
|
||||
.as_ref()
|
||||
.map(|completion| completion.completion.clone());
|
||||
self.select_completion(current_completion, false, cx);
|
||||
self.select_next_edit(&Default::default(), cx);
|
||||
self.confirm(&Default::default(), cx);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn thumbs_down_active(&mut self, _: &ThumbsDownActiveCompletion, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active) = &self.active_completion {
|
||||
if active.feedback_editor.read(cx).text(cx).is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.zeta.update(cx, |zeta, cx| {
|
||||
zeta.rate_completion(
|
||||
&active.completion,
|
||||
InlineCompletionRating::Negative,
|
||||
active.feedback_editor.read(cx).text(cx),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let current_completion = self
|
||||
.active_completion
|
||||
.as_ref()
|
||||
.map(|completion| completion.completion.clone());
|
||||
self.select_completion(current_completion, false, cx);
|
||||
self.select_next_edit(&Default::default(), cx);
|
||||
self.confirm(&Default::default(), cx);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn focus_completions(&mut self, _: &FocusCompletions, cx: &mut ViewContext<Self>) {
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn preview_completion(&mut self, _: &PreviewCompletion, cx: &mut ViewContext<Self>) {
|
||||
let completion = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.recent_completions()
|
||||
.skip(self.selected_index)
|
||||
.take(1)
|
||||
.next()
|
||||
.cloned();
|
||||
|
||||
self.select_completion(completion, false, cx);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
let completion = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.recent_completions()
|
||||
.skip(self.selected_index)
|
||||
.take(1)
|
||||
.next()
|
||||
.cloned();
|
||||
|
||||
self.select_completion(completion, true, cx);
|
||||
}
|
||||
|
||||
pub fn select_completion(
|
||||
&mut self,
|
||||
completion: Option<InlineCompletion>,
|
||||
focus: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// Avoid resetting completion rating if it's already selected.
|
||||
if let Some(completion) = completion.as_ref() {
|
||||
self.selected_index = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.recent_completions()
|
||||
.enumerate()
|
||||
.find(|(_, completion_b)| completion.id == completion_b.id)
|
||||
.map(|(ix, _)| ix)
|
||||
.unwrap_or(self.selected_index);
|
||||
cx.notify();
|
||||
|
||||
if let Some(prev_completion) = self.active_completion.as_ref() {
|
||||
if completion.id == prev_completion.completion.id {
|
||||
if focus {
|
||||
cx.focus_view(&prev_completion.feedback_editor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -69,10 +275,14 @@ impl RateCompletionModal {
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.set_placeholder_text("Your feedback about this completion...", cx);
|
||||
editor.set_placeholder_text("Add your feedback…", cx);
|
||||
if focus {
|
||||
cx.focus_self();
|
||||
}
|
||||
editor
|
||||
}),
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_active_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||
@@ -134,94 +344,131 @@ impl RateCompletionModal {
|
||||
};
|
||||
|
||||
let rated = self.zeta.read(cx).is_completion_rated(completion_id);
|
||||
let feedback_empty = active_completion
|
||||
.feedback_editor
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.is_empty();
|
||||
|
||||
let border_color = cx.theme().colors().border;
|
||||
let bg_color = cx.theme().colors().editor_background;
|
||||
|
||||
let label_container = || h_flex().pl_1().gap_1p5();
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.child(h_flex().justify_center().children(if rated {
|
||||
Some(
|
||||
Label::new("This completion was already rated")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Large),
|
||||
)
|
||||
} else if active_completion.completion.edits.is_empty() {
|
||||
Some(
|
||||
Label::new("This completion didn't produce any edits")
|
||||
.color(Color::Warning)
|
||||
.size(LabelSize::Large),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
v_flex()
|
||||
div()
|
||||
.id("diff")
|
||||
.flex_1()
|
||||
.flex_basis(relative(0.75))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.overflow_y_scroll()
|
||||
.p_2()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_1()
|
||||
.rounded_lg()
|
||||
.py_4()
|
||||
.px_6()
|
||||
.size_full()
|
||||
.bg(bg_color)
|
||||
.overflow_scroll()
|
||||
.child(StyledText::new(diff).with_highlights(&text_style, diff_highlights)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.border_y_1()
|
||||
.border_color(border_color)
|
||||
.child(
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
)
|
||||
.child(
|
||||
Label::new("Ensure you explain why this completion is negative or positive. In case it's negative, report what you expected instead.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
)
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex_basis(relative(0.25))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_1()
|
||||
.rounded_lg()
|
||||
.h_40()
|
||||
.pt_1()
|
||||
.bg(bg_color)
|
||||
.child(active_completion.feedback_editor.clone()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_end()
|
||||
.p_1()
|
||||
.h_8()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.max_w_full()
|
||||
.justify_between()
|
||||
.children(if rated {
|
||||
Some(
|
||||
label_container()
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
.child(Label::new("Rated completion").color(Color::Muted)),
|
||||
)
|
||||
} else if active_completion.completion.edits.is_empty() {
|
||||
Some(
|
||||
label_container()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(Label::new("No edits produced").color(Color::Muted)),
|
||||
)
|
||||
} else {
|
||||
Some(label_container())
|
||||
})
|
||||
.child(
|
||||
Button::new("bad", "👎 Bad Completion")
|
||||
.size(ButtonSize::Large)
|
||||
.disabled(rated)
|
||||
.label_size(LabelSize::Large)
|
||||
.color(Color::Error)
|
||||
.on_click({
|
||||
let completion = active_completion.completion.clone();
|
||||
let feedback_editor = active_completion.feedback_editor.clone();
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.zeta.update(cx, |zeta, cx| {
|
||||
zeta.rate_completion(
|
||||
&completion,
|
||||
InlineCompletionRating::Negative,
|
||||
feedback_editor.read(cx).text(cx),
|
||||
cx,
|
||||
)
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("bad", "Bad Completion")
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ThumbsDown,
|
||||
&self.focus_handle(cx),
|
||||
cx,
|
||||
))
|
||||
.style(ButtonStyle::Tinted(TintColor::Negative))
|
||||
.icon(IconName::ThumbsDown)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_color(Color::Error)
|
||||
.disabled(rated || feedback_empty)
|
||||
.when(feedback_empty, |this| {
|
||||
this.tooltip(|cx| {
|
||||
Tooltip::text("Explain why this completion is bad before reporting it", cx)
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("good", "👍 Good Completion")
|
||||
.size(ButtonSize::Large)
|
||||
.disabled(rated)
|
||||
.label_size(LabelSize::Large)
|
||||
.color(Color::Success)
|
||||
.on_click({
|
||||
let completion = active_completion.completion.clone();
|
||||
let feedback_editor = active_completion.feedback_editor.clone();
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.zeta.update(cx, |zeta, cx| {
|
||||
zeta.rate_completion(
|
||||
&completion,
|
||||
InlineCompletionRating::Positive,
|
||||
feedback_editor.read(cx).text(cx),
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.thumbs_down_active(
|
||||
&ThumbsDownActiveCompletion,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("good", "Good Completion")
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ThumbsUp,
|
||||
&self.focus_handle(cx),
|
||||
cx,
|
||||
))
|
||||
.style(ButtonStyle::Tinted(TintColor::Positive))
|
||||
.icon(IconName::ThumbsUp)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_color(Color::Success)
|
||||
.disabled(rated)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.thumbs_up_active(&ThumbsUpActiveCompletion, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -230,30 +477,53 @@ impl RateCompletionModal {
|
||||
|
||||
impl Render for RateCompletionModal {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let border_color = cx.theme().colors().border;
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.w(cx.viewport_size().width - px(256.))
|
||||
.h(cx.viewport_size().height - px(256.))
|
||||
.rounded_lg()
|
||||
.shadow_lg()
|
||||
.p_2()
|
||||
.key_context("RateCompletionModal")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::select_prev))
|
||||
.on_action(cx.listener(Self::select_prev_edit))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_next_edit))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::thumbs_up))
|
||||
.on_action(cx.listener(Self::thumbs_up_active))
|
||||
.on_action(cx.listener(Self::thumbs_down_active))
|
||||
.on_action(cx.listener(Self::focus_completions))
|
||||
.on_action(cx.listener(Self::preview_completion))
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.w(cx.viewport_size().width - px(320.))
|
||||
.h(cx.viewport_size().height - px(300.))
|
||||
.rounded_lg()
|
||||
.shadow_lg()
|
||||
.child(
|
||||
div()
|
||||
.id("completion_list")
|
||||
.border_r_1()
|
||||
.border_color(border_color)
|
||||
.w_96()
|
||||
.h_full()
|
||||
.p_0p5()
|
||||
.overflow_y_scroll()
|
||||
.child(
|
||||
ui::List::new()
|
||||
List::new()
|
||||
.empty_message(
|
||||
"No completions, use the editor to generate some and rate them!",
|
||||
div()
|
||||
.p_2()
|
||||
.child(
|
||||
Label::new("No completions yet. Use the editor to generate some and rate them!")
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.children(self.zeta.read(cx).recent_completions().cloned().map(
|
||||
|completion| {
|
||||
.children(self.zeta.read(cx).recent_completions().cloned().enumerate().map(
|
||||
|(index, completion)| {
|
||||
let selected =
|
||||
self.active_completion.as_ref().map_or(false, |selected| {
|
||||
selected.completion.id == completion.id
|
||||
@@ -261,25 +531,30 @@ impl Render for RateCompletionModal {
|
||||
let rated =
|
||||
self.zeta.read(cx).is_completion_rated(completion.id);
|
||||
ListItem::new(completion.id)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.focused(index == self.selected_index)
|
||||
.selected(selected)
|
||||
.end_slot(if rated {
|
||||
.start_slot(if rated {
|
||||
Icon::new(IconName::Check).color(Color::Success)
|
||||
} else if completion.edits.is_empty() {
|
||||
Icon::new(IconName::Ellipsis).color(Color::Muted)
|
||||
Icon::new(IconName::File).color(Color::Muted).size(IconSize::Small)
|
||||
} else {
|
||||
Icon::new(IconName::Diff).color(Color::Muted)
|
||||
Icon::new(IconName::FileDiff).color(Color::Accent).size(IconSize::Small)
|
||||
})
|
||||
.child(Label::new(
|
||||
completion.path.to_string_lossy().to_string(),
|
||||
))
|
||||
).size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(format!("({})", completion.id))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
div()
|
||||
.overflow_hidden()
|
||||
.text_ellipsis()
|
||||
.child(Label::new(format!("({})", completion.id))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall)),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.select_completion(Some(completion.clone()), cx);
|
||||
this.select_completion(Some(completion.clone()), true, cx);
|
||||
}))
|
||||
},
|
||||
)),
|
||||
|
||||
@@ -6,7 +6,10 @@ use anyhow::{anyhow, Context as _, Result};
|
||||
use client::Client;
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{actions, AppContext, Context, Global, Model, ModelContext, Subscription, Task};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context, EntityId, Global, Model, ModelContext,
|
||||
Subscription, Task,
|
||||
};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language::{
|
||||
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, OffsetRangeExt,
|
||||
@@ -18,6 +21,7 @@ use std::{
|
||||
borrow::Cow,
|
||||
cmp,
|
||||
fmt::Write,
|
||||
future::Future,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::Path,
|
||||
@@ -253,12 +257,17 @@ impl Zeta {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_completion(
|
||||
pub fn request_completion_impl<F, R>(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: language::Anchor,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<InlineCompletion>> {
|
||||
perform_predict_edits: F,
|
||||
) -> Task<Result<InlineCompletion>>
|
||||
where
|
||||
F: FnOnce(Arc<Client>, LlmApiToken, PredictEditsParams) -> R + 'static,
|
||||
R: Future<Output = Result<PredictEditsResponse>> + Send + 'static,
|
||||
{
|
||||
let snapshot = self.report_changes_for_buffer(buffer, cx);
|
||||
let point = position.to_point(&snapshot);
|
||||
let offset = point.to_offset(&snapshot);
|
||||
@@ -292,7 +301,7 @@ impl Zeta {
|
||||
input_excerpt: input_excerpt.clone(),
|
||||
};
|
||||
|
||||
let response = Self::perform_predict_edits(&client, llm_token, body).await?;
|
||||
let response = perform_predict_edits(client, llm_token, body).await?;
|
||||
|
||||
let output_excerpt = response.output_excerpt;
|
||||
log::debug!("prediction took: {:?}", start.elapsed());
|
||||
@@ -305,7 +314,9 @@ impl Zeta {
|
||||
path,
|
||||
input_events,
|
||||
input_excerpt,
|
||||
)?;
|
||||
&cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.recent_completions
|
||||
@@ -320,50 +331,210 @@ impl Zeta {
|
||||
})
|
||||
}
|
||||
|
||||
async fn perform_predict_edits(
|
||||
client: &Arc<Client>,
|
||||
// Generates several example completions of various states to fill the Zeta completion modal
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fill_with_fake_completions(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
|
||||
let test_buffer_text = indoc::indoc! {r#"a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
|
||||
And maybe a short line
|
||||
|
||||
Then a few lines
|
||||
|
||||
and then another
|
||||
"#};
|
||||
|
||||
let buffer = cx.new_model(|cx| Buffer::local(test_buffer_text, cx));
|
||||
let position = buffer.read(cx).anchor_before(Point::new(1, 0));
|
||||
|
||||
let completion_tasks = vec![
|
||||
self.fake_completion(
|
||||
&buffer,
|
||||
position,
|
||||
PredictEditsResponse {
|
||||
output_excerpt: format!("{EDITABLE_REGION_START_MARKER}
|
||||
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
|
||||
[here's an edit]
|
||||
And maybe a short line
|
||||
Then a few lines
|
||||
and then another
|
||||
{EDITABLE_REGION_END_MARKER}
|
||||
", ),
|
||||
},
|
||||
cx,
|
||||
),
|
||||
self.fake_completion(
|
||||
&buffer,
|
||||
position,
|
||||
PredictEditsResponse {
|
||||
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
|
||||
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
|
||||
And maybe a short line
|
||||
[and another edit]
|
||||
Then a few lines
|
||||
and then another
|
||||
{EDITABLE_REGION_END_MARKER}
|
||||
"#),
|
||||
},
|
||||
cx,
|
||||
),
|
||||
self.fake_completion(
|
||||
&buffer,
|
||||
position,
|
||||
PredictEditsResponse {
|
||||
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
|
||||
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
|
||||
And maybe a short line
|
||||
|
||||
Then a few lines
|
||||
|
||||
and then another
|
||||
{EDITABLE_REGION_END_MARKER}
|
||||
"#),
|
||||
},
|
||||
cx,
|
||||
),
|
||||
self.fake_completion(
|
||||
&buffer,
|
||||
position,
|
||||
PredictEditsResponse {
|
||||
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
|
||||
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
|
||||
And maybe a short line
|
||||
|
||||
Then a few lines
|
||||
|
||||
and then another
|
||||
{EDITABLE_REGION_END_MARKER}
|
||||
"#),
|
||||
},
|
||||
cx,
|
||||
),
|
||||
self.fake_completion(
|
||||
&buffer,
|
||||
position,
|
||||
PredictEditsResponse {
|
||||
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
|
||||
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
|
||||
And maybe a short line
|
||||
Then a few lines
|
||||
[a third completion]
|
||||
and then another
|
||||
{EDITABLE_REGION_END_MARKER}
|
||||
"#),
|
||||
},
|
||||
cx,
|
||||
),
|
||||
self.fake_completion(
|
||||
&buffer,
|
||||
position,
|
||||
PredictEditsResponse {
|
||||
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
|
||||
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
|
||||
And maybe a short line
|
||||
and then another
|
||||
[fourth completion example]
|
||||
{EDITABLE_REGION_END_MARKER}
|
||||
"#),
|
||||
},
|
||||
cx,
|
||||
),
|
||||
self.fake_completion(
|
||||
&buffer,
|
||||
position,
|
||||
PredictEditsResponse {
|
||||
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
|
||||
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
|
||||
And maybe a short line
|
||||
Then a few lines
|
||||
and then another
|
||||
[fifth and final completion]
|
||||
{EDITABLE_REGION_END_MARKER}
|
||||
"#),
|
||||
},
|
||||
cx,
|
||||
),
|
||||
];
|
||||
|
||||
cx.spawn(|zeta, mut cx| async move {
|
||||
for task in completion_tasks {
|
||||
task.await.unwrap();
|
||||
}
|
||||
|
||||
zeta.update(&mut cx, |zeta, _cx| {
|
||||
zeta.recent_completions.get_mut(2).unwrap().edits = Arc::new([]);
|
||||
zeta.recent_completions.get_mut(3).unwrap().edits = Arc::new([]);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fake_completion(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: language::Anchor,
|
||||
response: PredictEditsResponse,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<InlineCompletion>> {
|
||||
use std::future::ready;
|
||||
|
||||
self.request_completion_impl(buffer, position, cx, |_, _, _| ready(Ok(response)))
|
||||
}
|
||||
|
||||
pub fn request_completion(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: language::Anchor,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<InlineCompletion>> {
|
||||
self.request_completion_impl(buffer, position, cx, Self::perform_predict_edits)
|
||||
}
|
||||
|
||||
fn perform_predict_edits(
|
||||
client: Arc<Client>,
|
||||
llm_token: LlmApiToken,
|
||||
body: PredictEditsParams,
|
||||
) -> Result<PredictEditsResponse> {
|
||||
let http_client = client.http_client();
|
||||
let mut token = llm_token.acquire(client).await?;
|
||||
let mut did_retry = false;
|
||||
) -> impl Future<Output = Result<PredictEditsResponse>> {
|
||||
async move {
|
||||
let http_client = client.http_client();
|
||||
let mut token = llm_token.acquire(&client).await?;
|
||||
let mut did_retry = false;
|
||||
|
||||
loop {
|
||||
let request_builder = http_client::Request::builder();
|
||||
let request = request_builder
|
||||
.method(Method::POST)
|
||||
.uri(
|
||||
http_client
|
||||
.build_zed_llm_url("/predict_edits", &[])?
|
||||
.as_ref(),
|
||||
)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.body(serde_json::to_string(&body)?.into())?;
|
||||
loop {
|
||||
let request_builder = http_client::Request::builder();
|
||||
let request = request_builder
|
||||
.method(Method::POST)
|
||||
.uri(
|
||||
http_client
|
||||
.build_zed_llm_url("/predict_edits", &[])?
|
||||
.as_ref(),
|
||||
)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.body(serde_json::to_string(&body)?.into())?;
|
||||
|
||||
let mut response = http_client.send(request).await?;
|
||||
let mut response = http_client.send(request).await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
return Ok(serde_json::from_str(&body)?);
|
||||
} else if !did_retry
|
||||
&& response
|
||||
.headers()
|
||||
.get(EXPIRED_LLM_TOKEN_HEADER_NAME)
|
||||
.is_some()
|
||||
{
|
||||
did_retry = true;
|
||||
token = llm_token.refresh(client).await?;
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
return Err(anyhow!(
|
||||
"error predicting edits.\nStatus: {:?}\nBody: {}",
|
||||
response.status(),
|
||||
body
|
||||
));
|
||||
if response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
return Ok(serde_json::from_str(&body)?);
|
||||
} else if !did_retry
|
||||
&& response
|
||||
.headers()
|
||||
.get(EXPIRED_LLM_TOKEN_HEADER_NAME)
|
||||
.is_some()
|
||||
{
|
||||
did_retry = true;
|
||||
token = llm_token.refresh(&client).await?;
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
return Err(anyhow!(
|
||||
"error predicting edits.\nStatus: {:?}\nBody: {}",
|
||||
response.status(),
|
||||
body
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,41 +546,45 @@ impl Zeta {
|
||||
path: Arc<Path>,
|
||||
input_events: String,
|
||||
input_excerpt: String,
|
||||
) -> Result<InlineCompletion> {
|
||||
let content = output_excerpt.replace(CURSOR_MARKER, "");
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<InlineCompletion>> {
|
||||
let snapshot = snapshot.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let content = output_excerpt.replace(CURSOR_MARKER, "");
|
||||
|
||||
let codefence_start = content
|
||||
.find(EDITABLE_REGION_START_MARKER)
|
||||
.context("could not find start marker")?;
|
||||
let content = &content[codefence_start..];
|
||||
let codefence_start = content
|
||||
.find(EDITABLE_REGION_START_MARKER)
|
||||
.context("could not find start marker")?;
|
||||
let content = &content[codefence_start..];
|
||||
|
||||
let newline_ix = content.find('\n').context("could not find newline")?;
|
||||
let content = &content[newline_ix + 1..];
|
||||
let newline_ix = content.find('\n').context("could not find newline")?;
|
||||
let content = &content[newline_ix + 1..];
|
||||
|
||||
let codefence_end = content
|
||||
.rfind(&format!("\n{EDITABLE_REGION_END_MARKER}"))
|
||||
.context("could not find end marker")?;
|
||||
let new_text = &content[..codefence_end];
|
||||
let codefence_end = content
|
||||
.rfind(&format!("\n{EDITABLE_REGION_END_MARKER}"))
|
||||
.context("could not find end marker")?;
|
||||
let new_text = &content[..codefence_end];
|
||||
|
||||
let old_text = snapshot
|
||||
.text_for_range(excerpt_range.clone())
|
||||
.collect::<String>();
|
||||
let old_text = snapshot
|
||||
.text_for_range(excerpt_range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let edits = Self::compute_edits(old_text, new_text, excerpt_range.start, snapshot);
|
||||
let edits = Self::compute_edits(old_text, new_text, excerpt_range.start, &snapshot);
|
||||
|
||||
Ok(InlineCompletion {
|
||||
id: InlineCompletionId::new(),
|
||||
path,
|
||||
excerpt_range,
|
||||
edits: edits.into(),
|
||||
snapshot: snapshot.clone(),
|
||||
input_events: input_events.into(),
|
||||
input_excerpt: input_excerpt.into(),
|
||||
output_excerpt: output_excerpt.into(),
|
||||
Ok(InlineCompletion {
|
||||
id: InlineCompletionId::new(),
|
||||
path,
|
||||
excerpt_range,
|
||||
edits: edits.into(),
|
||||
snapshot: snapshot.clone(),
|
||||
input_events: input_events.into(),
|
||||
input_excerpt: input_excerpt.into(),
|
||||
output_excerpt: output_excerpt.into(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn compute_edits(
|
||||
pub fn compute_edits(
|
||||
old_text: String,
|
||||
new_text: &str,
|
||||
offset: usize,
|
||||
@@ -500,10 +675,14 @@ impl Zeta {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn recent_completions(&self) -> impl Iterator<Item = &InlineCompletion> {
|
||||
pub fn recent_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
|
||||
self.recent_completions.iter()
|
||||
}
|
||||
|
||||
pub fn recent_completions_len(&self) -> usize {
|
||||
self.recent_completions.len()
|
||||
}
|
||||
|
||||
fn report_changes_for_buffer(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -665,9 +844,14 @@ impl Event {
|
||||
}
|
||||
}
|
||||
|
||||
struct CurrentInlineCompletion {
|
||||
buffer_id: EntityId,
|
||||
completion: InlineCompletion,
|
||||
}
|
||||
|
||||
pub struct ZetaInlineCompletionProvider {
|
||||
zeta: Model<Zeta>,
|
||||
current_completion: Option<InlineCompletion>,
|
||||
current_completion: Option<CurrentInlineCompletion>,
|
||||
pending_refresh: Task<()>,
|
||||
}
|
||||
|
||||
@@ -708,28 +892,34 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
debounce: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.pending_refresh = cx.spawn(|this, mut cx| async move {
|
||||
if debounce {
|
||||
cx.background_executor().timer(Self::DEBOUNCE_TIMEOUT).await;
|
||||
}
|
||||
self.pending_refresh =
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if debounce {
|
||||
cx.background_executor().timer(Self::DEBOUNCE_TIMEOUT).await;
|
||||
}
|
||||
|
||||
let completion_request = this.update(&mut cx, |this, cx| {
|
||||
this.zeta.update(cx, |zeta, cx| {
|
||||
zeta.request_completion(&buffer, position, cx)
|
||||
let completion_request = this.update(&mut cx, |this, cx| {
|
||||
this.zeta.update(cx, |zeta, cx| {
|
||||
zeta.request_completion(&buffer, position, cx)
|
||||
})
|
||||
});
|
||||
|
||||
let mut completion = None;
|
||||
if let Ok(completion_request) = completion_request {
|
||||
completion = completion_request.await.log_err().map(|completion| {
|
||||
CurrentInlineCompletion {
|
||||
buffer_id: buffer.entity_id(),
|
||||
completion,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.current_completion = completion;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
let mut completion = None;
|
||||
if let Ok(completion_request) = completion_request {
|
||||
completion = completion_request.await.log_err();
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.current_completion = completion;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
@@ -754,7 +944,16 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
cursor_position: language::Anchor,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<inline_completion::InlineCompletion> {
|
||||
let completion = self.current_completion.as_mut()?;
|
||||
let CurrentInlineCompletion {
|
||||
buffer_id,
|
||||
completion,
|
||||
} = self.current_completion.as_mut()?;
|
||||
|
||||
// Invalidate previous completion if it was generated for a different buffer.
|
||||
if *buffer_id != buffer.entity_id() {
|
||||
self.current_completion.take();
|
||||
return None;
|
||||
}
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
let Some(edits) = completion.interpolate(buffer.snapshot()) else {
|
||||
|
||||
@@ -14,6 +14,16 @@ CompileFlags:
|
||||
Add: [-xc]
|
||||
```
|
||||
|
||||
By default clang and gcc by will recognize `*.C` and `*.H` (uppercase extensions) as C++ and not C and so Zed too follows this convention. If you are working with a C-only project (perhaps one with legacy uppercase pathing like `FILENAME.C`) you can override this behavior by adding this to your settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"file_types": {
|
||||
"C": ["C", "H"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Formatting
|
||||
|
||||
By default Zed will use the `clangd` language server for formatting C code. The Clangd is the same as the `clang-format` CLI tool. To configure this you can add a `.clang-format` file. For example:
|
||||
|
||||
@@ -167,6 +167,7 @@ if [[ -n $pacman ]]; then
|
||||
deps=(
|
||||
gcc
|
||||
clang
|
||||
musl
|
||||
cmake
|
||||
alsa-lib
|
||||
fontconfig
|
||||
|
||||