Compare commits
77 Commits
local-live
...
cole/diff-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e1b077915 | ||
|
|
74b7e8ca32 | ||
|
|
ba2760544a | ||
|
|
20e24dca68 | ||
|
|
59afc27f03 | ||
|
|
611abcadc0 | ||
|
|
fff12ec1e5 | ||
|
|
13a81e454a | ||
|
|
de89f8cf83 | ||
|
|
c594ccb0af | ||
|
|
937186da12 | ||
|
|
b3ffbea376 | ||
|
|
124e63d07c | ||
|
|
dd66a20d78 | ||
|
|
e8c72d91c3 | ||
|
|
dfe455b054 | ||
|
|
db7e38464a | ||
|
|
f8b6d71670 | ||
|
|
ae351298b4 | ||
|
|
664468d468 | ||
|
|
714f183ede | ||
|
|
b36dcf3b92 | ||
|
|
63e1bf01a4 | ||
|
|
62a6a755ec | ||
|
|
28faba12a2 | ||
|
|
c255e55599 | ||
|
|
f80eb73213 | ||
|
|
faf79e52fe | ||
|
|
ab595b0d55 | ||
|
|
ab1e9bf270 | ||
|
|
adc66473e7 | ||
|
|
119b5de384 | ||
|
|
c80ea60860 | ||
|
|
bac6896786 | ||
|
|
c6932d1f51 | ||
|
|
03efd0d1d9 | ||
|
|
43ba0c9fa6 | ||
|
|
4300ef840b | ||
|
|
e0f4c01794 | ||
|
|
58f9301253 | ||
|
|
96499b7b25 | ||
|
|
09006aaee9 | ||
|
|
2ca3b440a9 | ||
|
|
9219b05c85 | ||
|
|
bd2087675b | ||
|
|
44164dbbb8 | ||
|
|
3c053c7bc4 | ||
|
|
48eed7499f | ||
|
|
a35ef5b79f | ||
|
|
8a85d6ef96 | ||
|
|
158cdc33ba | ||
|
|
bdeac79d48 | ||
|
|
73e0d816c4 | ||
|
|
6538227f07 | ||
|
|
ef45eca88e | ||
|
|
803855e7b1 | ||
|
|
25a5ad54ae | ||
|
|
a5355e92e3 | ||
|
|
b7edf31170 | ||
|
|
7bd69130f8 | ||
|
|
2af9fa7785 | ||
|
|
16ecbafa7a | ||
|
|
e5f3a683f0 | ||
|
|
8c91eecb67 | ||
|
|
8fcaf8b870 | ||
|
|
77b8296fbb | ||
|
|
39e8944dcc | ||
|
|
a7d12eea39 | ||
|
|
ce9e4629be | ||
|
|
e58cdca044 | ||
|
|
4564273322 | ||
|
|
55ee72d84a | ||
|
|
2ce01ead93 | ||
|
|
bf1525588d | ||
|
|
d0e99f6496 | ||
|
|
ac07b9197a | ||
|
|
4b93a5ca44 |
@@ -1,6 +1,3 @@
|
||||
[env]
|
||||
LK_CUSTOM_WEBRTC = { value = "../livekit-rust-sdks/webrtc-sys/libwebrtc/linux-x64-release", relative = true }
|
||||
|
||||
[build]
|
||||
# v0 mangling scheme provides more detailed backtraces around closures
|
||||
rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,3 +1,4 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Language Request
|
||||
|
||||
1325
Cargo.lock
generated
18
Cargo.toml
@@ -141,15 +141,8 @@ members = [
|
||||
"crates/worktree",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
|
||||
"crates/livekit",
|
||||
"crates/livekit-api",
|
||||
"crates/livekit-protocol",
|
||||
"crates/livekit-ffi",
|
||||
"crates/libwebrtc",
|
||||
"crates/soxr-sys",
|
||||
"crates/webrtc-sys",
|
||||
"crates/webrtc-sys/build",
|
||||
"crates/zeta",
|
||||
"crates/git_ui",
|
||||
|
||||
#
|
||||
# Extensions
|
||||
@@ -235,6 +228,7 @@ fs = { path = "crates/fs" }
|
||||
fsevent = { path = "crates/fsevent" }
|
||||
fuzzy = { path = "crates/fuzzy" }
|
||||
git = { path = "crates/git" }
|
||||
git_ui = { path = "crates/git_ui" }
|
||||
git_hosting_providers = { path = "crates/git_hosting_providers" }
|
||||
go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
@@ -334,8 +328,7 @@ workspace = { path = "crates/workspace" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
|
||||
livekit = { path = "crates/livekit", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots" ], default-features = false }
|
||||
zeta = { path = "crates/zeta" }
|
||||
|
||||
#
|
||||
# External crates
|
||||
@@ -369,7 +362,6 @@ cargo_metadata = "0.19"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = "0.11.6"
|
||||
cocoa = "0.26"
|
||||
cocoa-foundation = "0.2.0"
|
||||
convert_case = "0.6.0"
|
||||
@@ -407,7 +399,7 @@ jupyter-websocket-client = { version = "0.8.0" }
|
||||
libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
# livekit = { git = "https://github.com/zed-industries/rust-sdks", rev="799f10133d93ba2a88642cd480d01ec4da53408c", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
|
||||
livekit = { git = "https://github.com/zed-industries/rust-sdks", rev="799f10133d93ba2a88642cd480d01ec4da53408c", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
|
||||
4
assets/icons/eraser.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eraser">
|
||||
<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/>
|
||||
<path d="M22 21H7"/><path d="m5 11 9 9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 365 B |
1
assets/icons/file_diff.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-diff"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M9 10h6"/><path d="M12 13V7"/><path d="M9 17h6"/></svg>
|
||||
|
After Width: | Height: | Size: 348 B |
1
assets/icons/git_branch.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-branch"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
|
||||
|
After Width: | Height: | Size: 348 B |
12
assets/icons/info.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2131_1193)">
|
||||
<circle cx="7" cy="7" r="6" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="4.5" r="1" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2131_1193">
|
||||
<rect width="14" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 479 B |
1
assets/icons/panel_left.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>
|
||||
|
After Width: | Height: | Size: 289 B |
1
assets/icons/panel_right.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>
|
||||
|
After Width: | Height: | Size: 291 B |
1
assets/icons/square_dot.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dot"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="12" cy="12" r="1"/></svg>
|
||||
|
After Width: | Height: | Size: 301 B |
1
assets/icons/square_minus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-minus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/></svg>
|
||||
|
After Width: | Height: | Size: 291 B |
1
assets/icons/square_plus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-plus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>
|
||||
|
After Width: | Height: | Size: 309 B |
1
assets/icons/thumbs_down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-down"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>
|
||||
|
After Width: | Height: | Size: 405 B |
1
assets/icons/thumbs_up.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>
|
||||
|
After Width: | Height: | Size: 404 B |
@@ -468,13 +468,21 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"context": "Editor && inline_completion",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"cmd-v": "editor::Paste",
|
||||
"cmd-z": "editor::Undo",
|
||||
"cmd-shift-z": "editor::Redo",
|
||||
"ctrl-shift-z": "zeta::RateCompletions",
|
||||
"up": "editor::MoveUp",
|
||||
"ctrl-up": "editor::MoveToStartOfParagraph",
|
||||
"pageup": "editor::MovePageUp",
|
||||
@@ -229,7 +230,7 @@
|
||||
"context": "MessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "assistant2::Chat"
|
||||
"enter": "assistant2::Chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -540,12 +541,18 @@
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"enter": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"context": "Editor && inline_completion",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
@@ -788,5 +795,24 @@
|
||||
"ctrl-k left": "pane::SplitLeft",
|
||||
"ctrl-k right": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RateCompletionModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "zeta::ThumbsUp",
|
||||
"shift-down": "zeta::NextEdit",
|
||||
"shift-up": "zeta::PreviousEdit",
|
||||
"right": "zeta::PreviewCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RateCompletionModal > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "zeta::FocusCompletions",
|
||||
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
||||
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -150,9 +150,6 @@
|
||||
// Whether to display inline and alongside documentation for items in the
|
||||
// completions menu
|
||||
"show_completion_documentation": true,
|
||||
// The debounce delay before re-querying the language server for completion
|
||||
// documentation when not included in original completion list.
|
||||
"completion_documentation_secondary_query_debounce": 300,
|
||||
// Show method signatures in the editor, when inside parentheses.
|
||||
"auto_signature_help": false,
|
||||
/// Whether to show the signature help after completion or a bracket pair inserted.
|
||||
@@ -564,9 +561,11 @@
|
||||
// What to do after closing the current tab.
|
||||
//
|
||||
// 1. Activate the tab that was open previously (default)
|
||||
// "History"
|
||||
// 2. Activate the neighbour tab (prefers the right one, if present)
|
||||
// "Neighbour"
|
||||
// "history"
|
||||
// 2. Activate the right neighbour tab if present
|
||||
// "neighbour"
|
||||
// 3. Activate the left neighbour tab if present
|
||||
// "left_neighbour"
|
||||
"activate_on_close": "history",
|
||||
/// Which files containing diagnostic errors/warnings to mark in the tabs.
|
||||
/// Diagnostics are only shown when file icons are also active.
|
||||
@@ -685,6 +684,7 @@
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/.jj",
|
||||
"**/CVS",
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
|
||||
@@ -5113,9 +5113,11 @@ fn make_lsp_adapter_delegate(
|
||||
return Ok(None::<Arc<dyn LspAdapterDelegate>>);
|
||||
};
|
||||
let http_client = project.client().http_client().clone();
|
||||
project.lsp_store().update(cx, |lsp_store, cx| {
|
||||
project.lsp_store().update(cx, |_, cx| {
|
||||
Ok(Some(LocalLspAdapterDelegate::new(
|
||||
lsp_store,
|
||||
project.languages().clone(),
|
||||
project.environment(),
|
||||
cx.weak_model(),
|
||||
&worktree,
|
||||
http_client,
|
||||
project.fs().clone(),
|
||||
|
||||
@@ -17,7 +17,7 @@ use futures::{
|
||||
channel::mpsc,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use parking_lot::Mutex;
|
||||
@@ -35,7 +35,7 @@ use std::{
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
|
||||
use ui::{Context as _, IconName, WindowContext};
|
||||
use ui::{IconName, WindowContext};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
|
||||
@@ -13,30 +13,51 @@ path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
async-watch.workspace = true
|
||||
client.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
context_server.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
handlebars.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
ordered-float.workspace = true
|
||||
paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
similar.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
@@ -45,3 +66,8 @@ unindent.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rand.workspace = true
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -15,6 +15,7 @@ use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
|
||||
use crate::ui::ContextPill;
|
||||
|
||||
pub struct ActiveThread {
|
||||
workspace: WeakView<Workspace>,
|
||||
@@ -202,6 +203,8 @@ impl ActiveThread {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let context = self.thread.read(cx).context_for_message(message_id);
|
||||
|
||||
let (role_icon, role_name) = match message.role {
|
||||
Role::User => (IconName::Person, "You"),
|
||||
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
|
||||
@@ -229,7 +232,16 @@ impl ActiveThread {
|
||||
.child(Label::new(role_name).size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())),
|
||||
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone()))
|
||||
.when_some(context, |parent, context| {
|
||||
parent.child(
|
||||
h_flex().flex_wrap().gap_2().p_1p5().children(
|
||||
context
|
||||
.iter()
|
||||
.map(|context| ContextPill::new(context.clone())),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
mod active_thread;
|
||||
mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod context;
|
||||
mod context_picker;
|
||||
mod inline_assistant;
|
||||
mod message_editor;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
mod thread;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod ui;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
use gpui::{actions, AppContext};
|
||||
use prompts::PromptLoadingParams;
|
||||
use settings::Settings as _;
|
||||
use util::ResultExt;
|
||||
|
||||
pub use crate::assistant_panel::AssistantPanel;
|
||||
|
||||
@@ -19,15 +33,37 @@ actions!(
|
||||
NewThread,
|
||||
ToggleModelSelector,
|
||||
OpenHistory,
|
||||
Chat
|
||||
Chat,
|
||||
ToggleInlineAssist,
|
||||
CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist
|
||||
]
|
||||
);
|
||||
|
||||
const NAMESPACE: &str = "assistant2";
|
||||
|
||||
/// Initializes the `assistant2` crate.
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mut AppContext) {
|
||||
AssistantSettings::register(cx);
|
||||
assistant_panel::init(cx);
|
||||
|
||||
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
|
||||
fs: fs.clone(),
|
||||
repo_path: stdout_is_a_pty
|
||||
.then(|| std::env::current_dir().log_err())
|
||||
.flatten(),
|
||||
cx,
|
||||
}))
|
||||
.log_err()
|
||||
.map(Arc::new)
|
||||
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
|
||||
inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
feature_gate_assistant2_actions(cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,8 @@ use gpui::{
|
||||
WindowContext,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use time::UtcOffset;
|
||||
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
|
||||
use ui::{prelude::*, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -21,7 +19,7 @@ use crate::message_editor::MessageEditor;
|
||||
use crate::thread::{ThreadError, ThreadId};
|
||||
use crate::thread_history::{PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
|
||||
use crate::{NewThread, OpenHistory, ToggleFocus};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
@@ -225,7 +223,6 @@ impl AssistantPanel {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap(DynamicSpacing::Base08.rems(cx))
|
||||
.child(self.render_language_model_selector(cx))
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
IconButton::new("new-thread", IconName::Plus)
|
||||
@@ -280,57 +277,6 @@ impl AssistantPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
|
||||
LanguageModelSelector::new(
|
||||
|model, _cx| {
|
||||
println!("Selected {:?}", model.name());
|
||||
},
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match (active_provider, active_model) {
|
||||
(Some(provider), Some(model)) => h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(
|
||||
model.icon().unwrap_or_else(|| provider.icon()),
|
||||
)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new(model.name().0)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
_ => Label::new("No model selected")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
if self.thread.read(cx).is_empty() {
|
||||
return self.render_thread_empty_state(cx).into_any_element();
|
||||
@@ -358,46 +304,6 @@ impl AssistantPanel {
|
||||
.mb_4(),
|
||||
),
|
||||
)
|
||||
.child(v_flex())
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.child(Label::new("Context Examples:").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_center()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.p_0p5()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Icon::new(IconName::Terminal)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(Label::new("Terminal").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.p_0p5()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Icon::new(IconName::Folder)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(Label::new("/src/components").size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.when(!recent_threads.is_empty(), |parent| {
|
||||
parent
|
||||
.child(
|
||||
|
||||
485
crates/assistant2/src/assistant_settings.rs
Normal file
@@ -0,0 +1,485 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use gpui::Pixels;
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
#[default]
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContentV1 {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev { default_model: Option<CloudModel> },
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
api_url: Option<String>,
|
||||
available_models: Option<Vec<OpenAiModel>>,
|
||||
},
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic {
|
||||
default_model: Option<AnthropicModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
#[serde(rename = "ollama")]
|
||||
Ollama {
|
||||
default_model: Option<OllamaModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AssistantSettings {
|
||||
pub enabled: bool,
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AssistantSettingsContent {
|
||||
Versioned(VersionedAssistantSettingsContent),
|
||||
Legacy(LegacyAssistantSettingsContent),
|
||||
}
|
||||
|
||||
impl JsonSchema for AssistantSettingsContent {
|
||||
fn schema_name() -> String {
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
VersionedAssistantSettingsContent::is_referenceable()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::Versioned(VersionedAssistantSettingsContent::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantSettingsContent {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(_) => true,
|
||||
VersionedAssistantSettingsContent::V2(_) => false,
|
||||
},
|
||||
AssistantSettingsContent::Legacy(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_width,
|
||||
default_model: settings
|
||||
.provider
|
||||
.clone()
|
||||
.and_then(|provider| match provider {
|
||||
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "zed.dev".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "openai".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "anthropic".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "ollama".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "openai".to_string(),
|
||||
model: settings
|
||||
.default_open_ai_model
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.id()
|
||||
.to_string(),
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
|
||||
let model = language_model.id().0.to_string();
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(tag = "version")]
|
||||
pub enum VersionedAssistantSettingsContent {
|
||||
#[serde(rename = "1")]
|
||||
V1(AssistantSettingsContentV1),
|
||||
#[serde(rename = "2")]
|
||||
V2(AssistantSettingsContentV2),
|
||||
}
|
||||
|
||||
impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
default_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV2 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The default model to use when creating new chats.
|
||||
default_model: Option<LanguageModelSelection>,
|
||||
/// Additional models with which to generate alternatives when performing inline assists.
|
||||
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
||||
/// Enable experimental live diffs in the assistant panel.
|
||||
///
|
||||
/// Default: false
|
||||
enable_experimental_live_diffs: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct LanguageModelSelection {
|
||||
#[schemars(schema_with = "providers_schema")]
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"google".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
impl Default for LanguageModelSelection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: "openai".to_string(),
|
||||
model: "gpt-4".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV1 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The provider of the assistant service.
|
||||
///
|
||||
/// This can be "openai", "anthropic", "ollama", "zed.dev"
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AssistantProviderContentV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct LegacyAssistantSettingsContent {
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
pub default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
pub default_height: Option<f32>,
|
||||
/// The default OpenAI model to use when creating new chats.
|
||||
///
|
||||
/// Default: gpt-4-1106-preview
|
||||
pub default_open_ai_model: Option<OpenAiModel>,
|
||||
/// OpenAI API base URL to use when creating new chats.
|
||||
///
|
||||
/// Default: https://api.openai.com/v1
|
||||
pub openai_api_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut settings = AssistantSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.is_version_outdated() {
|
||||
settings.using_outdated_settings_version = true;
|
||||
}
|
||||
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
merge(
|
||||
&mut settings.default_width,
|
||||
value.default_width.map(Into::into),
|
||||
);
|
||||
merge(
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model);
|
||||
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||
merge(
|
||||
&mut settings.enable_experimental_live_diffs,
|
||||
value.enable_experimental_live_diffs,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::Fs;
|
||||
use gpui::{ReadGlobal, TestAppContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let test_settings = settings::SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
AssistantSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: "claude-3-5-sonnet".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AssistantSettingsContent::Versioned(
|
||||
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
}),
|
||||
)
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
|
||||
assert!(raw_settings_value.contains(r#""version": "2""#));
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AssistantSettingsTest {
|
||||
assistant: AssistantSettingsContent,
|
||||
}
|
||||
|
||||
let assistant_settings: AssistantSettingsTest =
|
||||
serde_json_lenient::from_str(&raw_settings_value).unwrap();
|
||||
|
||||
assert!(!assistant_settings.assistant.is_version_outdated());
|
||||
}
|
||||
}
|
||||
26
crates/assistant2/src/context.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use gpui::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::post_inc;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct ContextId(pub(crate) usize);
|
||||
|
||||
impl ContextId {
|
||||
pub fn post_inc(&mut self) -> Self {
|
||||
Self(post_inc(&mut self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Some context attached to a message in a thread.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub id: ContextId,
|
||||
pub name: SharedString,
|
||||
pub kind: ContextKind,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
}
|
||||
3851
crates/assistant2/src/inline_assistant.rs
Normal file
@@ -1,38 +1,56 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use picker::Picker;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
|
||||
PopoverMenuHandle,
|
||||
PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
|
||||
use crate::context::{Context, ContextId, ContextKind};
|
||||
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::Chat;
|
||||
use crate::ui::ContextPill;
|
||||
use crate::{Chat, ToggleModelSelector};
|
||||
|
||||
pub struct MessageEditor {
|
||||
thread: Model<Thread>,
|
||||
editor: View<Editor>,
|
||||
context: Vec<Context>,
|
||||
next_context_id: ContextId,
|
||||
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
|
||||
use_tools: bool,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
let mut this = Self {
|
||||
thread,
|
||||
editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_placeholder_text("Ask anything…", cx);
|
||||
editor.set_placeholder_text("Ask anything or type @ to add context", cx);
|
||||
|
||||
editor
|
||||
}),
|
||||
context: Vec::new(),
|
||||
next_context_id: ContextId(0),
|
||||
context_picker_handle: PopoverMenuHandle::default(),
|
||||
use_tools: false,
|
||||
}
|
||||
};
|
||||
|
||||
this.context.push(Context {
|
||||
id: this.next_context_id.post_inc(),
|
||||
name: "shape.rs".into(),
|
||||
kind: ContextKind::File,
|
||||
text: "```rs\npub enum Shape {\n Circle,\n Square,\n Triangle,\n}".into(),
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
|
||||
@@ -61,9 +79,10 @@ impl MessageEditor {
|
||||
editor.clear(cx);
|
||||
text
|
||||
});
|
||||
let context = self.context.drain(..).collect::<Vec<_>>();
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(user_message, cx);
|
||||
thread.insert_user_message(user_message, context, cx);
|
||||
let mut request = thread.to_completion_request(request_kind, cx);
|
||||
|
||||
if self.use_tools {
|
||||
@@ -84,6 +103,57 @@ impl MessageEditor {
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
|
||||
LanguageModelSelector::new(
|
||||
|model, _cx| {
|
||||
println!("Selected {:?}", model.name());
|
||||
},
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match (active_provider, active_model) {
|
||||
(Some(provider), Some(model)) => h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(
|
||||
model.icon().unwrap_or_else(|| provider.icon()),
|
||||
)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new(model.name().0)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
_ => Label::new("No model selected")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for MessageEditor {
|
||||
@@ -106,12 +176,36 @@ impl Render for MessageEditor {
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex().gap_2().child(ContextPicker::new(
|
||||
cx.view().downgrade(),
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small),
|
||||
)),
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_2()
|
||||
.child(ContextPicker::new(
|
||||
cx.view().downgrade(),
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small),
|
||||
))
|
||||
.children(self.context.iter().map(|context| {
|
||||
ContextPill::new(context.clone()).on_remove({
|
||||
let context = context.clone();
|
||||
Rc::new(cx.listener(move |this, _event, cx| {
|
||||
this.context.retain(|other| other.id != context.id);
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
}))
|
||||
.when(!self.context.is_empty(), |parent| {
|
||||
parent.child(
|
||||
IconButton::new("remove-all-context", IconName::Eraser)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.context.clear();
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
@@ -152,13 +246,12 @@ impl Render for MessageEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled))
|
||||
.child(Label::new("or"))
|
||||
.child(self.render_language_model_selector(cx))
|
||||
.child(
|
||||
ButtonLike::new("chat")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Chat"))
|
||||
.child(Label::new("Submit"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
|
||||
291
crates/assistant2/src/prompts.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
use anyhow::Result;
|
||||
use assets::Assets;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::AssetSource;
|
||||
use handlebars::{Handlebars, RenderError};
|
||||
use language::{BufferSnapshot, LanguageName, Point};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use text::LineEnding;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContentPromptDiagnosticContext {
|
||||
pub line_number: usize,
|
||||
pub error_message: String,
|
||||
pub code_content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContentPromptContext {
|
||||
pub content_type: String,
|
||||
pub language_name: Option<String>,
|
||||
pub is_insert: bool,
|
||||
pub is_truncated: bool,
|
||||
pub document_content: String,
|
||||
pub user_prompt: String,
|
||||
pub rewrite_section: Option<String>,
|
||||
pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TerminalAssistantPromptContext {
|
||||
pub os: String,
|
||||
pub arch: String,
|
||||
pub shell: Option<String>,
|
||||
pub working_directory: Option<String>,
|
||||
pub latest_output: Vec<String>,
|
||||
pub user_prompt: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProjectSlashCommandPromptContext {
|
||||
pub context_buffer: String,
|
||||
}
|
||||
|
||||
pub struct PromptLoadingParams<'a> {
|
||||
pub fs: Arc<dyn Fs>,
|
||||
pub repo_path: Option<PathBuf>,
|
||||
pub cx: &'a gpui::AppContext,
|
||||
}
|
||||
|
||||
pub struct PromptBuilder {
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
}
|
||||
|
||||
impl PromptBuilder {
|
||||
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
|
||||
let mut handlebars = Handlebars::new();
|
||||
Self::register_built_in_templates(&mut handlebars)?;
|
||||
|
||||
let handlebars = Arc::new(Mutex::new(handlebars));
|
||||
|
||||
if let Some(params) = loading_params {
|
||||
Self::watch_fs_for_template_overrides(params, handlebars.clone());
|
||||
}
|
||||
|
||||
Ok(Self { handlebars })
|
||||
}
|
||||
|
||||
/// Watches the filesystem for changes to prompt template overrides.
|
||||
///
|
||||
/// This function sets up a file watcher on the prompt templates directory. It performs
|
||||
/// an initial scan of the directory and registers any existing template overrides.
|
||||
/// Then it continuously monitors for changes, reloading templates as they are
|
||||
/// modified or added.
|
||||
///
|
||||
/// If the templates directory doesn't exist initially, it waits for it to be created.
|
||||
/// If the directory is removed, it restores the built-in templates and waits for the
|
||||
/// directory to be recreated.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
|
||||
/// and application context.
|
||||
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
|
||||
fn watch_fs_for_template_overrides(
|
||||
params: PromptLoadingParams,
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
) {
|
||||
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
|
||||
params.cx.background_executor()
|
||||
.spawn(async move {
|
||||
let Some(parent_dir) = templates_dir.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut found_dir_once = false;
|
||||
loop {
|
||||
// Check if the templates directory exists and handle its status
|
||||
// If it exists, log its presence and check if it's a symlink
|
||||
// If it doesn't exist:
|
||||
// - Log that we're using built-in prompts
|
||||
// - Check if it's a broken symlink and log if so
|
||||
// - Set up a watcher to detect when it's created
|
||||
// After the first check, set the `found_dir_once` flag
|
||||
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
|
||||
let dir_status = params.fs.is_dir(&templates_dir).await;
|
||||
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
|
||||
if dir_status {
|
||||
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
} else {
|
||||
if !found_dir_once {
|
||||
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
|
||||
}
|
||||
}
|
||||
|
||||
if params.fs.is_dir(parent_dir).await {
|
||||
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
while let Some(changed_paths) = changes.next().await {
|
||||
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
|
||||
if let Ok(target) = params.fs.read_link(&templates_dir).await {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
found_dir_once = true;
|
||||
|
||||
// Initial scan of the prompt overrides directory
|
||||
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
|
||||
while let Some(Ok(file_path)) = entries.next().await {
|
||||
if file_path.to_string_lossy().ends_with(".hbs") {
|
||||
if let Ok(content) = params.fs.load(&file_path).await {
|
||||
let file_name = file_path.file_stem().unwrap().to_string_lossy();
|
||||
log::debug!("Registering prompt template override: {}", file_name);
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch both the parent directory and the template overrides directory:
|
||||
// - Monitor the parent directory to detect if the template overrides directory is deleted.
|
||||
// - Monitor the template overrides directory to re-register templates when they change.
|
||||
// Combine both watch streams into a single stream.
|
||||
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
|
||||
let mut combined_changes = futures::stream::select(changes, parent_changes);
|
||||
|
||||
while let Some(changed_paths) = combined_changes.next().await {
|
||||
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||
if !params.fs.is_dir(&templates_dir).await {
|
||||
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
|
||||
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
|
||||
break;
|
||||
}
|
||||
}
|
||||
for event in changed_paths {
|
||||
if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") {
|
||||
log::info!("Reloading prompt template override: {}", event.path.display());
|
||||
if let Some(content) = params.fs.load(&event.path).await.log_err() {
|
||||
let file_name = event.path.file_stem().unwrap().to_string_lossy();
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(watcher);
|
||||
drop(parent_watcher);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
|
||||
for path in Assets.list("prompts")? {
|
||||
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
|
||||
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
|
||||
log::debug!("Registering built-in prompt template: {}", id);
|
||||
let prompt = String::from_utf8_lossy(prompt.as_ref());
|
||||
handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_inline_transformation_prompt(
|
||||
&self,
|
||||
user_prompt: String,
|
||||
language_name: Option<&LanguageName>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
) -> Result<String, RenderError> {
|
||||
let content_type = match language_name.as_ref().map(|l| l.0.as_ref()) {
|
||||
None | Some("Markdown" | "Plain Text") => "text",
|
||||
Some(_) => "code",
|
||||
};
|
||||
|
||||
const MAX_CTX: usize = 50000;
|
||||
let is_insert = range.is_empty();
|
||||
let mut is_truncated = false;
|
||||
|
||||
let before_range = 0..range.start;
|
||||
let truncated_before = if before_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right);
|
||||
start..range.start
|
||||
} else {
|
||||
before_range
|
||||
};
|
||||
|
||||
let after_range = range.end..buffer.len();
|
||||
let truncated_after = if after_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left);
|
||||
range.end..end
|
||||
} else {
|
||||
after_range
|
||||
};
|
||||
|
||||
let mut document_content = String::new();
|
||||
for chunk in buffer.text_for_range(truncated_before) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
if is_insert {
|
||||
document_content.push_str("<insert_here></insert_here>");
|
||||
} else {
|
||||
document_content.push_str("<rewrite_this>\n");
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
document_content.push_str("\n</rewrite_this>");
|
||||
}
|
||||
for chunk in buffer.text_for_range(truncated_after) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
|
||||
let rewrite_section = if !is_insert {
|
||||
let mut section = String::new();
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
section.push_str(chunk);
|
||||
}
|
||||
Some(section)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
|
||||
let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
|
||||
.map(|entry| {
|
||||
let start = entry.range.start;
|
||||
ContentPromptDiagnosticContext {
|
||||
line_number: (start.row + 1) as usize,
|
||||
error_message: entry.diagnostic.message.clone(),
|
||||
code_content: buffer.text_for_range(entry.range.clone()).collect(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let context = ContentPromptContext {
|
||||
content_type: content_type.to_string(),
|
||||
language_name: language_name.map(|s| s.to_string()),
|
||||
is_insert,
|
||||
is_truncated,
|
||||
document_content,
|
||||
user_prompt,
|
||||
rewrite_section,
|
||||
diagnostic_errors,
|
||||
};
|
||||
self.handlebars.lock().render("content_prompt", &context)
|
||||
}
|
||||
}
|
||||
1102
crates/assistant2/src/streaming_diff.rs
Normal file
@@ -17,6 +17,8 @@ use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::{Context, ContextKind};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
Chat,
|
||||
@@ -62,6 +64,7 @@ pub struct Thread {
|
||||
pending_summary: Task<Option<()>>,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
context_by_message: HashMap<MessageId, Vec<Context>>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
@@ -79,6 +82,7 @@ impl Thread {
|
||||
pending_summary: Task::ready(None),
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
context_by_message: HashMap::default(),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
tools,
|
||||
@@ -125,12 +129,22 @@ impl Thread {
|
||||
&self.tools
|
||||
}
|
||||
|
||||
pub fn context_for_message(&self, id: MessageId) -> Option<&Vec<Context>> {
|
||||
self.context_by_message.get(&id)
|
||||
}
|
||||
|
||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
|
||||
pub fn insert_user_message(&mut self, text: impl Into<String>, cx: &mut ModelContext<Self>) {
|
||||
self.insert_message(Role::User, text, cx)
|
||||
pub fn insert_user_message(
|
||||
&mut self,
|
||||
text: impl Into<String>,
|
||||
context: Vec<Context>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let message_id = self.insert_message(Role::User, text, cx);
|
||||
self.context_by_message.insert(message_id, context);
|
||||
}
|
||||
|
||||
pub fn insert_message(
|
||||
@@ -138,7 +152,7 @@ impl Thread {
|
||||
role: Role,
|
||||
text: impl Into<String>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
) -> MessageId {
|
||||
let id = self.next_message_id.post_inc();
|
||||
self.messages.push(Message {
|
||||
id,
|
||||
@@ -147,6 +161,7 @@ impl Thread {
|
||||
});
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageAdded(id));
|
||||
id
|
||||
}
|
||||
|
||||
pub fn to_completion_request(
|
||||
@@ -176,6 +191,29 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(context) = self.context_for_message(message.id) {
|
||||
let mut file_context = String::new();
|
||||
|
||||
for context in context.iter() {
|
||||
match context.kind {
|
||||
ContextKind::File => {
|
||||
file_context.push_str(&context.text);
|
||||
file_context.push_str("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut context_text = String::new();
|
||||
if !file_context.is_empty() {
|
||||
context_text.push_str("The following files are available:\n");
|
||||
context_text.push_str(&file_context);
|
||||
}
|
||||
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(context_text))
|
||||
}
|
||||
|
||||
if !message.text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
|
||||
@@ -159,9 +159,9 @@ impl ThreadStore {
|
||||
self.threads.push(cx.new_model(|cx| {
|
||||
let mut thread = Thread::new(self.tools.clone(), cx);
|
||||
thread.set_summary("Introduction to quantum computing", cx);
|
||||
thread.insert_user_message("Hello! Can you help me understand quantum computing?", cx);
|
||||
thread.insert_user_message("Hello! Can you help me understand quantum computing?", Vec::new(), cx);
|
||||
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx);
|
||||
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", cx);
|
||||
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", Vec::new(), cx);
|
||||
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", cx);
|
||||
thread
|
||||
}));
|
||||
@@ -169,7 +169,7 @@ impl ThreadStore {
|
||||
self.threads.push(cx.new_model(|cx| {
|
||||
let mut thread = Thread::new(self.tools.clone(), cx);
|
||||
thread.set_summary("Rust web development and async programming", cx);
|
||||
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", cx);
|
||||
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", Vec::new(), cx);
|
||||
thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework:
|
||||
|
||||
```rust
|
||||
@@ -206,7 +206,7 @@ impl ThreadStore {
|
||||
```
|
||||
|
||||
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), cx);
|
||||
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", cx);
|
||||
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", Vec::new(), cx);
|
||||
thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview:
|
||||
|
||||
1. **Syntax**: Async functions are declared using the `async` keyword:
|
||||
|
||||
3
crates/assistant2/src/ui.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod context_pill;
|
||||
|
||||
pub use context_pill::*;
|
||||
49
crates/assistant2/src/ui/context_pill.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::ClickEvent;
|
||||
use ui::{prelude::*, IconButtonShape};
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ContextPill {
|
||||
context: Context,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||
}
|
||||
|
||||
impl ContextPill {
|
||||
pub fn new(context: Context) -> Self {
|
||||
Self {
|
||||
context,
|
||||
on_remove: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
|
||||
self.on_remove = Some(on_remove);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ContextPill {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
|
||||
.when_some(self.on_remove, |parent, on_remove| {
|
||||
parent.child(
|
||||
IconButton::new("remove", IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, cx| on_remove(event, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ test-support = [
|
||||
"collections/test-support",
|
||||
"gpui/test-support",
|
||||
"livekit_client/test-support",
|
||||
"livekit_client_macos/test-support",
|
||||
"project/test-support",
|
||||
"util/test-support"
|
||||
]
|
||||
|
||||
@@ -18,7 +18,8 @@ use std::time::Instant;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, InlineCompletionRating,
|
||||
InlineCompletionRatingEvent, ReplEvent, SettingEvent,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use worktree::{UpdatedEntriesSet, WorktreeId};
|
||||
@@ -355,6 +356,24 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_inline_completion_rating_event(
|
||||
self: &Arc<Self>,
|
||||
rating: InlineCompletionRating,
|
||||
input_events: Arc<str>,
|
||||
input_excerpt: Arc<str>,
|
||||
output_excerpt: Arc<str>,
|
||||
feedback: String,
|
||||
) {
|
||||
let event = Event::InlineCompletionRating(InlineCompletionRatingEvent {
|
||||
rating,
|
||||
input_events,
|
||||
input_excerpt,
|
||||
output_excerpt,
|
||||
feedback,
|
||||
});
|
||||
self.report_event(event);
|
||||
}
|
||||
|
||||
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
|
||||
self.report_event(Event::Assistant(event));
|
||||
}
|
||||
|
||||
@@ -19,11 +19,6 @@ LLM_DATABASE_URL = "postgres://postgres@localhost/zed_llm"
|
||||
LLM_DATABASE_MAX_CONNECTIONS = 5
|
||||
LLM_API_SECRET = "llm-secret"
|
||||
|
||||
# CLICKHOUSE_URL = ""
|
||||
# CLICKHOUSE_USER = "default"
|
||||
# CLICKHOUSE_PASSWORD = ""
|
||||
# CLICKHOUSE_DATABASE = "default"
|
||||
|
||||
# SLACK_PANICS_WEBHOOK = ""
|
||||
|
||||
# RUST_LOG=info
|
||||
|
||||
@@ -29,7 +29,6 @@ axum = { version = "0.6", features = ["json", "headers", "ws"] }
|
||||
axum-extra = { version = "0.4", features = ["erased-json"] }
|
||||
base64.workspace = true
|
||||
chrono.workspace = true
|
||||
clickhouse.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
dashmap.workspace = true
|
||||
@@ -77,12 +76,6 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
livekit_client_macos = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
livekit_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assistant = { workspace = true, features = ["test-support"] }
|
||||
assistant_tool.workspace = true
|
||||
|
||||
@@ -149,6 +149,21 @@ spec:
|
||||
secretKeyRef:
|
||||
name: google-ai
|
||||
key: api_key
|
||||
- name: PREDICTION_API_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: prediction
|
||||
key: api_url
|
||||
- name: PREDICTION_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: prediction
|
||||
key: api_key
|
||||
- name: PREDICTION_MODEL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: prediction
|
||||
key: model
|
||||
- name: BLOB_STORE_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -199,26 +214,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: blob-store
|
||||
key: bucket
|
||||
- name: CLICKHOUSE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: clickhouse
|
||||
key: url
|
||||
- name: CLICKHOUSE_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: clickhouse
|
||||
key: user
|
||||
- name: CLICKHOUSE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: clickhouse
|
||||
key: password
|
||||
- name: CLICKHOUSE_DATABASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: clickhouse
|
||||
key: database
|
||||
- name: SLACK_PANICS_WEBHOOK
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
||||
@@ -102,6 +102,9 @@ async fn update_billing_preferences(
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("user not found"))?;
|
||||
|
||||
let max_monthly_llm_usage_spending_in_cents =
|
||||
body.max_monthly_llm_usage_spending_in_cents.max(0);
|
||||
|
||||
let billing_preferences =
|
||||
if let Some(_billing_preferences) = app.db.get_billing_preferences(user.id).await? {
|
||||
app.db
|
||||
@@ -109,7 +112,7 @@ async fn update_billing_preferences(
|
||||
user.id,
|
||||
&UpdateBillingPreferencesParams {
|
||||
max_monthly_llm_usage_spending_in_cents: ActiveValue::set(
|
||||
body.max_monthly_llm_usage_spending_in_cents,
|
||||
max_monthly_llm_usage_spending_in_cents,
|
||||
),
|
||||
},
|
||||
)
|
||||
@@ -119,8 +122,7 @@ async fn update_billing_preferences(
|
||||
.create_billing_preferences(
|
||||
user.id,
|
||||
&crate::db::CreateBillingPreferencesParams {
|
||||
max_monthly_llm_usage_spending_in_cents: body
|
||||
.max_monthly_llm_usage_spending_in_cents,
|
||||
max_monthly_llm_usage_spending_in_cents,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
@@ -128,7 +130,7 @@ async fn update_billing_preferences(
|
||||
|
||||
SnowflakeRow::new(
|
||||
"Spend Limit Updated",
|
||||
Some(user.metrics_id),
|
||||
user.metrics_id,
|
||||
user.admin,
|
||||
None,
|
||||
json!({
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
use serde::Serialize;
|
||||
|
||||
/// Writes the given rows to the specified Clickhouse table.
|
||||
pub async fn write_to_table<T: clickhouse::Row + Serialize + std::fmt::Debug>(
|
||||
table: &str,
|
||||
rows: &[T],
|
||||
clickhouse_client: &clickhouse::Client,
|
||||
) -> anyhow::Result<()> {
|
||||
if rows.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut insert = clickhouse_client.insert(table)?;
|
||||
|
||||
for event in rows {
|
||||
insert.write(event).await?;
|
||||
}
|
||||
|
||||
insert.end().await?;
|
||||
|
||||
let event_count = rows.len();
|
||||
log::info!(
|
||||
"wrote {event_count} {event_specifier} to '{table}'",
|
||||
event_specifier = if event_count == 1 { "event" } else { "events" }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod api;
|
||||
pub mod auth;
|
||||
mod cents;
|
||||
pub mod clickhouse;
|
||||
pub mod db;
|
||||
pub mod env;
|
||||
pub mod executor;
|
||||
@@ -151,10 +150,6 @@ pub struct Config {
|
||||
pub seed_path: Option<PathBuf>,
|
||||
pub database_max_connections: u32,
|
||||
pub api_token: String,
|
||||
pub clickhouse_url: Option<String>,
|
||||
pub clickhouse_user: Option<String>,
|
||||
pub clickhouse_password: Option<String>,
|
||||
pub clickhouse_database: Option<String>,
|
||||
pub invite_link_prefix: String,
|
||||
pub livekit_server: Option<String>,
|
||||
pub livekit_key: Option<String>,
|
||||
@@ -180,6 +175,9 @@ pub struct Config {
|
||||
pub anthropic_api_key: Option<Arc<str>>,
|
||||
pub anthropic_staff_api_key: Option<Arc<str>>,
|
||||
pub llm_closed_beta_model_name: Option<Arc<str>>,
|
||||
pub prediction_api_url: Option<Arc<str>>,
|
||||
pub prediction_api_key: Option<Arc<str>>,
|
||||
pub prediction_model: Option<Arc<str>>,
|
||||
pub zed_client_checksum_seed: Option<String>,
|
||||
pub slack_panics_webhook: Option<String>,
|
||||
pub auto_join_channel_id: Option<ChannelId>,
|
||||
@@ -230,10 +228,9 @@ impl Config {
|
||||
anthropic_api_key: None,
|
||||
anthropic_staff_api_key: None,
|
||||
llm_closed_beta_model_name: None,
|
||||
clickhouse_url: None,
|
||||
clickhouse_user: None,
|
||||
clickhouse_password: None,
|
||||
clickhouse_database: None,
|
||||
prediction_api_url: None,
|
||||
prediction_api_key: None,
|
||||
prediction_model: None,
|
||||
zed_client_checksum_seed: None,
|
||||
slack_panics_webhook: None,
|
||||
auto_join_channel_id: None,
|
||||
@@ -283,7 +280,6 @@ pub struct AppState {
|
||||
pub stripe_billing: Option<Arc<StripeBilling>>,
|
||||
pub rate_limiter: Arc<RateLimiter>,
|
||||
pub executor: Executor,
|
||||
pub clickhouse_client: Option<::clickhouse::Client>,
|
||||
pub kinesis_client: Option<::aws_sdk_kinesis::Client>,
|
||||
pub config: Config,
|
||||
}
|
||||
@@ -337,10 +333,6 @@ impl AppState {
|
||||
stripe_client,
|
||||
rate_limiter: Arc::new(RateLimiter::new(db)),
|
||||
executor,
|
||||
clickhouse_client: config
|
||||
.clickhouse_url
|
||||
.as_ref()
|
||||
.and_then(|_| build_clickhouse_client(&config).log_err()),
|
||||
kinesis_client: if config.kinesis_access_key.is_some() {
|
||||
build_kinesis_client(&config).await.log_err()
|
||||
} else {
|
||||
@@ -423,31 +415,3 @@ async fn build_kinesis_client(config: &Config) -> anyhow::Result<aws_sdk_kinesis
|
||||
|
||||
Ok(aws_sdk_kinesis::Client::new(&kinesis_config))
|
||||
}
|
||||
|
||||
fn build_clickhouse_client(config: &Config) -> anyhow::Result<::clickhouse::Client> {
|
||||
Ok(::clickhouse::Client::default()
|
||||
.with_url(
|
||||
config
|
||||
.clickhouse_url
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("missing clickhouse_url"))?,
|
||||
)
|
||||
.with_user(
|
||||
config
|
||||
.clickhouse_user
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("missing clickhouse_user"))?,
|
||||
)
|
||||
.with_password(
|
||||
config
|
||||
.clickhouse_password
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("missing clickhouse_password"))?,
|
||||
)
|
||||
.with_database(
|
||||
config
|
||||
.clickhouse_database
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("missing clickhouse_database"))?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
mod authorization;
|
||||
pub mod db;
|
||||
mod telemetry;
|
||||
mod token;
|
||||
|
||||
use crate::api::events::SnowflakeRow;
|
||||
use crate::api::CloudflareIpCountryHeader;
|
||||
use crate::build_kinesis_client;
|
||||
use crate::{
|
||||
build_clickhouse_client, db::UserId, executor::Executor, Cents, Config, Error, Result,
|
||||
};
|
||||
use crate::{db::UserId, executor::Executor, Cents, Config, Error, Result};
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use authorization::authorize_access_to_language_model;
|
||||
use axum::routing::get;
|
||||
@@ -29,7 +26,10 @@ use reqwest_client::ReqwestClient;
|
||||
use rpc::{
|
||||
proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
};
|
||||
use rpc::{ListModelsResponse, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME};
|
||||
use rpc::{
|
||||
ListModelsResponse, PredictEditsParams, PredictEditsResponse,
|
||||
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
pin::Pin,
|
||||
@@ -37,7 +37,6 @@ use std::{
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use strum::IntoEnumIterator;
|
||||
use telemetry::{report_llm_rate_limit, report_llm_usage, LlmRateLimitEventRow, LlmUsageEventRow};
|
||||
use tokio::sync::RwLock;
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -49,7 +48,6 @@ pub struct LlmState {
|
||||
pub db: Arc<LlmDatabase>,
|
||||
pub http_client: ReqwestClient,
|
||||
pub kinesis_client: Option<aws_sdk_kinesis::Client>,
|
||||
pub clickhouse_client: Option<clickhouse::Client>,
|
||||
active_user_count_by_model:
|
||||
RwLock<HashMap<(LanguageModelProvider, String), (DateTime<Utc>, ActiveUserCount)>>,
|
||||
}
|
||||
@@ -86,10 +84,6 @@ impl LlmState {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
clickhouse_client: config
|
||||
.clickhouse_url
|
||||
.as_ref()
|
||||
.and_then(|_| build_clickhouse_client(&config).log_err()),
|
||||
active_user_count_by_model: RwLock::new(HashMap::default()),
|
||||
config,
|
||||
};
|
||||
@@ -126,6 +120,7 @@ pub fn routes() -> Router<(), Body> {
|
||||
Router::new()
|
||||
.route("/models", get(list_models))
|
||||
.route("/completion", post(perform_completion))
|
||||
.route("/predict_edits", post(predict_edits))
|
||||
.layer(middleware::from_fn(validate_api_token))
|
||||
}
|
||||
|
||||
@@ -439,6 +434,59 @@ fn normalize_model_name(known_models: Vec<String>, name: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
async fn predict_edits(
|
||||
Extension(state): Extension<Arc<LlmState>>,
|
||||
Extension(claims): Extension<LlmTokenClaims>,
|
||||
_country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
|
||||
Json(params): Json<PredictEditsParams>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
if !claims.is_staff {
|
||||
return Err(anyhow!("not found"))?;
|
||||
}
|
||||
|
||||
let api_url = state
|
||||
.config
|
||||
.prediction_api_url
|
||||
.as_ref()
|
||||
.context("no PREDICTION_API_URL configured on the server")?;
|
||||
let api_key = state
|
||||
.config
|
||||
.prediction_api_key
|
||||
.as_ref()
|
||||
.context("no PREDICTION_API_KEY configured on the server")?;
|
||||
let model = state
|
||||
.config
|
||||
.prediction_model
|
||||
.as_ref()
|
||||
.context("no PREDICTION_MODEL configured on the server")?;
|
||||
let prompt = include_str!("./llm/prediction_prompt.md")
|
||||
.replace("<events>", ¶ms.input_events)
|
||||
.replace("<excerpt>", ¶ms.input_excerpt);
|
||||
let mut response = open_ai::complete_text(
|
||||
&state.http_client,
|
||||
api_url,
|
||||
api_key,
|
||||
open_ai::CompletionRequest {
|
||||
model: model.to_string(),
|
||||
prompt: prompt.clone(),
|
||||
max_tokens: 1024,
|
||||
temperature: 0.,
|
||||
prediction: Some(open_ai::Prediction::Content {
|
||||
content: params.input_excerpt,
|
||||
}),
|
||||
rewrite_speculation: Some(true),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let choice = response
|
||||
.choices
|
||||
.pop()
|
||||
.context("no output from completion response")?;
|
||||
Ok(Json(PredictEditsResponse {
|
||||
output_excerpt: choice.text,
|
||||
}))
|
||||
}
|
||||
|
||||
/// The maximum monthly spending an individual user can reach on the free tier
|
||||
/// before they have to pay.
|
||||
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10);
|
||||
@@ -573,34 +621,6 @@ async fn check_usage_limit(
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
if let Some(client) = state.clickhouse_client.as_ref() {
|
||||
report_llm_rate_limit(
|
||||
client,
|
||||
LlmRateLimitEventRow {
|
||||
time: Utc::now().timestamp_millis(),
|
||||
user_id: claims.user_id as i32,
|
||||
is_staff: claims.is_staff,
|
||||
plan: match claims.plan {
|
||||
Plan::Free => "free".to_string(),
|
||||
Plan::ZedPro => "zed_pro".to_string(),
|
||||
},
|
||||
model: model.name.clone(),
|
||||
provider: provider.to_string(),
|
||||
usage_measure: resource.to_string(),
|
||||
requests_this_minute: usage.requests_this_minute as u64,
|
||||
tokens_this_minute: usage.tokens_this_minute as u64,
|
||||
tokens_this_day: usage.tokens_this_day as u64,
|
||||
users_in_recent_minutes: users_in_recent_minutes as u64,
|
||||
users_in_recent_days: users_in_recent_days as u64,
|
||||
max_requests_per_minute: per_user_max_requests_per_minute as u64,
|
||||
max_tokens_per_minute: per_user_max_tokens_per_minute as u64,
|
||||
max_tokens_per_day: per_user_max_tokens_per_day as u64,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
return Err(Error::http(
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
format!("Rate limit exceeded. Maximum {} reached.", resource),
|
||||
@@ -687,6 +707,8 @@ impl<S> Drop for TokenCountingStream<S> {
|
||||
);
|
||||
|
||||
let properties = json!({
|
||||
"has_llm_subscription": claims.has_llm_subscription,
|
||||
"max_monthly_spend_in_cents": claims.max_monthly_spend_in_cents,
|
||||
"plan": match claims.plan {
|
||||
Plan::Free => "free".to_string(),
|
||||
Plan::ZedPro => "zed_pro".to_string(),
|
||||
@@ -706,44 +728,6 @@ impl<S> Drop for TokenCountingStream<S> {
|
||||
.write(&state.kinesis_client, &state.config.kinesis_stream)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
if let Some(clickhouse_client) = state.clickhouse_client.as_ref() {
|
||||
report_llm_usage(
|
||||
clickhouse_client,
|
||||
LlmUsageEventRow {
|
||||
time: Utc::now().timestamp_millis(),
|
||||
user_id: claims.user_id as i32,
|
||||
is_staff: claims.is_staff,
|
||||
plan: match claims.plan {
|
||||
Plan::Free => "free".to_string(),
|
||||
Plan::ZedPro => "zed_pro".to_string(),
|
||||
},
|
||||
model,
|
||||
provider: provider.to_string(),
|
||||
input_token_count: tokens.input as u64,
|
||||
cache_creation_input_token_count: tokens.input_cache_creation as u64,
|
||||
cache_read_input_token_count: tokens.input_cache_read as u64,
|
||||
output_token_count: tokens.output as u64,
|
||||
requests_this_minute: usage.requests_this_minute as u64,
|
||||
tokens_this_minute: usage.tokens_this_minute as u64,
|
||||
tokens_this_day: usage.tokens_this_day as u64,
|
||||
input_tokens_this_month: usage.tokens_this_month.input as u64,
|
||||
cache_creation_input_tokens_this_month: usage
|
||||
.tokens_this_month
|
||||
.input_cache_creation
|
||||
as u64,
|
||||
cache_read_input_tokens_this_month: usage
|
||||
.tokens_this_month
|
||||
.input_cache_read
|
||||
as u64,
|
||||
output_tokens_this_month: usage.tokens_this_month.output as u64,
|
||||
spending_this_month: usage.spending_this_month.0 as u64,
|
||||
lifetime_spending: usage.lifetime_spending.0 as u64,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
12
crates/collab/src/llm/prediction_prompt.md
Normal file
@@ -0,0 +1,12 @@
|
||||
Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
|
||||
|
||||
### Instruction:
|
||||
You are a code completion assistant and your task is to analyze user edits and then rewrite an excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking into account the cursor location.
|
||||
|
||||
### Events:
|
||||
<events>
|
||||
|
||||
### Input:
|
||||
<excerpt>
|
||||
|
||||
### Response:
|
||||
@@ -1,65 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::clickhouse::write_to_table;
|
||||
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
pub struct LlmUsageEventRow {
|
||||
pub time: i64,
|
||||
pub user_id: i32,
|
||||
pub is_staff: bool,
|
||||
pub plan: String,
|
||||
pub model: String,
|
||||
pub provider: String,
|
||||
pub input_token_count: u64,
|
||||
pub cache_creation_input_token_count: u64,
|
||||
pub cache_read_input_token_count: u64,
|
||||
pub output_token_count: u64,
|
||||
pub requests_this_minute: u64,
|
||||
pub tokens_this_minute: u64,
|
||||
pub tokens_this_day: u64,
|
||||
pub input_tokens_this_month: u64,
|
||||
pub cache_creation_input_tokens_this_month: u64,
|
||||
pub cache_read_input_tokens_this_month: u64,
|
||||
pub output_tokens_this_month: u64,
|
||||
pub spending_this_month: u64,
|
||||
pub lifetime_spending: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
pub struct LlmRateLimitEventRow {
|
||||
pub time: i64,
|
||||
pub user_id: i32,
|
||||
pub is_staff: bool,
|
||||
pub plan: String,
|
||||
pub model: String,
|
||||
pub provider: String,
|
||||
pub usage_measure: String,
|
||||
pub requests_this_minute: u64,
|
||||
pub tokens_this_minute: u64,
|
||||
pub tokens_this_day: u64,
|
||||
pub users_in_recent_minutes: u64,
|
||||
pub users_in_recent_days: u64,
|
||||
pub max_requests_per_minute: u64,
|
||||
pub max_tokens_per_minute: u64,
|
||||
pub max_tokens_per_day: u64,
|
||||
}
|
||||
|
||||
pub async fn report_llm_usage(client: &clickhouse::Client, row: LlmUsageEventRow) -> Result<()> {
|
||||
const LLM_USAGE_EVENTS_TABLE: &str = "llm_usage_events";
|
||||
write_to_table(LLM_USAGE_EVENTS_TABLE, &[row], client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table '{LLM_USAGE_EVENTS_TABLE}'"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn report_llm_rate_limit(
|
||||
client: &clickhouse::Client,
|
||||
row: LlmRateLimitEventRow,
|
||||
) -> Result<()> {
|
||||
const LLM_RATE_LIMIT_EVENTS_TABLE: &str = "llm_rate_limit_events";
|
||||
write_to_table(LLM_RATE_LIMIT_EVENTS_TABLE, &[row], client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table '{LLM_RATE_LIMIT_EVENTS_TABLE}'"))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -17,10 +17,8 @@ pub struct LlmTokenClaims {
|
||||
pub exp: u64,
|
||||
pub jti: String,
|
||||
pub user_id: u64,
|
||||
#[serde(default)]
|
||||
pub system_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub metrics_id: Option<Uuid>,
|
||||
pub metrics_id: Uuid,
|
||||
pub github_user_login: String,
|
||||
pub is_staff: bool,
|
||||
pub has_llm_closed_beta_feature_flag: bool,
|
||||
@@ -56,7 +54,7 @@ impl LlmTokenClaims {
|
||||
jti: uuid::Uuid::new_v4().to_string(),
|
||||
user_id: user.id.to_proto(),
|
||||
system_id,
|
||||
metrics_id: Some(user.metrics_id),
|
||||
metrics_id: user.metrics_id,
|
||||
github_user_login: user.github_login.clone(),
|
||||
is_staff,
|
||||
has_llm_closed_beta_feature_flag,
|
||||
|
||||
@@ -310,6 +310,9 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetStagedText>)
|
||||
.add_request_handler(
|
||||
forward_mutating_project_request::<proto::RegisterBufferWithLanguageServers>,
|
||||
)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::UpdateGitBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
|
||||
.add_request_handler(
|
||||
|
||||
@@ -994,10 +994,12 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
|
||||
|
||||
let _buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_local_buffer_with_lsp("/dir/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1587,7 +1589,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
@@ -1597,6 +1598,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
|
||||
// Set up the language server to return an additional inlay hint on each request.
|
||||
let edits_made = Arc::new(AtomicUsize::new(0));
|
||||
let closure_edits_made = Arc::clone(&edits_made);
|
||||
|
||||
@@ -3891,13 +3891,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
// Cause the language server to start.
|
||||
let _buffer = project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.open_buffer(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Path::new("other.rs").into(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
project.open_local_buffer_with_lsp("/a/other.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4176,7 +4170,9 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
// Join the project as client B and open all three files.
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
|
||||
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
|
||||
project_b.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, file_name), cx)
|
||||
})
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4230,7 +4226,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
cx.subscribe(&project_b, move |_, _, event, cx| {
|
||||
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
|
||||
disk_based_diagnostics_finished.store(true, SeqCst);
|
||||
for buffer in &guest_buffers {
|
||||
for (buffer, _) in &guest_buffers {
|
||||
assert_eq!(
|
||||
buffer
|
||||
.read(cx)
|
||||
@@ -4351,7 +4347,6 @@ async fn test_formatting_buffer(
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
executor.allow_parking();
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
@@ -4379,10 +4374,16 @@ async fn test_formatting_buffer(
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let lsp_store_b = project_b.update(cx_b, |p, _| p.lsp_store());
|
||||
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _handle = lsp_store_b.update(cx_b, |lsp_store, cx| {
|
||||
lsp_store.register_buffer_with_language_servers(&buffer_b, cx)
|
||||
});
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
|
||||
Ok(Some(vec![
|
||||
@@ -4431,6 +4432,8 @@ async fn test_formatting_buffer(
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
executor.allow_parking();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.format(
|
||||
@@ -4503,8 +4506,12 @@ async fn test_prettier_formatting_buffer(
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
@@ -4620,8 +4627,12 @@ async fn test_definition(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Request the definition of a symbol as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -4765,8 +4776,12 @@ async fn test_references(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Request references to a symbol as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -5012,8 +5027,12 @@ async fn test_document_highlights(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Request document highlights as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -5130,8 +5149,12 @@ async fn test_lsp_hover(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file as the guest
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut servers_with_hover_requests = HashMap::default();
|
||||
for i in 0..language_server_names.len() {
|
||||
@@ -5306,9 +5329,12 @@ async fn test_project_symbols(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Cause the language server to start.
|
||||
let open_buffer_task =
|
||||
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
|
||||
let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
|
||||
let _buffer = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
|
||||
@@ -5400,8 +5426,12 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
|
||||
let (buffer_b1, _lsp) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
|
||||
@@ -5417,13 +5447,22 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
let buffer_b2;
|
||||
if rng.gen() {
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
|
||||
buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
|
||||
(buffer_b2, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
|
||||
(buffer_b2, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
|
||||
}
|
||||
|
||||
let buffer_b2 = buffer_b2.await.unwrap();
|
||||
let definitions = definitions.await.unwrap();
|
||||
assert_eq!(definitions.len(), 1);
|
||||
assert_eq!(definitions[0].target.buffer, buffer_b2);
|
||||
|
||||
@@ -426,8 +426,10 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
executor.run_until_parked();
|
||||
|
||||
// Opens the buffer and formats it
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
|
||||
})
|
||||
.await
|
||||
.expect("user B opens buffer for formatting");
|
||||
|
||||
|
||||
@@ -518,7 +518,6 @@ impl TestServer {
|
||||
stripe_billing: None,
|
||||
rate_limiter: Arc::new(RateLimiter::new(test_db.db().clone())),
|
||||
executor,
|
||||
clickhouse_client: None,
|
||||
kinesis_client: None,
|
||||
config: Config {
|
||||
http_port: 0,
|
||||
@@ -546,10 +545,9 @@ impl TestServer {
|
||||
anthropic_api_key: None,
|
||||
anthropic_staff_api_key: None,
|
||||
llm_closed_beta_model_name: None,
|
||||
clickhouse_url: None,
|
||||
clickhouse_user: None,
|
||||
clickhouse_password: None,
|
||||
clickhouse_database: None,
|
||||
prediction_api_url: None,
|
||||
prediction_api_key: None,
|
||||
prediction_model: None,
|
||||
zed_client_checksum_seed: None,
|
||||
slack_panics_webhook: None,
|
||||
auto_join_channel_id: None,
|
||||
|
||||
@@ -59,18 +59,21 @@ workspace.workspace = true
|
||||
async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
|
||||
[dev-dependencies]
|
||||
clock.workspace = true
|
||||
indoc.workspace = true
|
||||
serde_json.workspace = true
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
node_runtime = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -6,13 +6,12 @@ use anyhow::{anyhow, Result};
|
||||
use chrono::DateTime;
|
||||
use fs::Fs;
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Global};
|
||||
use gpui::{prelude::*, AppContext, AsyncAppContext, Global};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use paths::home_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::watch_config_file;
|
||||
use strum::EnumIter;
|
||||
use ui::Context;
|
||||
|
||||
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
|
||||
pub const COPILOT_CHAT_AUTH_URL: &str = "https://api.github.com/copilot_internal/v2/token";
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use crate::{Completion, Copilot};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
||||
use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
|
||||
use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, AllLanguageSettings},
|
||||
Buffer, OffsetRangeExt, ToOffset,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
|
||||
@@ -21,7 +20,6 @@ pub struct CopilotCompletionProvider {
|
||||
pending_refresh: Task<Result<()>>,
|
||||
pending_cycling_refresh: Task<Result<()>>,
|
||||
copilot: Model<Copilot>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
}
|
||||
|
||||
impl CopilotCompletionProvider {
|
||||
@@ -35,15 +33,9 @@ impl CopilotCompletionProvider {
|
||||
pending_refresh: Task::ready(Ok(())),
|
||||
pending_cycling_refresh: Task::ready(Ok(())),
|
||||
copilot,
|
||||
telemetry: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_telemetry(mut self, telemetry: Arc<Telemetry>) -> Self {
|
||||
self.telemetry = Some(telemetry);
|
||||
self
|
||||
}
|
||||
|
||||
fn active_completion(&self) -> Option<&Completion> {
|
||||
self.completions.get(self.active_completion_index)
|
||||
}
|
||||
@@ -190,23 +182,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
self.copilot
|
||||
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
|
||||
.detach_and_log_err(cx);
|
||||
if self.active_completion().is_some() {
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
telemetry.report_inline_completion_event(
|
||||
Self::name().to_string(),
|
||||
true,
|
||||
self.file_extension.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn discard(
|
||||
&mut self,
|
||||
should_report_inline_completion_event: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
fn discard(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
let copilot_enabled = settings.inline_completions_enabled(None, None, cx);
|
||||
@@ -220,24 +199,14 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
copilot.discard_completions(&self.completions, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
if should_report_inline_completion_event && self.active_completion().is_some() {
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
telemetry.report_inline_completion_event(
|
||||
Self::name().to_string(),
|
||||
false,
|
||||
self.file_extension.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn active_completion_text<'a>(
|
||||
&'a self,
|
||||
fn suggest(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<CompletionProposal> {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<InlineCompletion> {
|
||||
let buffer_id = buffer.entity_id();
|
||||
let buffer = buffer.read(cx);
|
||||
let completion = self.active_completion()?;
|
||||
@@ -267,13 +236,9 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
if completion_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(CompletionProposal {
|
||||
inlays: vec![InlayProposal::Suggestion(
|
||||
cursor_position.bias_right(buffer),
|
||||
completion_text.into(),
|
||||
)],
|
||||
text: completion_text.into(),
|
||||
delete_range: None,
|
||||
let position = cursor_position.bias_right(buffer);
|
||||
Some(InlineCompletion {
|
||||
edits: vec![(position..position, completion_text.into())],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -331,7 +296,6 @@ mod tests {
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
});
|
||||
|
||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
@@ -358,8 +322,9 @@ mod tests {
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
// We want to show both: the inline completion and the completion menu
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
|
||||
// Confirming a completion inserts it and hides the context menu, without showing
|
||||
// the copilot suggestion afterwards.
|
||||
@@ -368,45 +333,12 @@ mod tests {
|
||||
.unwrap()
|
||||
.detach();
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
|
||||
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Ensure Copilot suggestions are shown right away if no autocompletion is available.
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
two
|
||||
three
|
||||
"},
|
||||
vec![],
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
|
||||
// Reset editor and test that accepting completions works
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
@@ -434,22 +366,17 @@ mod tests {
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
|
||||
// When hiding the context menu, the Copilot suggestion becomes visible.
|
||||
editor.cancel(&Default::default(), cx);
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Ensure existing completion is interpolated when inserting again.
|
||||
// Ensure existing inline completion is interpolated when inserting again.
|
||||
cx.simulate_keystroke("c");
|
||||
executor.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
});
|
||||
@@ -467,25 +394,25 @@ mod tests {
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
|
||||
// Canceling should remove the active Copilot suggestion.
|
||||
editor.cancel(&Default::default(), cx);
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
|
||||
// After canceling, tabbing shouldn't insert the previously shown suggestion.
|
||||
editor.tab(&Default::default(), cx);
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
|
||||
|
||||
// When undoing the previously active suggestion is shown again.
|
||||
editor.undo(&Default::default(), cx);
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
});
|
||||
@@ -493,25 +420,25 @@ mod tests {
|
||||
// If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
|
||||
cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
|
||||
|
||||
// AcceptInlineCompletion when there is an active suggestion inserts it.
|
||||
editor.accept_inline_completion(&Default::default(), cx);
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
|
||||
// When undoing the previously active suggestion is shown again.
|
||||
editor.undo(&Default::default(), cx);
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
|
||||
|
||||
// Hide suggestion.
|
||||
editor.cancel(&Default::default(), cx);
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
|
||||
});
|
||||
@@ -520,7 +447,7 @@ mod tests {
|
||||
// we won't make it visible.
|
||||
cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
|
||||
});
|
||||
@@ -545,19 +472,19 @@ mod tests {
|
||||
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
|
||||
// Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
|
||||
editor.tab(&Default::default(), cx);
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
|
||||
// Using AcceptInlineCompletion again accepts the suggestion.
|
||||
editor.accept_inline_completion(&Default::default(), cx);
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
});
|
||||
@@ -615,17 +542,17 @@ mod tests {
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
|
||||
// Accepting the first word of the suggestion should only accept the first word and still show the rest.
|
||||
editor.accept_partial_inline_completion(&Default::default(), cx);
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
|
||||
// Accepting next word should accept the non-word and copilot suggestion should be gone
|
||||
editor.accept_partial_inline_completion(&Default::default(), cx);
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
});
|
||||
@@ -657,11 +584,11 @@ mod tests {
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
|
||||
// Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
|
||||
editor.accept_partial_inline_completion(&Default::default(), cx);
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
@@ -670,7 +597,7 @@ mod tests {
|
||||
|
||||
// Accepting next word should accept the next word and copilot suggestion should still exist
|
||||
editor.accept_partial_inline_completion(&Default::default(), cx);
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
@@ -679,7 +606,7 @@ mod tests {
|
||||
|
||||
// Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
|
||||
editor.accept_partial_inline_completion(&Default::default(), cx);
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
@@ -730,29 +657,29 @@ mod tests {
|
||||
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), cx);
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\nt\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), cx);
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\n\nthree\n");
|
||||
|
||||
// Deleting across the original suggestion range invalidates it.
|
||||
editor.backspace(&Default::default(), cx);
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\nthree\n");
|
||||
|
||||
// Undoing the deletion restores the suggestion.
|
||||
editor.undo(&Default::default(), cx);
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\n\nthree\n");
|
||||
});
|
||||
@@ -813,7 +740,7 @@ mod tests {
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
_ = editor.update(cx, |editor, cx| {
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\n\na = 1\nb = 2 + a\n\n\n\n\n\nc = 3\nd = 4\n\n"
|
||||
@@ -835,7 +762,7 @@ mod tests {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
|
||||
});
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4\n\n"
|
||||
@@ -844,7 +771,7 @@ mod tests {
|
||||
|
||||
// Type a character, ensuring we don't even try to interpolate the previous suggestion.
|
||||
editor.handle_input(" ", cx);
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 \n\n"
|
||||
@@ -855,7 +782,7 @@ mod tests {
|
||||
// Ensure the new suggestion is displayed when the debounce timeout expires.
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
_ = editor.update(cx, |editor, cx| {
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 + c\n\n"
|
||||
@@ -915,8 +842,8 @@ mod tests {
|
||||
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible(), "Even there are some completions available, those are not triggered when active copilot suggestion is present");
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||||
});
|
||||
@@ -943,7 +870,7 @@ mod tests {
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
|
||||
});
|
||||
@@ -969,15 +896,9 @@ mod tests {
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(
|
||||
editor.context_menu_visible(),
|
||||
"On completion trigger input, the completions should be fetched and visible"
|
||||
);
|
||||
assert!(
|
||||
!editor.has_active_inline_completion(cx),
|
||||
"On completion trigger input, copilot suggestion should be dismissed"
|
||||
);
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion(),);
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
||||
});
|
||||
}
|
||||
@@ -998,7 +919,7 @@ mod tests {
|
||||
"/test",
|
||||
json!({
|
||||
".env": "SECRET=something\n",
|
||||
"README.md": "hello\n"
|
||||
"README.md": "hello\nworld\nhow\nare\nyou\ntoday"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@@ -1030,7 +951,7 @@ mod tests {
|
||||
multibuffer.push_excerpts(
|
||||
public_buffer.clone(),
|
||||
[ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(1, 0),
|
||||
context: Point::new(0, 0)..Point::new(6, 0),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
@@ -1038,6 +959,7 @@ mod tests {
|
||||
multibuffer
|
||||
});
|
||||
let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx));
|
||||
editor.update(cx, |editor, cx| editor.focus(cx)).unwrap();
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
@@ -1073,7 +995,7 @@ mod tests {
|
||||
|
||||
_ = editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
|
||||
s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
|
||||
});
|
||||
editor.refresh_inline_completion(true, false, cx);
|
||||
});
|
||||
|
||||
@@ -138,27 +138,16 @@ impl ProjectDiagnosticsEditor {
|
||||
language_server_id,
|
||||
path,
|
||||
} => {
|
||||
let max_severity = this.max_severity();
|
||||
let has_diagnostics_to_display = project.read(cx).lsp_store().read(cx).diagnostics_for_buffer(path)
|
||||
.into_iter().flatten()
|
||||
.filter(|(server_id, _)| language_server_id == server_id)
|
||||
.flat_map(|(_, diagnostics)| diagnostics)
|
||||
.any(|diagnostic| diagnostic.diagnostic.severity <= max_severity);
|
||||
this.paths_to_update
|
||||
.insert((path.clone(), Some(*language_server_id)));
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
|
||||
if has_diagnostics_to_display {
|
||||
this.paths_to_update
|
||||
.insert((path.clone(), Some(*language_server_id)));
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
|
||||
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
|
||||
} else {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
|
||||
this.update_stale_excerpts(cx);
|
||||
}
|
||||
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
|
||||
} else {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. no diagnostics to display");
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
|
||||
this.update_stale_excerpts(cx);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -363,12 +352,16 @@ impl ProjectDiagnosticsEditor {
|
||||
ExcerptId::min()
|
||||
};
|
||||
|
||||
let max_severity = self.max_severity();
|
||||
let path_state = &mut self.path_states[path_ix];
|
||||
let mut new_group_ixs = Vec::new();
|
||||
let mut blocks_to_add = Vec::new();
|
||||
let mut blocks_to_remove = HashSet::default();
|
||||
let mut first_excerpt_id = None;
|
||||
let max_severity = if self.include_warnings {
|
||||
DiagnosticSeverity::WARNING
|
||||
} else {
|
||||
DiagnosticSeverity::ERROR
|
||||
};
|
||||
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, cx| {
|
||||
let mut old_groups = mem::take(&mut path_state.diagnostic_groups)
|
||||
.into_iter()
|
||||
@@ -657,14 +650,6 @@ impl ProjectDiagnosticsEditor {
|
||||
prev_path = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
fn max_severity(&self) -> DiagnosticSeverity {
|
||||
if self.include_warnings {
|
||||
DiagnosticSeverity::WARNING
|
||||
} else {
|
||||
DiagnosticSeverity::ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ProjectDiagnosticsEditor {
|
||||
|
||||
@@ -809,7 +809,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
|
||||
updated_language_servers.insert(server_id);
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
log::info!("updating diagnostics. language server {server_id} path {path:?}");
|
||||
randomly_update_diagnostics_for_path(
|
||||
&fs,
|
||||
@@ -818,10 +818,12 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
&mut next_group_id,
|
||||
&mut rng,
|
||||
);
|
||||
project
|
||||
lsp_store
|
||||
.update_diagnostic_entries(server_id, path, None, diagnostics.clone(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
|
||||
|
||||
cx.run_until_parked();
|
||||
}
|
||||
@@ -842,10 +844,25 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
|
||||
cx.run_until_parked();
|
||||
|
||||
let mutated_excerpts = get_diagnostics_excerpts(&mutated_view, cx);
|
||||
let reference_excerpts = get_diagnostics_excerpts(&reference_view, cx);
|
||||
|
||||
for ((path, language_server_id), diagnostics) in current_diagnostics {
|
||||
for diagnostic in diagnostics {
|
||||
let found_excerpt = reference_excerpts.iter().any(|info| {
|
||||
let row_range = info.range.context.start.row..info.range.context.end.row;
|
||||
info.path == path.strip_prefix("/test").unwrap()
|
||||
&& info.language_server == language_server_id
|
||||
&& row_range.contains(&diagnostic.range.start.0.row)
|
||||
});
|
||||
assert!(found_excerpt, "diagnostic not found in reference view");
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(mutated_excerpts, reference_excerpts);
|
||||
}
|
||||
|
||||
|
||||
963
crates/editor/src/code_context_menus.rs
Normal file
@@ -0,0 +1,963 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
cmp::{min, Reverse},
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
|
||||
Model, MouseButton, Pixels, ScrollStrategy, SharedString, StrikethroughStyle, StyledText,
|
||||
UniformListScrollHandle, ViewContext, WeakView,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language::{CodeLabel, Documentation};
|
||||
use lsp::LanguageServerId;
|
||||
use multi_buffer::{Anchor, ExcerptId};
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::{CodeAction, Completion, TaskSourceKind};
|
||||
use std::iter;
|
||||
use task::ResolvedTask;
|
||||
use ui::{
|
||||
h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
|
||||
Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover, Selectable as _,
|
||||
StatefulInteractiveElement as _, Styled, StyledExt as _,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
actions::{ConfirmCodeAction, ConfirmCompletion},
|
||||
display_map::DisplayPoint,
|
||||
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
|
||||
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
|
||||
};
|
||||
|
||||
pub enum CodeContextMenu {
|
||||
Completions(CompletionsMenu),
|
||||
CodeActions(CodeActionsMenu),
|
||||
}
|
||||
|
||||
impl CodeContextMenu {
|
||||
pub fn select_first(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
if self.visible() {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.select_first(provider, cx),
|
||||
CodeContextMenu::CodeActions(menu) => menu.select_first(cx),
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_prev(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
if self.visible() {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.select_prev(provider, cx),
|
||||
CodeContextMenu::CodeActions(menu) => menu.select_prev(cx),
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
if self.visible() {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.select_next(provider, cx),
|
||||
CodeContextMenu::CodeActions(menu) => menu.select_next(cx),
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_last(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
if self.visible() {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.select_last(provider, cx),
|
||||
CodeContextMenu::CodeActions(menu) => menu.select_last(cx),
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn visible(&self) -> bool {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.visible(),
|
||||
CodeContextMenu::CodeActions(menu) => menu.visible(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
cursor_position: DisplayPoint,
|
||||
style: &EditorStyle,
|
||||
max_height: Pixels,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> (ContextMenuOrigin, AnyElement) {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => (
|
||||
ContextMenuOrigin::EditorPoint(cursor_position),
|
||||
menu.render(style, max_height, workspace, cx),
|
||||
),
|
||||
CodeContextMenu::CodeActions(menu) => {
|
||||
menu.render(cursor_position, style, max_height, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextMenuOrigin {
|
||||
EditorPoint(DisplayPoint),
|
||||
GutterIndicator(DisplayRow),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CompletionsMenu {
|
||||
pub id: CompletionId,
|
||||
sort_completions: bool,
|
||||
pub initial_position: Anchor,
|
||||
pub buffer: Model<Buffer>,
|
||||
pub completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
match_candidates: Arc<[StringMatchCandidate]>,
|
||||
pub matches: Arc<[StringMatch]>,
|
||||
pub selected_item: usize,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
resolve_completions: bool,
|
||||
pub aside_was_displayed: Cell<bool>,
|
||||
show_completion_documentation: bool,
|
||||
last_rendered_range: Arc<Mutex<Option<Range<usize>>>>,
|
||||
}
|
||||
|
||||
impl CompletionsMenu {
|
||||
pub fn new(
|
||||
id: CompletionId,
|
||||
sort_completions: bool,
|
||||
show_completion_documentation: bool,
|
||||
initial_position: Anchor,
|
||||
buffer: Model<Buffer>,
|
||||
completions: Box<[Completion]>,
|
||||
aside_was_displayed: bool,
|
||||
) -> Self {
|
||||
let match_candidates = completions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| {
|
||||
StringMatchCandidate::new(
|
||||
id,
|
||||
completion.label.text[completion.label.filter_range.clone()].into(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
id,
|
||||
sort_completions,
|
||||
initial_position,
|
||||
buffer,
|
||||
completions: Arc::new(RwLock::new(completions)),
|
||||
match_candidates,
|
||||
matches: Vec::new().into(),
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: true,
|
||||
aside_was_displayed: Cell::new(aside_was_displayed),
|
||||
show_completion_documentation,
|
||||
last_rendered_range: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_snippet_choices(
|
||||
id: CompletionId,
|
||||
sort_completions: bool,
|
||||
choices: &Vec<String>,
|
||||
selection: Range<Anchor>,
|
||||
buffer: Model<Buffer>,
|
||||
) -> Self {
|
||||
let completions = choices
|
||||
.iter()
|
||||
.map(|choice| Completion {
|
||||
old_range: selection.start.text_anchor..selection.end.text_anchor,
|
||||
new_text: choice.to_string(),
|
||||
label: CodeLabel {
|
||||
text: choice.to_string(),
|
||||
runs: Default::default(),
|
||||
filter_range: Default::default(),
|
||||
},
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
documentation: None,
|
||||
lsp_completion: Default::default(),
|
||||
confirm: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let match_candidates = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string()))
|
||||
.collect();
|
||||
let matches = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatch {
|
||||
candidate_id: id,
|
||||
score: 1.,
|
||||
positions: vec![],
|
||||
string: completion.clone(),
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
id,
|
||||
sort_completions,
|
||||
initial_position: selection.start,
|
||||
buffer,
|
||||
completions: Arc::new(RwLock::new(completions)),
|
||||
match_candidates,
|
||||
matches,
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: false,
|
||||
aside_was_displayed: Cell::new(false),
|
||||
show_completion_documentation: false,
|
||||
last_rendered_range: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(0, provider, cx);
|
||||
}
|
||||
|
||||
fn select_prev(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(self.prev_match_index(), provider, cx);
|
||||
}
|
||||
|
||||
fn select_next(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(self.next_match_index(), provider, cx);
|
||||
}
|
||||
|
||||
fn select_last(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(self.matches.len() - 1, provider, cx);
|
||||
}
|
||||
|
||||
fn update_selection_index(
|
||||
&mut self,
|
||||
match_index: usize,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if self.selected_item != match_index {
|
||||
self.selected_item = match_index;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_visible_completions(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_match_index(&self) -> usize {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item - 1
|
||||
} else {
|
||||
self.matches.len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
fn next_match_index(&self) -> usize {
|
||||
if self.selected_item + 1 < self.matches.len() {
|
||||
self.selected_item + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_visible_completions(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if !self.resolve_completions {
|
||||
return;
|
||||
}
|
||||
let Some(provider) = provider else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Attempt to resolve completions for every item that will be displayed. This matters
|
||||
// because single line documentation may be displayed inline with the completion.
|
||||
//
|
||||
// When navigating to the very beginning or end of completions, `last_rendered_range` may
|
||||
// have no overlap with the completions that will be displayed, so instead use a range based
|
||||
// on the last rendered count.
|
||||
const APPROXIMATE_VISIBLE_COUNT: usize = 12;
|
||||
let last_rendered_range = self.last_rendered_range.lock().clone();
|
||||
let visible_count = last_rendered_range
|
||||
.clone()
|
||||
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
|
||||
let matches_range = if self.selected_item == 0 {
|
||||
0..min(visible_count, self.matches.len())
|
||||
} else if self.selected_item == self.matches.len() - 1 {
|
||||
self.matches.len().saturating_sub(visible_count)..self.matches.len()
|
||||
} else {
|
||||
last_rendered_range.unwrap_or_else(|| self.selected_item..self.selected_item + 1)
|
||||
};
|
||||
|
||||
// Expand the range to resolve more completions than are predicted to be visible, to reduce
|
||||
// jank on navigation.
|
||||
const EXTRA_TO_RESOLVE: usize = 4;
|
||||
let matches_indices = util::iterate_expanded_and_wrapped_usize_range(
|
||||
matches_range.clone(),
|
||||
EXTRA_TO_RESOLVE,
|
||||
EXTRA_TO_RESOLVE,
|
||||
self.matches.len(),
|
||||
);
|
||||
|
||||
// Avoid work by sometimes filtering out completions that already have documentation.
|
||||
// This filtering doesn't happen if the completions are currently being updated.
|
||||
let candidate_ids = matches_indices.map(|i| self.matches[i].candidate_id);
|
||||
let candidate_ids = match self.completions.try_read() {
|
||||
None => candidate_ids.collect::<Vec<usize>>(),
|
||||
Some(completions) => candidate_ids
|
||||
.filter(|i| completions[*i].documentation.is_none())
|
||||
.collect::<Vec<usize>>(),
|
||||
};
|
||||
|
||||
// Current selection is always resolved even if it already has documentation, to handle
|
||||
// out-of-spec language servers that return more results later.
|
||||
let selected_candidate_id = self.matches[self.selected_item].candidate_id;
|
||||
let candidate_ids = iter::once(selected_candidate_id)
|
||||
.chain(
|
||||
candidate_ids
|
||||
.into_iter()
|
||||
.filter(|id| *id != selected_candidate_id),
|
||||
)
|
||||
.collect::<Vec<usize>>();
|
||||
|
||||
let resolve_task = provider.resolve_completions(
|
||||
self.buffer.clone(),
|
||||
candidate_ids,
|
||||
self.completions.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(move |editor, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
editor.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn visible(&self) -> bool {
|
||||
!self.matches.is_empty()
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height: Pixels,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
let show_completion_documentation = self.show_completion_documentation;
|
||||
let widest_completion_ix = self
|
||||
.matches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|(_, mat)| {
|
||||
let completions = self.completions.read();
|
||||
let completion = &completions[mat.candidate_id];
|
||||
let documentation = &completion.documentation;
|
||||
|
||||
let mut len = completion.label.text.chars().count();
|
||||
if let Some(Documentation::SingleLine(text)) = documentation {
|
||||
if show_completion_documentation {
|
||||
len += text.chars().count();
|
||||
}
|
||||
}
|
||||
|
||||
len
|
||||
})
|
||||
.map(|(ix, _)| ix);
|
||||
|
||||
let completions = self.completions.clone();
|
||||
let matches = self.matches.clone();
|
||||
let selected_item = self.selected_item;
|
||||
let style = style.clone();
|
||||
|
||||
let multiline_docs = if show_completion_documentation {
|
||||
let mat = &self.matches[selected_item];
|
||||
match &self.completions.read()[mat.candidate_id].documentation {
|
||||
Some(Documentation::MultiLinePlainText(text)) => {
|
||||
Some(div().child(SharedString::from(text.clone())))
|
||||
}
|
||||
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
|
||||
Some(div().child(render_parsed_markdown(
|
||||
"completions_markdown",
|
||||
parsed,
|
||||
&style,
|
||||
workspace,
|
||||
cx,
|
||||
)))
|
||||
}
|
||||
Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
|
||||
Some(div().child("No documentation"))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let aside_contents = if let Some(multiline_docs) = multiline_docs {
|
||||
Some(multiline_docs)
|
||||
} else if self.aside_was_displayed.get() {
|
||||
Some(div().child("Fetching documentation..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.aside_was_displayed.set(aside_contents.is_some());
|
||||
|
||||
let aside_contents = aside_contents.map(|div| {
|
||||
div.id("multiline_docs")
|
||||
.max_h(max_height)
|
||||
.flex_1()
|
||||
.px_1p5()
|
||||
.py_1()
|
||||
.min_w(px(260.))
|
||||
.max_w(px(640.))
|
||||
.w(px(500.))
|
||||
.overflow_y_scroll()
|
||||
.occlude()
|
||||
});
|
||||
|
||||
let last_rendered_range = self.last_rendered_range.clone();
|
||||
|
||||
let list = uniform_list(
|
||||
cx.view().clone(),
|
||||
"completions",
|
||||
matches.len(),
|
||||
move |_editor, range, cx| {
|
||||
last_rendered_range.lock().replace(range.clone());
|
||||
let start_ix = range.start;
|
||||
let completions_guard = completions.read();
|
||||
|
||||
matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, mat)| {
|
||||
let item_ix = start_ix + ix;
|
||||
let candidate_id = mat.candidate_id;
|
||||
let completion = &completions_guard[candidate_id];
|
||||
|
||||
let documentation = if show_completion_documentation {
|
||||
&completion.documentation
|
||||
} else {
|
||||
&None
|
||||
};
|
||||
|
||||
let highlights = gpui::combine_highlights(
|
||||
mat.ranges().map(|range| (range, FontWeight::BOLD.into())),
|
||||
styled_runs_for_code_label(&completion.label, &style.syntax).map(
|
||||
|(range, mut highlight)| {
|
||||
// Ignore font weight for syntax highlighting, as we'll use it
|
||||
// for fuzzy matches.
|
||||
highlight.font_weight = None;
|
||||
|
||||
if completion.lsp_completion.deprecated.unwrap_or(false) {
|
||||
highlight.strikethrough = Some(StrikethroughStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
});
|
||||
highlight.color = Some(cx.theme().colors().text_muted);
|
||||
}
|
||||
|
||||
(range, highlight)
|
||||
},
|
||||
),
|
||||
);
|
||||
let completion_label = StyledText::new(completion.label.text.clone())
|
||||
.with_highlights(&style.text, highlights);
|
||||
let documentation_label =
|
||||
if let Some(Documentation::SingleLine(text)) = documentation {
|
||||
if text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(text.clone())
|
||||
.ml_4()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let color_swatch = completion
|
||||
.color()
|
||||
.map(|color| div().size_4().bg(color).rounded_sm());
|
||||
|
||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
||||
ListItem::new(mat.candidate_id)
|
||||
.inset(true)
|
||||
.selected(item_ix == selected_item)
|
||||
.on_click(cx.listener(move |editor, _event, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_completion(
|
||||
&ConfirmCompletion {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
.start_slot::<Div>(color_swatch)
|
||||
.child(h_flex().overflow_hidden().child(completion_label))
|
||||
.end_slot::<Label>(documentation_label),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.occlude()
|
||||
.max_h(max_height)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.with_width_from_item(widest_completion_ix)
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer);
|
||||
|
||||
Popover::new()
|
||||
.child(list)
|
||||
.when_some(aside_contents, |popover, aside_contents| {
|
||||
popover.aside(aside_contents)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
|
||||
let mut matches = if let Some(query) = query {
|
||||
fuzzy::match_strings(
|
||||
&self.match_candidates,
|
||||
query,
|
||||
query.chars().any(|c| c.is_uppercase()),
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
self.match_candidates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(candidate_id, candidate)| StringMatch {
|
||||
candidate_id,
|
||||
score: Default::default(),
|
||||
positions: Default::default(),
|
||||
string: candidate.string.clone(),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Remove all candidates where the query's start does not match the start of any word in the candidate
|
||||
if let Some(query) = query {
|
||||
if let Some(query_start) = query.chars().next() {
|
||||
matches.retain(|string_match| {
|
||||
split_words(&string_match.string).any(|word| {
|
||||
// Check that the first codepoint of the word as lowercase matches the first
|
||||
// codepoint of the query as lowercase
|
||||
word.chars()
|
||||
.flat_map(|codepoint| codepoint.to_lowercase())
|
||||
.zip(query_start.to_lowercase())
|
||||
.all(|(word_cp, query_cp)| word_cp == query_cp)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let completions = self.completions.read();
|
||||
if self.sort_completions {
|
||||
matches.sort_unstable_by_key(|mat| {
|
||||
// We do want to strike a balance here between what the language server tells us
|
||||
// to sort by (the sort_text) and what are "obvious" good matches (i.e. when you type
|
||||
// `Creat` and there is a local variable called `CreateComponent`).
|
||||
// So what we do is: we bucket all matches into two buckets
|
||||
// - Strong matches
|
||||
// - Weak matches
|
||||
// Strong matches are the ones with a high fuzzy-matcher score (the "obvious" matches)
|
||||
// and the Weak matches are the rest.
|
||||
//
|
||||
// For the strong matches, we sort by our fuzzy-finder score first and for the weak
|
||||
// matches, we prefer language-server sort_text first.
|
||||
//
|
||||
// The thinking behind that: we want to show strong matches first in order of relevance(fuzzy score).
|
||||
// Rest of the matches(weak) can be sorted as language-server expects.
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum MatchScore<'a> {
|
||||
Strong {
|
||||
score: Reverse<OrderedFloat<f64>>,
|
||||
sort_text: Option<&'a str>,
|
||||
sort_key: (usize, &'a str),
|
||||
},
|
||||
Weak {
|
||||
sort_text: Option<&'a str>,
|
||||
score: Reverse<OrderedFloat<f64>>,
|
||||
sort_key: (usize, &'a str),
|
||||
},
|
||||
}
|
||||
|
||||
let completion = &completions[mat.candidate_id];
|
||||
let sort_key = completion.sort_key();
|
||||
let sort_text = completion.lsp_completion.sort_text.as_deref();
|
||||
let score = Reverse(OrderedFloat(mat.score));
|
||||
|
||||
if mat.score >= 0.2 {
|
||||
MatchScore::Strong {
|
||||
score,
|
||||
sort_text,
|
||||
sort_key,
|
||||
}
|
||||
} else {
|
||||
MatchScore::Weak {
|
||||
sort_text,
|
||||
score,
|
||||
sort_key,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for mat in &mut matches {
|
||||
let completion = &completions[mat.candidate_id];
|
||||
mat.string.clone_from(&completion.label.text);
|
||||
for position in &mut mat.positions {
|
||||
*position += completion.label.filter_range.start;
|
||||
}
|
||||
}
|
||||
drop(completions);
|
||||
|
||||
self.matches = matches.into();
|
||||
self.selected_item = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AvailableCodeAction {
|
||||
pub excerpt_id: ExcerptId,
|
||||
pub action: CodeAction,
|
||||
pub provider: Arc<dyn CodeActionProvider>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CodeActionContents {
|
||||
pub tasks: Option<Arc<ResolvedTasks>>,
|
||||
pub actions: Option<Arc<[AvailableCodeAction]>>,
|
||||
}
|
||||
|
||||
impl CodeActionContents {
|
||||
fn len(&self) -> usize {
|
||||
match (&self.tasks, &self.actions) {
|
||||
(Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
|
||||
(Some(tasks), None) => tasks.templates.len(),
|
||||
(None, Some(actions)) => actions.len(),
|
||||
(None, None) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
match (&self.tasks, &self.actions) {
|
||||
(Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
|
||||
(Some(tasks), None) => tasks.templates.is_empty(),
|
||||
(None, Some(actions)) => actions.is_empty(),
|
||||
(None, None) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
|
||||
self.tasks
|
||||
.iter()
|
||||
.flat_map(|tasks| {
|
||||
tasks
|
||||
.templates
|
||||
.iter()
|
||||
.map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
|
||||
})
|
||||
.chain(self.actions.iter().flat_map(|actions| {
|
||||
actions.iter().map(|available| CodeActionsItem::CodeAction {
|
||||
excerpt_id: available.excerpt_id,
|
||||
action: available.action.clone(),
|
||||
provider: available.provider.clone(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> Option<CodeActionsItem> {
|
||||
match (&self.tasks, &self.actions) {
|
||||
(Some(tasks), Some(actions)) => {
|
||||
if index < tasks.templates.len() {
|
||||
tasks
|
||||
.templates
|
||||
.get(index)
|
||||
.cloned()
|
||||
.map(|(kind, task)| CodeActionsItem::Task(kind, task))
|
||||
} else {
|
||||
actions.get(index - tasks.templates.len()).map(|available| {
|
||||
CodeActionsItem::CodeAction {
|
||||
excerpt_id: available.excerpt_id,
|
||||
action: available.action.clone(),
|
||||
provider: available.provider.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
(Some(tasks), None) => tasks
|
||||
.templates
|
||||
.get(index)
|
||||
.cloned()
|
||||
.map(|(kind, task)| CodeActionsItem::Task(kind, task)),
|
||||
(None, Some(actions)) => {
|
||||
actions
|
||||
.get(index)
|
||||
.map(|available| CodeActionsItem::CodeAction {
|
||||
excerpt_id: available.excerpt_id,
|
||||
action: available.action.clone(),
|
||||
provider: available.provider.clone(),
|
||||
})
|
||||
}
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone)]
|
||||
pub enum CodeActionsItem {
|
||||
Task(TaskSourceKind, ResolvedTask),
|
||||
CodeAction {
|
||||
excerpt_id: ExcerptId,
|
||||
action: CodeAction,
|
||||
provider: Arc<dyn CodeActionProvider>,
|
||||
},
|
||||
}
|
||||
|
||||
impl CodeActionsItem {
|
||||
fn as_task(&self) -> Option<&ResolvedTask> {
|
||||
let Self::Task(_, task) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(task)
|
||||
}
|
||||
|
||||
fn as_code_action(&self) -> Option<&CodeAction> {
|
||||
let Self::CodeAction { action, .. } = self else {
|
||||
return None;
|
||||
};
|
||||
Some(action)
|
||||
}
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title.clone(),
|
||||
Self::Task(_, task) => task.resolved_label.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodeActionsMenu {
|
||||
pub actions: CodeActionContents,
|
||||
pub buffer: Model<Buffer>,
|
||||
pub selected_item: usize,
|
||||
pub scroll_handle: UniformListScrollHandle,
|
||||
pub deployed_from_indicator: Option<DisplayRow>,
|
||||
}
|
||||
|
||||
impl CodeActionsMenu {
|
||||
fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
self.selected_item = 0;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item -= 1;
|
||||
} else {
|
||||
self.selected_item = self.actions.len() - 1;
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
if self.selected_item + 1 < self.actions.len() {
|
||||
self.selected_item += 1;
|
||||
} else {
|
||||
self.selected_item = 0;
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
|
||||
self.selected_item = self.actions.len() - 1;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn visible(&self) -> bool {
|
||||
!self.actions.is_empty()
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
cursor_position: DisplayPoint,
|
||||
_style: &EditorStyle,
|
||||
max_height: Pixels,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> (ContextMenuOrigin, AnyElement) {
|
||||
let actions = self.actions.clone();
|
||||
let selected_item = self.selected_item;
|
||||
let element = uniform_list(
|
||||
cx.view().clone(),
|
||||
"code_actions_menu",
|
||||
self.actions.len(),
|
||||
move |_this, range, cx| {
|
||||
actions
|
||||
.iter()
|
||||
.skip(range.start)
|
||||
.take(range.end - range.start)
|
||||
.enumerate()
|
||||
.map(|(ix, action)| {
|
||||
let item_ix = range.start + ix;
|
||||
let selected = selected_item == item_ix;
|
||||
let colors = cx.theme().colors();
|
||||
div()
|
||||
.px_1()
|
||||
.rounded_md()
|
||||
.text_color(colors.text)
|
||||
.when(selected, |style| {
|
||||
style
|
||||
.bg(colors.element_active)
|
||||
.text_color(colors.text_accent)
|
||||
})
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(colors.element_hover)
|
||||
.text_color(colors.text_accent)
|
||||
})
|
||||
.whitespace_nowrap()
|
||||
.when_some(action.as_code_action(), |this, action| {
|
||||
this.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}),
|
||||
)
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
.child(SharedString::from(
|
||||
action.lsp_action.title.replace("\n", ""),
|
||||
))
|
||||
})
|
||||
.when_some(action.as_task(), |this, task| {
|
||||
this.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(SharedString::from(task.resolved_label.replace("\n", "")))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.elevation_1(cx)
|
||||
.p_1()
|
||||
.max_h(max_height)
|
||||
.occlude()
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.with_width_from_item(
|
||||
self.actions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|(_, action)| match action {
|
||||
CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
|
||||
CodeActionsItem::CodeAction { action, .. } => {
|
||||
action.lsp_action.title.chars().count()
|
||||
}
|
||||
})
|
||||
.map(|(ix, _)| ix),
|
||||
)
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer)
|
||||
.into_any_element();
|
||||
|
||||
let cursor_position = if let Some(row) = self.deployed_from_indicator {
|
||||
ContextMenuOrigin::GutterIndicator(row)
|
||||
} else {
|
||||
ContextMenuOrigin::EditorPoint(cursor_position)
|
||||
};
|
||||
|
||||
(cursor_position, element)
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::{channel::oneshot, FutureExt};
|
||||
use gpui::{Task, ViewContext};
|
||||
|
||||
use crate::Editor;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DebouncedDelay {
|
||||
task: Option<Task<()>>,
|
||||
cancel_channel: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl DebouncedDelay {
|
||||
pub fn new() -> DebouncedDelay {
|
||||
DebouncedDelay {
|
||||
task: None,
|
||||
cancel_channel: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Editor>, func: F)
|
||||
where
|
||||
F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> Task<()>,
|
||||
{
|
||||
if let Some(channel) = self.cancel_channel.take() {
|
||||
_ = channel.send(());
|
||||
}
|
||||
|
||||
let (sender, mut receiver) = oneshot::channel::<()>();
|
||||
self.cancel_channel = Some(sender);
|
||||
|
||||
drop(self.task.take());
|
||||
self.task = Some(cx.spawn(move |model, mut cx| async move {
|
||||
let mut timer = cx.background_executor().timer(delay).fuse();
|
||||
futures::select_biased! {
|
||||
_ = receiver => return,
|
||||
_ = timer => {}
|
||||
}
|
||||
|
||||
if let Ok(task) = model.update(&mut cx, |project, cx| (func)(project, cx)) {
|
||||
task.await;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ pub trait ToDisplayPoint {
|
||||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
|
||||
}
|
||||
|
||||
type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
|
||||
type TextHighlights = TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
|
||||
type InlayHighlights = TreeMap<TypeId, TreeMap<InlayId, (HighlightStyle, InlayHighlight)>>;
|
||||
|
||||
/// Decides how text in a [`MultiBuffer`] should be displayed in a buffer, handling inlay hints,
|
||||
@@ -434,7 +434,7 @@ impl DisplayMap {
|
||||
style: HighlightStyle,
|
||||
) {
|
||||
self.text_highlights
|
||||
.insert(Some(type_id), Arc::new((style, ranges)));
|
||||
.insert(type_id, Arc::new((style, ranges)));
|
||||
}
|
||||
|
||||
pub(crate) fn highlight_inlays(
|
||||
@@ -457,11 +457,11 @@ impl DisplayMap {
|
||||
}
|
||||
|
||||
pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
|
||||
let highlights = self.text_highlights.get(&Some(type_id))?;
|
||||
let highlights = self.text_highlights.get(&type_id)?;
|
||||
Some((highlights.0, &highlights.1))
|
||||
}
|
||||
pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
|
||||
let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some();
|
||||
let mut cleared = self.text_highlights.remove(&type_id).is_some();
|
||||
cleared |= self.inlay_highlights.remove(&type_id).is_some();
|
||||
cleared
|
||||
}
|
||||
@@ -1125,6 +1125,12 @@ impl DisplaySnapshot {
|
||||
DisplayRow(self.block_snapshot.longest_row())
|
||||
}
|
||||
|
||||
pub fn longest_row_in_range(&self, range: Range<DisplayRow>) -> DisplayRow {
|
||||
let block_range = BlockRow(range.start.0)..BlockRow(range.end.0);
|
||||
let longest_row = self.block_snapshot.longest_row_in_range(block_range);
|
||||
DisplayRow(longest_row.0)
|
||||
}
|
||||
|
||||
pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool {
|
||||
let max_row = self.buffer_snapshot.max_row();
|
||||
if buffer_row >= max_row {
|
||||
@@ -1239,7 +1245,7 @@ impl DisplaySnapshot {
|
||||
&self,
|
||||
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
|
||||
let type_id = TypeId::of::<Tag>();
|
||||
self.text_highlights.get(&Some(type_id)).cloned()
|
||||
self.text_highlights.get(&type_id).cloned()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
|
||||
@@ -1339,6 +1339,57 @@ impl BlockSnapshot {
|
||||
self.transforms.summary().longest_row
|
||||
}
|
||||
|
||||
pub fn longest_row_in_range(&self, range: Range<BlockRow>) -> BlockRow {
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
|
||||
let mut longest_row = range.start;
|
||||
let mut longest_row_chars = 0;
|
||||
if let Some(transform) = cursor.item() {
|
||||
if transform.block.is_none() {
|
||||
let (output_start, input_start) = cursor.start();
|
||||
let overshoot = range.start.0 - output_start.0;
|
||||
let wrap_start_row = input_start.0 + overshoot;
|
||||
let wrap_end_row = cmp::min(
|
||||
input_start.0 + (range.end.0 - output_start.0),
|
||||
cursor.end(&()).1 .0,
|
||||
);
|
||||
let summary = self
|
||||
.wrap_snapshot
|
||||
.text_summary_for_range(wrap_start_row..wrap_end_row);
|
||||
longest_row = BlockRow(range.start.0 + summary.longest_row);
|
||||
longest_row_chars = summary.longest_row_chars;
|
||||
}
|
||||
cursor.next(&());
|
||||
}
|
||||
|
||||
let cursor_start_row = cursor.start().0;
|
||||
if range.end > cursor_start_row {
|
||||
let summary = cursor.summary::<_, TransformSummary>(&range.end, Bias::Right, &());
|
||||
if summary.longest_row_chars > longest_row_chars {
|
||||
longest_row = BlockRow(cursor_start_row.0 + summary.longest_row);
|
||||
longest_row_chars = summary.longest_row_chars;
|
||||
}
|
||||
|
||||
if let Some(transform) = cursor.item() {
|
||||
if transform.block.is_none() {
|
||||
let (output_start, input_start) = cursor.start();
|
||||
let overshoot = range.end.0 - output_start.0;
|
||||
let wrap_start_row = input_start.0;
|
||||
let wrap_end_row = input_start.0 + overshoot;
|
||||
let summary = self
|
||||
.wrap_snapshot
|
||||
.text_summary_for_range(wrap_start_row..wrap_end_row);
|
||||
if summary.longest_row_chars > longest_row_chars {
|
||||
longest_row = BlockRow(output_start.0 + summary.longest_row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
longest_row
|
||||
}
|
||||
|
||||
pub(super) fn line_len(&self, row: BlockRow) -> u32 {
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
|
||||
cursor.seek(&BlockRow(row.0), Bias::Right, &());
|
||||
@@ -2705,6 +2756,40 @@ mod tests {
|
||||
longest_line_len,
|
||||
);
|
||||
|
||||
for _ in 0..10 {
|
||||
let end_row = rng.gen_range(1..=expected_lines.len());
|
||||
let start_row = rng.gen_range(0..end_row);
|
||||
|
||||
let mut expected_longest_rows_in_range = vec![];
|
||||
let mut longest_line_len_in_range = 0;
|
||||
|
||||
let mut row = start_row as u32;
|
||||
for line in &expected_lines[start_row..end_row] {
|
||||
let line_char_count = line.chars().count() as isize;
|
||||
match line_char_count.cmp(&longest_line_len_in_range) {
|
||||
Ordering::Less => {}
|
||||
Ordering::Equal => expected_longest_rows_in_range.push(row),
|
||||
Ordering::Greater => {
|
||||
longest_line_len_in_range = line_char_count;
|
||||
expected_longest_rows_in_range.clear();
|
||||
expected_longest_rows_in_range.push(row);
|
||||
}
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
|
||||
let longest_row_in_range = blocks_snapshot
|
||||
.longest_row_in_range(BlockRow(start_row as u32)..BlockRow(end_row as u32));
|
||||
assert!(
|
||||
expected_longest_rows_in_range.contains(&longest_row_in_range.0),
|
||||
"incorrect longest row {} in range {:?}. expected {:?} with length {}",
|
||||
longest_row,
|
||||
start_row..end_row,
|
||||
expected_longest_rows_in_range,
|
||||
longest_line_len_in_range,
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure that conversion between block points and wrap points is stable.
|
||||
for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
|
||||
let wrap_point = WrapPoint::new(row, 0);
|
||||
|
||||
@@ -211,7 +211,7 @@ pub struct InlayBufferRows<'a> {
|
||||
struct HighlightEndpoint {
|
||||
offset: InlayOffset,
|
||||
is_start: bool,
|
||||
tag: Option<TypeId>,
|
||||
tag: TypeId,
|
||||
style: HighlightStyle,
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ pub struct InlayChunks<'a> {
|
||||
max_output_offset: InlayOffset,
|
||||
highlight_styles: HighlightStyles,
|
||||
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
|
||||
active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
|
||||
active_highlights: BTreeMap<TypeId, HighlightStyle>,
|
||||
highlights: Highlights<'a>,
|
||||
snapshot: &'a InlaySnapshot,
|
||||
}
|
||||
@@ -1096,7 +1096,7 @@ impl InlaySnapshot {
|
||||
&self,
|
||||
cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>,
|
||||
range: &Range<InlayOffset>,
|
||||
text_highlights: &TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
|
||||
text_highlights: &TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
|
||||
highlight_endpoints: &mut Vec<HighlightEndpoint>,
|
||||
) {
|
||||
while cursor.start().0 < range.end {
|
||||
@@ -1112,7 +1112,7 @@ impl InlaySnapshot {
|
||||
)))
|
||||
};
|
||||
|
||||
for (tag, text_highlights) in text_highlights.iter() {
|
||||
for (&tag, text_highlights) in text_highlights.iter() {
|
||||
let style = text_highlights.0;
|
||||
let ranges = &text_highlights.1;
|
||||
|
||||
@@ -1134,13 +1134,13 @@ impl InlaySnapshot {
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)),
|
||||
is_start: true,
|
||||
tag: *tag,
|
||||
tag,
|
||||
style,
|
||||
});
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
|
||||
is_start: false,
|
||||
tag: *tag,
|
||||
tag,
|
||||
style,
|
||||
});
|
||||
}
|
||||
@@ -1708,7 +1708,7 @@ mod tests {
|
||||
text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
|
||||
log::info!("highlighting text ranges {text_highlight_ranges:?}");
|
||||
text_highlights.insert(
|
||||
Some(TypeId::of::<()>()),
|
||||
TypeId::of::<()>(),
|
||||
Arc::new((
|
||||
HighlightStyle::default(),
|
||||
text_highlight_ranges
|
||||
|
||||
@@ -10,9 +10,6 @@ pub struct EditorSettings {
|
||||
pub cursor_shape: Option<CursorShape>,
|
||||
pub current_line_highlight: CurrentLineHighlight,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub show_completions_on_input: bool,
|
||||
pub show_completion_documentation: bool,
|
||||
pub completion_documentation_secondary_query_debounce: u64,
|
||||
pub toolbar: Toolbar,
|
||||
pub scrollbar: Scrollbar,
|
||||
pub gutter: Gutter,
|
||||
@@ -194,21 +191,6 @@ pub struct EditorSettingsContent {
|
||||
/// Default: true
|
||||
pub hover_popover_enabled: Option<bool>,
|
||||
|
||||
/// Whether to pop the completions menu while typing in an editor without
|
||||
/// explicitly requesting it.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_completions_on_input: Option<bool>,
|
||||
/// Whether to display inline and alongside documentation for items in the
|
||||
/// completions menu.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_completion_documentation: Option<bool>,
|
||||
/// The debounce delay before re-querying the language server for completion
|
||||
/// documentation when not included in original completion list.
|
||||
///
|
||||
/// Default: 300 ms
|
||||
pub completion_documentation_secondary_query_debounce: Option<u64>,
|
||||
/// Toolbar related settings
|
||||
pub toolbar: Option<ToolbarContent>,
|
||||
/// Scrollbar related settings
|
||||
|
||||
@@ -9,8 +9,8 @@ use crate::{
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
div, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, WindowBounds,
|
||||
WindowOptions,
|
||||
div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
|
||||
WindowBounds, WindowOptions,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
@@ -25,16 +25,19 @@ use language::{
|
||||
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
||||
use multi_buffer::MultiBufferIndentGuide;
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::{buffer_store::BufferChangeSet, FakeFs};
|
||||
use project::{
|
||||
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use test::editor_lsp_test_context::rust_lang;
|
||||
use std::{
|
||||
iter,
|
||||
sync::atomic::{self, AtomicUsize},
|
||||
};
|
||||
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
@@ -6836,14 +6839,15 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let (editor, cx) =
|
||||
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let save = editor
|
||||
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
|
||||
.unwrap();
|
||||
@@ -7117,6 +7121,7 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
assert!(!buffer.is_dirty());
|
||||
assert_eq!(buffer.text(), sample_text_3,)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let save = multi_buffer_editor
|
||||
@@ -7188,14 +7193,15 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
let (editor, cx) =
|
||||
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let save = editor
|
||||
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
|
||||
.unwrap();
|
||||
@@ -7339,13 +7345,14 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) =
|
||||
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
|
||||
let format = editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.perform_format(
|
||||
@@ -8376,12 +8383,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.show_completions_on_input = Some(false);
|
||||
});
|
||||
})
|
||||
update_test_language_settings(&mut cx, |settings| {
|
||||
settings.defaults.show_completions_on_input = Some(false);
|
||||
});
|
||||
cx.set_state("editorˇ");
|
||||
cx.simulate_keystroke(".");
|
||||
@@ -8447,7 +8450,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["first", "last"]
|
||||
@@ -8459,7 +8462,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.move_page_down(&MovePageDown::default(), cx);
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert!(
|
||||
menu.selected_item == 1,
|
||||
"expected PageDown to select the last item from the context menu"
|
||||
@@ -8471,7 +8474,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.move_page_up(&MovePageUp::default(), cx);
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert!(
|
||||
menu.selected_item == 0,
|
||||
"expected PageUp to select the first item from the context menu"
|
||||
@@ -8539,7 +8542,7 @@ async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["r", "ret", "Range", "return"]
|
||||
@@ -9927,7 +9930,8 @@ async fn go_to_prev_overlapping_diagnostic(
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
|
||||
let lsp_store =
|
||||
cx.update_editor(|editor, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
ˇfn func(abc def: i32) -> u32 {
|
||||
@@ -9935,8 +9939,8 @@ async fn go_to_prev_overlapping_diagnostic(
|
||||
"});
|
||||
|
||||
cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store
|
||||
.update_diagnostics(
|
||||
LanguageServerId(0),
|
||||
lsp::PublishDiagnosticsParams {
|
||||
@@ -10025,11 +10029,12 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
|
||||
fn func(abˇc def: i32) -> u32 {
|
||||
}
|
||||
"});
|
||||
let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
|
||||
let lsp_store =
|
||||
cx.update_editor(|editor, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
|
||||
|
||||
cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.update_diagnostics(
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.update_diagnostics(
|
||||
LanguageServerId(0),
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path("/root/file").unwrap(),
|
||||
@@ -10334,9 +10339,6 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
let editor_handle = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
@@ -10347,6 +10349,9 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
@@ -10436,7 +10441,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
|
||||
let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let _buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/a/main.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/a/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -10670,12 +10675,12 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
|
||||
.as_ref()
|
||||
.expect("Should have the context menu deployed");
|
||||
match context_menu {
|
||||
ContextMenu::Completions(completions_menu) => {
|
||||
CodeContextMenu::Completions(completions_menu) => {
|
||||
let completions = completions_menu.completions.read();
|
||||
assert_eq!(completions.len(), 1, "Should have one completion");
|
||||
assert_eq!(completions.get(0).unwrap().label.text, "unresolved");
|
||||
}
|
||||
ContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
|
||||
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10701,7 +10706,7 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
|
||||
.as_ref()
|
||||
.expect("Should have the context menu deployed");
|
||||
match context_menu {
|
||||
ContextMenu::Completions(completions_menu) => {
|
||||
CodeContextMenu::Completions(completions_menu) => {
|
||||
let completions = completions_menu.completions.read();
|
||||
assert_eq!(completions.len(), 1, "Should have one completion");
|
||||
assert_eq!(
|
||||
@@ -10710,7 +10715,7 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
|
||||
"Should update the completion label after resolving"
|
||||
);
|
||||
}
|
||||
ContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
|
||||
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -10719,6 +10724,62 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
|
||||
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let item_0 = lsp::CompletionItem {
|
||||
label: "abs".into(),
|
||||
insert_text: Some("abs".into()),
|
||||
data: Some(json!({ "very": "special"})),
|
||||
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: "abs".to_string(),
|
||||
insert: lsp::Range::default(),
|
||||
replace: lsp::Range::default(),
|
||||
},
|
||||
)),
|
||||
..lsp::CompletionItem::default()
|
||||
};
|
||||
let items = iter::once(item_0.clone())
|
||||
.chain((11..51).map(|i| lsp::CompletionItem {
|
||||
label: format!("item_{}", i),
|
||||
insert_text: Some(format!("item_{}", i)),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
..lsp::CompletionItem::default()
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let default_commit_characters = vec!["?".to_string()];
|
||||
let default_data = json!({ "default": "data"});
|
||||
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
|
||||
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
|
||||
let default_edit_range = lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
};
|
||||
|
||||
let item_0_out = lsp::CompletionItem {
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
insert_text_format: Some(default_insert_text_format),
|
||||
..item_0
|
||||
};
|
||||
let items_out = iter::once(item_0_out)
|
||||
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
data: Some(default_data.clone()),
|
||||
insert_text_mode: Some(default_insert_text_mode),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: default_edit_range,
|
||||
new_text: item.label.clone(),
|
||||
})),
|
||||
..item.clone()
|
||||
}))
|
||||
.collect::<Vec<lsp::CompletionItem>>();
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
@@ -10735,138 +10796,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
|
||||
cx.simulate_keystroke(".");
|
||||
|
||||
let default_commit_characters = vec!["?".to_string()];
|
||||
let default_data = json!({ "very": "special"});
|
||||
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
|
||||
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
|
||||
let default_edit_range = lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
};
|
||||
|
||||
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
|
||||
let expect_first_item = Arc::new(AtomicBool::new(true));
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
|
||||
let closure_default_data = default_data.clone();
|
||||
let closure_resolve_requests_number = resolve_requests_number.clone();
|
||||
let closure_expect_first_item = expect_first_item.clone();
|
||||
let closure_default_commit_characters = default_commit_characters.clone();
|
||||
move |item_to_resolve, _| {
|
||||
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
|
||||
let default_data = closure_default_data.clone();
|
||||
let default_commit_characters = closure_default_commit_characters.clone();
|
||||
let expect_first_item = closure_expect_first_item.clone();
|
||||
async move {
|
||||
if expect_first_item.load(atomic::Ordering::Acquire) {
|
||||
assert_eq!(
|
||||
item_to_resolve.label, "Some(2)",
|
||||
"Should have selected the first item"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.data,
|
||||
Some(json!({ "very": "special"})),
|
||||
"First item should bring its own data for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.commit_characters,
|
||||
Some(default_commit_characters),
|
||||
"First item had no own commit characters and should inherit the default ones"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
item_to_resolve.text_edit,
|
||||
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
|
||||
),
|
||||
"First item should bring its own edit range for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_format,
|
||||
Some(default_insert_text_format),
|
||||
"First item had no own insert text format and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_mode,
|
||||
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
"First item should bring its own insert text mode for resolving"
|
||||
);
|
||||
Ok(item_to_resolve)
|
||||
} else {
|
||||
assert_eq!(
|
||||
item_to_resolve.label, "vec![2]",
|
||||
"Should have selected the last item"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.data,
|
||||
Some(default_data),
|
||||
"Last item has no own resolve data and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.commit_characters,
|
||||
Some(default_commit_characters),
|
||||
"Last item had no own commit characters and should inherit the default ones"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.text_edit,
|
||||
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: default_edit_range,
|
||||
new_text: "vec![2]".to_string()
|
||||
})),
|
||||
"Last item had no own edit range and should inherit the default one"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_format,
|
||||
Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
"Last item should bring its own insert text format for resolving"
|
||||
);
|
||||
assert_eq!(
|
||||
item_to_resolve.insert_text_mode,
|
||||
Some(default_insert_text_mode),
|
||||
"Last item had no own insert text mode and should inherit the default one"
|
||||
);
|
||||
|
||||
Ok(item_to_resolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
|
||||
let completion_data = default_data.clone();
|
||||
let completion_characters = default_commit_characters.clone();
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||||
let default_data = completion_data.clone();
|
||||
let default_commit_characters = completion_characters.clone();
|
||||
let items = items.clone();
|
||||
async move {
|
||||
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
items: vec![
|
||||
lsp::CompletionItem {
|
||||
label: "Some(2)".into(),
|
||||
insert_text: Some("Some(2)".into()),
|
||||
data: Some(json!({ "very": "special"})),
|
||||
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: "Some(2)".to_string(),
|
||||
insert: lsp::Range::default(),
|
||||
replace: lsp::Range::default(),
|
||||
},
|
||||
)),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "vec![2]".into(),
|
||||
insert_text: Some("vec![2]".into()),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
],
|
||||
items,
|
||||
item_defaults: Some(lsp::CompletionListItemDefaults {
|
||||
data: Some(default_data.clone()),
|
||||
commit_characters: Some(default_commit_characters.clone()),
|
||||
@@ -10883,51 +10821,76 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
.next()
|
||||
.await;
|
||||
|
||||
let resolved_items = Arc::new(Mutex::new(Vec::new()));
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
|
||||
let closure_resolved_items = resolved_items.clone();
|
||||
move |item_to_resolve, _| {
|
||||
let closure_resolved_items = closure_resolved_items.clone();
|
||||
async move {
|
||||
closure_resolved_items.lock().push(item_to_resolve.clone());
|
||||
Ok(item_to_resolve)
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
let menu = editor.context_menu.read();
|
||||
match menu.as_ref().expect("should have the completions menu") {
|
||||
ContextMenu::Completions(completions_menu) => {
|
||||
CodeContextMenu::Completions(completions_menu) => {
|
||||
assert_eq!(
|
||||
completions_menu
|
||||
.matches
|
||||
.iter()
|
||||
.map(|c| c.string.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Some(2)", "vec![2]"]
|
||||
.map(|c| c.string.clone())
|
||||
.collect::<Vec<String>>(),
|
||||
items_out
|
||||
.iter()
|
||||
.map(|completion| completion.label.clone())
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
}
|
||||
ContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
|
||||
CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
|
||||
}
|
||||
});
|
||||
// Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
|
||||
// with 4 from the end.
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
|
||||
*resolved_items.lock(),
|
||||
[
|
||||
&items_out[0..16],
|
||||
&items_out[items_out.len() - 4..items_out.len()]
|
||||
]
|
||||
.concat()
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
);
|
||||
resolved_items.lock().clear();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_first(&ContextMenuFirst, cx);
|
||||
editor.context_menu_prev(&ContextMenuPrev, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
// Completions that have already been resolved are skipped.
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
2,
|
||||
"After re-selecting the first item, another resolve request should have been sent"
|
||||
);
|
||||
|
||||
expect_first_item.store(false, atomic::Ordering::Release);
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_last(&ContextMenuLast, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
resolve_requests_number.load(atomic::Ordering::Acquire),
|
||||
3,
|
||||
"After selecting the other item, another resolve request should have been sent"
|
||||
*resolved_items.lock(),
|
||||
[
|
||||
// Selected item is always resolved even if it was resolved before.
|
||||
&items_out[items_out.len() - 1..items_out.len()],
|
||||
&items_out[items_out.len() - 16..items_out.len() - 4]
|
||||
]
|
||||
.concat()
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
);
|
||||
resolved_items.lock().clear();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -10992,7 +10955,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.simulate_keystroke("-");
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-red", "bg-blue", "bg-yellow"]
|
||||
@@ -11005,7 +10968,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.simulate_keystroke("l");
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-blue", "bg-yellow"]
|
||||
@@ -11021,7 +10984,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.simulate_keystroke("l");
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-yellow"]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
|
||||
code_context_menus::CodeActionsMenu,
|
||||
display_map::{
|
||||
Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
|
||||
},
|
||||
@@ -16,13 +17,13 @@ use crate::{
|
||||
items::BufferSearchHighlights,
|
||||
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint,
|
||||
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
|
||||
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
|
||||
DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
|
||||
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
|
||||
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts,
|
||||
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
|
||||
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
|
||||
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, LineDown,
|
||||
LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection,
|
||||
SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
|
||||
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
};
|
||||
use client::ParticipantIndex;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
@@ -31,7 +32,7 @@ use gpui::{
|
||||
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
|
||||
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
|
||||
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
|
||||
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
|
||||
@@ -47,7 +48,10 @@ use language::{
|
||||
ChunkRendererContext,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{Anchor, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow};
|
||||
use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
MultiBufferSnapshot, ToOffset,
|
||||
};
|
||||
use project::{
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
ProjectPath,
|
||||
@@ -1677,7 +1681,7 @@ impl EditorElement {
|
||||
) -> Vec<AnyElement> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let active_task_indicator_row =
|
||||
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
|
||||
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
|
||||
deployed_from_indicator,
|
||||
actions,
|
||||
..
|
||||
@@ -1692,16 +1696,23 @@ impl EditorElement {
|
||||
None
|
||||
};
|
||||
|
||||
let offset_range_start = snapshot
|
||||
.display_point_to_anchor(DisplayPoint::new(range.start, 0), Bias::Left)
|
||||
.to_offset(&snapshot.buffer_snapshot);
|
||||
let offset_range_end = snapshot
|
||||
.display_point_to_anchor(DisplayPoint::new(range.end, 0), Bias::Right)
|
||||
.to_offset(&snapshot.buffer_snapshot);
|
||||
|
||||
editor
|
||||
.tasks
|
||||
.iter()
|
||||
.filter_map(|(_, tasks)| {
|
||||
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
|
||||
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
|
||||
let display_row = multibuffer_point.to_display_point(snapshot).row();
|
||||
if range.start > display_row || range.end < display_row {
|
||||
if tasks.offset.0 < offset_range_start || tasks.offset.0 >= offset_range_end {
|
||||
return None;
|
||||
}
|
||||
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
|
||||
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
|
||||
|
||||
if snapshot.is_line_folded(multibuffer_row) {
|
||||
// Skip folded indicators, unless it's the starting line of a fold.
|
||||
if multibuffer_row
|
||||
@@ -1714,6 +1725,7 @@ impl EditorElement {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let display_row = multibuffer_point.to_display_point(snapshot).row();
|
||||
let button = editor.render_run_indicator(
|
||||
&self.style,
|
||||
Some(display_row) == active_task_indicator_row,
|
||||
@@ -1752,7 +1764,7 @@ impl EditorElement {
|
||||
let mut button = None;
|
||||
let row = newest_selection_head.row();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
|
||||
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
|
||||
deployed_from_indicator,
|
||||
..
|
||||
})) = editor.context_menu.read().as_ref()
|
||||
@@ -2720,6 +2732,156 @@ impl EditorElement {
|
||||
true
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_completion_popover(
|
||||
&self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
visible_row_range: Range<DisplayRow>,
|
||||
scroll_top: f32,
|
||||
scroll_bottom: f32,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
editor_width: Pixels,
|
||||
style: &EditorStyle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
const PADDING_X: Pixels = Pixels(25.);
|
||||
const PADDING_Y: Pixels = Pixels(2.);
|
||||
|
||||
let active_inline_completion = self.editor.read(cx).active_inline_completion.as_ref()?;
|
||||
|
||||
match &active_inline_completion.completion {
|
||||
InlineCompletion::Move(target_position) => {
|
||||
let container_element = div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.px_1();
|
||||
|
||||
let target_display_point = target_position.to_display_point(editor_snapshot);
|
||||
if target_display_point.row().as_f32() < scroll_top {
|
||||
let mut element = container_element
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Tab))
|
||||
.child(Label::new("Jump to Edit"))
|
||||
.child(Icon::new(IconName::ArrowUp)),
|
||||
)
|
||||
.into_any();
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y);
|
||||
element.prepaint_at(text_bounds.origin + offset, cx);
|
||||
Some(element)
|
||||
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
|
||||
let mut element = container_element
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Tab))
|
||||
.child(Label::new("Jump to Edit"))
|
||||
.child(Icon::new(IconName::ArrowDown)),
|
||||
)
|
||||
.into_any();
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
let offset = point(
|
||||
(text_bounds.size.width - size.width) / 2.,
|
||||
text_bounds.size.height - size.height - PADDING_Y,
|
||||
);
|
||||
element.prepaint_at(text_bounds.origin + offset, cx);
|
||||
Some(element)
|
||||
} else {
|
||||
let mut element = container_element
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Tab))
|
||||
.child(Label::new("Jump to Edit")),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let target_line_end = DisplayPoint::new(
|
||||
target_display_point.row(),
|
||||
editor_snapshot.line_len(target_display_point.row()),
|
||||
);
|
||||
let origin = self.editor.update(cx, |editor, cx| {
|
||||
editor.display_to_pixel_point(target_line_end, editor_snapshot, cx)
|
||||
})?;
|
||||
element.prepaint_as_root(
|
||||
text_bounds.origin + origin + point(PADDING_X, px(0.)),
|
||||
AvailableSpace::min_size(),
|
||||
cx,
|
||||
);
|
||||
Some(element)
|
||||
}
|
||||
}
|
||||
InlineCompletion::Edit(edits) => {
|
||||
let edit_start = edits
|
||||
.first()
|
||||
.unwrap()
|
||||
.0
|
||||
.start
|
||||
.to_display_point(editor_snapshot);
|
||||
let edit_end = edits
|
||||
.last()
|
||||
.unwrap()
|
||||
.0
|
||||
.end
|
||||
.to_display_point(editor_snapshot);
|
||||
|
||||
let is_visible = visible_row_range.contains(&edit_start.row())
|
||||
|| visible_row_range.contains(&edit_end.row());
|
||||
if !is_visible {
|
||||
return None;
|
||||
}
|
||||
|
||||
if all_edits_insertions_or_deletions(edits, &editor_snapshot.buffer_snapshot) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (text, highlights) = inline_completion_popover_text(editor_snapshot, edits, cx);
|
||||
|
||||
let longest_row =
|
||||
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
|
||||
let longest_line_width = if visible_row_range.contains(&longest_row) {
|
||||
line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
|
||||
} else {
|
||||
layout_line(
|
||||
longest_row,
|
||||
editor_snapshot,
|
||||
style,
|
||||
editor_width,
|
||||
|_| false,
|
||||
cx,
|
||||
)
|
||||
.width
|
||||
};
|
||||
|
||||
let text = gpui::StyledText::new(text).with_highlights(&style.text, highlights);
|
||||
|
||||
let mut element = div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.px_1()
|
||||
.child(text)
|
||||
.into_any();
|
||||
|
||||
let origin = text_bounds.origin
|
||||
+ point(
|
||||
longest_line_width + PADDING_X - scroll_pixel_position.x,
|
||||
edit_start.row().as_f32() * line_height - scroll_pixel_position.y,
|
||||
);
|
||||
element.prepaint_as_root(origin, AvailableSpace::min_size(), cx);
|
||||
Some(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_mouse_context_menu(
|
||||
&self,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
@@ -3942,6 +4104,16 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_inline_completion_popover(
|
||||
&mut self,
|
||||
layout: &mut EditorLayout,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() {
|
||||
inline_completion_popover.paint(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_mouse_context_menu(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() {
|
||||
mouse_context_menu.paint(cx);
|
||||
@@ -4134,6 +4306,89 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn inline_completion_popover_text(
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
cx: &WindowContext,
|
||||
) -> (String, Vec<(Range<usize>, HighlightStyle)>) {
|
||||
let edit_start = edits
|
||||
.first()
|
||||
.unwrap()
|
||||
.0
|
||||
.start
|
||||
.to_display_point(editor_snapshot);
|
||||
|
||||
let mut text = String::new();
|
||||
let mut offset = DisplayPoint::new(edit_start.row(), 0).to_offset(editor_snapshot, Bias::Left);
|
||||
let mut highlights = Vec::new();
|
||||
for (old_range, new_text) in edits {
|
||||
let old_offset_range = old_range.to_offset(&editor_snapshot.buffer_snapshot);
|
||||
text.extend(
|
||||
editor_snapshot
|
||||
.buffer_snapshot
|
||||
.chunks(offset..old_offset_range.start, false)
|
||||
.map(|chunk| chunk.text),
|
||||
);
|
||||
offset = old_offset_range.end;
|
||||
|
||||
let start = text.len();
|
||||
text.push_str(new_text);
|
||||
let end = text.len();
|
||||
highlights.push((
|
||||
start..end,
|
||||
HighlightStyle {
|
||||
background_color: Some(cx.theme().status().created_background),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let edit_end = edits
|
||||
.last()
|
||||
.unwrap()
|
||||
.0
|
||||
.end
|
||||
.to_display_point(editor_snapshot);
|
||||
let end_of_line = DisplayPoint::new(edit_end.row(), editor_snapshot.line_len(edit_end.row()))
|
||||
.to_offset(editor_snapshot, Bias::Right);
|
||||
text.extend(
|
||||
editor_snapshot
|
||||
.buffer_snapshot
|
||||
.chunks(offset..end_of_line, false)
|
||||
.map(|chunk| chunk.text),
|
||||
);
|
||||
|
||||
(text, highlights)
|
||||
}
|
||||
|
||||
fn all_edits_insertions_or_deletions(
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> bool {
|
||||
let mut all_insertions = true;
|
||||
let mut all_deletions = true;
|
||||
|
||||
for (range, new_text) in edits.iter() {
|
||||
let range_is_empty = range.to_offset(&snapshot).is_empty();
|
||||
let text_is_empty = new_text.is_empty();
|
||||
|
||||
if range_is_empty != text_is_empty {
|
||||
if range_is_empty {
|
||||
all_deletions = false;
|
||||
} else {
|
||||
all_insertions = false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !all_insertions && !all_deletions {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
all_insertions || all_deletions
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_gutter_button(
|
||||
button: IconButton,
|
||||
@@ -5566,6 +5821,20 @@ impl Element for EditorElement {
|
||||
);
|
||||
}
|
||||
|
||||
let inline_completion_popover = self.layout_inline_completion_popover(
|
||||
&text_hitbox.bounds,
|
||||
&snapshot,
|
||||
start_row..end_row,
|
||||
scroll_position.y,
|
||||
scroll_position.y + height_in_lines,
|
||||
&line_layouts,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
editor_width,
|
||||
&style,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mouse_context_menu = self.layout_mouse_context_menu(
|
||||
&snapshot,
|
||||
start_row..end_row,
|
||||
@@ -5652,6 +5921,7 @@ impl Element for EditorElement {
|
||||
cursors,
|
||||
visible_cursors,
|
||||
selections,
|
||||
inline_completion_popover,
|
||||
mouse_context_menu,
|
||||
test_indicators,
|
||||
code_actions_indicator,
|
||||
@@ -5741,6 +6011,7 @@ impl Element for EditorElement {
|
||||
}
|
||||
|
||||
self.paint_scrollbar(layout, cx);
|
||||
self.paint_inline_completion_popover(layout, cx);
|
||||
self.paint_mouse_context_menu(layout, cx);
|
||||
});
|
||||
})
|
||||
@@ -5796,6 +6067,7 @@ pub struct EditorLayout {
|
||||
test_indicators: Vec<AnyElement>,
|
||||
crease_toggles: Vec<Option<AnyElement>>,
|
||||
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
|
||||
inline_completion_popover: Option<AnyElement>,
|
||||
mouse_context_menu: Option<AnyElement>,
|
||||
tab_invisible: ShapedLine,
|
||||
space_invisible: ShapedLine,
|
||||
@@ -6389,7 +6661,6 @@ mod tests {
|
||||
use language::language_settings;
|
||||
use log::info;
|
||||
use std::num::NonZeroU32;
|
||||
use ui::Context;
|
||||
use util::test::sample_text;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -6837,6 +7108,161 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_inline_completion_popover_text(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
// Test case 1: Simple insertion
|
||||
{
|
||||
let window = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
|
||||
let edits = vec![(edit_range, " beautiful".to_string())];
|
||||
|
||||
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
|
||||
|
||||
assert_eq!(text, "Hello, beautiful world!");
|
||||
assert_eq!(highlights.len(), 1);
|
||||
assert_eq!(highlights[0].0, 6..16);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Test case 2: Replacement
|
||||
{
|
||||
let window = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("This is a test.", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let edits = vec![(
|
||||
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 4)),
|
||||
"That".to_string(),
|
||||
)];
|
||||
|
||||
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
|
||||
|
||||
assert_eq!(text, "That is a test.");
|
||||
assert_eq!(highlights.len(), 1);
|
||||
assert_eq!(highlights[0].0, 0..4);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Test case 3: Multiple edits
|
||||
{
|
||||
let window = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let edits = vec![
|
||||
(
|
||||
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 5)),
|
||||
"Greetings".into(),
|
||||
),
|
||||
(
|
||||
snapshot.buffer_snapshot.anchor_after(Point::new(0, 12))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 12)),
|
||||
" and universe".into(),
|
||||
),
|
||||
];
|
||||
|
||||
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
|
||||
|
||||
assert_eq!(text, "Greetings, world and universe!");
|
||||
assert_eq!(highlights.len(), 2);
|
||||
assert_eq!(highlights[0].0, 0..9);
|
||||
assert_eq!(highlights[1].0, 16..29);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
assert_eq!(
|
||||
highlights[1].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Test case 4: Multiple lines with edits
|
||||
{
|
||||
let window = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple(
|
||||
"First line\nSecond line\nThird line\nFourth line",
|
||||
cx,
|
||||
);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let edits = vec![
|
||||
(
|
||||
snapshot.buffer_snapshot.anchor_before(Point::new(1, 7))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(1, 11)),
|
||||
"modified".to_string(),
|
||||
),
|
||||
(
|
||||
snapshot.buffer_snapshot.anchor_before(Point::new(2, 0))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(2, 10)),
|
||||
"New third line".to_string(),
|
||||
),
|
||||
(
|
||||
snapshot.buffer_snapshot.anchor_before(Point::new(3, 6))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)),
|
||||
" updated".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
|
||||
|
||||
assert_eq!(text, "Second modified\nNew third line\nFourth updated line");
|
||||
assert_eq!(highlights.len(), 3);
|
||||
assert_eq!(highlights[0].0, 7..15); // "modified"
|
||||
assert_eq!(highlights[1].0, 16..30); // "New third line"
|
||||
assert_eq!(highlights[2].0, 37..45); // " updated"
|
||||
|
||||
for highlight in &highlights {
|
||||
assert_eq!(
|
||||
highlight.1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_invisibles_from_new_editor(
|
||||
cx: &mut TestAppContext,
|
||||
editor_mode: EditorMode,
|
||||
|
||||
@@ -22,10 +22,7 @@ use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer}
|
||||
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
|
||||
use text::{OffsetRangeExt, ToPoint};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
div, h_flex, Color, Context, FluentBuilder, Icon, IconName, IntoElement, Label, LabelCommon,
|
||||
ParentElement, SharedString, Styled, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::{paths::compare_paths, ResultExt};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
@@ -243,7 +240,7 @@ impl ProjectDiffEditor {
|
||||
.map_err(|_| anyhow!("Unexpected non-buffer"))
|
||||
})
|
||||
.with_context(|| {
|
||||
format!("loading {} for git diff", entry_path.path.display())
|
||||
format!("loading {:?} for git diff", entry_path.path)
|
||||
})
|
||||
.log_err()
|
||||
else {
|
||||
@@ -313,11 +310,11 @@ impl ProjectDiffEditor {
|
||||
project_diff_editor
|
||||
.update(&mut cx, |project_diff_editor, cx| {
|
||||
project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx);
|
||||
for change_set in change_sets {
|
||||
project_diff_editor.editor.update(cx, |editor, cx| {
|
||||
project_diff_editor.editor.update(cx, |editor, cx| {
|
||||
for change_set in change_sets {
|
||||
editor.diff_map.add_change_set(change_set, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
@@ -1099,136 +1096,290 @@ impl Render for ProjectDiffEditor {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use futures::future::join_all;
|
||||
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
||||
use project::buffer_store::BufferChangeSet;
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, DistString},
|
||||
prelude::*,
|
||||
};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
ops::Deref as _,
|
||||
ffi::OsString,
|
||||
ops::Not,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use text::{Edit, Patch, Rope};
|
||||
|
||||
use super::*;
|
||||
|
||||
// TODO finish
|
||||
// #[gpui::test]
|
||||
// async fn randomized_tests(cx: &mut TestAppContext) {
|
||||
// // Create a new project (how?? temp fs?),
|
||||
// let fs = FakeFs::new(cx.executor());
|
||||
// let project = Project::test(fs, [], cx).await;
|
||||
struct TestFile {
|
||||
name: OsString,
|
||||
staged_text: String,
|
||||
buffer: text::BufferSnapshot,
|
||||
editor: View<Editor>,
|
||||
patch: Patch<usize>,
|
||||
}
|
||||
|
||||
// // create random files with random content
|
||||
|
||||
// // Commit it into git somehow (technically can do with "real" fs in a temp dir)
|
||||
// //
|
||||
// // Apply randomized changes to the project: select a random file, random change and apply to buffers
|
||||
// }
|
||||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn simple_edit_test(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn random_edits(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
let rng = &mut rng;
|
||||
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
".git": {},
|
||||
"file_a": "This is file_a",
|
||||
"file_b": "This is file_b",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [Path::new("/root")], cx).await;
|
||||
let names = ["file0", "file1", "file2", "file3", "file4"].map(str::to_owned);
|
||||
let fs = {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
let mut files = json!(names
|
||||
.clone()
|
||||
.into_iter()
|
||||
.zip(std::iter::repeat_with(|| gen_file(rng).into()))
|
||||
.collect::<serde_json::Map<_, _>>());
|
||||
files
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert(".git".to_owned(), json!({}));
|
||||
fs.insert_tree("/project", files).await;
|
||||
fs
|
||||
};
|
||||
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let file_a_editor = workspace
|
||||
let (file_editors, project_diff_editor) = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let file_a_editor =
|
||||
workspace.open_abs_path(PathBuf::from("/root/file_a"), true, cx);
|
||||
let file_editors = names.clone().map(|name| {
|
||||
workspace.open_abs_path(PathBuf::from(format!("/project/{}", name)), true, cx)
|
||||
});
|
||||
ProjectDiffEditor::deploy(workspace, &Deploy, cx);
|
||||
file_a_editor
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.expect("did not open an item at all")
|
||||
.downcast::<Editor>()
|
||||
.expect("did not open an editor for file_a");
|
||||
let project_diff_editor = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
let project_diff_editor = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| item.downcast::<ProjectDiffEditor>())
|
||||
.expect("Didn't open project diff editor");
|
||||
(file_editors, project_diff_editor)
|
||||
})
|
||||
.unwrap()
|
||||
.expect("did not find a ProjectDiffEditor");
|
||||
project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
assert!(
|
||||
project_diff_editor.editor.read(cx).text(cx).is_empty(),
|
||||
"Should have no changes after opening the diff on no git changes"
|
||||
);
|
||||
});
|
||||
|
||||
let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx));
|
||||
let change = "an edit after git add";
|
||||
file_a_editor
|
||||
.update(cx, |file_a_editor, cx| {
|
||||
file_a_editor.insert(change, cx);
|
||||
file_a_editor.save(false, project.clone(), cx)
|
||||
})
|
||||
.unwrap();
|
||||
let file_editors = join_all(file_editors)
|
||||
.await
|
||||
.expect("failed to save a file");
|
||||
file_a_editor.update(cx, |file_a_editor, cx| {
|
||||
let change_set = cx.new_model(|cx| {
|
||||
BufferChangeSet::new_with_base_text(
|
||||
old_text.clone(),
|
||||
file_a_editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.text_snapshot(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
file_a_editor
|
||||
.diff_map
|
||||
.add_change_set(change_set.clone(), cx);
|
||||
project.update(cx, |project, cx| {
|
||||
project.buffer_store().update(cx, |buffer_store, cx| {
|
||||
buffer_store.set_change_set(
|
||||
file_a_editor
|
||||
.into_iter()
|
||||
.map(|result| {
|
||||
result
|
||||
.expect("Failed to open file editor")
|
||||
.downcast::<Editor>()
|
||||
.expect("Unexpected non-editor")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for file_editor in &file_editors {
|
||||
file_editor
|
||||
.update(cx, |editor, cx| editor.save(false, project.clone(), cx))
|
||||
.await
|
||||
.expect("Failed to save file");
|
||||
}
|
||||
let buffers = file_editors.clone().into_iter().map(|file_editor| {
|
||||
file_editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.text_snapshot()
|
||||
})
|
||||
});
|
||||
let mut files = names
|
||||
.into_iter()
|
||||
.zip(file_editors)
|
||||
.zip(buffers)
|
||||
.map(|((name, editor), buffer)| {
|
||||
let staged_text = buffer.text();
|
||||
TestFile {
|
||||
name: name.into(),
|
||||
editor,
|
||||
buffer,
|
||||
staged_text,
|
||||
patch: Patch::new(Vec::new()),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
check(project_diff_editor.clone(), &files, cx);
|
||||
for _ in 0..10 {
|
||||
let file = files.choose_mut(rng).unwrap();
|
||||
match rng.gen_range(0..5) {
|
||||
0..3 => {
|
||||
let old_text = file.buffer.as_rope().clone();
|
||||
let new_edits = gen_edits(rng, &old_text);
|
||||
file.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.edit(
|
||||
new_edits
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(old, _, content)| (old, content)),
|
||||
cx,
|
||||
);
|
||||
editor.save(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to save file");
|
||||
let snapshot = file.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.remote_id(),
|
||||
change_set,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/.git"),
|
||||
&[(Path::new("file_a"), GitFileStatus::Modified)],
|
||||
);
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
.text_snapshot()
|
||||
});
|
||||
let diff = file
|
||||
.editor
|
||||
.update(cx, |editor, cx| {
|
||||
let change_set = cx.new_model(|cx| {
|
||||
BufferChangeSet::new_with_base_text(
|
||||
old_text.to_string(),
|
||||
snapshot.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
editor.diff_map.add_change_set_with_project(
|
||||
project.clone(),
|
||||
change_set,
|
||||
cx,
|
||||
);
|
||||
let diff = BufferDiff::build(&old_text, &snapshot);
|
||||
diff
|
||||
})
|
||||
.await;
|
||||
let patch = std::mem::take(&mut file.patch);
|
||||
file.patch = patch.compose(diff.hunks(&snapshot).map(|hunk| {
|
||||
let new = hunk.buffer_range.to_offset(&snapshot);
|
||||
let old = if hunk.diff_base_byte_range == (0..0) {
|
||||
new_edits
|
||||
.iter()
|
||||
.find_map(|(old, edit_new, _)| (edit_new == &new).then_some(old))
|
||||
.cloned()
|
||||
.unwrap()
|
||||
} else {
|
||||
hunk.diff_base_byte_range
|
||||
};
|
||||
Edit { old, new }
|
||||
}));
|
||||
file.buffer = snapshot;
|
||||
}
|
||||
3 => {
|
||||
file.staged_text = file.buffer.text();
|
||||
file.patch = Patch::new(Vec::new());
|
||||
}
|
||||
4 => {
|
||||
file.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.set_text(file.staged_text.as_str(), cx);
|
||||
editor.save(false, project.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to save file");
|
||||
file.buffer = file.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.text_snapshot()
|
||||
});
|
||||
file.patch = Patch::new(Vec::new());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
assert_eq!(
|
||||
// TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added)
|
||||
project_diff_editor.editor.read(cx).text(cx),
|
||||
format!("{change}{old_text}"),
|
||||
"Should have a new change shown in the beginning, and the old text shown as deleted text afterwards"
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/project/.git"),
|
||||
&files
|
||||
.iter()
|
||||
.filter_map(|file| {
|
||||
file.patch
|
||||
.is_empty()
|
||||
.not()
|
||||
.then_some((file.name.as_ref(), GitFileStatus::Modified))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
check(project_diff_editor.clone(), &files, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn check(
|
||||
project_diff_editor: View<ProjectDiffEditor>,
|
||||
files: &[TestFile],
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
for file in files {
|
||||
assert_eq!(
|
||||
file.patch.is_empty(),
|
||||
file.staged_text == file.buffer.text(),
|
||||
)
|
||||
}
|
||||
project_diff_editor.update(cx, |project_diff_editor, cx| {
|
||||
let snapshot = project_diff_editor
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
let hunks = snapshot
|
||||
.diff_map
|
||||
.diff_hunks(&snapshot.buffer_snapshot)
|
||||
.map(|hunk| {
|
||||
let buffer_snapshot = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_for_excerpt(hunk.excerpt_id)
|
||||
.unwrap();
|
||||
(
|
||||
hunk.buffer_id,
|
||||
hunk.diff_base_byte_range,
|
||||
hunk.buffer_range.to_offset(buffer_snapshot),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let edit_hunks = files
|
||||
.iter()
|
||||
.flat_map(|file| {
|
||||
file.patch.edits().iter().map(|Edit { old, new }| {
|
||||
(file.buffer.remote_id(), old.clone(), new.clone())
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(hunks.len(), edit_hunks.len());
|
||||
for edit_hunk in edit_hunks {
|
||||
assert!(hunks.iter().any(|hunk| hunk == &edit_hunk));
|
||||
}
|
||||
|
||||
let mut hunks = hunks.into_iter().peekable();
|
||||
for excerpt in snapshot.buffer_snapshot.all_excerpts() {
|
||||
let Some((buffer_id, _, new)) = hunks.next() else {
|
||||
panic!("Excerpt without a hunk")
|
||||
};
|
||||
let excerpt_buffer_id = excerpt.buffer().remote_id();
|
||||
let excerpt_range = excerpt.buffer_range().to_offset(excerpt.buffer());
|
||||
assert!(excerpt_buffer_id == buffer_id);
|
||||
assert!(excerpt_range.start <= new.start);
|
||||
assert!(excerpt_range.end >= new.end);
|
||||
while let Some((_, _, new)) = hunks.peek().map_or(None, |hunk @ (id, _, new)| {
|
||||
(id == &excerpt_buffer_id
|
||||
&& (excerpt_range.end > new.start
|
||||
|| (new.is_empty() && excerpt_range.end == new.start)))
|
||||
.then_some(hunk)
|
||||
}) {
|
||||
assert!(excerpt_range.start <= new.start);
|
||||
assert!(excerpt_range.end >= new.end);
|
||||
hunks.next();
|
||||
}
|
||||
}
|
||||
if let Some(hunk) = hunks.next() {
|
||||
panic!("Hunk without an excerpt: {hunk:?}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1251,4 +1402,71 @@ mod tests {
|
||||
cx.set_staff(true);
|
||||
});
|
||||
}
|
||||
|
||||
fn gen_line(rng: &mut StdRng) -> String {
|
||||
let len = rng.gen_range(0..20);
|
||||
let mut s = Alphanumeric.sample_string(rng, len);
|
||||
s.push('\n');
|
||||
s
|
||||
}
|
||||
|
||||
fn gen_file(rng: &mut StdRng) -> String {
|
||||
let line_count = rng.gen_range(0..10);
|
||||
(0..line_count).map(|_| gen_line(rng)).collect()
|
||||
}
|
||||
|
||||
fn gen_edits(rng: &mut StdRng, old: &Rope) -> Vec<(Range<usize>, Range<usize>, String)> {
|
||||
let mut old_lines = {
|
||||
let mut old_lines = Vec::new();
|
||||
let mut old_lines_iter = old.chunks().lines();
|
||||
while let Some(line) = old_lines_iter.next() {
|
||||
assert!(!line.ends_with("\n"));
|
||||
old_lines.push(line.to_owned());
|
||||
}
|
||||
if old_lines.last().is_some_and(|line| line.is_empty()) {
|
||||
old_lines.pop();
|
||||
}
|
||||
old_lines.into_iter()
|
||||
};
|
||||
let mut edits = Vec::new();
|
||||
let unchanged_count = rng.gen_range(0..=old_lines.len());
|
||||
let mut old_offset = old_lines
|
||||
.by_ref()
|
||||
.take(unchanged_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
let mut new_offset = old_offset;
|
||||
while old_lines.len() > 0 {
|
||||
let deleted_count = rng.gen_range(0..=old_lines.len());
|
||||
let advance = old_lines
|
||||
.by_ref()
|
||||
.take(deleted_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
let deleted_range = old_offset..old_offset + advance;
|
||||
old_offset += advance;
|
||||
let minimum_added = if deleted_count == 0 { 1 } else { 0 };
|
||||
let added_count = rng.gen_range(minimum_added..=5);
|
||||
let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
|
||||
let added_range = new_offset..new_offset + addition.len();
|
||||
new_offset += addition.len();
|
||||
edits.push((deleted_range, added_range, addition));
|
||||
|
||||
if old_lines.len() > 0 {
|
||||
let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
|
||||
if blank_lines == old_lines.len() {
|
||||
break;
|
||||
};
|
||||
let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
|
||||
let advance = old_lines
|
||||
.by_ref()
|
||||
.take(unchanged_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
old_offset += advance;
|
||||
new_offset += advance;
|
||||
}
|
||||
}
|
||||
edits
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +80,10 @@ impl DiffMap {
|
||||
self.snapshot.clone()
|
||||
}
|
||||
|
||||
pub fn add_change_set(
|
||||
#[cfg(test)]
|
||||
pub fn add_change_set_with_project(
|
||||
&mut self,
|
||||
project: Model<project::Project>,
|
||||
change_set: Model<BufferChangeSet>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
@@ -90,6 +92,37 @@ impl DiffMap {
|
||||
.0
|
||||
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
|
||||
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
|
||||
self.diff_bases.insert(
|
||||
buffer_id,
|
||||
DiffBaseState {
|
||||
last_version: None,
|
||||
_subscription: cx.observe(&change_set, move |editor, change_set, cx| {
|
||||
editor
|
||||
.diff_map
|
||||
.snapshot
|
||||
.0
|
||||
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
|
||||
Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, cx);
|
||||
}),
|
||||
change_set: change_set.clone(),
|
||||
},
|
||||
);
|
||||
project.update(cx, |project, cx| {
|
||||
project.buffer_store().update(cx, |buffer_store, _cx| {
|
||||
buffer_store.set_change_set(buffer_id, change_set);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_change_set(
|
||||
&mut self,
|
||||
change_set: Model<BufferChangeSet>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let buffer_id = change_set.read(cx).buffer_id;
|
||||
self.snapshot
|
||||
.0
|
||||
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
|
||||
self.diff_bases.insert(
|
||||
buffer_id,
|
||||
DiffBaseState {
|
||||
@@ -105,6 +138,7 @@ impl DiffMap {
|
||||
change_set,
|
||||
},
|
||||
);
|
||||
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
|
||||
}
|
||||
|
||||
pub fn hunks(&self, include_folded: bool) -> impl Iterator<Item = &ExpandedHunk> {
|
||||
@@ -149,6 +183,7 @@ impl DiffMapSnapshot {
|
||||
let end =
|
||||
excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0));
|
||||
MultiBufferDiffHunk {
|
||||
excerpt_id: excerpt.id(),
|
||||
row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row),
|
||||
buffer_id,
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
@@ -185,6 +220,7 @@ impl DiffMapSnapshot {
|
||||
.map_point_from_buffer(Point::new(hunk.row_range.end, 0))
|
||||
.row;
|
||||
MultiBufferDiffHunk {
|
||||
excerpt_id: excerpt.id(),
|
||||
row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row),
|
||||
buffer_id,
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
@@ -1112,6 +1148,7 @@ pub(crate) fn to_diff_hunk(
|
||||
.multi_buffer_range
|
||||
.to_point(multi_buffer_snapshot);
|
||||
Some(MultiBufferDiffHunk {
|
||||
excerpt_id: hovered_hunk.multi_buffer_range.start.excerpt_id,
|
||||
row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
|
||||
buffer_id,
|
||||
buffer_range,
|
||||
|
||||
359
crates/editor/src/inline_completion_tests.rs
Normal file
@@ -0,0 +1,359 @@
|
||||
use gpui::{prelude::*, Model};
|
||||
use indoc::indoc;
|
||||
use inline_completion::InlineCompletionProvider;
|
||||
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
|
||||
use std::ops::Range;
|
||||
use text::{Point, ToOffset};
|
||||
|
||||
use crate::{
|
||||
editor_tests::init_test, test::editor_test_context::EditorTestContext, InlineCompletion,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
|
||||
assign_editor_completion_provider(provider.clone(), &mut cx);
|
||||
cx.set_state("let absolute_zero_celsius = ˇ;");
|
||||
|
||||
propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx);
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
|
||||
assert_editor_active_edit_completion(&mut cx, |_, edits| {
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].1.as_str(), "-273.15");
|
||||
});
|
||||
|
||||
accept_completion(&mut cx);
|
||||
|
||||
cx.assert_editor_state("let absolute_zero_celsius = -273.15ˇ;")
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
|
||||
assign_editor_completion_provider(provider.clone(), &mut cx);
|
||||
cx.set_state("let pi = ˇ\"foo\";");
|
||||
|
||||
propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx);
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
|
||||
assert_editor_active_edit_completion(&mut cx, |_, edits| {
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].1.as_str(), "3.14159");
|
||||
});
|
||||
|
||||
accept_completion(&mut cx);
|
||||
|
||||
cx.assert_editor_state("let pi = 3.14159ˇ;")
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
|
||||
assign_editor_completion_provider(provider.clone(), &mut cx);
|
||||
|
||||
// Cursor is 2+ lines above the proposed edit
|
||||
cx.set_state(indoc! {"
|
||||
line 0
|
||||
line ˇ1
|
||||
line 2
|
||||
line 3
|
||||
line
|
||||
"});
|
||||
|
||||
propose_edits(
|
||||
&provider,
|
||||
vec![(Point::new(4, 3)..Point::new(4, 3), " 4")],
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3));
|
||||
});
|
||||
|
||||
// When accepting, cursor is moved to the proposed location
|
||||
accept_completion(&mut cx);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
line 0
|
||||
line 1
|
||||
line 2
|
||||
line 3
|
||||
linˇe
|
||||
"});
|
||||
|
||||
// Cursor is 2+ lines below the proposed edit
|
||||
cx.set_state(indoc! {"
|
||||
line 0
|
||||
line
|
||||
line 2
|
||||
line 3
|
||||
line ˇ4
|
||||
"});
|
||||
|
||||
propose_edits(
|
||||
&provider,
|
||||
vec![(Point::new(1, 3)..Point::new(1, 3), " 1")],
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3));
|
||||
});
|
||||
|
||||
// When accepting, cursor is moved to the proposed location
|
||||
accept_completion(&mut cx);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
line 0
|
||||
linˇe
|
||||
line 2
|
||||
line 3
|
||||
line 4
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
|
||||
assign_editor_completion_provider(provider.clone(), &mut cx);
|
||||
|
||||
// Cursor is 3+ lines above the proposed edit
|
||||
cx.set_state(indoc! {"
|
||||
line 0
|
||||
line ˇ1
|
||||
line 2
|
||||
line 3
|
||||
line 4
|
||||
line
|
||||
"});
|
||||
let edit_location = Point::new(5, 3);
|
||||
|
||||
propose_edits(
|
||||
&provider,
|
||||
vec![(edit_location..edit_location, " 5")],
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), edit_location);
|
||||
});
|
||||
|
||||
// If we move *towards* the completion, it stays active
|
||||
cx.set_selections_state(indoc! {"
|
||||
line 0
|
||||
line 1
|
||||
line ˇ2
|
||||
line 3
|
||||
line 4
|
||||
line
|
||||
"});
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), edit_location);
|
||||
});
|
||||
|
||||
// If we move *away* from the completion, it is discarded
|
||||
cx.set_selections_state(indoc! {"
|
||||
line ˇ0
|
||||
line 1
|
||||
line 2
|
||||
line 3
|
||||
line 4
|
||||
line
|
||||
"});
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.active_inline_completion.is_none());
|
||||
});
|
||||
|
||||
// Cursor is 3+ lines below the proposed edit
|
||||
cx.set_state(indoc! {"
|
||||
line
|
||||
line 1
|
||||
line 2
|
||||
line 3
|
||||
line ˇ4
|
||||
line 5
|
||||
"});
|
||||
let edit_location = Point::new(0, 3);
|
||||
|
||||
propose_edits(
|
||||
&provider,
|
||||
vec![(edit_location..edit_location, " 0")],
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), edit_location);
|
||||
});
|
||||
|
||||
// If we move *towards* the completion, it stays active
|
||||
cx.set_selections_state(indoc! {"
|
||||
line
|
||||
line 1
|
||||
line 2
|
||||
line ˇ3
|
||||
line 4
|
||||
line 5
|
||||
"});
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), edit_location);
|
||||
});
|
||||
|
||||
// If we move *away* from the completion, it is discarded
|
||||
cx.set_selections_state(indoc! {"
|
||||
line
|
||||
line 1
|
||||
line 2
|
||||
line 3
|
||||
line 4
|
||||
line ˇ5
|
||||
"});
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.active_inline_completion.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
fn assert_editor_active_edit_completion(
|
||||
cx: &mut EditorTestContext,
|
||||
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
|
||||
) {
|
||||
cx.editor(|editor, cx| {
|
||||
let completion_state = editor
|
||||
.active_inline_completion
|
||||
.as_ref()
|
||||
.expect("editor has no active completion");
|
||||
|
||||
if let InlineCompletion::Edit(edits) = &completion_state.completion {
|
||||
assert(editor.buffer().read(cx).snapshot(cx), edits);
|
||||
} else {
|
||||
panic!("expected edit completion");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_editor_active_move_completion(
|
||||
cx: &mut EditorTestContext,
|
||||
assert: impl FnOnce(MultiBufferSnapshot, Anchor),
|
||||
) {
|
||||
cx.editor(|editor, cx| {
|
||||
let completion_state = editor
|
||||
.active_inline_completion
|
||||
.as_ref()
|
||||
.expect("editor has no active completion");
|
||||
|
||||
if let InlineCompletion::Move(anchor) = &completion_state.completion {
|
||||
assert(editor.buffer().read(cx).snapshot(cx), *anchor);
|
||||
} else {
|
||||
panic!("expected move completion");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn accept_completion(cx: &mut EditorTestContext) {
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.accept_inline_completion(&crate::AcceptInlineCompletion, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn propose_edits<T: ToOffset>(
|
||||
provider: &Model<FakeInlineCompletionProvider>,
|
||||
edits: Vec<(Range<T>, &str)>,
|
||||
cx: &mut EditorTestContext,
|
||||
) {
|
||||
let snapshot = cx.buffer_snapshot();
|
||||
let edits = edits.into_iter().map(|(range, text)| {
|
||||
let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
|
||||
(range, text.into())
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
provider.update(cx, |provider, _| {
|
||||
provider.set_inline_completion(Some(inline_completion::InlineCompletion {
|
||||
edits: edits.collect(),
|
||||
}))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn assign_editor_completion_provider(
|
||||
provider: Model<FakeInlineCompletionProvider>,
|
||||
cx: &mut EditorTestContext,
|
||||
) {
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.set_inline_completion_provider(Some(provider), cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct FakeInlineCompletionProvider {
|
||||
completion: Option<inline_completion::InlineCompletion>,
|
||||
}
|
||||
|
||||
impl FakeInlineCompletionProvider {
|
||||
pub fn set_inline_completion(
|
||||
&mut self,
|
||||
completion: Option<inline_completion::InlineCompletion>,
|
||||
) {
|
||||
self.completion = completion;
|
||||
}
|
||||
}
|
||||
|
||||
impl InlineCompletionProvider for FakeInlineCompletionProvider {
|
||||
fn name() -> &'static str {
|
||||
"fake-completion-provider"
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
_buffer: &gpui::Model<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_cx: &gpui::AppContext,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&mut self,
|
||||
_buffer: gpui::Model<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_debounce: bool,
|
||||
_cx: &mut gpui::ModelContext<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: gpui::Model<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: inline_completion::Direction,
|
||||
_cx: &mut gpui::ModelContext<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
|
||||
|
||||
fn discard(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
|
||||
|
||||
fn suggest<'a>(
|
||||
&mut self,
|
||||
_buffer: &gpui::Model<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_cx: &mut gpui::ModelContext<Self>,
|
||||
) -> Option<inline_completion::InlineCompletion> {
|
||||
self.completion.clone()
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ where
|
||||
.find_map(|(trigger_anchor, language, buffer)| {
|
||||
project
|
||||
.read(cx)
|
||||
.language_servers_for_buffer(buffer.read(cx), cx)
|
||||
.language_servers_for_local_buffer(buffer.read(cx), cx)
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == language_server_name {
|
||||
Some((
|
||||
|
||||
@@ -6,8 +6,8 @@ use collections::BTreeMap;
|
||||
use futures::Future;
|
||||
use git::diff::DiffHunkStatus;
|
||||
use gpui::{
|
||||
AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View, ViewContext,
|
||||
VisualTestContext, WindowHandle,
|
||||
prelude::*, AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View,
|
||||
ViewContext, VisualTestContext, WindowHandle,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry};
|
||||
@@ -23,8 +23,6 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use ui::Context;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
|
||||
@@ -24,6 +24,8 @@ interface github {
|
||||
}
|
||||
|
||||
/// Returns the latest release for the given GitHub repository.
|
||||
///
|
||||
/// Takes repo as a string in the form "<owner-name>/<repo-name>", for example: "zed-industries/zed".
|
||||
latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
|
||||
|
||||
/// Returns the GitHub release with the specified tag name for the given GitHub repository.
|
||||
|
||||
@@ -32,7 +32,7 @@ use gpui::{
|
||||
};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::{
|
||||
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage,
|
||||
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, Rope,
|
||||
QUERY_FILENAME_PREFIXES,
|
||||
};
|
||||
use node_runtime::NodeRuntime;
|
||||
@@ -1387,6 +1387,7 @@ impl ExtensionStore {
|
||||
fn prepare_remote_extension(
|
||||
&mut self,
|
||||
extension_id: Arc<str>,
|
||||
is_dev: bool,
|
||||
tmp_dir: PathBuf,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
@@ -1397,26 +1398,45 @@ impl ExtensionStore {
|
||||
};
|
||||
let fs = self.fs.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
for well_known_path in ["extension.toml", "extension.json", "extension.wasm"] {
|
||||
if fs.is_file(&src_dir.join(well_known_path)).await {
|
||||
fs.copy_file(
|
||||
&src_dir.join(well_known_path),
|
||||
&tmp_dir.join(well_known_path),
|
||||
fs::CopyOptions::default(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
const EXTENSION_TOML: &str = "extension.toml";
|
||||
const EXTENSION_WASM: &str = "extension.wasm";
|
||||
const CONFIG_TOML: &str = "config.toml";
|
||||
|
||||
if is_dev {
|
||||
let manifest_toml = toml::to_string(&loaded_extension.manifest)?;
|
||||
fs.save(
|
||||
&tmp_dir.join(EXTENSION_TOML),
|
||||
&Rope::from(manifest_toml),
|
||||
language::LineEnding::Unix,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
fs.copy_file(
|
||||
&src_dir.join(EXTENSION_TOML),
|
||||
&tmp_dir.join(EXTENSION_TOML),
|
||||
fs::CopyOptions::default(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
if fs.is_file(&src_dir.join(EXTENSION_WASM)).await {
|
||||
fs.copy_file(
|
||||
&src_dir.join(EXTENSION_WASM),
|
||||
&tmp_dir.join(EXTENSION_WASM),
|
||||
fs::CopyOptions::default(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
for language_path in loaded_extension.manifest.languages.iter() {
|
||||
if fs
|
||||
.is_file(&src_dir.join(language_path).join("config.toml"))
|
||||
.is_file(&src_dir.join(language_path).join(CONFIG_TOML))
|
||||
.await
|
||||
{
|
||||
fs.create_dir(&tmp_dir.join(language_path)).await?;
|
||||
fs.copy_file(
|
||||
&src_dir.join(language_path).join("config.toml"),
|
||||
&tmp_dir.join(language_path).join("config.toml"),
|
||||
&src_dir.join(language_path).join(CONFIG_TOML),
|
||||
&tmp_dir.join(language_path).join(CONFIG_TOML),
|
||||
fs::CopyOptions::default(),
|
||||
)
|
||||
.await?
|
||||
@@ -1462,6 +1482,7 @@ impl ExtensionStore {
|
||||
this.update(cx, |this, cx| {
|
||||
this.prepare_remote_extension(
|
||||
missing_extension.id.clone().into(),
|
||||
missing_extension.dev,
|
||||
tmp_dir.path().to_owned(),
|
||||
cx,
|
||||
)
|
||||
@@ -1476,6 +1497,11 @@ impl ExtensionStore {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
log::info!(
|
||||
"Finished uploading extension {}",
|
||||
missing_extension.clone().id
|
||||
);
|
||||
|
||||
client
|
||||
.update(cx, |client, _cx| {
|
||||
client.proto_client().request(proto::InstallExtension {
|
||||
|
||||
@@ -623,9 +623,9 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
None,
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(project_dir.join("test.gleam"), cx)
|
||||
project.open_local_buffer_with_lsp(project_dir.join("test.gleam"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -59,6 +59,16 @@ impl FeatureFlag for ToolUseFeatureFlag {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ZetaFeatureFlag;
|
||||
impl FeatureFlag for ZetaFeatureFlag {
|
||||
const NAME: &'static str = "zeta";
|
||||
}
|
||||
|
||||
pub struct GitUiFeatureFlag;
|
||||
impl FeatureFlag for GitUiFeatureFlag {
|
||||
const NAME: &'static str = "git-ui";
|
||||
}
|
||||
|
||||
pub struct Remoting {}
|
||||
impl FeatureFlag for Remoting {
|
||||
const NAME: &'static str = "remoting";
|
||||
|
||||
@@ -74,16 +74,23 @@ impl BufferDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self {
|
||||
pub async fn build(diff_base: &Rope, buffer: &text::BufferSnapshot) -> Self {
|
||||
let mut tree = SumTree::new(buffer);
|
||||
|
||||
let buffer_text = buffer.as_rope().to_string();
|
||||
let patch = Self::diff(diff_base, &buffer_text);
|
||||
let base_text = diff_base.to_string();
|
||||
let buffer_text = buffer.text();
|
||||
let patch = Self::diff(&base_text, &buffer_text);
|
||||
|
||||
if let Some(patch) = patch {
|
||||
let mut divergence = 0;
|
||||
for hunk_index in 0..patch.num_hunks() {
|
||||
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
|
||||
let hunk = Self::process_patch_hunk(
|
||||
&patch,
|
||||
hunk_index,
|
||||
diff_base,
|
||||
buffer,
|
||||
&mut divergence,
|
||||
);
|
||||
tree.push(hunk, buffer);
|
||||
}
|
||||
}
|
||||
@@ -187,11 +194,11 @@ impl BufferDiff {
|
||||
}
|
||||
|
||||
pub async fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
|
||||
*self = Self::build(&diff_base.to_string(), buffer).await;
|
||||
*self = Self::build(diff_base, buffer).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let start = text.anchor_before(Point::new(0, 0));
|
||||
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
|
||||
self.hunks_intersecting_range(start..end, text)
|
||||
@@ -222,6 +229,7 @@ impl BufferDiff {
|
||||
fn process_patch_hunk(
|
||||
patch: &GitPatch<'_>,
|
||||
hunk_index: usize,
|
||||
diff_base: &Rope,
|
||||
buffer: &text::BufferSnapshot,
|
||||
buffer_row_divergence: &mut i64,
|
||||
) -> InternalDiffHunk {
|
||||
@@ -231,51 +239,59 @@ impl BufferDiff {
|
||||
let mut first_deletion_buffer_row: Option<u32> = None;
|
||||
let mut buffer_row_range: Option<Range<u32>> = None;
|
||||
let mut diff_base_byte_range: Option<Range<usize>> = None;
|
||||
let mut first_addition_old_row: Option<u32> = None;
|
||||
|
||||
for line_index in 0..line_item_count {
|
||||
let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
|
||||
let kind = line.origin_value();
|
||||
let content_offset = line.content_offset() as isize;
|
||||
let content_len = line.content().len() as isize;
|
||||
match kind {
|
||||
GitDiffLineType::Addition => {
|
||||
if first_addition_old_row.is_none() {
|
||||
first_addition_old_row = Some(
|
||||
(line.new_lineno().unwrap() as i64 - *buffer_row_divergence - 1) as u32,
|
||||
);
|
||||
}
|
||||
*buffer_row_divergence += 1;
|
||||
let row = line.new_lineno().unwrap().saturating_sub(1);
|
||||
|
||||
if kind == GitDiffLineType::Addition {
|
||||
*buffer_row_divergence += 1;
|
||||
let row = line.new_lineno().unwrap().saturating_sub(1);
|
||||
|
||||
match &mut buffer_row_range {
|
||||
Some(buffer_row_range) => buffer_row_range.end = row + 1,
|
||||
None => buffer_row_range = Some(row..row + 1),
|
||||
match &mut buffer_row_range {
|
||||
Some(Range { end, .. }) => *end = row + 1,
|
||||
None => buffer_row_range = Some(row..row + 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
GitDiffLineType::Deletion => {
|
||||
let end = content_offset + content_len;
|
||||
|
||||
if kind == GitDiffLineType::Deletion {
|
||||
let end = content_offset + content_len;
|
||||
match &mut diff_base_byte_range {
|
||||
Some(head_byte_range) => head_byte_range.end = end as usize,
|
||||
None => diff_base_byte_range = Some(content_offset as usize..end as usize),
|
||||
}
|
||||
|
||||
match &mut diff_base_byte_range {
|
||||
Some(head_byte_range) => head_byte_range.end = end as usize,
|
||||
None => diff_base_byte_range = Some(content_offset as usize..end as usize),
|
||||
if first_deletion_buffer_row.is_none() {
|
||||
let old_row = line.old_lineno().unwrap().saturating_sub(1);
|
||||
let row = old_row as i64 + *buffer_row_divergence;
|
||||
first_deletion_buffer_row = Some(row as u32);
|
||||
}
|
||||
|
||||
*buffer_row_divergence -= 1;
|
||||
}
|
||||
|
||||
if first_deletion_buffer_row.is_none() {
|
||||
let old_row = line.old_lineno().unwrap().saturating_sub(1);
|
||||
let row = old_row as i64 + *buffer_row_divergence;
|
||||
first_deletion_buffer_row = Some(row as u32);
|
||||
}
|
||||
|
||||
*buffer_row_divergence -= 1;
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
//unwrap_or deletion without addition
|
||||
let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
|
||||
//we cannot have an addition-less hunk without deletion(s) or else there would be no hunk
|
||||
// Pure deletion hunk without addition.
|
||||
let row = first_deletion_buffer_row.unwrap();
|
||||
row..row
|
||||
});
|
||||
|
||||
//unwrap_or addition without deletion
|
||||
let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0);
|
||||
|
||||
let diff_base_byte_range = diff_base_byte_range.unwrap_or_else(|| {
|
||||
// Pure addition hunk without deletion.
|
||||
let row = first_addition_old_row.unwrap();
|
||||
let offset = diff_base.point_to_offset(Point::new(row, 0));
|
||||
offset..offset
|
||||
});
|
||||
let start = Point::new(buffer_row_range.start, 0);
|
||||
let end = Point::new(buffer_row_range.end, 0);
|
||||
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
|
||||
|
||||
25
crates/git_ui/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "git_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
name = "git_ui"
|
||||
path = "src/git_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
serde.workspace = true
|
||||
workspace.workspace = true
|
||||
ui.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
1
crates/git_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
181
crates/git_ui/src/git_panel.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use gpui::*;
|
||||
use ui::{prelude::*, Checkbox, Divider, DividerColor, ElevationIndex};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<GitPanel>(cx);
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
actions!(git_panel, [Deploy, ToggleFocus]);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GitPanel {
|
||||
_workspace: WeakView<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
width: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl GitPanel {
|
||||
pub fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
|
||||
cx.new_view(|cx| Self::new(workspace_handle, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(workspace: WeakView<Workspace>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
_workspace: workspace,
|
||||
focus_handle: cx.focus_handle(),
|
||||
width: Some(px(360.)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.h(px(32.))
|
||||
.items_center()
|
||||
.px_3()
|
||||
.bg(ElevationIndex::Surface.bg(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Checkbox::new("all-changes", true.into()).disabled(true))
|
||||
.child(div().text_buffer(cx).text_ui_sm(cx).child("0 changes")),
|
||||
)
|
||||
.child(div().flex_grow())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("discard-changes", IconName::Undo)
|
||||
.icon_size(IconSize::Small)
|
||||
.disabled(true),
|
||||
)
|
||||
.child(
|
||||
Button::new("stage-all", "Stage All")
|
||||
.label_size(LabelSize::Small)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
|
||||
v_flex()
|
||||
.h_full()
|
||||
.py_2p5()
|
||||
.px_3()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.font_buffer(cx)
|
||||
.text_ui_sm(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child("Add a message")
|
||||
.gap_1()
|
||||
.child(div().flex_grow())
|
||||
.child(
|
||||
h_flex().child(div().gap_1().flex_grow()).child(
|
||||
Button::new("commit", "Commit")
|
||||
.label_size(LabelSize::Small)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(true),
|
||||
),
|
||||
)
|
||||
.cursor(CursorStyle::OperationNotAllowed)
|
||||
.opacity(0.5),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GitPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("GitPanel")
|
||||
.font_buffer(cx)
|
||||
.py_1()
|
||||
.id("git_panel")
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.bg(ElevationIndex::Surface.bg(cx))
|
||||
.child(self.render_panel_header(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.h(px(8.))
|
||||
.child(Divider::horizontal_dashed().color(DividerColor::Border)),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.h(px(8.))
|
||||
.child(Divider::horizontal_dashed().color(DividerColor::Border)),
|
||||
)
|
||||
.child(self.render_commit_editor(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for GitPanel {
|
||||
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for GitPanel {}
|
||||
|
||||
impl Panel for GitPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"GitPanel"
|
||||
}
|
||||
|
||||
fn position(&self, _cx: &gpui::WindowContext) -> DockPosition {
|
||||
DockPosition::Left
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
matches!(position, DockPosition::Left | DockPosition::Right)
|
||||
}
|
||||
|
||||
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn size(&self, _cx: &gpui::WindowContext) -> Pixels {
|
||||
self.width.unwrap_or(px(360.))
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
|
||||
self.width = size;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &gpui::WindowContext) -> Option<ui::IconName> {
|
||||
Some(ui::IconName::GitBranch)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
Some("Git Panel")
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
}
|
||||
1
crates/git_ui/src/git_ui.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod git_panel;
|
||||
@@ -181,7 +181,7 @@ wayland-protocols-plasma = { version = "0.2.0", features = [
|
||||
|
||||
# X11
|
||||
as-raw-xcb-connection = { version = "1", optional = true }
|
||||
x11rb = { version = "0.13.0", features = [
|
||||
x11rb = { version = "0.13.1", features = [
|
||||
"allow-unsafe-code",
|
||||
"xkb",
|
||||
"randr",
|
||||
@@ -198,7 +198,7 @@ xim = { git = "https://github.com/XDeme1/xim-rs", rev = "d50d461764c2213655cd9cf
|
||||
"x11rb-xcb",
|
||||
"x11rb-client",
|
||||
], optional = true }
|
||||
x11-clipboard = { version = "0.9.2", optional = true }
|
||||
x11-clipboard = { version = "0.9.3", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
blade-util.workspace = true
|
||||
|
||||
254
crates/gpui/examples/gradient.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use gpui::{
|
||||
canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, size, App, AppContext,
|
||||
Bounds, ColorSpace, Half, Render, ViewContext, WindowOptions,
|
||||
};
|
||||
|
||||
struct GradientViewer {
|
||||
color_space: ColorSpace,
|
||||
}
|
||||
|
||||
impl GradientViewer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
color_space: ColorSpace::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GradientViewer {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let color_space = self.color_space;
|
||||
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child("Gradient Examples")
|
||||
.child(
|
||||
div().flex().gap_2().items_center().child(
|
||||
div()
|
||||
.id("method")
|
||||
.flex()
|
||||
.px_3()
|
||||
.py_1()
|
||||
.text_sm()
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.child(format!("{}", color_space))
|
||||
.active(|this| this.opacity(0.8))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.color_space = match this.color_space {
|
||||
ColorSpace::Oklab => ColorSpace::Srgb,
|
||||
ColorSpace::Srgb => ColorSpace::Oklab,
|
||||
};
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.rounded_xl()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(gpui::red())
|
||||
.text_color(gpui::white())
|
||||
.child("Solid Color"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.rounded_xl()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(gpui::blue())
|
||||
.text_color(gpui::white())
|
||||
.child("Solid Color"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.h_24()
|
||||
.text_color(gpui::white())
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
45.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
135.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::green(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
225.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
315.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::yellow(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.h_24()
|
||||
.text_color(gpui::white())
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
360.,
|
||||
linear_color_stop(gpui::yellow(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(gpui::green(), 0.05),
|
||||
linear_color_stop(gpui::yellow(), 0.95),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.05),
|
||||
linear_color_stop(gpui::red(), 0.95),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.child(
|
||||
div().flex().flex_1().gap_3().child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.5),
|
||||
linear_color_stop(gpui::red(), 0.5),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::blue(), 0.5),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(div().h_24().child(canvas(
|
||||
move |_, _| {},
|
||||
move |bounds, _, cx| {
|
||||
let size = size(bounds.size.width * 0.8, px(80.));
|
||||
let square_bounds = Bounds {
|
||||
origin: point(
|
||||
bounds.size.width.half() - size.width.half(),
|
||||
bounds.origin.y,
|
||||
),
|
||||
size,
|
||||
};
|
||||
let height = square_bounds.size.height;
|
||||
let horizontal_offset = height;
|
||||
let vertical_offset = px(30.);
|
||||
let mut path = gpui::Path::new(square_bounds.lower_left());
|
||||
path.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset));
|
||||
path.line_to(
|
||||
square_bounds.upper_right() + point(-horizontal_offset, vertical_offset),
|
||||
);
|
||||
path.line_to(square_bounds.lower_right());
|
||||
path.line_to(square_bounds.lower_left());
|
||||
cx.paint_path(
|
||||
path,
|
||||
linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space),
|
||||
);
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|_| GradientViewer::new()),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
@@ -548,6 +548,164 @@ impl<'de> Deserialize<'de> for Hsla {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) enum BackgroundTag {
|
||||
Solid = 0,
|
||||
LinearGradient = 1,
|
||||
}
|
||||
|
||||
/// A color space for color interpolation.
|
||||
///
|
||||
/// References:
|
||||
/// - https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method
|
||||
/// - https://www.w3.org/TR/css-color-4/#typedef-color-space
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
#[repr(C)]
|
||||
pub enum ColorSpace {
|
||||
#[default]
|
||||
/// The sRGB color space.
|
||||
Srgb = 0,
|
||||
/// The Oklab color space.
|
||||
Oklab = 1,
|
||||
}
|
||||
|
||||
impl Display for ColorSpace {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ColorSpace::Srgb => write!(f, "sRGB"),
|
||||
ColorSpace::Oklab => write!(f, "Oklab"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A background color, which can be either a solid color or a linear gradient.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct Background {
|
||||
pub(crate) tag: BackgroundTag,
|
||||
pub(crate) color_space: ColorSpace,
|
||||
pub(crate) solid: Hsla,
|
||||
pub(crate) angle: f32,
|
||||
pub(crate) colors: [LinearColorStop; 2],
|
||||
/// Padding for alignment for repr(C) layout.
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
impl Eq for Background {}
|
||||
impl Default for Background {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tag: BackgroundTag::Solid,
|
||||
solid: Hsla::default(),
|
||||
color_space: ColorSpace::default(),
|
||||
angle: 0.0,
|
||||
colors: [LinearColorStop::default(), LinearColorStop::default()],
|
||||
pad: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a LinearGradient background color.
|
||||
///
|
||||
/// The gradient line's angle of direction. A value of `0.` is equivalent to to top; increasing values rotate clockwise from there.
|
||||
///
|
||||
/// The `angle` is in degrees value in the range 0.0 to 360.0.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient
|
||||
pub fn linear_gradient(
|
||||
angle: f32,
|
||||
from: impl Into<LinearColorStop>,
|
||||
to: impl Into<LinearColorStop>,
|
||||
) -> Background {
|
||||
Background {
|
||||
tag: BackgroundTag::LinearGradient,
|
||||
angle,
|
||||
colors: [from.into(), to.into()],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A color stop in a linear gradient.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#linear-color-stop
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct LinearColorStop {
|
||||
/// The color of the color stop.
|
||||
pub color: Hsla,
|
||||
/// The percentage of the gradient, in the range 0.0 to 1.0.
|
||||
pub percentage: f32,
|
||||
}
|
||||
|
||||
/// Creates a new linear color stop.
|
||||
///
|
||||
/// The percentage of the gradient, in the range 0.0 to 1.0.
|
||||
pub fn linear_color_stop(color: impl Into<Hsla>, percentage: f32) -> LinearColorStop {
|
||||
LinearColorStop {
|
||||
color: color.into(),
|
||||
percentage,
|
||||
}
|
||||
}
|
||||
|
||||
impl LinearColorStop {
|
||||
/// Returns a new color stop with the same color, but with a modified alpha value.
|
||||
pub fn opacity(&self, factor: f32) -> Self {
|
||||
Self {
|
||||
percentage: self.percentage,
|
||||
color: self.color.opacity(factor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Background {
|
||||
/// Use specified color space for color interpolation.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method
|
||||
pub fn color_space(mut self, color_space: ColorSpace) -> Self {
|
||||
self.color_space = color_space;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value.
|
||||
pub fn opacity(&self, factor: f32) -> Self {
|
||||
let mut background = *self;
|
||||
background.solid = background.solid.opacity(factor);
|
||||
background.colors = [
|
||||
self.colors[0].opacity(factor),
|
||||
self.colors[1].opacity(factor),
|
||||
];
|
||||
background
|
||||
}
|
||||
|
||||
/// Returns whether the background color is transparent.
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
match self.tag {
|
||||
BackgroundTag::Solid => self.solid.is_transparent(),
|
||||
BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsla> for Background {
|
||||
fn from(value: Hsla) -> Self {
|
||||
Background {
|
||||
tag: BackgroundTag::Solid,
|
||||
solid: value,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<Rgba> for Background {
|
||||
fn from(value: Rgba) -> Self {
|
||||
Background {
|
||||
tag: BackgroundTag::Solid,
|
||||
solid: Hsla::from(value),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
@@ -595,4 +753,32 @@ mod tests {
|
||||
|
||||
assert_eq!(actual, rgba(0xdeadbeef))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_background_solid() {
|
||||
let color = Hsla::from(rgba(0xff0099ff));
|
||||
let mut background = Background::from(color);
|
||||
assert_eq!(background.tag, BackgroundTag::Solid);
|
||||
assert_eq!(background.solid, color);
|
||||
|
||||
assert_eq!(background.opacity(0.5).solid, color.opacity(0.5));
|
||||
assert_eq!(background.is_transparent(), false);
|
||||
background.solid = hsla(0.0, 0.0, 0.0, 0.0);
|
||||
assert_eq!(background.is_transparent(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_background_linear_gradient() {
|
||||
let from = linear_color_stop(rgba(0xff0099ff), 0.0);
|
||||
let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
|
||||
let background = linear_gradient(90.0, from, to);
|
||||
assert_eq!(background.tag, BackgroundTag::LinearGradient);
|
||||
assert_eq!(background.colors[0], from);
|
||||
assert_eq!(background.colors[1], to);
|
||||
|
||||
assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
|
||||
assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
|
||||
assert_eq!(background.is_transparent(), false);
|
||||
assert_eq!(background.opacity(0.0).is_transparent(), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use crate::{AppContext, PlatformDispatcher};
|
||||
use async_task::Runnable;
|
||||
use futures::channel::mpsc;
|
||||
use smol::prelude::*;
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::panic::Location;
|
||||
use std::thread::{self, ThreadId};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
marker::PhantomData,
|
||||
@@ -328,6 +332,9 @@ impl BackgroundExecutor {
|
||||
/// Depending on other concurrent tasks the elapsed duration may be longer
|
||||
/// than requested.
|
||||
pub fn timer(&self, duration: Duration) -> Task<()> {
|
||||
if duration.is_zero() {
|
||||
return Task::ready(());
|
||||
}
|
||||
let (runnable, task) = async_task::spawn(async move {}, {
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
move |runnable| dispatcher.dispatch_after(duration, runnable)
|
||||
@@ -437,16 +444,19 @@ impl ForegroundExecutor {
|
||||
}
|
||||
|
||||
/// Enqueues the given Task to run on the main thread at some point in the future.
|
||||
#[track_caller]
|
||||
pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
|
||||
#[track_caller]
|
||||
fn inner<R: 'static>(
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
future: AnyLocalFuture<R>,
|
||||
) -> Task<R> {
|
||||
let (runnable, task) = async_task::spawn_local(future, move |runnable| {
|
||||
let (runnable, task) = spawn_local_with_source_location(future, move |runnable| {
|
||||
dispatcher.dispatch_on_main_thread(runnable)
|
||||
});
|
||||
runnable.schedule();
|
||||
@@ -456,6 +466,71 @@ impl ForegroundExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics.
|
||||
///
|
||||
/// Copy-modified from:
|
||||
/// https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L405
|
||||
#[track_caller]
|
||||
fn spawn_local_with_source_location<Fut, S>(
|
||||
future: Fut,
|
||||
schedule: S,
|
||||
) -> (Runnable<()>, async_task::Task<Fut::Output, ()>)
|
||||
where
|
||||
Fut: Future + 'static,
|
||||
Fut::Output: 'static,
|
||||
S: async_task::Schedule<()> + Send + Sync + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn thread_id() -> ThreadId {
|
||||
std::thread_local! {
|
||||
static ID: ThreadId = thread::current().id();
|
||||
}
|
||||
ID.try_with(|id| *id)
|
||||
.unwrap_or_else(|_| thread::current().id())
|
||||
}
|
||||
|
||||
struct Checked<F> {
|
||||
id: ThreadId,
|
||||
inner: ManuallyDrop<F>,
|
||||
location: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<F> Drop for Checked<F> {
|
||||
fn drop(&mut self) {
|
||||
assert!(
|
||||
self.id == thread_id(),
|
||||
"local task dropped by a thread that didn't spawn it. Task spawned at {}",
|
||||
self.location
|
||||
);
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Future> Future for Checked<F> {
|
||||
type Output = F::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
assert!(
|
||||
self.id == thread_id(),
|
||||
"local task polled by a thread that didn't spawn it. Task spawned at {}",
|
||||
self.location
|
||||
);
|
||||
unsafe { self.map_unchecked_mut(|c| &mut *c.inner).poll(cx) }
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the future into one that checks which thread it's on.
|
||||
let future = Checked {
|
||||
id: thread_id(),
|
||||
inner: ManuallyDrop::new(future),
|
||||
location: Location::caller(),
|
||||
};
|
||||
|
||||
unsafe { async_task::spawn_unchecked(future, schedule) }
|
||||
}
|
||||
|
||||
/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`].
|
||||
pub struct Scope<'a> {
|
||||
executor: BackgroundExecutor,
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::{
|
||||
use crate::{AppContext, DisplayId};
|
||||
|
||||
/// An axis along which a measurement can be made.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
pub enum Axis {
|
||||
/// The y axis, or up and down
|
||||
Vertical,
|
||||
@@ -58,7 +58,21 @@ pub trait Along {
|
||||
/// let point = Point { x: 10, y: 20 };
|
||||
/// println!("{:?}", point); // Outputs: Point { x: 10, y: 20 }
|
||||
/// ```
|
||||
#[derive(Refineable, Default, Add, AddAssign, Sub, SubAssign, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(
|
||||
Refineable,
|
||||
Default,
|
||||
Add,
|
||||
AddAssign,
|
||||
Sub,
|
||||
SubAssign,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Hash,
|
||||
)]
|
||||
#[refineable(Debug)]
|
||||
#[repr(C)]
|
||||
pub struct Point<T: Default + Clone + Debug> {
|
||||
@@ -694,7 +708,7 @@ impl Size<Length> {
|
||||
/// assert_eq!(bounds.origin, origin);
|
||||
/// assert_eq!(bounds.size, size);
|
||||
/// ```
|
||||
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Hash)]
|
||||
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
|
||||
#[refineable(Debug)]
|
||||
#[repr(C)]
|
||||
pub struct Bounds<T: Clone + Default + Debug> {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use super::{BladeAtlas, PATH_TEXTURE_FORMAT};
|
||||
use crate::{
|
||||
AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, GPUSpecs, Hsla,
|
||||
AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, GPUSpecs,
|
||||
MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
|
||||
ScaledPixels, Scene, Shadow, Size, Underline,
|
||||
};
|
||||
@@ -174,7 +174,7 @@ struct ShaderSurfacesData {
|
||||
#[repr(C)]
|
||||
struct PathSprite {
|
||||
bounds: Bounds<ScaledPixels>,
|
||||
color: Hsla,
|
||||
color: Background,
|
||||
tile: AtlasTile,
|
||||
}
|
||||
|
||||
@@ -431,13 +431,25 @@ impl BladeRenderer {
|
||||
}
|
||||
|
||||
pub fn update_drawable_size(&mut self, size: Size<DevicePixels>) {
|
||||
self.update_drawable_size_impl(size, false);
|
||||
}
|
||||
|
||||
/// Like `update_drawable_size` but skips the check that the size has changed. This is useful in
|
||||
/// cases like restoring a window from minimization where the size is the same but the
|
||||
/// renderer's swap chain needs to be recreated.
|
||||
#[cfg_attr(any(target_os = "macos", target_os = "linux"), allow(dead_code))]
|
||||
pub fn update_drawable_size_even_if_unchanged(&mut self, size: Size<DevicePixels>) {
|
||||
self.update_drawable_size_impl(size, true);
|
||||
}
|
||||
|
||||
fn update_drawable_size_impl(&mut self, size: Size<DevicePixels>, always_resize: bool) {
|
||||
let gpu_size = gpu::Extent {
|
||||
width: size.width.0 as u32,
|
||||
height: size.height.0 as u32,
|
||||
depth: 1,
|
||||
};
|
||||
|
||||
if gpu_size != self.surface_config.size {
|
||||
if always_resize || gpu_size != self.surface_config.size {
|
||||
self.wait_for_gpu();
|
||||
self.surface_config.size = gpu_size;
|
||||
self.gpu.resize(self.surface_config);
|
||||
|
||||
@@ -15,18 +15,21 @@ struct Bounds {
|
||||
origin: vec2<f32>,
|
||||
size: vec2<f32>,
|
||||
}
|
||||
|
||||
struct Corners {
|
||||
top_left: f32,
|
||||
top_right: f32,
|
||||
bottom_right: f32,
|
||||
bottom_left: f32,
|
||||
}
|
||||
|
||||
struct Edges {
|
||||
top: f32,
|
||||
right: f32,
|
||||
bottom: f32,
|
||||
left: f32,
|
||||
}
|
||||
|
||||
struct Hsla {
|
||||
h: f32,
|
||||
s: f32,
|
||||
@@ -34,6 +37,24 @@ struct Hsla {
|
||||
a: f32,
|
||||
}
|
||||
|
||||
struct LinearColorStop {
|
||||
color: Hsla,
|
||||
percentage: f32,
|
||||
}
|
||||
|
||||
struct Background {
|
||||
// 0u is Solid
|
||||
// 1u is LinearGradient
|
||||
tag: u32,
|
||||
// 0u is sRGB linear color
|
||||
// 1u is Oklab color
|
||||
color_space: u32,
|
||||
solid: Hsla,
|
||||
angle: f32,
|
||||
colors: array<LinearColorStop, 2>,
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
struct AtlasTextureId {
|
||||
index: u32,
|
||||
kind: u32,
|
||||
@@ -43,6 +64,7 @@ struct AtlasBounds {
|
||||
origin: vec2<i32>,
|
||||
size: vec2<i32>,
|
||||
}
|
||||
|
||||
struct AtlasTile {
|
||||
texture_id: AtlasTextureId,
|
||||
tile_id: u32,
|
||||
@@ -96,6 +118,24 @@ fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
fn linear_to_srgb(linear: vec3<f32>) -> vec3<f32> {
|
||||
let cutoff = linear < vec3<f32>(0.0031308);
|
||||
let higher = vec3<f32>(1.055) * pow(linear, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
|
||||
let lower = linear * vec3<f32>(12.92);
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
/// Convert a linear color to sRGBA space.
|
||||
fn linear_to_srgba(color: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(linear_to_srgb(color.rgb), color.a);
|
||||
}
|
||||
|
||||
/// Convert a sRGBA color to linear space.
|
||||
fn srgba_to_linear(color: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(srgb_to_linear(color.rgb), color.a);
|
||||
}
|
||||
|
||||
/// Hsla to linear RGBA conversion.
|
||||
fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
|
||||
let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
|
||||
let s = hsla.s;
|
||||
@@ -135,6 +175,43 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
|
||||
return vec4<f32>(linear, a);
|
||||
}
|
||||
|
||||
/// Convert a linear sRGB to Oklab space.
|
||||
/// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
|
||||
fn linear_srgb_to_oklab(color: vec4<f32>) -> vec4<f32> {
|
||||
let l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
|
||||
let m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
|
||||
let s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
|
||||
|
||||
let l_ = pow(l, 1.0 / 3.0);
|
||||
let m_ = pow(m, 1.0 / 3.0);
|
||||
let s_ = pow(s, 1.0 / 3.0);
|
||||
|
||||
return vec4<f32>(
|
||||
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
|
||||
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
|
||||
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
|
||||
color.a
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert an Oklab color to linear sRGB space.
|
||||
fn oklab_to_linear_srgb(color: vec4<f32>) -> vec4<f32> {
|
||||
let l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
|
||||
let m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
|
||||
let s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
|
||||
|
||||
let l = l_ * l_ * l_;
|
||||
let m = m_ * m_ * m_;
|
||||
let s = s_ * s_ * s_;
|
||||
|
||||
return vec4<f32>(
|
||||
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
||||
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
||||
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
|
||||
color.a
|
||||
);
|
||||
}
|
||||
|
||||
fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
|
||||
let alpha = above.a + below.a * (1.0 - above.a);
|
||||
let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
|
||||
@@ -197,6 +274,94 @@ fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
|
||||
return vec4<f32>(color.rgb * multiplier, alpha);
|
||||
}
|
||||
|
||||
|
||||
struct GradientColor {
|
||||
solid: vec4<f32>,
|
||||
color0: vec4<f32>,
|
||||
color1: vec4<f32>,
|
||||
}
|
||||
|
||||
fn prepare_gradient_color(tag: u32, color_space: u32,
|
||||
solid: Hsla, colors: array<LinearColorStop, 2>) -> GradientColor {
|
||||
var result = GradientColor();
|
||||
|
||||
if (tag == 0u) {
|
||||
result.solid = hsla_to_rgba(solid);
|
||||
} else if (tag == 1u) {
|
||||
// The hsla_to_rgba is returns a linear sRGB color
|
||||
result.color0 = hsla_to_rgba(colors[0].color);
|
||||
result.color1 = hsla_to_rgba(colors[1].color);
|
||||
|
||||
// Prepare color space in vertex for avoid conversion
|
||||
// in fragment shader for performance reasons
|
||||
if (color_space == 0u) {
|
||||
// sRGB
|
||||
result.color0 = linear_to_srgba(result.color0);
|
||||
result.color1 = linear_to_srgba(result.color1);
|
||||
} else if (color_space == 1u) {
|
||||
// Oklab
|
||||
result.color0 = linear_srgb_to_oklab(result.color0);
|
||||
result.color1 = linear_srgb_to_oklab(result.color1);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn gradient_color(background: Background, position: vec2<f32>, bounds: Bounds,
|
||||
sold_color: vec4<f32>, color0: vec4<f32>, color1: vec4<f32>) -> vec4<f32> {
|
||||
var background_color = vec4<f32>(0.0);
|
||||
|
||||
switch (background.tag) {
|
||||
default: {
|
||||
return sold_color;
|
||||
}
|
||||
case 1u: {
|
||||
// Linear gradient background.
|
||||
// -90 degrees to match the CSS gradient angle.
|
||||
let radians = (background.angle % 360.0 - 90.0) * M_PI_F / 180.0;
|
||||
var direction = vec2<f32>(cos(radians), sin(radians));
|
||||
let stop0_percentage = background.colors[0].percentage;
|
||||
let stop1_percentage = background.colors[1].percentage;
|
||||
|
||||
// Expand the short side to be the same as the long side
|
||||
if (bounds.size.x > bounds.size.y) {
|
||||
direction.y *= bounds.size.y / bounds.size.x;
|
||||
} else {
|
||||
direction.x *= bounds.size.x / bounds.size.y;
|
||||
}
|
||||
|
||||
// Get the t value for the linear gradient with the color stop percentages.
|
||||
let half_size = bounds.size / 2.0;
|
||||
let center = bounds.origin + half_size;
|
||||
let center_to_point = position - center;
|
||||
var t = dot(center_to_point, direction) / length(direction);
|
||||
// Check the direct to determine the use x or y
|
||||
if (abs(direction.x) > abs(direction.y)) {
|
||||
t = (t + half_size.x) / bounds.size.x;
|
||||
} else {
|
||||
t = (t + half_size.y) / bounds.size.y;
|
||||
}
|
||||
|
||||
// Adjust t based on the stop percentages
|
||||
t = (t - stop0_percentage) / (stop1_percentage - stop0_percentage);
|
||||
t = clamp(t, 0.0, 1.0);
|
||||
|
||||
switch (background.color_space) {
|
||||
default: {
|
||||
background_color = srgba_to_linear(mix(color0, color1, t));
|
||||
}
|
||||
case 1u: {
|
||||
let oklab_color = mix(color0, color1, t);
|
||||
background_color = oklab_to_linear_srgb(oklab_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return background_color;
|
||||
}
|
||||
|
||||
// --- quads --- //
|
||||
|
||||
struct Quad {
|
||||
@@ -204,7 +369,7 @@ struct Quad {
|
||||
pad: u32,
|
||||
bounds: Bounds,
|
||||
content_mask: Bounds,
|
||||
background: Hsla,
|
||||
background: Background,
|
||||
border_color: Hsla,
|
||||
corner_radii: Corners,
|
||||
border_widths: Edges,
|
||||
@@ -213,11 +378,13 @@ var<storage, read> b_quads: array<Quad>;
|
||||
|
||||
struct QuadVarying {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) @interpolate(flat) background_color: vec4<f32>,
|
||||
@location(1) @interpolate(flat) border_color: vec4<f32>,
|
||||
@location(2) @interpolate(flat) quad_id: u32,
|
||||
//TODO: use `clip_distance` once Naga supports it
|
||||
@location(3) clip_distances: vec4<f32>,
|
||||
@location(0) @interpolate(flat) border_color: vec4<f32>,
|
||||
@location(1) @interpolate(flat) quad_id: u32,
|
||||
// TODO: use `clip_distance` once Naga supports it
|
||||
@location(2) clip_distances: vec4<f32>,
|
||||
@location(3) @interpolate(flat) background_solid: vec4<f32>,
|
||||
@location(4) @interpolate(flat) background_color0: vec4<f32>,
|
||||
@location(5) @interpolate(flat) background_color1: vec4<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
@@ -227,7 +394,16 @@ fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
|
||||
|
||||
var out = QuadVarying();
|
||||
out.position = to_device_position(unit_vertex, quad.bounds);
|
||||
out.background_color = hsla_to_rgba(quad.background);
|
||||
|
||||
let gradient = prepare_gradient_color(
|
||||
quad.background.tag,
|
||||
quad.background.color_space,
|
||||
quad.background.solid,
|
||||
quad.background.colors
|
||||
);
|
||||
out.background_solid = gradient.solid;
|
||||
out.background_color0 = gradient.color0;
|
||||
out.background_color1 = gradient.color1;
|
||||
out.border_color = hsla_to_rgba(quad.border_color);
|
||||
out.quad_id = instance_id;
|
||||
out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
|
||||
@@ -242,21 +418,23 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
}
|
||||
|
||||
let quad = b_quads[input.quad_id];
|
||||
let half_size = quad.bounds.size / 2.0;
|
||||
let center = quad.bounds.origin + half_size;
|
||||
let center_to_point = input.position.xy - center;
|
||||
|
||||
let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
|
||||
input.background_solid, input.background_color0, input.background_color1);
|
||||
|
||||
// Fast path when the quad is not rounded and doesn't have any border.
|
||||
if (quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 &&
|
||||
quad.corner_radii.top_right == 0.0 &&
|
||||
quad.corner_radii.bottom_right == 0.0 && quad.border_widths.top == 0.0 &&
|
||||
quad.border_widths.left == 0.0 && quad.border_widths.right == 0.0 &&
|
||||
quad.border_widths.bottom == 0.0) {
|
||||
return blend_color(input.background_color, 1.0);
|
||||
return blend_color(background_color, 1.0);
|
||||
}
|
||||
|
||||
let half_size = quad.bounds.size / 2.0;
|
||||
let center = quad.bounds.origin + half_size;
|
||||
let center_to_point = input.position.xy - center;
|
||||
|
||||
let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
|
||||
|
||||
let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
|
||||
let distance =
|
||||
length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
|
||||
@@ -277,13 +455,13 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
border_width = vertical_border;
|
||||
}
|
||||
|
||||
var color = input.background_color;
|
||||
var color = background_color;
|
||||
if (border_width > 0.0) {
|
||||
let inset_distance = distance + border_width;
|
||||
// Blend the border on top of the background and then linearly interpolate
|
||||
// between the two as we slide inside the background.
|
||||
let blended_border = over(input.background_color, input.border_color);
|
||||
color = mix(blended_border, input.background_color,
|
||||
let blended_border = over(background_color, input.border_color);
|
||||
color = mix(blended_border, background_color,
|
||||
saturate(0.5 - inset_distance));
|
||||
}
|
||||
|
||||
@@ -408,7 +586,7 @@ fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 {
|
||||
|
||||
struct PathSprite {
|
||||
bounds: Bounds,
|
||||
color: Hsla,
|
||||
color: Background,
|
||||
tile: AtlasTile,
|
||||
}
|
||||
var<storage, read> b_path_sprites: array<PathSprite>;
|
||||
@@ -416,7 +594,10 @@ var<storage, read> b_path_sprites: array<PathSprite>;
|
||||
struct PathVarying {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) tile_position: vec2<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
@location(1) @interpolate(flat) instance_id: u32,
|
||||
@location(2) @interpolate(flat) color_solid: vec4<f32>,
|
||||
@location(3) @interpolate(flat) color0: vec4<f32>,
|
||||
@location(4) @interpolate(flat) color1: vec4<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
@@ -428,7 +609,17 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
|
||||
var out = PathVarying();
|
||||
out.position = to_device_position(unit_vertex, sprite.bounds);
|
||||
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
|
||||
out.color = hsla_to_rgba(sprite.color);
|
||||
out.instance_id = instance_id;
|
||||
|
||||
let gradient = prepare_gradient_color(
|
||||
sprite.color.tag,
|
||||
sprite.color.color_space,
|
||||
sprite.color.solid,
|
||||
sprite.color.colors
|
||||
);
|
||||
out.color_solid = gradient.solid;
|
||||
out.color0 = gradient.color0;
|
||||
out.color1 = gradient.color1;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -436,7 +627,11 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
|
||||
fn fs_path(input: PathVarying) -> @location(0) vec4<f32> {
|
||||
let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
|
||||
let mask = 1.0 - abs(1.0 - sample % 2.0);
|
||||
return blend_color(input.color, mask);
|
||||
let sprite = b_path_sprites[input.instance_id];
|
||||
let background = sprite.color;
|
||||
let color = gradient_color(background, input.position.xy, sprite.bounds,
|
||||
input.color_solid, input.color0, input.color1);
|
||||
return blend_color(color, mask);
|
||||
}
|
||||
|
||||
// --- underlines --- //
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::time::{Duration, Instant};
|
||||
use calloop::generic::{FdWrapper, Generic};
|
||||
use calloop::{EventLoop, LoopHandle, RegistrationToken};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use collections::HashMap;
|
||||
use http_client::Url;
|
||||
use smallvec::SmallVec;
|
||||
@@ -1417,9 +1418,10 @@ impl LinuxClient for X11Client {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("failed to change window cursor")
|
||||
.check()
|
||||
.unwrap();
|
||||
.anyhow()
|
||||
.and_then(|cookie| cookie.check().anyhow())
|
||||
.context("setting cursor style")
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn open_uri(&self, uri: &str) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::metal_atlas::MetalAtlas;
|
||||
use crate::{
|
||||
point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels,
|
||||
Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
|
||||
point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask,
|
||||
DevicePixels, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
|
||||
PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -1242,7 +1242,7 @@ enum PathRasterizationInputIndex {
|
||||
#[repr(C)]
|
||||
pub struct PathSprite {
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub color: Hsla,
|
||||
pub color: Background,
|
||||
pub tile: AtlasTile,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
using namespace metal;
|
||||
|
||||
float4 hsla_to_rgba(Hsla hsla);
|
||||
float3 srgb_to_linear(float3 color);
|
||||
float3 linear_to_srgb(float3 color);
|
||||
float4 srgb_to_oklab(float4 color);
|
||||
float4 oklab_to_srgb(float4 color);
|
||||
float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
|
||||
constant Size_DevicePixels *viewport_size);
|
||||
float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds,
|
||||
@@ -21,20 +25,34 @@ float2 erf(float2 x);
|
||||
float blur_along_x(float x, float y, float sigma, float corner,
|
||||
float2 half_size);
|
||||
float4 over(float4 below, float4 above);
|
||||
float radians(float degrees);
|
||||
float4 gradient_color(Background background, float2 position, Bounds_ScaledPixels bounds,
|
||||
float4 solid_color, float4 color0, float4 color1);
|
||||
|
||||
struct GradientColor {
|
||||
float4 solid;
|
||||
float4 color0;
|
||||
float4 color1;
|
||||
};
|
||||
GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1);
|
||||
|
||||
struct QuadVertexOutput {
|
||||
float4 position [[position]];
|
||||
float4 background_color [[flat]];
|
||||
float4 border_color [[flat]];
|
||||
uint quad_id [[flat]];
|
||||
float4 position [[position]];
|
||||
float4 border_color [[flat]];
|
||||
float4 background_solid [[flat]];
|
||||
float4 background_color0 [[flat]];
|
||||
float4 background_color1 [[flat]];
|
||||
float clip_distance [[clip_distance]][4];
|
||||
};
|
||||
|
||||
struct QuadFragmentInput {
|
||||
float4 position [[position]];
|
||||
float4 background_color [[flat]];
|
||||
float4 border_color [[flat]];
|
||||
uint quad_id [[flat]];
|
||||
float4 position [[position]];
|
||||
float4 border_color [[flat]];
|
||||
float4 background_solid [[flat]];
|
||||
float4 background_color0 [[flat]];
|
||||
float4 background_color1 [[flat]];
|
||||
};
|
||||
|
||||
vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
|
||||
@@ -51,13 +69,23 @@ vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
|
||||
to_device_position(unit_vertex, quad.bounds, viewport_size);
|
||||
float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds,
|
||||
quad.content_mask.bounds);
|
||||
float4 background_color = hsla_to_rgba(quad.background);
|
||||
float4 border_color = hsla_to_rgba(quad.border_color);
|
||||
|
||||
GradientColor gradient = prepare_gradient_color(
|
||||
quad.background.tag,
|
||||
quad.background.color_space,
|
||||
quad.background.solid,
|
||||
quad.background.colors[0].color,
|
||||
quad.background.colors[1].color
|
||||
);
|
||||
|
||||
return QuadVertexOutput{
|
||||
device_position,
|
||||
background_color,
|
||||
border_color,
|
||||
quad_id,
|
||||
device_position,
|
||||
border_color,
|
||||
gradient.solid,
|
||||
gradient.color0,
|
||||
gradient.color1,
|
||||
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
|
||||
}
|
||||
|
||||
@@ -65,6 +93,11 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
constant Quad *quads
|
||||
[[buffer(QuadInputIndex_Quads)]]) {
|
||||
Quad quad = quads[input.quad_id];
|
||||
float2 half_size = float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
|
||||
float2 center = float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
|
||||
float2 center_to_point = input.position.xy - center;
|
||||
float4 color = gradient_color(quad.background, input.position.xy, quad.bounds,
|
||||
input.background_solid, input.background_color0, input.background_color1);
|
||||
|
||||
// Fast path when the quad is not rounded and doesn't have any border.
|
||||
if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. &&
|
||||
@@ -72,14 +105,9 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. &&
|
||||
quad.border_widths.left == 0. && quad.border_widths.right == 0. &&
|
||||
quad.border_widths.bottom == 0.) {
|
||||
return input.background_color;
|
||||
return color;
|
||||
}
|
||||
|
||||
float2 half_size =
|
||||
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
|
||||
float2 center =
|
||||
float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
|
||||
float2 center_to_point = input.position.xy - center;
|
||||
float corner_radius;
|
||||
if (center_to_point.x < 0.) {
|
||||
if (center_to_point.y < 0.) {
|
||||
@@ -118,15 +146,12 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
border_width = vertical_border;
|
||||
}
|
||||
|
||||
float4 color;
|
||||
if (border_width == 0.) {
|
||||
color = input.background_color;
|
||||
} else {
|
||||
if (border_width != 0.) {
|
||||
float inset_distance = distance + border_width;
|
||||
// Blend the border on top of the background and then linearly interpolate
|
||||
// between the two as we slide inside the background.
|
||||
float4 blended_border = over(input.background_color, input.border_color);
|
||||
color = mix(blended_border, input.background_color,
|
||||
float4 blended_border = over(color, input.border_color);
|
||||
color = mix(blended_border, color,
|
||||
saturate(0.5 - inset_distance));
|
||||
}
|
||||
|
||||
@@ -437,7 +462,10 @@ fragment float4 path_rasterization_fragment(PathRasterizationFragmentInput input
|
||||
struct PathSpriteVertexOutput {
|
||||
float4 position [[position]];
|
||||
float2 tile_position;
|
||||
float4 color [[flat]];
|
||||
uint sprite_id [[flat]];
|
||||
float4 solid_color [[flat]];
|
||||
float4 color0 [[flat]];
|
||||
float4 color1 [[flat]];
|
||||
};
|
||||
|
||||
vertex PathSpriteVertexOutput path_sprite_vertex(
|
||||
@@ -456,8 +484,23 @@ vertex PathSpriteVertexOutput path_sprite_vertex(
|
||||
float4 device_position =
|
||||
to_device_position(unit_vertex, sprite.bounds, viewport_size);
|
||||
float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
|
||||
float4 color = hsla_to_rgba(sprite.color);
|
||||
return PathSpriteVertexOutput{device_position, tile_position, color};
|
||||
|
||||
GradientColor gradient = prepare_gradient_color(
|
||||
sprite.color.tag,
|
||||
sprite.color.color_space,
|
||||
sprite.color.solid,
|
||||
sprite.color.colors[0].color,
|
||||
sprite.color.colors[1].color
|
||||
);
|
||||
|
||||
return PathSpriteVertexOutput{
|
||||
device_position,
|
||||
tile_position,
|
||||
sprite_id,
|
||||
gradient.solid,
|
||||
gradient.color0,
|
||||
gradient.color1
|
||||
};
|
||||
}
|
||||
|
||||
fragment float4 path_sprite_fragment(
|
||||
@@ -469,7 +512,10 @@ fragment float4 path_sprite_fragment(
|
||||
float4 sample =
|
||||
atlas_texture.sample(atlas_texture_sampler, input.tile_position);
|
||||
float mask = 1. - abs(1. - fmod(sample.r, 2.));
|
||||
float4 color = input.color;
|
||||
PathSprite sprite = sprites[input.sprite_id];
|
||||
Background background = sprite.color;
|
||||
float4 color = gradient_color(background, input.position.xy, sprite.bounds,
|
||||
input.solid_color, input.color0, input.color1);
|
||||
color.a *= mask;
|
||||
return color;
|
||||
}
|
||||
@@ -574,6 +620,56 @@ float4 hsla_to_rgba(Hsla hsla) {
|
||||
return rgba;
|
||||
}
|
||||
|
||||
float3 srgb_to_linear(float3 color) {
|
||||
return pow(color, float3(2.2));
|
||||
}
|
||||
|
||||
float3 linear_to_srgb(float3 color) {
|
||||
return pow(color, float3(1.0 / 2.2));
|
||||
}
|
||||
|
||||
// Converts a sRGB color to the Oklab color space.
|
||||
// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
|
||||
float4 srgb_to_oklab(float4 color) {
|
||||
// Convert non-linear sRGB to linear sRGB
|
||||
color = float4(srgb_to_linear(color.rgb), color.a);
|
||||
|
||||
float l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
|
||||
float m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
|
||||
float s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
|
||||
|
||||
float l_ = pow(l, 1.0/3.0);
|
||||
float m_ = pow(m, 1.0/3.0);
|
||||
float s_ = pow(s, 1.0/3.0);
|
||||
|
||||
return float4(
|
||||
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
|
||||
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
|
||||
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
|
||||
color.a
|
||||
);
|
||||
}
|
||||
|
||||
// Converts an Oklab color to the sRGB color space.
|
||||
float4 oklab_to_srgb(float4 color) {
|
||||
float l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
|
||||
float m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
|
||||
float s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
|
||||
|
||||
float l = l_ * l_ * l_;
|
||||
float m = m_ * m_ * m_;
|
||||
float s = s_ * s_ * s_;
|
||||
|
||||
float3 linear_rgb = float3(
|
||||
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
||||
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
||||
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
|
||||
);
|
||||
|
||||
// Convert linear sRGB to non-linear sRGB
|
||||
return float4(linear_to_srgb(linear_rgb), color.a);
|
||||
}
|
||||
|
||||
float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
|
||||
constant Size_DevicePixels *input_viewport_size) {
|
||||
float2 position =
|
||||
@@ -691,3 +787,81 @@ float4 over(float4 below, float4 above) {
|
||||
result.a = alpha;
|
||||
return result;
|
||||
}
|
||||
|
||||
GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid,
|
||||
Hsla color0, Hsla color1) {
|
||||
GradientColor out;
|
||||
if (tag == 0) {
|
||||
out.solid = hsla_to_rgba(solid);
|
||||
} else if (tag == 1) {
|
||||
out.color0 = hsla_to_rgba(color0);
|
||||
out.color1 = hsla_to_rgba(color1);
|
||||
|
||||
// Prepare color space in vertex for avoid conversion
|
||||
// in fragment shader for performance reasons
|
||||
if (color_space == 1) {
|
||||
// Oklab
|
||||
out.color0 = srgb_to_oklab(out.color0);
|
||||
out.color1 = srgb_to_oklab(out.color1);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
float4 gradient_color(Background background,
|
||||
float2 position,
|
||||
Bounds_ScaledPixels bounds,
|
||||
float4 solid_color, float4 color0, float4 color1) {
|
||||
float4 color;
|
||||
|
||||
switch (background.tag) {
|
||||
case 0:
|
||||
color = solid_color;
|
||||
break;
|
||||
case 1: {
|
||||
// -90 degrees to match the CSS gradient angle.
|
||||
float radians = (fmod(background.angle, 360.0) - 90.0) * (M_PI_F / 180.0);
|
||||
float2 direction = float2(cos(radians), sin(radians));
|
||||
|
||||
// Expand the short side to be the same as the long side
|
||||
if (bounds.size.width > bounds.size.height) {
|
||||
direction.y *= bounds.size.height / bounds.size.width;
|
||||
} else {
|
||||
direction.x *= bounds.size.width / bounds.size.height;
|
||||
}
|
||||
|
||||
// Get the t value for the linear gradient with the color stop percentages.
|
||||
float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.;
|
||||
float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
|
||||
float2 center_to_point = position - center;
|
||||
float t = dot(center_to_point, direction) / length(direction);
|
||||
// Check the direct to determine the use x or y
|
||||
if (abs(direction.x) > abs(direction.y)) {
|
||||
t = (t + half_size.x) / bounds.size.width;
|
||||
} else {
|
||||
t = (t + half_size.y) / bounds.size.height;
|
||||
}
|
||||
|
||||
// Adjust t based on the stop percentages
|
||||
t = (t - background.colors[0].percentage)
|
||||
/ (background.colors[1].percentage
|
||||
- background.colors[0].percentage);
|
||||
t = clamp(t, 0.0, 1.0);
|
||||
|
||||
switch (background.color_space) {
|
||||
case 0:
|
||||
color = mix(color0, color1, t);
|
||||
break;
|
||||
case 1: {
|
||||
float4 oklab_color = mix(color0, color1, t);
|
||||
color = oklab_to_srgb(oklab_color);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
@@ -141,16 +141,27 @@ fn handle_size_msg(
|
||||
lparam: LPARAM,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
|
||||
// Don't resize the renderer when the window is minimized, but record that it was minimized so
|
||||
// that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`.
|
||||
if wparam.0 == SIZE_MINIMIZED as usize {
|
||||
lock.is_minimized = Some(true);
|
||||
return Some(0);
|
||||
}
|
||||
let may_have_been_minimized = lock.is_minimized.unwrap_or(true);
|
||||
lock.is_minimized = Some(false);
|
||||
|
||||
let width = lparam.loword().max(1) as i32;
|
||||
let height = lparam.hiword().max(1) as i32;
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
let new_size = size(DevicePixels(width), DevicePixels(height));
|
||||
let scale_factor = lock.scale_factor;
|
||||
lock.renderer.update_drawable_size(new_size);
|
||||
if may_have_been_minimized {
|
||||
lock.renderer
|
||||
.update_drawable_size_even_if_unchanged(new_size);
|
||||
} else {
|
||||
lock.renderer.update_drawable_size(new_size);
|
||||
}
|
||||
let new_size = new_size.to_pixels(scale_factor);
|
||||
lock.logical_size = new_size;
|
||||
if let Some(mut callback) = lock.callbacks.resize.take() {
|
||||
|
||||
@@ -38,6 +38,7 @@ pub struct WindowsWindowState {
|
||||
pub fullscreen_restore_bounds: Bounds<Pixels>,
|
||||
pub border_offset: WindowBorderOffset,
|
||||
pub scale_factor: f32,
|
||||
pub is_minimized: Option<bool>,
|
||||
|
||||
pub callbacks: Callbacks,
|
||||
pub input_handler: Option<PlatformInputHandler>,
|
||||
@@ -92,6 +93,7 @@ impl WindowsWindowState {
|
||||
size: logical_size,
|
||||
};
|
||||
let border_offset = WindowBorderOffset::default();
|
||||
let is_minimized = None;
|
||||
let renderer = windows_renderer::windows_renderer(hwnd, transparent)?;
|
||||
let callbacks = Callbacks::default();
|
||||
let input_handler = None;
|
||||
@@ -109,6 +111,7 @@ impl WindowsWindowState {
|
||||
fullscreen_restore_bounds,
|
||||
border_offset,
|
||||
scale_factor,
|
||||
is_minimized,
|
||||
callbacks,
|
||||
input_handler,
|
||||
system_key_handled,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! application to avoid having to import each trait individually.
|
||||
|
||||
pub use crate::{
|
||||
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
|
||||
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context as _, Element, FocusableElement,
|
||||
InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
|
||||
StatefulInteractiveElement, Styled, StyledImage, VisualContext,
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#![cfg_attr(windows, allow(dead_code))]
|
||||
|
||||
use crate::{
|
||||
bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges,
|
||||
Hsla, Pixels, Point, Radians, ScaledPixels, Size,
|
||||
bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Background, Bounds, ContentMask,
|
||||
Corners, Edges, Hsla, Pixels, Point, Radians, ScaledPixels, Size,
|
||||
};
|
||||
use std::{fmt::Debug, iter::Peekable, ops::Range, slice};
|
||||
|
||||
@@ -458,7 +458,7 @@ pub(crate) struct Quad {
|
||||
pub pad: u32, // align to 8 bytes
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub background: Hsla,
|
||||
pub background: Background,
|
||||
pub border_color: Hsla,
|
||||
pub corner_radii: Corners<ScaledPixels>,
|
||||
pub border_widths: Edges<ScaledPixels>,
|
||||
@@ -671,7 +671,7 @@ pub struct Path<P: Clone + Default + Debug> {
|
||||
pub(crate) bounds: Bounds<P>,
|
||||
pub(crate) content_mask: ContentMask<P>,
|
||||
pub(crate) vertices: Vec<PathVertex<P>>,
|
||||
pub(crate) color: Hsla,
|
||||
pub(crate) color: Background,
|
||||
start: Point<P>,
|
||||
current: Point<P>,
|
||||
contour_count: usize,
|
||||
|
||||
@@ -5,10 +5,11 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
black, phi, point, quad, rems, size, AbsoluteLength, Bounds, ContentMask, Corners,
|
||||
CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font,
|
||||
FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point,
|
||||
PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
|
||||
black, phi, point, quad, rems, size, AbsoluteLength, Background, BackgroundTag, Bounds,
|
||||
ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges,
|
||||
EdgesRefinement, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length,
|
||||
Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun,
|
||||
WindowContext,
|
||||
};
|
||||
use collections::HashSet;
|
||||
use refineable::Refineable;
|
||||
@@ -572,7 +573,17 @@ impl Style {
|
||||
|
||||
let background_color = self.background.as_ref().and_then(Fill::color);
|
||||
if background_color.map_or(false, |color| !color.is_transparent()) {
|
||||
let mut border_color = background_color.unwrap_or_default();
|
||||
let mut border_color = match background_color {
|
||||
Some(color) => match color.tag {
|
||||
BackgroundTag::Solid => color.solid,
|
||||
BackgroundTag::LinearGradient => color
|
||||
.colors
|
||||
.first()
|
||||
.map(|stop| stop.color)
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
None => Hsla::default(),
|
||||
};
|
||||
border_color.a = 0.;
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
@@ -737,12 +748,14 @@ pub struct StrikethroughStyle {
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Fill {
|
||||
/// A solid color fill.
|
||||
Color(Hsla),
|
||||
Color(Background),
|
||||
}
|
||||
|
||||
impl Fill {
|
||||
/// Unwrap this fill into a solid color, if it is one.
|
||||
pub fn color(&self) -> Option<Hsla> {
|
||||
///
|
||||
/// If the fill is not a solid color, this method returns `None`.
|
||||
pub fn color(&self) -> Option<Background> {
|
||||
match self {
|
||||
Fill::Color(color) => Some(*color),
|
||||
}
|
||||
@@ -751,13 +764,13 @@ impl Fill {
|
||||
|
||||
impl Default for Fill {
|
||||
fn default() -> Self {
|
||||
Self::Color(Hsla::default())
|
||||
Self::Color(Background::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsla> for Fill {
|
||||
fn from(color: Hsla) -> Self {
|
||||
Self::Color(color)
|
||||
Self::Color(color.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,6 +780,12 @@ impl From<Rgba> for Fill {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Background> for Fill {
|
||||
fn from(background: Background) -> Self {
|
||||
Self::Color(background)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TextStyle> for HighlightStyle {
|
||||
fn from(other: TextStyle) -> Self {
|
||||
Self::from(&other)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip,
|
||||
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow,
|
||||
Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
|
||||
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Background, Bounds,
|
||||
BoxShadow, Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
|
||||
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
|
||||
FileDropEvent, Flatten, FontId, GPUSpecs, Global, GlobalElementId, GlyphId, Hsla, InputHandler,
|
||||
IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent,
|
||||
@@ -2325,7 +2325,7 @@ impl<'a> WindowContext<'a> {
|
||||
/// Paint the given `Path` into the scene for the next frame at the current z-index.
|
||||
///
|
||||
/// This method should only be called as part of the paint phase of element drawing.
|
||||
pub fn paint_path(&mut self, mut path: Path<Pixels>, color: impl Into<Hsla>) {
|
||||
pub fn paint_path(&mut self, mut path: Path<Pixels>, color: impl Into<Background>) {
|
||||
debug_assert_eq!(
|
||||
self.window.draw_phase,
|
||||
DrawPhase::Paint,
|
||||
@@ -2336,7 +2336,8 @@ impl<'a> WindowContext<'a> {
|
||||
let content_mask = self.content_mask();
|
||||
let opacity = self.element_opacity();
|
||||
path.content_mask = content_mask;
|
||||
path.color = color.into().opacity(opacity);
|
||||
let color: Background = color.into();
|
||||
path.color = color.opacity(opacity);
|
||||
self.window
|
||||
.next_frame
|
||||
.scene
|
||||
@@ -4980,7 +4981,7 @@ pub struct PaintQuad {
|
||||
/// The radii of the quad's corners.
|
||||
pub corner_radii: Corners<Pixels>,
|
||||
/// The background color of the quad.
|
||||
pub background: Hsla,
|
||||
pub background: Background,
|
||||
/// The widths of the quad's borders.
|
||||
pub border_widths: Edges<Pixels>,
|
||||
/// The color of the quad's borders.
|
||||
@@ -5013,7 +5014,7 @@ impl PaintQuad {
|
||||
}
|
||||
|
||||
/// Sets the background color of the quad.
|
||||
pub fn background(self, background: impl Into<Hsla>) -> Self {
|
||||
pub fn background(self, background: impl Into<Background>) -> Self {
|
||||
PaintQuad {
|
||||
background: background.into(),
|
||||
..self
|
||||
@@ -5025,7 +5026,7 @@ impl PaintQuad {
|
||||
pub fn quad(
|
||||
bounds: Bounds<Pixels>,
|
||||
corner_radii: impl Into<Corners<Pixels>>,
|
||||
background: impl Into<Hsla>,
|
||||
background: impl Into<Background>,
|
||||
border_widths: impl Into<Edges<Pixels>>,
|
||||
border_color: impl Into<Hsla>,
|
||||
) -> PaintQuad {
|
||||
@@ -5039,7 +5040,7 @@ pub fn quad(
|
||||
}
|
||||
|
||||
/// Creates a filled quad with the given bounds and background color.
|
||||
pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Hsla>) -> PaintQuad {
|
||||
pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Background>) -> PaintQuad {
|
||||
PaintQuad {
|
||||
bounds: bounds.into(),
|
||||
corner_radii: (0.).into(),
|
||||
@@ -5054,7 +5055,7 @@ pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>)
|
||||
PaintQuad {
|
||||
bounds: bounds.into(),
|
||||
corner_radii: (0.).into(),
|
||||
background: transparent_black(),
|
||||
background: transparent_black().into(),
|
||||
border_widths: (1.).into(),
|
||||
border_color: border_color.into(),
|
||||
}
|
||||
|
||||
@@ -271,6 +271,20 @@ pub fn cursor_style_methods(input: TokenStream) -> TokenStream {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `nesw-resize`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
#visibility fn cursor_nesw_resize(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpRightDownLeft);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `nwse-resize`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
#visibility fn cursor_nwse_resize(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpLeftDownRight);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `col-resize`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
#visibility fn cursor_col_resize(mut self) -> Self {
|
||||
|
||||
@@ -15,6 +15,7 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
file_icons.workspace = true
|
||||
gpui.workspace = true
|
||||
project.workspace = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use editor::items::entry_git_aware_label_color;
|
||||
use gpui::{
|
||||
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, EventEmitter,
|
||||
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ObjectFit, ParentElement,
|
||||
@@ -94,15 +95,29 @@ impl Item for ImageView {
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||
let path = self.image_item.read(cx).file.path();
|
||||
let title = path
|
||||
.file_name()
|
||||
.unwrap_or_else(|| path.as_os_str())
|
||||
let project_path = self.image_item.read(cx).project_path(cx);
|
||||
let label_color = if ItemSettings::get_global(cx).git_status {
|
||||
self.project
|
||||
.read(cx)
|
||||
.entry_for_path(&project_path, cx)
|
||||
.map(|entry| {
|
||||
entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected)
|
||||
})
|
||||
.unwrap_or_else(|| params.text_color())
|
||||
} else {
|
||||
params.text_color()
|
||||
};
|
||||
|
||||
let title = self
|
||||
.image_item
|
||||
.read(cx)
|
||||
.file
|
||||
.file_name(cx)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
Label::new(title)
|
||||
.single_line()
|
||||
.color(params.text_color())
|
||||
.color(label_color)
|
||||
.italic(params.preview)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -146,7 +161,7 @@ impl Item for ImageView {
|
||||
}
|
||||
|
||||
fn breadcrumbs_text_for_image(project: &Project, image: &ImageItem, cx: &AppContext) -> String {
|
||||
let path = image.path();
|
||||
let path = image.file.file_name(cx);
|
||||
if project.visible_worktrees(cx).count() <= 1 {
|
||||
return path.to_string_lossy().to_string();
|
||||
}
|
||||
|
||||