Compare commits

...

131 Commits

Author SHA1 Message Date
Antonio Scandurra
f667a06003 WIP 2024-04-16 18:09:10 +02:00
Antonio Scandurra
3c57523920 Fix clicking collapsible container 2024-04-16 15:33:49 +02:00
Antonio Scandurra
5d76b4202a WIP: autoscroll list when editor's selections change 2024-04-16 15:33:46 +02:00
Nathan Sobo
dc08b3e174 Merge remote-tracking branch 'origin/assistant2' into assistant2 2024-04-15 19:25:14 -06:00
Nathan Sobo
b5ffb12510 Continue to render focused list items, even when they aren't visible
This ensures keyboard interaction continues to work.
2024-04-15 19:24:25 -06:00
Kyle Kelley
205480670c debug the on_click handler 2024-04-15 16:49:09 -07:00
Kyle Kelley
d37b05d376 set up on_click for excerpts 2024-04-15 16:31:34 -07:00
Kyle Kelley
6cfa746dda establish view for CodebaseContext
Co-Authored-By: Nathan <nathan@zed.dev>
2024-04-15 15:51:18 -07:00
Nathan Sobo
c30afd6599 Merge remote-tracking branch 'origin/assistant2' into assistant2 2024-04-15 15:46:10 -06:00
Nathan Sobo
c4a0312c03 WIP: Try to render off-screen focused elements in lists
Not working yet
2024-04-15 15:44:45 -06:00
Kyle Kelley
60cd888d22 use collapsible container for excerpts
Co-Authored-By: nate <nate@zed.dev>
2024-04-15 14:41:32 -07:00
Nathan Sobo
28fba36176 Render off-screen focusable items, but the API isn't going to work
Invoking a callback during splice isn't viable because it causes a double lease
of the view that owns the list state.
2024-04-15 15:00:25 -06:00
Nate Butler
7f293c9d9e Remove inline story 2024-04-15 16:17:18 -04:00
Nate Butler
849c7dd46c Merge branch 'disclosable_container' into assistant2 2024-04-15 16:05:23 -04:00
Nate Butler
5c6dc82860 Add collapsible container 2024-04-15 16:04:56 -04:00
Kyle Kelley
bb1aa2e0e5 make the range rely on the text size, comment on stale ranges 2024-04-15 11:28:38 -07:00
Kyle Kelley
d33304d41a Merge remote-tracking branch 'origin/main' into assistant2 2024-04-15 11:11:00 -07:00
Kyle Kelley
451230d843 Merge branch 'main' into assistant2 2024-04-15 11:10:38 -07:00
Kyle Kelley
43a3f63cab handle range out of bounds 2024-04-15 11:04:18 -07:00
Kyle Kelley
1e28882260 clean up the query creation for code base search 2024-04-15 10:10:17 -07:00
Kyle Kelley
4476e02bd6 wip List work 2024-04-15 10:10:05 -07:00
Antonio Scandurra
2cb9083c5a WIP 2024-04-15 17:51:07 +02:00
Nate Butler
ad60bbc242 wip 2024-04-15 11:20:28 -04:00
Antonio Scandurra
e701fc113b Fix running Zed 2024-04-15 15:20:23 +02:00
Antonio Scandurra
367bd32789 Merge branch 'main' into assistant2
# Conflicts:
#	crates/editor/src/element.rs
2024-04-15 15:13:57 +02:00
Antonio Scandurra
85b34bb1cf Show message author 2024-04-15 14:23:42 +02:00
Nathan Sobo
e00601f96f Create empty database directory for semantic index if it doesn't exist 2024-04-14 09:49:30 -06:00
Nathan Sobo
250c481c63 Merge branch 'main' into assistant2 2024-04-14 09:15:14 -06:00
Kyle Kelley
152b77a2c1 allow model to craft a query for the semantic index 2024-04-14 00:15:15 -07:00
Kyle Kelley
8805d185a3 provide means for extracting a query based on the previous history 2024-04-13 23:51:39 -07:00
Kyle Kelley
e494772cd6 clear contexts first on populate 2024-04-13 11:47:36 -07:00
Kyle Kelley
1d80ea1751 increase results 2024-04-13 11:42:26 -07:00
Kyle Kelley
ad12a23159 clean up a bit more, introduce score 2024-04-13 11:40:43 -07:00
Kyle Kelley
9d681bc163 improve formatting for code search 2024-04-13 11:13:01 -07:00
Kyle Kelley
0d5dfba815 Show the codebase context to the model
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2024-04-12 16:34:18 -07:00
Kyle Kelley
39ace6c108 populate results from semantic index into chat for user
Co-Authored-By: Nathan <nathan@zed.dev>
2024-04-12 16:16:37 -07:00
Kyle Kelley
2953aab1c7 perform search on cmd-enter
no results passed to model, but we render that we're doing it!

