Compare commits
131 Commits
github-tok
...
assistant2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f667a06003 | ||
|
|
3c57523920 | ||
|
|
5d76b4202a | ||
|
|
dc08b3e174 | ||
|
|
b5ffb12510 | ||
|
|
205480670c | ||
|
|
d37b05d376 | ||
|
|
6cfa746dda | ||
|
|
c30afd6599 | ||
|
|
c4a0312c03 | ||
|
|
60cd888d22 | ||
|
|
28fba36176 | ||
|
|
7f293c9d9e | ||
|
|
849c7dd46c | ||
|
|
5c6dc82860 | ||
|
|
bb1aa2e0e5 | ||
|
|
d33304d41a | ||
|
|
451230d843 | ||
|
|
43a3f63cab | ||
|
|
1e28882260 | ||
|
|
4476e02bd6 | ||
|
|
2cb9083c5a | ||
|
|
ad60bbc242 | ||
|
|
e701fc113b | ||
|
|
367bd32789 | ||
|
|
85b34bb1cf | ||
|
|
e00601f96f | ||
|
|
250c481c63 | ||
|
|
152b77a2c1 | ||
|
|
8805d185a3 | ||
|
|
e494772cd6 | ||
|
|
1d80ea1751 | ||
|
|
ad12a23159 | ||
|
|
9d681bc163 | ||
|
|
0d5dfba815 | ||
|
|
39ace6c108 | ||
|
|
2953aab1c7 | ||
|
|
b7ba5d3c27 | ||
|
|
19111d6d15 | ||
|
|
f3d8a777ad | ||
|
|
f0fdd7459f | ||
|
|
fa029b038e | ||
|
|
02aa68f997 | ||
|
|
c98e4eb41b | ||
|
|
9d5d28e583 | ||
|
|
34e7d31800 | ||
|
|
d8c9b0dd11 | ||
|
|
342fa96fb0 | ||
|
|
fa62a5abfa | ||
|
|
0780bafc5a | ||
|
|
385da79021 | ||
|
|
b857beb2c6 | ||
|
|
3d938fcfdf | ||
|
|
15dec0e2b4 | ||
|
|
c936b66cca | ||
|
|
4e652e9214 | ||
|
|
8243401d42 | ||
|
|
b0243eb8bd | ||
|
|
93a247809c | ||
|
|
661b8ca305 | ||
|
|
e66f8230a1 | ||
|
|
3f30f27ce8 | ||
|
|
7f76b63bd4 | ||
|
|
11527c5822 | ||
|
|
30dca54a34 | ||
|
|
14567e6400 | ||
|
|
83392354e5 | ||
|
|
27a230d75a | ||
|
|
1a1b8010ce | ||
|
|
1ee8682fdb | ||
|
|
e767221817 | ||
|
|
f7e458b598 | ||
|
|
88100c956f | ||
|
|
4429495fb5 | ||
|
|
02e269a5c5 | ||
|
|
7e69dafd94 | ||
|
|
aab44814be | ||
|
|
41feda5abe | ||
|
|
89d5d9a16f | ||
|
|
4ff38fc823 | ||
|
|
3007fb51c3 | ||
|
|
cd3972bc52 | ||
|
|
5ceb6ff351 | ||
|
|
06da24697d | ||
|
|
b503dd63e6 | ||
|
|
9589630dfe | ||
|
|
1dbbde02ae | ||
|
|
fc1ff4b061 | ||
|
|
f0b7ea9a50 | ||
|
|
912d5469d4 | ||
|
|
225f21dd95 | ||
|
|
26be3c22a1 | ||
|
|
c6c53d8fd3 | ||
|
|
f035697232 | ||
|
|
3a6ffc7de4 | ||
|
|
8cce847ea7 | ||
|
|
32b3c1e378 | ||
|
|
8389c8e254 | ||
|
|
757532a09e | ||
|
|
228a4286ad | ||
|
|
6b27c860a8 | ||
|
|
e76b0dc38c | ||
|
|
355ce405cb | ||
|
|
ec3dd27bc6 | ||
|
|
9705e26cff | ||
|
|
7ddf0467a5 | ||
|
|
19aadacdef | ||
|
|
f80ac2c190 | ||
|
|
876d017294 | ||
|
|
84e3063d4b | ||
|
|
4999cf136f | ||
|
|
8099fb9845 | ||
|
|
636bdf1196 | ||
|
|
93501bcb0c | ||
|
|
f72e74e310 | ||
|
|
cc753b88e1 | ||
|
|
55a8d3b696 | ||
|
|
02a5da3e0e | ||
|
|
2f8dc894e1 | ||
|
|
7e5a585ca7 | ||
|
|
078f9ed689 | ||
|
|
514902cbac | ||
|
|
6a61f9577f | ||
|
|
57d4878d4a | ||
|
|
9e1706feb0 | ||
|
|
a7345fa596 | ||
|
|
ba4c2a56e0 | ||
|
|
9bfcc631b9 | ||
|
|
8ee48a7133 | ||
|
|
240909d7cb | ||
|
|
47c75a6c89 |
@@ -3,5 +3,10 @@
|
||||
"label": "clippy",
|
||||
"command": "cargo",
|
||||
"args": ["xtask", "clippy"]
|
||||
},
|
||||
{
|
||||
"label": "assistant2",
|
||||
"command": "cargo",
|
||||
"args": ["run", "-p", "assistant2", "--example", "assistant_example"]
|
||||
}
|
||||
]
|
||||
|
||||
30
Cargo.lock
generated
30
Cargo.lock
generated
@@ -371,6 +371,35 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"client",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"languages",
|
||||
"log",
|
||||
"nanoid",
|
||||
"node_runtime",
|
||||
"open_ai",
|
||||
"project",
|
||||
"release_channel",
|
||||
"rich_text",
|
||||
"semantic_index",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.0"
|
||||
@@ -10738,6 +10767,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"itertools 0.11.0",
|
||||
"menu",
|
||||
"nanoid",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"story",
|
||||
|
||||
@@ -4,6 +4,7 @@ members = [
|
||||
"crates/anthropic",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant2",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@@ -205,6 +206,7 @@ rpc = { path = "crates/rpc" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
settings = { path = "crates/settings" }
|
||||
snippet = { path = "crates/snippet" }
|
||||
|
||||
@@ -209,7 +209,14 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"context": "AssistantChat > Editor", // Used in the assistant2 crate
|
||||
"bindings": {
|
||||
"enter": ["assistant::Submit", "Simple"],
|
||||
"cmd-enter": ["assistant::Submit", "Codebase"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel", // Used in the assistant crate, which we're replacing
|
||||
"bindings": {
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch"
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
path = "src/assets.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build.
|
||||
use anyhow::anyhow;
|
||||
|
||||
use gpui::{AssetSource, Result, SharedString};
|
||||
use gpui::{AppContext, AssetSource, Result, SharedString};
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
@@ -34,3 +34,19 @@ impl AssetSource for Assets {
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Assets {
|
||||
/// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory.
|
||||
pub fn load_fonts(&self, cx: &AppContext) -> gpui::Result<()> {
|
||||
let font_paths = self.list("fonts")?;
|
||||
let mut embedded_fonts = Vec::new();
|
||||
for font_path in font_paths {
|
||||
if font_path.ends_with(".ttf") {
|
||||
let font_bytes = cx.asset_source().load(&font_path)?;
|
||||
embedded_fonts.push(font_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
cx.text_system().add_fonts(embedded_fonts)
|
||||
}
|
||||
}
|
||||
52
crates/assistant2/Cargo.toml
Normal file
52
crates/assistant2/Cargo.toml
Normal file
@@ -0,0 +1,52 @@
|
||||
[package]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/assistant2.rs"
|
||||
|
||||
[[example]]
|
||||
name = "assistant_example"
|
||||
path = "examples/assistant_example.rs"
|
||||
crate-type = ["bin"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
open_ai.workspace = true
|
||||
project.workspace = true
|
||||
rich_text.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
nanoid = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
assets.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
languages.workspace = true
|
||||
node_runtime.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
120
crates/assistant2/examples/assistant_example.rs
Normal file
120
crates/assistant2/examples/assistant_example.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use assets::Assets;
|
||||
use assistant2::AssistantPanel;
|
||||
use client::Client;
|
||||
use gpui::{actions, App, AppContext, KeyBinding, Model, Task, View, WindowOptions};
|
||||
use language::LanguageRegistry;
|
||||
use project::{Fs, Project};
|
||||
use semantic_index::{OpenAiEmbeddingModel, OpenAiEmbeddingProvider, ProjectIndex, SemanticIndex};
|
||||
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::LoadThemes;
|
||||
use ui::{div, prelude::*, Render};
|
||||
use util::http::HttpClientWithUrl;
|
||||
|
||||
actions!(example, [Quit]);
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
env_logger::init();
|
||||
App::new().with_assets(Assets).run(|cx| {
|
||||
cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
|
||||
cx.on_action(|_: &Quit, cx: &mut AppContext| {
|
||||
cx.quit();
|
||||
});
|
||||
|
||||
if args.len() < 2 {
|
||||
eprintln!(
|
||||
"Usage: cargo run --example assistant_example -p assistant2 -- <project_path>"
|
||||
);
|
||||
cx.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
settings::init(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
editor::init(cx);
|
||||
theme::init(LoadThemes::JustBase, cx);
|
||||
Assets.load_fonts(cx).unwrap();
|
||||
KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
|
||||
client::init_settings(cx);
|
||||
release_channel::init("0.130.0", cx);
|
||||
|
||||
let client = Client::production(cx);
|
||||
{
|
||||
let client = client.clone();
|
||||
cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
assistant2::init(client.clone(), cx);
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::new(
|
||||
Task::ready(()),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
|
||||
languages::init(language_registry.clone(), node_runtime, cx);
|
||||
|
||||
let http = Arc::new(HttpClientWithUrl::new("http://localhost:11434"));
|
||||
|
||||
let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set");
|
||||
let embedding_provider = OpenAiEmbeddingProvider::new(
|
||||
http.clone(),
|
||||
OpenAiEmbeddingModel::TextEmbedding3Small,
|
||||
open_ai::OPEN_AI_API_URL.to_string(),
|
||||
api_key,
|
||||
);
|
||||
|
||||
let semantic_index = SemanticIndex::new(
|
||||
PathBuf::from("/tmp/semantic-index-db.mdb"),
|
||||
Arc::new(embedding_provider),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let project_path = Path::new(&args[1]);
|
||||
dbg!(project_path);
|
||||
let project = Project::example([project_path], &mut cx).await;
|
||||
let mut semantic_index = semantic_index.await?;
|
||||
|
||||
cx.update(|cx| {
|
||||
let fs = project.read(cx).fs().clone();
|
||||
|
||||
let project_index = semantic_index.project_index(project.clone(), cx);
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
cx.new_view(|cx| Example::new(language_registry, project_index, fs, cx))
|
||||
});
|
||||
cx.activate(true);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
|
||||
struct Example {
|
||||
assistant_panel: View<AssistantPanel>,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project_index: Model<ProjectIndex>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
assistant_panel: cx
|
||||
.new_view(|cx| AssistantPanel::new(language_registry, project_index, fs, cx)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
|
||||
div().size_full().child(self.assistant_panel.clone())
|
||||
}
|
||||
}
|
||||
788
crates/assistant2/src/assistant2.rs
Normal file
788
crates/assistant2/src/assistant2.rs
Normal file
@@ -0,0 +1,788 @@
|
||||
mod completion_provider;
|
||||
|
||||
use anyhow::Result;
|
||||
use client::Client;
|
||||
use completion_provider::*;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use futures::{channel::oneshot, Future, FutureExt as _, StreamExt};
|
||||
use gpui::{
|
||||
list, prelude::*, AnyElement, AppContext, FocusHandle, Global, ListAlignment, ListState, Model,
|
||||
Render, Task, View,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, LanguageRegistry};
|
||||
use project::Fs;
|
||||
use rich_text::RichText;
|
||||
use semantic_index::ProjectIndex;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{cmp, sync::Arc};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{popover_menu, prelude::*, ButtonLike, CollapsibleContainer, Color, ContextMenu, Tooltip};
|
||||
use util::ResultExt;
|
||||
|
||||
// gpui::actions!(assistant, [Submit]);
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
|
||||
pub struct Submit(SubmitMode);
|
||||
|
||||
/// There are multiple different ways to submit a model request, represented by this enum.
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
|
||||
pub enum SubmitMode {
|
||||
/// Only include the conversation.
|
||||
Simple,
|
||||
/// Send the current file as context.
|
||||
CurrentFile,
|
||||
/// Search the codebase and send relevant excerpts.
|
||||
Codebase,
|
||||
}
|
||||
|
||||
gpui::impl_actions!(assistant, [Submit]);
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
cx.set_global(CompletionProvider::new(CloudCompletionProvider::new(
|
||||
client,
|
||||
)));
|
||||
}
|
||||
|
||||
pub struct AssistantPanel {
|
||||
#[allow(dead_code)]
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
#[allow(dead_code)]
|
||||
project_index: Model<ProjectIndex>,
|
||||
#[allow(dead_code)]
|
||||
fs: Arc<dyn Fs>,
|
||||
chat: View<AssistantChat>,
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
pub fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project_index: Model<ProjectIndex>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let chat = cx.new_view(|cx| {
|
||||
AssistantChat::new(
|
||||
language_registry.clone(),
|
||||
project_index.clone(),
|
||||
fs.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
Self {
|
||||
language_registry,
|
||||
project_index,
|
||||
fs,
|
||||
chat,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.v_flex()
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(self.chat.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct AssistantChat {
|
||||
model: String,
|
||||
messages: Vec<ChatMessage>,
|
||||
list_state: ListState,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project_index: Model<ProjectIndex>,
|
||||
fs: Arc<dyn Fs>,
|
||||
next_message_id: MessageId,
|
||||
pending_completion: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl AssistantChat {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project_index: Model<ProjectIndex>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let model = CompletionProvider::get(cx).default_model();
|
||||
let view = cx.view().downgrade();
|
||||
let list_state = ListState::new(
|
||||
0,
|
||||
ListAlignment::Bottom,
|
||||
px(1024.),
|
||||
move |ix, cx: &mut WindowContext| {
|
||||
view.update(cx, |this, cx| this.render_message(ix, cx))
|
||||
.unwrap()
|
||||
},
|
||||
);
|
||||
|
||||
let mut this = Self {
|
||||
model,
|
||||
messages: Vec::new(),
|
||||
list_state,
|
||||
language_registry,
|
||||
project_index,
|
||||
fs,
|
||||
next_message_id: MessageId(0),
|
||||
pending_completion: None,
|
||||
};
|
||||
this.push_new_user_message(true, cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
|
||||
self.messages.iter().find_map(|message| match message {
|
||||
ChatMessage::User(message) => message
|
||||
.body
|
||||
.focus_handle(cx)
|
||||
.contains_focused(cx)
|
||||
.then_some(message.id),
|
||||
ChatMessage::Assistant(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
|
||||
let Some(focused_message_id) = self.focused_message_id(cx) else {
|
||||
log::error!("unexpected state: no user message editor is focused.");
|
||||
return;
|
||||
};
|
||||
|
||||
self.truncate_messages(focused_message_id, cx);
|
||||
self.push_new_assistant_message(cx);
|
||||
|
||||
let populate = self.populate_context_on_submit(focused_message_id, mode, cx);
|
||||
|
||||
self.pending_completion = Some(cx.spawn(|this, mut cx| async move {
|
||||
let complete = async {
|
||||
populate.await?;
|
||||
|
||||
let completion = this.update(&mut cx, |this, cx| {
|
||||
CompletionProvider::get(cx).complete(
|
||||
this.model.clone(),
|
||||
this.completion_messages(cx),
|
||||
Vec::new(),
|
||||
1.0,
|
||||
)
|
||||
});
|
||||
|
||||
let mut stream = completion?.await?;
|
||||
|
||||
let mut body = String::new();
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(ChatMessage::Assistant(AssistantMessage {
|
||||
body: message_body,
|
||||
..
|
||||
})) = this.messages.last_mut()
|
||||
{
|
||||
body.push_str(&chunk);
|
||||
*message_body =
|
||||
RichText::new(body.clone(), &[], &this.language_registry);
|
||||
cx.notify();
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Err(error) = complete {
|
||||
if let Some(ChatMessage::Assistant(AssistantMessage {
|
||||
error: message_error,
|
||||
..
|
||||
})) = this.messages.last_mut()
|
||||
{
|
||||
message_error.replace(SharedString::from(error.to_string()));
|
||||
cx.notify();
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
let focus = this
|
||||
.user_message(focused_message_id)
|
||||
.body
|
||||
.focus_handle(cx)
|
||||
.contains_focused(cx);
|
||||
this.push_new_user_message(focus, cx);
|
||||
})
|
||||
.log_err();
|
||||
}));
|
||||
}
|
||||
|
||||
/// Set up the query designed for the semantic index, based on previous conversation
|
||||
fn setup_query(&self, cx: &mut ViewContext<Self>) -> Task<Result<String>> {
|
||||
// Let's try another approach where we take the user's previous messages and turn that into a query
|
||||
// by calling for a completion.
|
||||
|
||||
// For now, we'll set up a summary request message, where we tell the model we need something simple to summarize
|
||||
|
||||
let mut query_creation_messages = self.completion_messages(cx);
|
||||
|
||||
query_creation_messages.push(CompletionMessage {
|
||||
role: CompletionRole::System,
|
||||
body: r#"
|
||||
Turn the user's query into a single search string that can be used to search for code base snippets relevant to the user's query. Everything you respond with will be fed directly to a semantic index.
|
||||
|
||||
## Example
|
||||
|
||||
**User**: How can I create a component in GPUI that works like a `<details>` / `<summary>` pair in HTML?
|
||||
|
||||
GPUI create component like HTML details summary example
|
||||
"#.into(),
|
||||
});
|
||||
|
||||
let query = CompletionProvider::get(cx).complete(
|
||||
self.model.clone(),
|
||||
query_creation_messages,
|
||||
Vec::new(),
|
||||
1.0,
|
||||
);
|
||||
|
||||
cx.spawn(|_, _| async move {
|
||||
let mut stream = query.await?;
|
||||
|
||||
// todo!(): Show the query in the UI as part of the context view
|
||||
let mut query = String::new();
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk?;
|
||||
query.push_str(&chunk);
|
||||
}
|
||||
|
||||
dbg!(&query);
|
||||
|
||||
anyhow::Ok(query)
|
||||
})
|
||||
}
|
||||
|
||||
// Returns a oneshot channel which resolves to true when the context is successfully populated.
|
||||
fn populate_context_on_submit(
|
||||
&mut self,
|
||||
submitted_id: MessageId,
|
||||
mode: &SubmitMode,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> oneshot::Receiver<bool> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
match mode {
|
||||
SubmitMode::Simple => {
|
||||
tx.send(true).ok();
|
||||
}
|
||||
SubmitMode::CurrentFile => {
|
||||
tx.send(true).ok();
|
||||
}
|
||||
SubmitMode::Codebase => {
|
||||
self.user_message(submitted_id).contexts.clear();
|
||||
|
||||
let query = self.setup_query(cx);
|
||||
|
||||
let project_index = self.project_index.clone();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
self.user_message(submitted_id)
|
||||
.contexts
|
||||
.push(AssistantContext::Codebase(cx.new_view(|cx| {
|
||||
CodebaseContext::new(query, tx, project_index, fs, cx)
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
fn user_message(&mut self, message_id: MessageId) -> &mut UserMessage {
|
||||
self.messages
|
||||
.iter_mut()
|
||||
.find_map(|message| match message {
|
||||
ChatMessage::User(user_message) if user_message.id == message_id => {
|
||||
Some(user_message)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("User message not found")
|
||||
}
|
||||
|
||||
fn push_new_user_message(&mut self, focus: bool, cx: &mut ViewContext<Self>) {
|
||||
let id = self.next_message_id.post_inc();
|
||||
let body = cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
if focus {
|
||||
cx.focus_self();
|
||||
}
|
||||
editor
|
||||
});
|
||||
let _subscription = cx.subscribe(&body, move |this, editor, event, cx| match event {
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
if editor.read(cx).is_focused(cx) {
|
||||
let (message_ix, message) = this
|
||||
.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(ix, message)| match message {
|
||||
ChatMessage::User(user_message) if user_message.id == id => {
|
||||
Some((ix, user_message))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("user message not found");
|
||||
message.body.update(cx, |body, cx| {
|
||||
if let Some(editor_style) = body.style() {
|
||||
let row = body.selections.newest_display(cx).head().row();
|
||||
let line_height =
|
||||
editor_style.text.line_height_in_pixels(cx.rem_size());
|
||||
let row_y = row as f32 * line_height;
|
||||
this.list_state.scroll_to_fit(
|
||||
message_ix,
|
||||
row_y,
|
||||
row_y + 5. * line_height,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
let message = ChatMessage::User(UserMessage {
|
||||
id,
|
||||
body,
|
||||
contexts: Vec::new(),
|
||||
_subscription,
|
||||
});
|
||||
self.push_message(message, cx);
|
||||
}
|
||||
|
||||
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let message = ChatMessage::Assistant(AssistantMessage {
|
||||
id: self.next_message_id.post_inc(),
|
||||
body: RichText::default(),
|
||||
error: None,
|
||||
});
|
||||
self.push_message(message, cx);
|
||||
}
|
||||
|
||||
fn push_message(&mut self, message: ChatMessage, cx: &mut ViewContext<Self>) {
|
||||
let old_len = self.messages.len();
|
||||
let focus_handle = Some(message.focus_handle(cx));
|
||||
self.messages.push(message);
|
||||
self.list_state
|
||||
.splice_focusable(old_len..old_len, focus_handle);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = self.messages.iter().position(|message| match message {
|
||||
ChatMessage::User(message) => message.id == last_message_id,
|
||||
ChatMessage::Assistant(message) => message.id == last_message_id,
|
||||
}) {
|
||||
self.list_state.splice(index + 1..self.messages.len(), 0);
|
||||
self.messages.truncate(index + 1);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_error(
|
||||
&self,
|
||||
error: Option<SharedString>,
|
||||
_ix: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
|
||||
if let Some(error) = error {
|
||||
div()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.neg_mx_1()
|
||||
.rounded_md()
|
||||
.border()
|
||||
.border_color(theme.status().error_border)
|
||||
// .bg(theme.status().error_background)
|
||||
.text_color(theme.status().error)
|
||||
.child(error.clone())
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
let is_last = ix == self.messages.len() - 1;
|
||||
|
||||
match &self.messages[ix] {
|
||||
ChatMessage::User(UserMessage { body, contexts, .. }) => div()
|
||||
.when(!is_last, |element| element.mb_2())
|
||||
.child(div().p_2().child(Label::new("You").color(Color::Default)))
|
||||
.child(
|
||||
div()
|
||||
.on_action(cx.listener(Self::submit))
|
||||
.p_2()
|
||||
.text_color(cx.theme().colors().editor_foreground)
|
||||
.font(ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(body.clone())
|
||||
.children(contexts.iter().map(|context| context.render(cx))),
|
||||
)
|
||||
.into_any(),
|
||||
ChatMessage::Assistant(AssistantMessage { id, body, error }) => div()
|
||||
.when(!is_last, |element| element.mb_2())
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.child(Label::new("Assistant").color(Color::Modified)),
|
||||
)
|
||||
.child(div().p_2().child(body.element(ElementId::from(id.0), cx)))
|
||||
.child(self.render_error(error.clone(), ix, cx))
|
||||
.into_any(),
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_messages(&self, cx: &WindowContext) -> Vec<CompletionMessage> {
|
||||
let mut completion_messages = Vec::new();
|
||||
|
||||
for message in &self.messages {
|
||||
match message {
|
||||
ChatMessage::User(UserMessage { body, contexts, .. }) => {
|
||||
// setup context for model
|
||||
contexts.iter().for_each(|context| {
|
||||
completion_messages.extend(context.completion_messages(cx))
|
||||
});
|
||||
|
||||
// Show user's message last so that the assistant is grounded in the user's request
|
||||
completion_messages.push(CompletionMessage {
|
||||
role: CompletionRole::User,
|
||||
body: body.read(cx).text(cx),
|
||||
});
|
||||
}
|
||||
ChatMessage::Assistant(AssistantMessage { body, .. }) => {
|
||||
completion_messages.push(CompletionMessage {
|
||||
role: CompletionRole::Assistant,
|
||||
body: body.text.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completion_messages
|
||||
}
|
||||
|
||||
fn render_model_dropdown(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let this = cx.view().downgrade();
|
||||
div().h_flex().justify_end().child(
|
||||
div().w_32().child(
|
||||
popover_menu("user-menu")
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::get(cx).available_models() {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
move |_| Label::new(model.clone()).into_any_element()
|
||||
},
|
||||
{
|
||||
let this = this.clone();
|
||||
move |cx| {
|
||||
_ = this.update(cx, |this, cx| {
|
||||
this.model = model.clone();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
menu
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("active-model")
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(Label::new(self.model.clone())),
|
||||
)
|
||||
.child(div().child(
|
||||
Icon::new(IconName::ChevronDown).color(Color::Muted),
|
||||
)),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Change Model", cx)),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::TopRight),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AssistantChat {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.relative()
|
||||
.flex_1()
|
||||
.v_flex()
|
||||
.key_context("AssistantChat")
|
||||
.text_color(Color::Default.color(cx))
|
||||
.child(self.render_model_dropdown(cx))
|
||||
.child(list(self.list_state.clone()).flex_1())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
struct MessageId(usize);
|
||||
|
||||
impl MessageId {
|
||||
fn post_inc(&mut self) -> Self {
|
||||
let id = *self;
|
||||
self.0 += 1;
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatMessage {
|
||||
User(UserMessage),
|
||||
Assistant(AssistantMessage),
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
fn focus_handle(&self, cx: &WindowContext) -> Option<FocusHandle> {
|
||||
match self {
|
||||
ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
|
||||
ChatMessage::Assistant(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserMessage {
|
||||
id: MessageId,
|
||||
body: View<Editor>,
|
||||
contexts: Vec<AssistantContext>,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
// chain_of_thought: ... -> search -> search_results -> produce_new_message -> send for the real chat message
|
||||
|
||||
struct AssistantMessage {
|
||||
id: MessageId,
|
||||
body: RichText,
|
||||
error: Option<SharedString>,
|
||||
}
|
||||
|
||||
enum AssistantContext {
|
||||
Codebase(View<CodebaseContext>),
|
||||
}
|
||||
|
||||
struct CodebaseExcerpt {
|
||||
element_id: ElementId,
|
||||
path: SharedString,
|
||||
text: SharedString,
|
||||
score: f32,
|
||||
expanded: bool,
|
||||
}
|
||||
|
||||
impl AssistantContext {
|
||||
fn render(&self, _cx: &mut ViewContext<AssistantChat>) -> AnyElement {
|
||||
match self {
|
||||
AssistantContext::Codebase(context) => context.clone().into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_messages(&self, cx: &WindowContext) -> Vec<CompletionMessage> {
|
||||
match self {
|
||||
AssistantContext::Codebase(context) => context.read(cx).completion_messages(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CodebaseContext {
|
||||
Pending { _task: Task<()> },
|
||||
Done(Result<Vec<CodebaseExcerpt>>),
|
||||
}
|
||||
|
||||
impl CodebaseContext {
|
||||
fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext<Self>) {
|
||||
if let CodebaseContext::Done(Ok(excerpts)) = self {
|
||||
if let Some(excerpt) = excerpts
|
||||
.iter_mut()
|
||||
.find(|excerpt| excerpt.element_id == element_id)
|
||||
{
|
||||
excerpt.expanded = !excerpt.expanded;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CodebaseContext {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
match self {
|
||||
CodebaseContext::Pending { .. } => div()
|
||||
.h_flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Ai).color(Color::Muted).into_element())
|
||||
.child("Searching codebase..."),
|
||||
CodebaseContext::Done(Ok(excerpts)) => {
|
||||
div()
|
||||
.v_flex()
|
||||
.gap_2()
|
||||
.children(excerpts.iter().map(|excerpt| {
|
||||
let expanded = excerpt.expanded;
|
||||
let element_id = excerpt.element_id.clone();
|
||||
|
||||
CollapsibleContainer::new(element_id.clone(), expanded.clone())
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::File).color(Color::Muted))
|
||||
.child(Label::new(excerpt.path.clone()).color(Color::Muted)),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
dbg!("listener callback fired");
|
||||
this.toggle_expanded(element_id.clone(), cx);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
excerpt.text.clone(), // todo!(): Show as an editor block
|
||||
),
|
||||
)
|
||||
}))
|
||||
}
|
||||
CodebaseContext::Done(Err(error)) => div().child(error.to_string()), // todo!,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodebaseContext {
|
||||
fn new(
|
||||
query: impl 'static + Future<Output = Result<String>>,
|
||||
populated: oneshot::Sender<bool>,
|
||||
project_index: Model<ProjectIndex>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let query = query.boxed_local();
|
||||
let _task = cx.spawn(|this, mut cx| async move {
|
||||
let result = async {
|
||||
let query = query.await?;
|
||||
let results = this
|
||||
.update(&mut cx, |_this, cx| {
|
||||
project_index.read(cx).search(&query, 16, cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
let excerpts = results.into_iter().map(|result| {
|
||||
let abs_path = result
|
||||
.worktree
|
||||
.read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path));
|
||||
let fs = fs.clone();
|
||||
|
||||
async move {
|
||||
let path = result.path.clone();
|
||||
let text = fs.load(&abs_path?).await?;
|
||||
// todo!("what should we do with stale ranges?");
|
||||
let range = cmp::min(result.range.start, text.len())
|
||||
..cmp::min(result.range.end, text.len());
|
||||
|
||||
let text = SharedString::from(text[range].to_string());
|
||||
|
||||
anyhow::Ok(CodebaseExcerpt {
|
||||
element_id: ElementId::Name(nanoid::nanoid!().into()),
|
||||
path: path.to_string_lossy().to_string().into(),
|
||||
text,
|
||||
score: result.score,
|
||||
expanded: false,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
anyhow::Ok(
|
||||
futures::future::join_all(excerpts)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|result| result.log_err())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.populate(result, populated, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
Self::Pending { _task }
|
||||
}
|
||||
|
||||
fn populate(
|
||||
&mut self,
|
||||
result: Result<Vec<CodebaseExcerpt>>,
|
||||
populated: oneshot::Sender<bool>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let success = result.is_ok();
|
||||
*self = Self::Done(result);
|
||||
populated.send(success).ok();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn completion_messages(&self) -> Vec<CompletionMessage> {
|
||||
// One system message for the whole batch of excerpts:
|
||||
|
||||
// Semantic search results for user query:
|
||||
//
|
||||
// Excerpt from $path:
|
||||
// ~~~
|
||||
// `text`
|
||||
// ~~~
|
||||
//
|
||||
// Excerpt from $path:
|
||||
|
||||
match self {
|
||||
CodebaseContext::Done(Ok(excerpts)) => {
|
||||
if excerpts.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut body = "Semantic search reasults for user query:\n".to_string();
|
||||
|
||||
for excerpt in excerpts {
|
||||
body.push_str("Excerpt from ");
|
||||
body.push_str(excerpt.path.as_ref());
|
||||
body.push_str(", score ");
|
||||
body.push_str(&excerpt.score.to_string());
|
||||
body.push_str(":\n");
|
||||
body.push_str("~~~\n");
|
||||
body.push_str(excerpt.text.as_ref());
|
||||
body.push_str("~~~\n");
|
||||
}
|
||||
|
||||
vec![CompletionMessage {
|
||||
role: CompletionRole::System,
|
||||
body,
|
||||
}]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
123
crates/assistant2/src/completion_provider.rs
Normal file
123
crates/assistant2/src/completion_provider.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use anyhow::Result;
|
||||
use client::{proto, Client};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::Global;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub enum CompletionRole {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
pub struct CompletionMessage {
|
||||
pub role: CompletionRole,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CompletionProvider(Arc<dyn CompletionProviderBackend>);
|
||||
|
||||
impl CompletionProvider {
|
||||
pub fn new(backend: impl CompletionProviderBackend) -> Self {
|
||||
Self(Arc::new(backend))
|
||||
}
|
||||
|
||||
pub fn default_model(&self) -> String {
|
||||
self.0.default_model()
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> Vec<String> {
|
||||
self.0.available_models()
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
model: String,
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
self.0.complete(model, messages, stop, temperature)
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for CompletionProvider {}
|
||||
|
||||
pub trait CompletionProviderBackend: 'static {
|
||||
fn default_model(&self) -> String;
|
||||
fn available_models(&self) -> Vec<String>;
|
||||
fn complete(
|
||||
&self,
|
||||
model: String,
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
|
||||
}
|
||||
|
||||
pub struct CloudCompletionProvider {
|
||||
client: Arc<Client>,
|
||||
}
|
||||
|
||||
impl CloudCompletionProvider {
|
||||
pub fn new(client: Arc<Client>) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionProviderBackend for CloudCompletionProvider {
|
||||
fn default_model(&self) -> String {
|
||||
"gpt-4-turbo".into()
|
||||
}
|
||||
|
||||
fn available_models(&self) -> Vec<String> {
|
||||
vec!["gpt-4-turbo".into(), "gpt-4".into(), "gpt-3.5-turbo".into()]
|
||||
}
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
model: String,
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
let stream = client
|
||||
.request_stream(proto::CompleteWithLanguageModel {
|
||||
model,
|
||||
messages: messages
|
||||
.into_iter()
|
||||
.map(|message| proto::LanguageModelRequestMessage {
|
||||
role: match message.role {
|
||||
CompletionRole::User => {
|
||||
proto::LanguageModelRole::LanguageModelUser as i32
|
||||
}
|
||||
CompletionRole::Assistant => {
|
||||
proto::LanguageModelRole::LanguageModelAssistant as i32
|
||||
}
|
||||
CompletionRole::System => {
|
||||
proto::LanguageModelRole::LanguageModelSystem as i32
|
||||
}
|
||||
},
|
||||
content: message.body,
|
||||
})
|
||||
.collect(),
|
||||
stop,
|
||||
temperature,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(stream
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(mut response) => Some(Ok(response.choices.pop()?.delta?.content?)),
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
@@ -457,6 +457,14 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn production(cx: &mut AppContext) -> Arc<Self> {
|
||||
let clock = Arc::new(clock::RealSystemClock);
|
||||
let http = Arc::new(HttpClientWithUrl::new(
|
||||
&ClientSettings::get_global(cx).server_url,
|
||||
));
|
||||
Self::new(clock, http.clone(), cx)
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u64 {
|
||||
self.id.load(Ordering::SeqCst)
|
||||
}
|
||||
@@ -1116,9 +1124,13 @@ impl Client {
|
||||
let public_key_string = String::try_from(public_key)
|
||||
.expect("failed to serialize public key for auth");
|
||||
|
||||
dbg!(ADMIN_API_TOKEN.as_ref());
|
||||
|
||||
if let Some((login, token)) =
|
||||
IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref())
|
||||
{
|
||||
eprintln!("authenticate as admin {login}, {token}");
|
||||
|
||||
return Self::authenticate_as_admin(http, login.clone(), token.clone())
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -234,10 +234,11 @@ impl ChatPanel {
|
||||
let channel_id = chat.read(cx).channel_id;
|
||||
{
|
||||
self.markdown_data.clear();
|
||||
let chat = chat.read(cx);
|
||||
self.message_list.reset(chat.message_count());
|
||||
|
||||
let chat = chat.read(cx);
|
||||
let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
|
||||
let message_count = chat.message_count();
|
||||
self.message_list.reset(message_count);
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_channel(channel_id, channel_name, cx);
|
||||
editor.clear_reply_to_message_id();
|
||||
@@ -766,7 +767,7 @@ impl ChatPanel {
|
||||
body.push_str(MESSAGE_EDITED);
|
||||
}
|
||||
|
||||
let mut rich_text = rich_text::render_rich_text(body, &mentions, language_registry, None);
|
||||
let mut rich_text = RichText::new(body, &mentions, language_registry);
|
||||
|
||||
if message.edited_at.is_some() {
|
||||
let range = (rich_text.text.len() - MESSAGE_EDITED.len())..rich_text.text.len();
|
||||
|
||||
@@ -3075,7 +3075,7 @@ impl Render for DraggedChannelView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
||||
h_flex()
|
||||
.font(ui_font)
|
||||
.font_family(ui_font)
|
||||
.bg(cx.theme().colors().background)
|
||||
.w(self.width)
|
||||
.p_1()
|
||||
|
||||
@@ -125,7 +125,7 @@ impl Render for IncomingCallNotification {
|
||||
|
||||
cx.set_rem_size(ui_font_size);
|
||||
|
||||
div().size_full().font(ui_font).child(
|
||||
div().size_full().font_family(ui_font).child(
|
||||
CollabNotification::new(
|
||||
self.state.call.calling_user.avatar_uri.clone(),
|
||||
Button::new("accept", "Accept").on_click({
|
||||
|
||||
@@ -129,7 +129,7 @@ impl Render for ProjectSharedNotification {
|
||||
|
||||
cx.set_rem_size(ui_font_size);
|
||||
|
||||
div().size_full().font(ui_font).child(
|
||||
div().size_full().font_family(ui_font).child(
|
||||
CollabNotification::new(
|
||||
self.owner.avatar_uri.clone(),
|
||||
Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| {
|
||||
|
||||
@@ -60,13 +60,13 @@ use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use git::blame::GitBlame;
|
||||
use git::diff_hunk_to_display;
|
||||
use gpui::{
|
||||
div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action,
|
||||
AnyElement, AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds,
|
||||
ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView,
|
||||
FontId, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model,
|
||||
MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle,
|
||||
Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle,
|
||||
View, ViewContext, ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
|
||||
Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle,
|
||||
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton, PaintQuad,
|
||||
ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle, Styled, StyledText,
|
||||
Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, View, ViewContext,
|
||||
ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -10226,21 +10226,9 @@ impl FocusableView for Editor {
|
||||
impl Render for Editor {
|
||||
fn render<'a>(&mut self, cx: &mut ViewContext<'a, Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = match self.mode {
|
||||
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
},
|
||||
|
||||
let text_style = match self.mode {
|
||||
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => cx.text_style(),
|
||||
EditorMode::Full => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
|
||||
@@ -2989,7 +2989,7 @@ fn render_inline_blame_entry(
|
||||
h_flex()
|
||||
.id("inline-blame")
|
||||
.w_full()
|
||||
.font(style.text.font().family)
|
||||
.font_family(style.text.font().family)
|
||||
.text_color(cx.theme().status().hint)
|
||||
.line_height(style.text.line_height)
|
||||
.child(Icon::new(IconName::FileGit).color(Color::Hint))
|
||||
@@ -3193,7 +3193,7 @@ fn render_blame_entry(
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.font(style.text.font().family)
|
||||
.font_family(style.text.font().family)
|
||||
.line_height(style.text.line_height)
|
||||
.id(("blame", ix))
|
||||
.children([
|
||||
@@ -3884,6 +3884,20 @@ impl Element for EditorElement {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let focus_target_bounds = newest_selection_head.and_then(|cursor| {
|
||||
if (start_row..end_row).contains(&cursor.row()) {
|
||||
let cursor_y = line_height
|
||||
* (cursor.row() as f32 - scroll_pixel_position.y / line_height);
|
||||
let cursor_origin =
|
||||
content_origin + gpui::point(-scroll_pixel_position.x, cursor_y);
|
||||
Some(Bounds::new(cursor_origin, size(em_width, line_height)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
cx.set_focus_target(&focus_handle, focus_target_bounds);
|
||||
|
||||
EditorLayout {
|
||||
mode: snapshot.mode,
|
||||
position_map: Arc::new(PositionMap {
|
||||
@@ -3936,7 +3950,6 @@ impl Element for EditorElement {
|
||||
) {
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let key_context = self.editor.read(cx).key_context(cx);
|
||||
cx.set_focus_handle(&focus_handle);
|
||||
cx.set_key_context(key_context);
|
||||
cx.set_view_id(self.editor.entity_id());
|
||||
cx.handle_input(
|
||||
|
||||
@@ -1375,6 +1375,10 @@ impl Interactivity {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
||||
cx.set_focus_target(focus_handle, Some(bounds));
|
||||
}
|
||||
|
||||
let scroll_offset = self.clamp_scroll_position(bounds, &style, cx);
|
||||
let result = f(&style, scroll_offset, hitbox, cx);
|
||||
(result, element_state)
|
||||
@@ -1977,9 +1981,6 @@ impl Interactivity {
|
||||
if let Some(context) = self.key_context.clone() {
|
||||
cx.set_key_context(context);
|
||||
}
|
||||
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
||||
cx.set_focus_handle(focus_handle);
|
||||
}
|
||||
|
||||
for listener in key_down_listeners {
|
||||
cx.on_key_event(move |event: &KeyDownEvent, phase, cx| {
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges,
|
||||
Element, ElementContext, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style,
|
||||
StyleRefinement, Styled, WindowContext,
|
||||
Element, ElementContext, FocusHandle, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent,
|
||||
Size, Style, StyleRefinement, Styled, WindowContext,
|
||||
};
|
||||
use collections::VecDeque;
|
||||
use refineable::Refineable as _;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc};
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
use sum_tree::{Bias, SumTree};
|
||||
use taffy::style::Overflow;
|
||||
|
||||
@@ -94,6 +94,7 @@ struct LayoutItemsResponse {
|
||||
scroll_top: ListOffset,
|
||||
available_item_space: Size<AvailableSpace>,
|
||||
item_elements: VecDeque<AnyElement>,
|
||||
focused_offscreen_element: Option<AnyElement>,
|
||||
}
|
||||
|
||||
/// Frame state used by the [List] element after layout.
|
||||
@@ -104,8 +105,41 @@ pub struct ListAfterLayoutState {
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ListItem {
|
||||
Unrendered,
|
||||
Rendered { size: Size<Pixels> },
|
||||
Unmeasured {
|
||||
focus_handle: Option<FocusHandle>,
|
||||
},
|
||||
Measured {
|
||||
size: Size<Pixels>,
|
||||
focus_handle: Option<FocusHandle>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
fn size(&self) -> Option<Size<Pixels>> {
|
||||
if let ListItem::Measured { size, .. } = self {
|
||||
Some(*size)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_handle(&self) -> Option<FocusHandle> {
|
||||
match self {
|
||||
ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
|
||||
focus_handle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_focused(&self, cx: &WindowContext) -> bool {
|
||||
match self {
|
||||
ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => {
|
||||
focus_handle
|
||||
.as_ref()
|
||||
.is_some_and(|handle| handle.contains_focused(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
@@ -114,6 +148,7 @@ struct ListItemSummary {
|
||||
rendered_count: usize,
|
||||
unrendered_count: usize,
|
||||
height: Pixels,
|
||||
has_focus_handles: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
@@ -131,45 +166,45 @@ struct Height(Pixels);
|
||||
impl ListState {
|
||||
/// Construct a new list state, for storage on a view.
|
||||
///
|
||||
/// the overdraw parameter controls how much extra space is rendered
|
||||
/// above and below the visible area. This can help ensure that the list
|
||||
/// doesn't flicker or pop in when scrolling.
|
||||
pub fn new<F>(
|
||||
element_count: usize,
|
||||
orientation: ListAlignment,
|
||||
/// The overdraw parameter controls how much extra space is rendered
|
||||
/// above and below the visible area. Elements within this area will
|
||||
/// be measured even though they are not visible. This can help ensure
|
||||
/// that the list doesn't flicker or pop in when scrolling.
|
||||
pub fn new<R>(
|
||||
item_count: usize,
|
||||
alignment: ListAlignment,
|
||||
overdraw: Pixels,
|
||||
render_item: F,
|
||||
render_item: R,
|
||||
) -> Self
|
||||
where
|
||||
F: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
|
||||
R: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
|
||||
{
|
||||
let mut items = SumTree::new();
|
||||
items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
|
||||
Self(Rc::new(RefCell::new(StateInner {
|
||||
let this = Self(Rc::new(RefCell::new(StateInner {
|
||||
last_layout_bounds: None,
|
||||
last_padding: None,
|
||||
render_item: Box::new(render_item),
|
||||
items,
|
||||
items: SumTree::new(),
|
||||
logical_scroll_top: None,
|
||||
alignment: orientation,
|
||||
alignment,
|
||||
overdraw,
|
||||
scroll_handler: None,
|
||||
reset: false,
|
||||
})))
|
||||
})));
|
||||
this.splice(0..0, item_count);
|
||||
this
|
||||
}
|
||||
|
||||
/// Reset this instantiation of the list state.
|
||||
///
|
||||
/// Note that this will cause scroll events to be dropped until the next paint.
|
||||
pub fn reset(&self, element_count: usize) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
state.reset = true;
|
||||
{
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
state.reset = true;
|
||||
state.logical_scroll_top = None;
|
||||
}
|
||||
|
||||
state.logical_scroll_top = None;
|
||||
state.items = SumTree::new();
|
||||
state
|
||||
.items
|
||||
.extend((0..element_count).map(|_| ListItem::Unrendered), &());
|
||||
self.splice(0..element_count, element_count);
|
||||
}
|
||||
|
||||
/// The number of items in this list.
|
||||
@@ -177,11 +212,39 @@ impl ListState {
|
||||
self.0.borrow().items.summary().count
|
||||
}
|
||||
|
||||
/// Register with the list state that the items in `old_range` have been replaced
|
||||
/// Inform the list state that the items in `old_range` have been replaced
|
||||
/// by `count` new items that must be recalculated.
|
||||
pub fn splice(&self, old_range: Range<usize>, count: usize) {
|
||||
self.splice_focusable(old_range, (0..count).into_iter().map(|_| None))
|
||||
}
|
||||
|
||||
/// Register with the list state that the items in `old_range` have been replaced
|
||||
/// by new items. As opposed to [`splice`], this method allows an iterator of optional focus handles
|
||||
/// to be supplied to properly integrate with items in the list that can be focused. If a focused item
|
||||
/// is scrolled out of view, the list will continue to render it to allow keyboard interaction.
|
||||
pub fn splice_focusable(
|
||||
&self,
|
||||
old_range: Range<usize>,
|
||||
focus_handles: impl IntoIterator<Item = Option<FocusHandle>>,
|
||||
) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
|
||||
let mut old_items = state.items.cursor::<Count>();
|
||||
let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right, &());
|
||||
old_items.seek_forward(&Count(old_range.end), Bias::Right, &());
|
||||
|
||||
let mut spliced_count = 0;
|
||||
new_items.extend(
|
||||
focus_handles.into_iter().map(|focus_handle| {
|
||||
spliced_count += 1;
|
||||
ListItem::Unmeasured { focus_handle }
|
||||
}),
|
||||
&(),
|
||||
);
|
||||
new_items.append(old_items.suffix(&()), &());
|
||||
drop(old_items);
|
||||
state.items = new_items;
|
||||
|
||||
if let Some(ListOffset {
|
||||
item_ix,
|
||||
offset_in_item,
|
||||
@@ -191,18 +254,9 @@ impl ListState {
|
||||
*item_ix = old_range.start;
|
||||
*offset_in_item = px(0.);
|
||||
} else if old_range.end <= *item_ix {
|
||||
*item_ix = *item_ix - (old_range.end - old_range.start) + count;
|
||||
*item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count;
|
||||
}
|
||||
}
|
||||
|
||||
let mut old_heights = state.items.cursor::<Count>();
|
||||
let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
|
||||
old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
|
||||
|
||||
new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
|
||||
new_heights.append(old_heights.suffix(&()), &());
|
||||
drop(old_heights);
|
||||
state.items = new_heights;
|
||||
}
|
||||
|
||||
/// Set a handler that will be called when the list is scrolled.
|
||||
@@ -230,6 +284,57 @@ impl ListState {
|
||||
state.logical_scroll_top = Some(scroll_top);
|
||||
}
|
||||
|
||||
pub fn scroll_to_focused(&self) {
|
||||
// state.requested_autoscroll = Some();
|
||||
}
|
||||
|
||||
/// Scroll the list to the given item index and top/bottom offset.
|
||||
/// If the given position is already visibile, this method won't scroll.
|
||||
pub fn scroll_to_fit(
|
||||
&self,
|
||||
mut item_ix: usize,
|
||||
mut top_offset_in_item: Pixels,
|
||||
mut bottom_offset_in_item: Pixels,
|
||||
) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
let Some(bounds) = state.last_layout_bounds else {
|
||||
return;
|
||||
};
|
||||
|
||||
let item_count = state.items.summary().count;
|
||||
if item_ix >= item_count {
|
||||
item_ix = item_count;
|
||||
top_offset_in_item = Pixels::ZERO;
|
||||
bottom_offset_in_item = Pixels::ZERO;
|
||||
}
|
||||
|
||||
let logical_scroll_top = state.logical_scroll_top();
|
||||
|
||||
let mut cursor = state.items.cursor::<(Count, Height)>();
|
||||
cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
|
||||
let scroll_top = cursor.start().1 .0 + logical_scroll_top.offset_in_item;
|
||||
let scroll_bottom = scroll_top + bounds.size.height;
|
||||
|
||||
cursor.seek(&Count(item_ix), Bias::Right, &());
|
||||
let item_scroll_top = cursor.start().1 .0;
|
||||
let desired_scroll_top = item_scroll_top + top_offset_in_item;
|
||||
let desired_scroll_bottom = item_scroll_top + bottom_offset_in_item;
|
||||
if desired_scroll_top < scroll_top {
|
||||
state.logical_scroll_top = Some(ListOffset {
|
||||
item_ix,
|
||||
offset_in_item: desired_scroll_top - item_scroll_top,
|
||||
});
|
||||
} else if desired_scroll_bottom > scroll_bottom {
|
||||
state.logical_scroll_top = Some(ListOffset {
|
||||
item_ix,
|
||||
offset_in_item: cmp::max(
|
||||
Pixels::ZERO,
|
||||
desired_scroll_bottom - item_scroll_top - bounds.size.height,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Scroll the list to the given item, such that the item is fully visible.
|
||||
pub fn scroll_to_reveal_item(&self, ix: usize) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
@@ -279,7 +384,7 @@ impl ListState {
|
||||
let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
|
||||
|
||||
cursor.seek_forward(&Count(ix), Bias::Right, &());
|
||||
if let Some(&ListItem::Rendered { size }) = cursor.item() {
|
||||
if let Some(&ListItem::Measured { size, .. }) = cursor.item() {
|
||||
let &(Count(count), Height(top)) = cursor.start();
|
||||
if count == ix {
|
||||
let top = bounds.top() + top - scroll_top;
|
||||
@@ -383,6 +488,7 @@ impl StateInner {
|
||||
let mut rendered_height = padding.top;
|
||||
let mut max_item_width = px(0.);
|
||||
let mut scroll_top = self.logical_scroll_top();
|
||||
let mut rendered_focused_item = false;
|
||||
|
||||
let available_item_space = size(
|
||||
available_width.map_or(AvailableSpace::MinContent, |width| {
|
||||
@@ -401,12 +507,8 @@ impl StateInner {
|
||||
break;
|
||||
}
|
||||
|
||||
// Use the previously cached height if available
|
||||
let mut size = if let ListItem::Rendered { size } = item {
|
||||
Some(*size)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Use the previously cached height and focus handle if available
|
||||
let mut size = item.size();
|
||||
|
||||
// If we're within the visible area or the height wasn't cached, render and measure the item's element
|
||||
if visible_height < available_height || size.is_none() {
|
||||
@@ -415,13 +517,19 @@ impl StateInner {
|
||||
size = Some(element_size);
|
||||
if visible_height < available_height {
|
||||
item_elements.push_back(element);
|
||||
if item.contains_focused(cx) {
|
||||
rendered_focused_item = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let size = size.unwrap();
|
||||
rendered_height += size.height;
|
||||
max_item_width = max_item_width.max(size.width);
|
||||
measured_items.push_back(ListItem::Rendered { size });
|
||||
measured_items.push_back(ListItem::Measured {
|
||||
size,
|
||||
focus_handle: item.focus_handle(),
|
||||
});
|
||||
}
|
||||
rendered_height += padding.bottom;
|
||||
|
||||
@@ -433,13 +541,19 @@ impl StateInner {
|
||||
if rendered_height - scroll_top.offset_in_item < available_height {
|
||||
while rendered_height < available_height {
|
||||
cursor.prev(&());
|
||||
if cursor.item().is_some() {
|
||||
if let Some(item) = cursor.item() {
|
||||
let mut element = (self.render_item)(cursor.start().0, cx);
|
||||
let element_size = element.measure(available_item_space, cx);
|
||||
|
||||
let focus_handle = item.focus_handle();
|
||||
rendered_height += element_size.height;
|
||||
measured_items.push_front(ListItem::Rendered { size: element_size });
|
||||
item_elements.push_front(element)
|
||||
measured_items.push_front(ListItem::Measured {
|
||||
size: element_size,
|
||||
focus_handle,
|
||||
});
|
||||
item_elements.push_front(element);
|
||||
if item.contains_focused(cx) {
|
||||
rendered_focused_item = true;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -470,7 +584,7 @@ impl StateInner {
|
||||
while leading_overdraw < self.overdraw {
|
||||
cursor.prev(&());
|
||||
if let Some(item) = cursor.item() {
|
||||
let size = if let ListItem::Rendered { size } = item {
|
||||
let size = if let ListItem::Measured { size, .. } = item {
|
||||
*size
|
||||
} else {
|
||||
let mut element = (self.render_item)(cursor.start().0, cx);
|
||||
@@ -478,7 +592,10 @@ impl StateInner {
|
||||
};
|
||||
|
||||
leading_overdraw += size.height;
|
||||
measured_items.push_front(ListItem::Rendered { size });
|
||||
measured_items.push_front(ListItem::Measured {
|
||||
size,
|
||||
focus_handle: item.focus_handle(),
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -490,14 +607,36 @@ impl StateInner {
|
||||
new_items.extend(measured_items, &());
|
||||
cursor.seek(&Count(measured_range.end), Bias::Right, &());
|
||||
new_items.append(cursor.suffix(&()), &());
|
||||
|
||||
self.items = new_items;
|
||||
|
||||
// If the none of the visible items are focused, check if an off-screen item is focused
|
||||
// and include it to be rendered after the visible items so keyboard interaction continues
|
||||
// to work for it.
|
||||
let mut focused_offscreen_element = None;
|
||||
if self.scroll_to_focused_item {
|
||||
self.scroll_to_focused_item = false;
|
||||
// Are the focused item and its focus target bounds visible?
|
||||
// If not, update the scroll position and call this method recursively?
|
||||
} else if !rendered_focused_item {
|
||||
let mut cursor = self
|
||||
.items
|
||||
.filter::<_, Count>(|summary| summary.has_focus_handles);
|
||||
cursor.next(&());
|
||||
while let Some(item) = cursor.item() {
|
||||
if item.contains_focused(cx) {
|
||||
focused_offscreen_element = Some((self.render_item)(cursor.start().0, cx));
|
||||
break;
|
||||
}
|
||||
cursor.next(&());
|
||||
}
|
||||
}
|
||||
|
||||
LayoutItemsResponse {
|
||||
max_item_width,
|
||||
scroll_top,
|
||||
available_item_space,
|
||||
item_elements,
|
||||
focused_offscreen_element,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -505,8 +644,8 @@ impl StateInner {
|
||||
impl std::fmt::Debug for ListItem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Unrendered => write!(f, "Unrendered"),
|
||||
Self::Rendered { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
|
||||
Self::Unmeasured { .. } => write!(f, "Unrendered"),
|
||||
Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -607,10 +746,14 @@ impl Element for List {
|
||||
if state.last_layout_bounds.map_or(true, |last_bounds| {
|
||||
last_bounds.size.width != bounds.size.width
|
||||
}) {
|
||||
state.items = SumTree::from_iter(
|
||||
(0..state.items.summary().count).map(|_| ListItem::Unrendered),
|
||||
let new_items = SumTree::from_iter(
|
||||
state.items.iter().map(|item| ListItem::Unmeasured {
|
||||
focus_handle: item.focus_handle(),
|
||||
}),
|
||||
&(),
|
||||
)
|
||||
);
|
||||
|
||||
state.items = new_items;
|
||||
}
|
||||
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
@@ -619,7 +762,7 @@ impl Element for List {
|
||||
|
||||
// Only paint the visible items, if there is actually any space for them (taking padding into account)
|
||||
if bounds.size.height > padding.top + padding.bottom {
|
||||
// Paint the visible items
|
||||
// Layout the visible items followed by the offscreen focused item if it is present
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
let mut item_origin = bounds.origin + Point::new(px(0.), padding.top);
|
||||
item_origin.y -= layout_response.scroll_top.offset_in_item;
|
||||
@@ -628,6 +771,9 @@ impl Element for List {
|
||||
item_element.layout(item_origin, layout_response.available_item_space, cx);
|
||||
item_origin.y += item_size.height;
|
||||
}
|
||||
if let Some(focused_element) = layout_response.focused_offscreen_element.as_mut() {
|
||||
focused_element.layout(item_origin, layout_response.available_item_space, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -650,6 +796,9 @@ impl Element for List {
|
||||
for item in &mut after_layout.layout.item_elements {
|
||||
item.paint(cx);
|
||||
}
|
||||
if let Some(focused_element) = after_layout.layout.focused_offscreen_element.as_mut() {
|
||||
focused_element.paint(cx);
|
||||
}
|
||||
});
|
||||
|
||||
let list_state = self.state.clone();
|
||||
@@ -688,17 +837,21 @@ impl sum_tree::Item for ListItem {
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
match self {
|
||||
ListItem::Unrendered => ListItemSummary {
|
||||
ListItem::Unmeasured { focus_handle } => ListItemSummary {
|
||||
count: 1,
|
||||
rendered_count: 0,
|
||||
unrendered_count: 1,
|
||||
height: px(0.),
|
||||
has_focus_handles: focus_handle.is_some(),
|
||||
},
|
||||
ListItem::Rendered { size } => ListItemSummary {
|
||||
ListItem::Measured {
|
||||
size, focus_handle, ..
|
||||
} => ListItemSummary {
|
||||
count: 1,
|
||||
rendered_count: 1,
|
||||
unrendered_count: 0,
|
||||
height: size.height,
|
||||
has_focus_handles: focus_handle.is_some(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -712,6 +865,7 @@ impl sum_tree::Summary for ListItemSummary {
|
||||
self.rendered_count += summary.rendered_count;
|
||||
self.unrendered_count += summary.unrendered_count;
|
||||
self.height += summary.height;
|
||||
self.has_focus_handles |= summary.has_focus_handles;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@
|
||||
/// KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane"))
|
||||
///
|
||||
use crate::{
|
||||
Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding,
|
||||
KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, ModifiersChangedEvent,
|
||||
WindowContext,
|
||||
Action, ActionRegistry, Bounds, DispatchPhase, ElementContext, EntityId, FocusTargetId,
|
||||
KeyBinding, KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher,
|
||||
ModifiersChangedEvent, Pixels, WindowContext,
|
||||
};
|
||||
use collections::FxHashMap;
|
||||
use smallvec::SmallVec;
|
||||
@@ -72,7 +72,7 @@ pub(crate) struct DispatchTree {
|
||||
pub(crate) context_stack: Vec<KeyContext>,
|
||||
view_stack: Vec<EntityId>,
|
||||
nodes: Vec<DispatchNode>,
|
||||
focusable_node_ids: FxHashMap<FocusId, DispatchNodeId>,
|
||||
focusable_node_ids: FxHashMap<FocusTargetId, DispatchNodeId>,
|
||||
view_node_ids: FxHashMap<EntityId, DispatchNodeId>,
|
||||
keystroke_matchers: FxHashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
|
||||
keymap: Rc<RefCell<Keymap>>,
|
||||
@@ -85,11 +85,17 @@ pub(crate) struct DispatchNode {
|
||||
pub action_listeners: Vec<DispatchActionListener>,
|
||||
pub modifiers_changed_listeners: Vec<ModifiersChangedListener>,
|
||||
pub context: Option<KeyContext>,
|
||||
pub focus_id: Option<FocusId>,
|
||||
focus_target: Option<FocusTarget>,
|
||||
view_id: Option<EntityId>,
|
||||
parent: Option<DispatchNodeId>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FocusTarget {
|
||||
id: FocusTargetId,
|
||||
bounds: Option<Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
pub(crate) struct ReusedSubtree {
|
||||
old_range: Range<usize>,
|
||||
new_range: Range<usize>,
|
||||
@@ -199,12 +205,36 @@ impl DispatchTree {
|
||||
self.context_stack.push(context);
|
||||
}
|
||||
|
||||
pub fn set_focus_id(&mut self, focus_id: FocusId) {
|
||||
pub fn set_focus_target(&mut self, focus_id: FocusTargetId, bounds: Option<Bounds<Pixels>>) {
|
||||
let node_id = *self.node_stack.last().unwrap();
|
||||
self.nodes[node_id.0].focus_id = Some(focus_id);
|
||||
self.nodes[node_id.0].focus_target = Some(FocusTarget {
|
||||
id: focus_id,
|
||||
bounds,
|
||||
});
|
||||
self.focusable_node_ids.insert(focus_id, node_id);
|
||||
}
|
||||
|
||||
pub fn focus_target_bounds(&self, focus_id: FocusTargetId) -> Option<Bounds<Pixels>> {
|
||||
let active_node_id = self.active_node_id()?;
|
||||
|
||||
let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied()?;
|
||||
let focus_target_bounds = self.node(current_node_id).focus_target.as_ref()?.bounds?;
|
||||
loop {
|
||||
// Only return the bounds if the focused node is within the active subtree.
|
||||
if current_node_id == active_node_id {
|
||||
return Some(focus_target_bounds);
|
||||
}
|
||||
|
||||
if let Some(parent) = self.node(current_node_id).parent {
|
||||
current_node_id = parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn parent_view_id(&mut self) -> Option<EntityId> {
|
||||
self.view_stack.last().copied()
|
||||
}
|
||||
@@ -234,8 +264,8 @@ impl DispatchTree {
|
||||
if let Some(context) = source.context.clone() {
|
||||
self.set_key_context(context);
|
||||
}
|
||||
if let Some(focus_id) = source.focus_id {
|
||||
self.set_focus_id(focus_id);
|
||||
if let Some(focus) = source.focus_target.clone() {
|
||||
self.set_focus_target(focus.id, focus.bounds);
|
||||
}
|
||||
if let Some(view_id) = source.view_id {
|
||||
self.set_view_id(view_id);
|
||||
@@ -289,7 +319,11 @@ impl DispatchTree {
|
||||
|
||||
/// Preserve keystroke matchers from previous frames to support multi-stroke
|
||||
/// bindings across multiple frames.
|
||||
pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
|
||||
pub fn preserve_pending_keystrokes(
|
||||
&mut self,
|
||||
old_tree: &mut Self,
|
||||
focus_id: Option<FocusTargetId>,
|
||||
) {
|
||||
if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) {
|
||||
let dispatch_path = self.dispatch_path(node_id);
|
||||
|
||||
@@ -333,7 +367,7 @@ impl DispatchTree {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool {
|
||||
pub fn focus_contains(&self, parent: FocusTargetId, child: FocusTargetId) -> bool {
|
||||
if parent == child {
|
||||
return true;
|
||||
}
|
||||
@@ -473,13 +507,13 @@ impl DispatchTree {
|
||||
dispatch_path
|
||||
}
|
||||
|
||||
pub fn focus_path(&self, focus_id: FocusId) -> SmallVec<[FocusId; 8]> {
|
||||
let mut focus_path: SmallVec<[FocusId; 8]> = SmallVec::new();
|
||||
pub fn focus_path(&self, focus_id: FocusTargetId) -> SmallVec<[FocusTargetId; 8]> {
|
||||
let mut focus_path: SmallVec<[FocusTargetId; 8]> = SmallVec::new();
|
||||
let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied();
|
||||
while let Some(node_id) = current_node_id {
|
||||
let node = self.node(node_id);
|
||||
if let Some(focus_id) = node.focus_id {
|
||||
focus_path.push(focus_id);
|
||||
if let Some(focus) = node.focus_target.as_ref() {
|
||||
focus_path.push(focus.id);
|
||||
}
|
||||
current_node_id = node.parent;
|
||||
}
|
||||
@@ -510,7 +544,7 @@ impl DispatchTree {
|
||||
&mut self.nodes[active_node_id.0]
|
||||
}
|
||||
|
||||
pub fn focusable_node_id(&self, target: FocusId) -> Option<DispatchNodeId> {
|
||||
pub fn focusable_node_id(&self, target: FocusTargetId) -> Option<DispatchNodeId> {
|
||||
self.focusable_node_ids.get(&target).copied()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
|
||||
DefiniteLength, Fill, FlexDirection, FlexWrap, FontStyle, FontWeight, Hsla, JustifyContent,
|
||||
Length, Position, SharedString, StyleRefinement, Visibility, WhiteSpace,
|
||||
DefiniteLength, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla,
|
||||
JustifyContent, Length, Position, SharedString, StyleRefinement, Visibility, WhiteSpace,
|
||||
};
|
||||
use crate::{BoxShadow, TextStyleRefinement};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@@ -771,14 +771,32 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the font on this element and its children.
|
||||
fn font(mut self, family_name: impl Into<SharedString>) -> Self {
|
||||
/// Change the font family on this element and its children.
|
||||
fn font_family(mut self, family_name: impl Into<SharedString>) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_family = Some(family_name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the font of this element and its children.
|
||||
fn font(mut self, font: Font) -> Self {
|
||||
let Font {
|
||||
family,
|
||||
features,
|
||||
weight,
|
||||
style,
|
||||
} = font;
|
||||
|
||||
let text_style = self.text_style().get_or_insert_with(Default::default);
|
||||
text_style.font_family = Some(family);
|
||||
text_style.font_features = Some(features);
|
||||
text_style.font_weight = Some(weight);
|
||||
text_style.font_style = Some(style);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the line height on this element and its children.
|
||||
fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self {
|
||||
self.text_style()
|
||||
|
||||
@@ -76,20 +76,20 @@ type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
|
||||
type AnyWindowFocusListener = Box<dyn FnMut(&FocusEvent, &mut WindowContext) -> bool + 'static>;
|
||||
|
||||
struct FocusEvent {
|
||||
previous_focus_path: SmallVec<[FocusId; 8]>,
|
||||
current_focus_path: SmallVec<[FocusId; 8]>,
|
||||
previous_focus_path: SmallVec<[FocusTargetId; 8]>,
|
||||
current_focus_path: SmallVec<[FocusTargetId; 8]>,
|
||||
}
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// A globally unique identifier for a focusable element.
|
||||
pub struct FocusId;
|
||||
pub struct FocusTargetId;
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub(crate) static ELEMENT_ARENA: RefCell<Arena> = RefCell::new(Arena::new(8 * 1024 * 1024));
|
||||
}
|
||||
|
||||
impl FocusId {
|
||||
impl FocusTargetId {
|
||||
/// Obtains whether the element associated with this handle is currently focused.
|
||||
pub fn is_focused(&self, cx: &WindowContext) -> bool {
|
||||
cx.window.focus == Some(*self)
|
||||
@@ -120,8 +120,8 @@ impl FocusId {
|
||||
|
||||
/// A handle which can be used to track and manipulate the focused element in a window.
|
||||
pub struct FocusHandle {
|
||||
pub(crate) id: FocusId,
|
||||
handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
|
||||
pub(crate) id: FocusTargetId,
|
||||
handles: Arc<RwLock<SlotMap<FocusTargetId, AtomicUsize>>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FocusHandle {
|
||||
@@ -131,7 +131,7 @@ impl std::fmt::Debug for FocusHandle {
|
||||
}
|
||||
|
||||
impl FocusHandle {
|
||||
pub(crate) fn new(handles: &Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>) -> Self {
|
||||
pub(crate) fn new(handles: &Arc<RwLock<SlotMap<FocusTargetId, AtomicUsize>>>) -> Self {
|
||||
let id = handles.write().insert(AtomicUsize::new(1));
|
||||
Self {
|
||||
id,
|
||||
@@ -140,8 +140,8 @@ impl FocusHandle {
|
||||
}
|
||||
|
||||
pub(crate) fn for_id(
|
||||
id: FocusId,
|
||||
handles: &Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
|
||||
id: FocusTargetId,
|
||||
handles: &Arc<RwLock<SlotMap<FocusTargetId, AtomicUsize>>>,
|
||||
) -> Option<Self> {
|
||||
let lock = handles.read();
|
||||
let ref_count = lock.get(id)?;
|
||||
@@ -219,8 +219,8 @@ impl Drop for FocusHandle {
|
||||
/// A weak reference to a focus handle.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WeakFocusHandle {
|
||||
pub(crate) id: FocusId,
|
||||
handles: Weak<RwLock<SlotMap<FocusId, AtomicUsize>>>,
|
||||
pub(crate) id: FocusTargetId,
|
||||
handles: Weak<RwLock<SlotMap<FocusTargetId, AtomicUsize>>>,
|
||||
}
|
||||
|
||||
impl WeakFocusHandle {
|
||||
@@ -291,7 +291,7 @@ pub struct Window {
|
||||
pub(crate) tooltip_bounds: Option<TooltipBounds>,
|
||||
next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>>,
|
||||
pub(crate) dirty_views: FxHashSet<EntityId>,
|
||||
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
|
||||
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusTargetId, AtomicUsize>>>,
|
||||
focus_listeners: SubscriberSet<(), AnyWindowFocusListener>,
|
||||
focus_lost_listeners: SubscriberSet<(), AnyObserver>,
|
||||
default_prevented: bool,
|
||||
@@ -309,7 +309,7 @@ pub struct Window {
|
||||
pub(crate) refreshing: bool,
|
||||
pub(crate) draw_phase: DrawPhase,
|
||||
activation_observers: SubscriberSet<(), AnyObserver>,
|
||||
pub(crate) focus: Option<FocusId>,
|
||||
pub(crate) focus: Option<FocusTargetId>,
|
||||
focus_enabled: bool,
|
||||
pending_input: Option<PendingInput>,
|
||||
prompt: Option<RenderablePromptHandle>,
|
||||
@@ -327,7 +327,7 @@ pub(crate) enum DrawPhase {
|
||||
struct PendingInput {
|
||||
keystrokes: SmallVec<[Keystroke; 1]>,
|
||||
bindings: SmallVec<[KeyBinding; 1]>,
|
||||
focus: Option<FocusId>,
|
||||
focus: Option<FocusTargetId>,
|
||||
timer: Option<Task<()>>,
|
||||
}
|
||||
|
||||
@@ -2808,7 +2808,7 @@ pub enum ElementId {
|
||||
/// A string based ID.
|
||||
Name(SharedString),
|
||||
/// An ID that's equated with a focus handle.
|
||||
FocusHandle(FocusId),
|
||||
FocusHandle(FocusTargetId),
|
||||
/// A combination of a name and an integer.
|
||||
NamedInteger(SharedString, usize),
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ use crate::{
|
||||
hash, point, prelude::*, px, size, AnyElement, AnyTooltip, AppContext, Asset, AvailableSpace,
|
||||
Bounds, BoxShadow, ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId,
|
||||
DispatchPhase, DispatchTree, DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle,
|
||||
FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext,
|
||||
KeyEvent, LayoutId, LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent,
|
||||
PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad,
|
||||
FocusTargetId, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero,
|
||||
KeyContext, KeyEvent, LayoutId, LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite,
|
||||
MouseEvent, PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad,
|
||||
RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size,
|
||||
StrikethroughStyle, Style, Task, TextStyleRefinement, TransformationMatrix, Underline,
|
||||
UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS,
|
||||
@@ -126,7 +126,7 @@ pub(crate) struct DeferredDraw {
|
||||
}
|
||||
|
||||
pub(crate) struct Frame {
|
||||
pub(crate) focus: Option<FocusId>,
|
||||
pub(crate) focus: Option<FocusTargetId>,
|
||||
pub(crate) window_active: bool,
|
||||
pub(crate) element_states: FxHashMap<(GlobalElementId, TypeId), ElementStateBox>,
|
||||
accessed_element_states: Vec<(GlobalElementId, TypeId)>,
|
||||
@@ -214,7 +214,7 @@ impl Frame {
|
||||
hit_test
|
||||
}
|
||||
|
||||
pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> {
|
||||
pub(crate) fn focus_path(&self) -> SmallVec<[FocusTargetId; 8]> {
|
||||
self.focus
|
||||
.map(|focus_id| self.dispatch_tree.focus_path(focus_id))
|
||||
.unwrap_or_default()
|
||||
@@ -1426,11 +1426,16 @@ impl<'a> ElementContext<'a> {
|
||||
|
||||
/// Sets the focus handle for the current element. This handle will be used to manage focus state
|
||||
/// and keyboard event dispatch for the element.
|
||||
pub fn set_focus_handle(&mut self, focus_handle: &FocusHandle) {
|
||||
pub fn set_focus_target(&mut self, focus_handle: &FocusHandle, bounds: Option<Bounds<Pixels>>) {
|
||||
debug_assert_eq!(
|
||||
self.window.draw_phase,
|
||||
DrawPhase::Layout,
|
||||
"you must set the focus target during after_layout"
|
||||
);
|
||||
self.window
|
||||
.next_frame
|
||||
.dispatch_tree
|
||||
.set_focus_id(focus_handle.id);
|
||||
.set_focus_target(focus_handle.id, bounds);
|
||||
}
|
||||
|
||||
/// Sets the view id for the current element, which will be used to manage view caching.
|
||||
@@ -1443,6 +1448,15 @@ impl<'a> ElementContext<'a> {
|
||||
self.window.next_frame.dispatch_tree.parent_view_id()
|
||||
}
|
||||
|
||||
/// Returns the bounds for the focus target if it is a descendant of the current element.
|
||||
pub fn focus_target_bounds(&self) -> Option<Bounds<Pixels>> {
|
||||
let focus_target_id = self.window.focus?;
|
||||
self.window
|
||||
.next_frame
|
||||
.dispatch_tree
|
||||
.focus_target_bounds(focus_target_id)
|
||||
}
|
||||
|
||||
/// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the
|
||||
/// platform to receive textual input with proper integration with concerns such
|
||||
/// as IME interactions. This handler will be active for the upcoming frame until the following frame is
|
||||
|
||||
@@ -1835,7 +1835,7 @@ impl Render for DraggedProjectEntryView {
|
||||
let settings = ProjectPanelSettings::get_global(cx);
|
||||
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
||||
h_flex()
|
||||
.font(ui_font)
|
||||
.font_family(ui_font)
|
||||
.bg(cx.theme().colors().background)
|
||||
.w(self.width)
|
||||
.child(
|
||||
|
||||
@@ -43,6 +43,19 @@ pub struct RichText {
|
||||
Option<Arc<dyn Fn(usize, Range<usize>, &mut WindowContext) -> Option<AnyView>>>,
|
||||
}
|
||||
|
||||
impl Default for RichText {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: SharedString::default(),
|
||||
highlights: Vec::new(),
|
||||
link_ranges: Vec::new(),
|
||||
link_urls: Arc::from([]),
|
||||
custom_ranges: Vec::new(),
|
||||
custom_ranges_tooltip_fn: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows one to specify extra links to the rendered markdown, which can be used
|
||||
/// for e.g. mentions.
|
||||
#[derive(Debug)]
|
||||
@@ -52,6 +65,37 @@ pub struct Mention {
|
||||
}
|
||||
|
||||
impl RichText {
|
||||
pub fn new(
|
||||
block: String,
|
||||
mentions: &[Mention],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
) -> Self {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut link_ranges = Vec::new();
|
||||
let mut link_urls = Vec::new();
|
||||
render_markdown_mut(
|
||||
&block,
|
||||
mentions,
|
||||
language_registry,
|
||||
None,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
&mut link_ranges,
|
||||
&mut link_urls,
|
||||
);
|
||||
text.truncate(text.trim_end().len());
|
||||
|
||||
RichText {
|
||||
text: SharedString::from(text),
|
||||
link_urls: link_urls.into(),
|
||||
link_ranges,
|
||||
highlights,
|
||||
custom_ranges: Vec::new(),
|
||||
custom_ranges_tooltip_fn: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tooltip_builder_for_custom_ranges(
|
||||
&mut self,
|
||||
f: impl Fn(usize, Range<usize>, &mut WindowContext) -> Option<AnyView> + 'static,
|
||||
@@ -347,38 +391,6 @@ pub fn render_markdown_mut(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_rich_text(
|
||||
block: String,
|
||||
mentions: &[Mention],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<&Arc<Language>>,
|
||||
) -> RichText {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut link_ranges = Vec::new();
|
||||
let mut link_urls = Vec::new();
|
||||
render_markdown_mut(
|
||||
&block,
|
||||
mentions,
|
||||
language_registry,
|
||||
language,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
&mut link_ranges,
|
||||
&mut link_urls,
|
||||
);
|
||||
text.truncate(text.trim_end().len());
|
||||
|
||||
RichText {
|
||||
text: SharedString::from(text),
|
||||
link_urls: link_urls.into(),
|
||||
link_ranges,
|
||||
highlights,
|
||||
custom_ranges: Vec::new(),
|
||||
custom_ranges_tooltip_fn: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_code(
|
||||
text: &mut String,
|
||||
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
||||
|
||||
@@ -9,6 +9,11 @@ license = "GPL-3.0-or-later"
|
||||
[lib]
|
||||
path = "src/semantic_index.rs"
|
||||
|
||||
[[example]]
|
||||
name = "index"
|
||||
path = "examples/index.rs"
|
||||
crate-type = ["bin"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
use client::Client;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{App, Global, TestAppContext};
|
||||
use gpui::{App, Global};
|
||||
use language::language_settings::AllLanguageSettings;
|
||||
use project::Project;
|
||||
use semantic_index::{OpenAiEmbeddingModel, OpenAiEmbeddingProvider, SemanticIndex};
|
||||
use settings::SettingsStore;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::http::HttpClientWithUrl;
|
||||
|
||||
pub fn init_test(cx: &mut TestAppContext) {
|
||||
_ = cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
SettingsStore::update(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |_| {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
@@ -58,7 +49,7 @@ fn main() {
|
||||
);
|
||||
|
||||
let semantic_index = SemanticIndex::new(
|
||||
Path::new("/tmp/semantic-index-db.mdb"),
|
||||
PathBuf::from("/tmp/semantic-index-db.mdb"),
|
||||
Arc::new(embedding_provider),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ use std::{
|
||||
cmp::Ordering,
|
||||
future::Future,
|
||||
ops::Range,
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
@@ -38,15 +38,15 @@ impl Global for SemanticIndex {}
|
||||
|
||||
impl SemanticIndex {
|
||||
pub fn new(
|
||||
db_path: &Path,
|
||||
db_path: PathBuf,
|
||||
embedding_provider: Arc<dyn EmbeddingProvider>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Self>> {
|
||||
let db_path = db_path.to_path_buf();
|
||||
cx.spawn(|cx| async move {
|
||||
let db_connection = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
std::fs::create_dir_all(&db_path)?;
|
||||
unsafe {
|
||||
heed::EnvOpenOptions::new()
|
||||
.map_size(1024 * 1024 * 1024)
|
||||
@@ -54,7 +54,8 @@ impl SemanticIndex {
|
||||
.open(db_path)
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
.await
|
||||
.context("opening database connection")?;
|
||||
|
||||
Ok(SemanticIndex {
|
||||
db_connection,
|
||||
@@ -879,11 +880,8 @@ mod tests {
|
||||
|
||||
let mut semantic_index = cx
|
||||
.update(|cx| {
|
||||
let semantic_index = SemanticIndex::new(
|
||||
Path::new(temp_dir.path()),
|
||||
Arc::new(TestEmbeddingProvider),
|
||||
cx,
|
||||
);
|
||||
let semantic_index =
|
||||
SemanticIndex::new(temp_dir.path().into(), Arc::new(TestEmbeddingProvider), cx);
|
||||
semantic_index
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -2,6 +2,7 @@ mod keymap_file;
|
||||
mod settings_file;
|
||||
mod settings_store;
|
||||
|
||||
use gpui::AppContext;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{borrow::Cow, str};
|
||||
use util::asset_str;
|
||||
@@ -19,6 +20,14 @@ pub use settings_store::{
|
||||
#[exclude = "*.DS_Store"]
|
||||
pub struct SettingsAssets;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let mut settings = SettingsStore::default();
|
||||
settings
|
||||
.set_default_settings(&default_settings(), cx)
|
||||
.unwrap();
|
||||
cx.set_global(settings);
|
||||
}
|
||||
|
||||
pub fn default_settings() -> Cow<'static, str> {
|
||||
asset_str::<SettingsAssets>("settings/default.json")
|
||||
}
|
||||
|
||||
@@ -29,14 +29,14 @@ pub enum ComponentStory {
|
||||
ListHeader,
|
||||
ListItem,
|
||||
OverflowScroll,
|
||||
Picker,
|
||||
Scroll,
|
||||
Tab,
|
||||
TabBar,
|
||||
Text,
|
||||
TitleBar,
|
||||
ToggleButton,
|
||||
Text,
|
||||
ViewportUnits,
|
||||
Picker,
|
||||
}
|
||||
|
||||
impl ComponentStory {
|
||||
|
||||
@@ -10,7 +10,7 @@ use gpui::{
|
||||
div, px, size, AnyView, AppContext, Bounds, Render, ViewContext, VisualContext, WindowOptions,
|
||||
};
|
||||
use log::LevelFilter;
|
||||
use settings::{default_settings, KeymapFile, Settings, SettingsStore};
|
||||
use settings::{KeymapFile, Settings};
|
||||
use simplelog::SimpleLogger;
|
||||
use strum::IntoEnumIterator;
|
||||
use theme::{ThemeRegistry, ThemeSettings};
|
||||
@@ -63,12 +63,7 @@ fn main() {
|
||||
gpui::App::new().with_assets(Assets).run(move |cx| {
|
||||
load_embedded_fonts(cx).unwrap();
|
||||
|
||||
let mut store = SettingsStore::default();
|
||||
store
|
||||
.set_default_settings(default_settings().as_ref(), cx)
|
||||
.unwrap();
|
||||
cx.set_global(store);
|
||||
|
||||
settings::init(cx);
|
||||
theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
|
||||
|
||||
let selector = story_selector;
|
||||
@@ -120,7 +115,7 @@ impl Render for StoryWrapper {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.font("Zed Mono")
|
||||
.font_family("Zed Mono")
|
||||
.child(self.story.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ smallvec.workspace = true
|
||||
story = { workspace = true, optional = true }
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
theme.workspace = true
|
||||
nanoid = "0.4"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod avatar;
|
||||
mod button;
|
||||
mod checkbox;
|
||||
mod collapsible_container;
|
||||
mod context_menu;
|
||||
mod disclosure;
|
||||
mod divider;
|
||||
@@ -25,6 +26,7 @@ mod stories;
|
||||
pub use avatar::*;
|
||||
pub use button::*;
|
||||
pub use checkbox::*;
|
||||
pub use collapsible_container::*;
|
||||
pub use context_menu::*;
|
||||
pub use disclosure::*;
|
||||
pub use divider::*;
|
||||
|
||||
152
crates/ui/src/components/collapsible_container.rs
Normal file
152
crates/ui/src/components/collapsible_container.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use crate::{prelude::*, ButtonLike};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use gpui::*;
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ContainerStyle {
|
||||
#[default]
|
||||
None,
|
||||
Card,
|
||||
}
|
||||
|
||||
struct ContainerStyles {
|
||||
pub background_color: Hsla,
|
||||
pub border_color: Hsla,
|
||||
pub text_color: Hsla,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct CollapsibleContainer {
|
||||
id: ElementId,
|
||||
base: ButtonLike,
|
||||
toggle: bool,
|
||||
/// A slot for content that appears before the label, like an icon or avatar.
|
||||
start_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears after the label, usually on the other side of the header.
|
||||
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||
end_slot: Option<AnyElement>,
|
||||
style: ContainerStyle,
|
||||
children: SmallVec<[AnyElement; 1]>,
|
||||
}
|
||||
|
||||
impl CollapsibleContainer {
|
||||
pub fn new(id: impl Into<ElementId>, toggle: bool) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
base: ButtonLike::new("button_base"),
|
||||
toggle,
|
||||
start_slot: None,
|
||||
end_slot: None,
|
||||
style: ContainerStyle::Card,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
|
||||
self.start_slot = start_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn child<E: IntoElement>(mut self, child: E) -> Self {
|
||||
self.children.push(child.into_any_element());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clickable for CollapsibleContainer {
|
||||
fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
self.base = self.base.on_click(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CollapsibleContainer {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let color = cx.theme().colors();
|
||||
|
||||
let styles = match self.style {
|
||||
ContainerStyle::None => ContainerStyles {
|
||||
background_color: color.ghost_element_background,
|
||||
border_color: color.border_transparent,
|
||||
text_color: color.text,
|
||||
},
|
||||
ContainerStyle::Card => ContainerStyles {
|
||||
background_color: color.elevated_surface_background,
|
||||
border_color: color.border,
|
||||
text_color: color.text,
|
||||
},
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.id(self.id)
|
||||
.relative()
|
||||
.rounded_md()
|
||||
.bg(styles.background_color)
|
||||
.border()
|
||||
.border_color(styles.border_color)
|
||||
.text_color(styles.text_color)
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.w_full()
|
||||
.group("toggleable_container_header")
|
||||
.border_b()
|
||||
.border_color(if self.toggle {
|
||||
styles.border_color
|
||||
} else {
|
||||
color.border_transparent
|
||||
})
|
||||
.child(
|
||||
self.base.full_width().style(ButtonStyle::Subtle).child(
|
||||
div()
|
||||
.h_7()
|
||||
.p_1()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.cursor_pointer()
|
||||
.group_hover("toggleable_container_header", |this| {
|
||||
this.bg(color.element_hover)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"toggle_icon",
|
||||
match self.toggle {
|
||||
true => IconName::ChevronDown,
|
||||
false => IconName::ChevronRight,
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("label_container")
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.start_slot),
|
||||
),
|
||||
)
|
||||
.child(h_flex().children(self.end_slot)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.when(self.toggle, |this| {
|
||||
this.child(h_flex().flex_1().w_full().p_1().children(self.children))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ impl RenderOnce for WindowsCaptionButton {
|
||||
.content_center()
|
||||
.w(width)
|
||||
.h_full()
|
||||
.font("Segoe Fluent Icons")
|
||||
.font_family("Segoe Fluent Icons")
|
||||
.text_size(px(10.0))
|
||||
.hover(|style| style.bg(self.hover_background_color))
|
||||
.active(|style| {
|
||||
|
||||
@@ -95,7 +95,7 @@ pub fn tooltip_container<V>(
|
||||
div().pl_2().pt_2p5().child(
|
||||
v_flex()
|
||||
.elevation_2(cx)
|
||||
.font(ui_font)
|
||||
.font_family(ui_font)
|
||||
.text_ui()
|
||||
.text_color(cx.theme().colors().text)
|
||||
.py_1()
|
||||
|
||||
@@ -93,7 +93,7 @@ impl RenderOnce for Headline {
|
||||
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
||||
|
||||
div()
|
||||
.font(ui_font)
|
||||
.font_family(ui_font)
|
||||
.line_height(self.size.line_height())
|
||||
.text_size(self.size.size())
|
||||
.text_color(cx.theme().colors().text)
|
||||
|
||||
@@ -2852,6 +2852,6 @@ impl Render for DraggedTab {
|
||||
.selected(self.is_active)
|
||||
.child(label)
|
||||
.render(cx)
|
||||
.font(ui_font)
|
||||
.font_family(ui_font)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3935,7 +3935,7 @@ impl Render for Workspace {
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.font(ui_font)
|
||||
.font_family(ui_font)
|
||||
.gap_0()
|
||||
.justify_start()
|
||||
.items_start()
|
||||
|
||||
@@ -231,27 +231,18 @@ fn init_ui() {
|
||||
|
||||
load_embedded_fonts(cx);
|
||||
|
||||
let mut store = SettingsStore::default();
|
||||
store
|
||||
.set_default_settings(default_settings().as_ref(), cx)
|
||||
.unwrap();
|
||||
cx.set_global(store);
|
||||
settings::init(cx);
|
||||
handle_settings_file_changes(user_settings_file_rx, cx);
|
||||
handle_keymap_file_changes(user_keymap_file_rx, cx);
|
||||
|
||||
client::init_settings(cx);
|
||||
|
||||
let clock = Arc::new(clock::RealSystemClock);
|
||||
let http = Arc::new(HttpClientWithUrl::new(
|
||||
&client::ClientSettings::get_global(cx).server_url,
|
||||
));
|
||||
|
||||
let client = client::Client::new(clock, http.clone(), cx);
|
||||
let client = Client::production(cx);
|
||||
let mut languages =
|
||||
LanguageRegistry::new(login_shell_env_loaded, cx.background_executor().clone());
|
||||
let copilot_language_server_id = languages.next_language_server_id();
|
||||
languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
|
||||
let languages = Arc::new(languages);
|
||||
let node_runtime = RealNodeRuntime::new(http.clone());
|
||||
let node_runtime = RealNodeRuntime::new(client.http_client());
|
||||
|
||||
language::init(cx);
|
||||
languages::init(languages.clone(), node_runtime.clone(), cx);
|
||||
@@ -271,7 +262,7 @@ fn init_ui() {
|
||||
diagnostics::init(cx);
|
||||
copilot::init(
|
||||
copilot_language_server_id,
|
||||
http.clone(),
|
||||
client.http_client(),
|
||||
node_runtime.clone(),
|
||||
cx,
|
||||
);
|
||||
@@ -296,7 +287,7 @@ fn init_ui() {
|
||||
|
||||
cx.observe_global::<SettingsStore>({
|
||||
let languages = languages.clone();
|
||||
let http = http.clone();
|
||||
let http = client.http_client();
|
||||
let client = client.clone();
|
||||
|
||||
move |cx| {
|
||||
@@ -344,7 +335,7 @@ fn init_ui() {
|
||||
AppState::set_global(Arc::downgrade(&app_state), cx);
|
||||
|
||||
audio::init(Assets, cx);
|
||||
auto_update::init(http.clone(), cx);
|
||||
auto_update::init(client.http_client(), cx);
|
||||
|
||||
workspace::init(app_state.clone(), cx);
|
||||
recent_projects::init(cx);
|
||||
@@ -377,7 +368,7 @@ fn init_ui() {
|
||||
initialize_workspace(app_state.clone(), cx);
|
||||
|
||||
// todo(linux): unblock this
|
||||
upload_panics_and_crashes(http.clone(), cx);
|
||||
upload_panics_and_crashes(client.http_client(), cx);
|
||||
|
||||
cx.activate(true);
|
||||
|
||||
|
||||
@@ -3030,11 +3030,7 @@ mod tests {
|
||||
])
|
||||
.unwrap();
|
||||
let themes = ThemeRegistry::default();
|
||||
let mut settings = SettingsStore::default();
|
||||
settings
|
||||
.set_default_settings(&settings::default_settings(), cx)
|
||||
.unwrap();
|
||||
cx.set_global(settings);
|
||||
settings::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
|
||||
let mut has_default_theme = false;
|
||||
|
||||
Reference in New Issue
Block a user