Co-Authored-By: Nathan <nathan@zed.dev>
2024-04-12 15:27:43 -07:00
Kyle Kelley
b7ba5d3c27 hook up a project index to the assistant chat 2024-04-12 13:30:28 -07:00
Kyle Kelley
19111d6d15 remove unused init_test from semantic index example 2024-04-12 12:38:59 -07:00
Kyle Kelley
f3d8a777ad include a fake error (for now) 2024-04-12 12:34:23 -07:00
Kyle Kelley
f0fdd7459f create an assistant example task
It's ok if this is removed later
2024-04-12 12:34:05 -07:00
Kyle Kelley
fa029b038e create an error view at the bottom of the assistant message
Co-Authored-By: Nate Butler <nate@zed.dev>
2024-04-12 12:30:28 -07:00
Antonio Scandurra
02aa68f997 Change orientation of assistant list to work more like a chat
Co-Authored-By: Nathan <nathan@zed.dev>
Co-Authored-By: Kyle <kylek@zed.dev>
2024-04-12 17:58:19 +02:00
Antonio Scandurra
c98e4eb41b Truncate messages after the focused message on submit
Co-Authored-By: Nathan <nathan@zed.dev>
Co-Authored-By: Kyle <kylek@zed.dev>
2024-04-12 17:50:11 +02:00
Antonio Scandurra
9d5d28e583 Render the assistant output as markdown
Co-Authored-By: Nathan <nathan@zed.dev>
Co-Authored-By: Kyle <kylek@zed.dev>
2024-04-12 17:07:21 +02:00
Antonio Scandurra
34e7d31800 Fix color of labels in model dropdown
Co-Authored-By: Nathan <nathan@zed.dev>
Co-Authored-By: Kyle <kylek@zed.dev>
2024-04-12 16:28:13 +02:00
Antonio Scandurra
d8c9b0dd11 Allow selecting a language model via a dropdown 2024-04-12 15:46:26 +02:00
Nathan Sobo
342fa96fb0 Merge branch 'main' into assistant2 2024-04-11 14:35:59 -06:00
Nathan Sobo
fa62a5abfa Stream completions from cloud on enter
Print them to the console for now.

Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
Co-Authored-By: Kyle Kelley <kylek@zed.dev>
2024-04-11 09:31:10 -06:00
Nathan Sobo
0780bafc5a Merge branch 'main' into assistant2 2024-04-11 09:16:24 -06:00
Nathan Sobo
385da79021 WIP
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
Co-Authored-By: Kyle Kelley <kylek@zed.dev>
2024-04-11 09:15:32 -06:00
Nathan Sobo
b857beb2c6 Load the keymap and fix padding
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2024-04-11 08:01:34 -06:00
Nathan Sobo
3d938fcfdf Get editor rendering in assistant2 with buffer font
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2024-04-11 07:55:44 -06:00
Nathan Sobo
15dec0e2b4 Start on new assistant panel with an example 2024-04-10 20:49:15 -06:00
Kyle Kelley
c936b66cca test against a fixture with a few chunks to work through 2024-04-10 15:58:54 -07:00
Kyle Kelley
4e652e9214 💄 2024-04-10 15:42:11 -07:00
Kyle Kelley
8243401d42 set up initial test for semantic index 2024-04-10 15:42:11 -07:00
Kyle Kelley
b0243eb8bd create tests for chunking 2024-04-10 15:42:11 -07:00
Kyle Kelley
93a247809c test chunking 2024-04-10 15:42:11 -07:00
Nathan Sobo
661b8ca305 Test embedding queries 2024-04-10 15:42:11 -07:00
Marshall Bowers
e66f8230a1 Fix license symlink 2024-04-10 15:42:11 -07:00
Kyle Kelley
3f30f27ce8 expose with_parser from language crate 2024-04-10 15:42:11 -07:00
Kyle Kelley
7f76b63bd4 move dependencies up to workspace root 2024-04-10 15:42:11 -07:00
Kyle Kelley
11527c5822 🦜 - adapt types
Co-Authored-By: Conrad Irwin <conrad@zed.dev>
2024-04-10 15:42:11 -07:00
Kyle Kelley
30dca54a34 wrap debug durations in cfg(debug_assertions) 2024-04-10 15:42:11 -07:00
Kyle Kelley
14567e6400 include license 2024-04-10 15:42:11 -07:00
Kyle Kelley
83392354e5 show elapsed time when a debug build 2024-04-10 15:42:11 -07:00
Kyle Kelley
27a230d75a remove unused dep 2024-04-10 15:42:11 -07:00
Kyle Kelley
1a1b8010ce 💄 2024-04-10 15:42:11 -07:00
Nathan Sobo
1ee8682fdb Fix errors and warnings 2024-04-10 15:42:11 -07:00
Kyle Kelley
e767221817 purge embeddings older than a week 2024-04-10 15:42:11 -07:00
Kyle Kelley
f7e458b598 put quit back in 2024-04-10 15:42:10 -07:00
Antonio Scandurra
88100c956f WIP 2024-04-10 15:42:10 -07:00
Antonio Scandurra
4429495fb5 WIP: start on caching embeddings 2024-04-10 15:42:10 -07:00
Antonio Scandurra
02e269a5c5 Wire up proto::EmbedTexts request handler on the server 2024-04-10 15:42:10 -07:00
Antonio Scandurra
7e69dafd94 Extract embedding API into open_ai crate 2024-04-10 15:42:10 -07:00
Antonio Scandurra
aab44814be Implement ZedEmbeddingProvider 2024-04-10 15:42:10 -07:00
Antonio Scandurra
41feda5abe Add/remove worktree indices as they change in the underlying project 2024-04-10 15:42:10 -07:00
Antonio Scandurra
89d5d9a16f Rework embedding provider module structure 2024-04-10 15:42:10 -07:00
Antonio Scandurra
4ff38fc823 Index files as they change on disk 2024-04-10 15:42:10 -07:00
Kyle Kelley
3007fb51c3 show score 2024-04-10 15:42:10 -07:00
Kyle Kelley
cd3972bc52 touchup 2024-04-10 15:42:10 -07:00
Kyle Kelley
5ceb6ff351 switch to OpenAI provider for example 2024-04-10 15:42:10 -07:00
Kyle Kelley
06da24697d show snippets from search results 2024-04-10 15:42:10 -07:00
Antonio Scandurra
b503dd63e6 Fix compile errors and make search work 2024-04-10 15:42:10 -07:00
Kyle Kelley
9589630dfe wip 2024-04-10 15:42:10 -07:00
Kyle Kelley
1dbbde02ae show just three digits and truncate otherwise for display 2024-04-10 15:42:10 -07:00
Kyle Kelley
fc1ff4b061 wip 2024-04-10 15:42:10 -07:00
Kyle Kelley
f0b7ea9a50 adapt tests to new batching setup 2024-04-10 15:42:10 -07:00
Kyle Kelley
912d5469d4 move args check further up 2024-04-10 15:42:10 -07:00
Kyle Kelley
225f21dd95 implement batching for OpenAI 2024-04-10 15:42:10 -07:00
Kyle Kelley
26be3c22a1 readjust tests 2024-04-10 15:42:10 -07:00
Kyle Kelley
c6c53d8fd3 make EmbeddingProvider trait be Send + Sync 2024-04-10 15:42:10 -07:00
Kyle Kelley
f035697232 WIP 2024-04-10 15:42:10 -07:00
Kyle Kelley
3a6ffc7de4 WIP 2024-04-10 15:42:10 -07:00
Kyle Kelley
8cce847ea7 WIP 2024-04-10 15:42:10 -07:00
Kyle Kelley
32b3c1e378 WIP 2024-04-10 15:42:10 -07:00
Antonio Scandurra
8389c8e254 WIP 2024-04-10 15:42:10 -07:00
Antonio Scandurra
757532a09e Fix key ordering in database 2024-04-10 15:42:10 -07:00
Antonio Scandurra
228a4286ad 🎨 2024-04-10 15:42:10 -07:00
Antonio Scandurra
6b27c860a8 Maintain embeddings in the database 2024-04-10 15:42:10 -07:00
Kyle Kelley
e76b0dc38c add support for OpenAI embedding 2024-04-10 15:42:10 -07:00
Kyle Kelley
355ce405cb create an OpenAI embedding client 2024-04-10 15:42:10 -07:00
Kyle Kelley
ec3dd27bc6 reorganize embedding provider segment 2024-04-10 15:42:10 -07:00
Kyle Kelley
9705e26cff create a not-exactly-a-benchmark 2024-04-10 15:42:10 -07:00
Kyle Kelley
7ddf0467a5 pass embedding model on through, rely on data types for more safety 2024-04-10 15:42:10 -07:00
Kyle Kelley
19aadacdef create more specificity around the embedding's backing model 2024-04-10 15:42:10 -07:00
Kyle Kelley
f80ac2c190 set up way to run the indexing example with actual embeddings 2024-04-10 15:42:10 -07:00
Kyle Kelley
876d017294 embed each chunk 2024-04-10 15:42:10 -07:00
Kyle Kelley
84e3063d4b test vector normalization 2024-04-10 15:42:10 -07:00
Kyle Kelley
4999cf136f quick integration test on embedding 2024-04-10 15:42:10 -07:00
Kyle Kelley
8099fb9845 set up embedding 2024-04-10 15:42:10 -07:00
Kyle Kelley
636bdf1196 create an embedding provider 2024-04-10 15:42:10 -07:00
Kyle Kelley
93501bcb0c create an enum for embedding sizes 2024-04-10 15:42:10 -07:00
Antonio Scandurra
f72e74e310 Add next steps 2024-04-10 15:42:10 -07:00
Antonio Scandurra
cc753b88e1 WIP 2024-04-10 15:42:10 -07:00
Antonio Scandurra
55a8d3b696 WIP: Start on making indexing a long-lived task 2024-04-10 15:42:10 -07:00
Antonio Scandurra
02a5da3e0e Rework indexing to be on a per-worktree basis 2024-04-10 15:42:10 -07:00
Antonio Scandurra
2f8dc894e1 Checkpoint 2024-04-10 15:42:10 -07:00
Antonio Scandurra
7e5a585ca7 Fall back to using line-based splitting when a grammar can't be found 2024-04-10 15:42:10 -07:00
Antonio Scandurra
078f9ed689 🎨 2024-04-10 15:42:10 -07:00
Antonio Scandurra
514902cbac Rework embedding to simplify determining when a project scan completes 2024-04-10 15:42:10 -07:00
Nathan Sobo
6a61f9577f Start on an embed_chunks task that processes batches of chunked files 2024-04-10 15:42:10 -07:00
Nathan Sobo
57d4878d4a Compute a digest for each chunk 2024-04-10 15:42:10 -07:00
Nathan Sobo
9e1706feb0 Introduce ChunkedFile struct to prepare to fetch and store embeddings 2024-04-10 15:42:10 -07:00
Antonio Scandurra
a7345fa596 WIP: flush less eagerly 2024-04-10 15:42:10 -07:00
Kyle Kelley
ba4c2a56e0 accept a path by arg 2024-04-10 15:42:10 -07:00
Antonio Scandurra
9bfcc631b9 WIP 2024-04-10 15:42:10 -07:00
Antonio Scandurra
8ee48a7133 WIP 2024-04-10 15:42:10 -07:00
Antonio Scandurra
240909d7cb WIP: Start on an example that indexes all the paths in a folder
Co-Authored-By: Nathan <nathan@zed.dev>
Co-Authored-By: Kyle <kylek@zed.dev>
2024-04-10 15:42:10 -07:00
Nathan Sobo
47c75a6c89 Start on a semantic_index crate 2024-04-10 15:42:10 -07:00
41 changed files with 1764 additions and 231 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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" }

View File

@@ -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"

View File

@@ -5,6 +5,9 @@ edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lib]
path = "src/assets.rs"
[lints]
workspace = true

View File

@@ -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)
}
}

View 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

View 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())
}
}

View 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![],
}
}
}

View 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()
}
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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()

View File

@@ -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({

View File

@@ -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| {

View File

@@ -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(),

View File

@@ -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(

View File

@@ -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| {

View File

@@ -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;
}
}

View File

@@ -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()
}

View File

@@ -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()

View File

@@ -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),
}

View File

@@ -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

View File

@@ -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(

View File

@@ -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)>,

View File

@@ -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

View File

@@ -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,
);

View File

@@ -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

View File

@@ -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")
}

View File

@@ -29,14 +29,14 @@ pub enum ComponentStory {
ListHeader,
ListItem,
OverflowScroll,
Picker,
Scroll,
Tab,
TabBar,
Text,
TitleBar,
ToggleButton,
Text,
ViewportUnits,
Picker,
}
impl ComponentStory {

View File

@@ -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())
}
}

View File

@@ -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

View File

@@ -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::*;

View 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))
})
}
}

View File

@@ -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| {

View File

@@ -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()

View File

@@ -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)

View File

@@ -2852,6 +2852,6 @@ impl Render for DraggedTab {
.selected(self.is_active)
.child(label)
.render(cx)
.font(ui_font)
.font_family(ui_font)
}
}

View File

@@ -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()

View File

@@ -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);

View File

@@ -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;