Compare commits

..

25 Commits

Author SHA1 Message Date
Cole Miller
2e1b077915 Clippy?? 2024-12-13 10:24:47 -05:00
Cole Miller
74b7e8ca32 Clippy? 2024-12-13 10:21:31 -05:00
Cole Miller
ba2760544a Clippy 2024-12-13 10:12:33 -05:00
Cole Miller
20e24dca68 Randomized diff view test
Co-authored-by: Conrad Irwin <conrad@zed.dev>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Max Brunsfeld <max@zed.dev>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Thorsten Ball <thorsten@zed.dev>
2024-12-12 22:14:23 -05:00
Nate Butler
59afc27f03 Add placeholder git panel (#21894)
Adds a simple git placeholder panel for us to iterate from. Also
includes a number of assets from the git prototyping branch that we will
use.

Note: This panel is staff flagged for now.

Release Notes:

- N/A
2024-12-11 22:13:52 -05:00
Peter Tripp
611abcadc0 Add schema to .github/ISSUE_TEMPLATE/config.yml (#21836)
Workaround for upstream issue where yaml-language-server
2024-12-11 17:16:21 -05:00
Peter Tripp
fff12ec1e5 Mention Lllama 3.3 in Ollama config panel (#21866)
Trivial, but makes us not look outdated.
2024-12-11 16:38:03 -05:00
Conrad Irwin
13a81e454a Start to split out initialization and registration (#21787)
Still TODO:

* [x] Factor out `start_language_server` so we can call it on register
(instead of on detect language)
* [x] Only call register in singleton editors (or when
editing/go-to-definition etc. in a multibuffer?)
* [x] Refcount on register so we can unregister when no buffer remain
* [ ] (maybe) Stop language servers that are no longer needed after some
time

Release Notes:

- Fixed language servers starting when doing project search
- Fixed high CPU usage when ignoring warnings in the diagnostics view

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Cole <cole@zed.dev>
2024-12-11 14:05:10 -07:00
Jason Lee
de89f8cf83 gpui: Add linear gradient support to fill background (#20812)
Release Notes:

- gpui: Add linear gradient support to fill background

Run example:

```
cargo run -p gpui --example gradient
cargo run -p gpui --example gradient --features macos-blade
```

## Demo

In GPUI (sRGB):

<img width="761" alt="image"
src="https://github.com/user-attachments/assets/568c02e8-3065-43c2-b5c2-5618d553dd6e">

In GPUI (Oklab):

<img width="761" alt="image"
src="https://github.com/user-attachments/assets/b008b0de-2705-4f99-831d-998ce48eed42">

In CSS (sRGB): 

https://codepen.io/huacnlee/pen/rNXgxBY

<img width="505" alt="image"
src="https://github.com/user-attachments/assets/239f4b65-24b3-4797-9491-a13eea420158">

In CSS (Oklab):

https://codepen.io/huacnlee/pen/wBwBKOp

<img width="658" alt="image"
src="https://github.com/user-attachments/assets/56fdd55f-d219-45de-922f-7227f535b210">


---

Currently only support 2 color stops with linear-gradient. I think this
is we first introduce the gradient feature in GPUI, and the
linear-gradient is most popular for use. So we can just add this first
and then to add more other supports.
2024-12-11 21:52:52 +02:00
Richard Feldman
c594ccb0af Inline assistant v2 (#21828)
This is behind the Assistant v2 feature flag. As @maxdeviant and I
discussed, the state is currently decoupled from the Assistant Panel's
state, although in the future we plan to introduce a way to refer to
conversations from the panel. Also, we're intentionally duplicating some
code with the v2 panel right now; the plan is to do a future PR to make
them share code more.


https://github.com/user-attachments/assets/bb163bd3-a02d-4a91-8f8f-2a8e60acbc34

It doesn't include the terminal inline assistant, which will be in a
separate PR.

Release Notes:

- N/A
2024-12-11 14:32:30 -05:00
Marshall Bowers
937186da12 gpui: Don't export named Context from prelude (#21869)
This PR updates the `gpui::prelude` to not export the `Context` trait
named.

This prevents some naming clashes in downstream consumers.

Release Notes:

- N/A
2024-12-11 13:21:40 -05:00
Marshall Bowers
b3ffbea376 assistant2: Allow removing individual context (#21868)
This PR adds the ability to remove individual pieces of context from the
message editor:

<img width="1159" alt="Screenshot 2024-12-11 at 12 38 45 PM"
src="https://github.com/user-attachments/assets/77d04272-f667-4ebb-a567-84b382afef3d"
/>

Release Notes:

- N/A
2024-12-11 12:51:05 -05:00
Thorsten Ball
124e63d07c Show inline completions when completion menu is visible (#21858)
This changes the behavior of how we display inline completions and
non-inline completions (i.e. completion menu).

Previously we would never show inline completions if a completion menu
was visible, meaning that we'd never show Copilot/Supermaven/...
suggestions if the language server had a suggestion.

With this change, we now display the inline completions even if there is
a completion menu visible.

In that case `<tab>` then accepts the inline completion and `<enter>`
accepts the selected entry in the completion menu.

Release Notes:

- Changed how inline completions (Copilot, Supermaven, ...) and normal
completions (from language servers) interact. Zed will now also show
inline completions when the completion menu is visible. The user can
accept the inline completion with `<tab>` and the active entry in the
completion menu with `<enter>`. Previously, `<tab>` would also select
the active entry in the completion menu.

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-11 17:13:22 +01:00
Antonio Scandurra
dd66a20d78 Move prediction diff computation to background thread (#21862)
Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-11 17:12:58 +01:00
Joseph T. Lyons
e8c72d91c3 v0.167.x dev 2024-12-11 11:00:35 -05:00
Danilo Leal
dfe455b054 zeta: Improve UI for feedback instructions (#21857)
If the instructions are added as the input placeholder, when in a
smaller window size (like the one from the screenshot), scrolling is
needed to see them all. So, thought of extracting it out of there. Also
thought it looked more refined this way!

<img width="800" alt="Screenshot 2024-12-11 at 11 48 17"
src="https://github.com/user-attachments/assets/46974b94-6365-4a59-bf71-a6c0863aac68"
/>

Release Notes:

- N/A
2024-12-11 12:07:41 -03:00
Danilo Leal
db7e38464a zeta: Show keybinding on rating buttons (#21853)
<img width="800" alt="Screenshot 2024-12-11 at 10 57 00"
src="https://github.com/user-attachments/assets/6055639c-5b38-444d-b76d-bf7584a82efc"
/>

Release Notes:

- N/A
2024-12-11 11:54:39 -03:00
Kyle Kelley
f8b6d71670 Optimize REPL kernel spec refresh (#21844)
Python kernelspec refresh now only performed on (known) python files. 

Release Notes:

- N/A
2024-12-11 06:20:44 -08:00
Thorsten Ball
ae351298b4 zeta: Fixes to completion-rating modal (#21852)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-11 15:00:27 +01:00
Thorsten Ball
664468d468 zeta: Invalidate completion in different buffers (#21850)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-11 14:37:53 +01:00
Piotr Osiewicz
714f183ede multi_buffer: optimize runnables layout (#21849)
Related to #21481 ; it fixes a bunch of hotspots I saw while looking at
the provided profiles. MultiBuffer still takes up 100% CPU on the
foreground thread for me - this time around it's on selection updates
(when dragging the selected text towards an edge of a screen).

Release Notes:

- N/A
2024-12-11 13:46:08 +01:00
Mikayla Maki
b36dcf3b92 Improve Zeta rating ergonomics (#21845)
This PR adds keyboard shortcuts to common interactions you might want to
do in the Zeta rating panel.

This PR also adds a way to fake inline completion requests, as well as
the test data used to create this PR, to make it easier to adjust the UI
in the future.

It also changes the status bar from the text "Zeta" to "ζ", because I
thought it looked cool.

Release Notes:

- N/A
2024-12-11 01:57:46 -08:00
Danilo Leal
63e1bf01a4 zeta: Improve reviewing UI (#21838)
Starting to fine-tune it.

| No edits scenario | Rated edits scenario |
|--------|--------|
| <img width="1577" alt="Screenshot 2024-12-11 at 01 57 46"
src="https://github.com/user-attachments/assets/42926e84-7a7f-4692-af44-672b52d3d377">
| <img width="1577" alt="Screenshot 2024-12-11 at 01 58 37"
src="https://github.com/user-attachments/assets/ee8ab0ef-75af-424c-b916-9f1ce8b5264d">

Release Notes:

- N/A
2024-12-11 02:19:57 -03:00
Connor Tsui
62a6a755ec Add musl package for Arch Linux (#21830)
It seems like `musl` is required to build on Arch Linux, but it is not included in the dependencies list.
2024-12-10 21:05:53 -05:00
Ethan Budd
28faba12a2 Recognize .C and .H as supported cpp extensions (#21647)
Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-10 19:55:21 -05:00
98 changed files with 11337 additions and 3469 deletions

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
blank_issues_enabled: false
contact_links:
- name: Language Request

1194
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -142,6 +142,7 @@ members = [
"crates/zed",
"crates/zed_actions",
"crates/zeta",
"crates/git_ui",
#
# Extensions
@@ -227,6 +228,7 @@ fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" }
git = { path = "crates/git" }
git_ui = { path = "crates/git_ui" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

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

View File

@@ -66,6 +66,7 @@
"cmd-v": "editor::Paste",
"cmd-z": "editor::Undo",
"cmd-shift-z": "editor::Redo",
"ctrl-shift-z": "zeta::RateCompletions",
"up": "editor::MoveUp",
"ctrl-up": "editor::MoveToStartOfParagraph",
"pageup": "editor::MovePageUp",
@@ -540,12 +541,18 @@
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"enter": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"context": "Editor && inline_completion",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::AcceptInlineCompletion"
@@ -788,5 +795,24 @@
"ctrl-k left": "pane::SplitLeft",
"ctrl-k right": "pane::SplitRight"
}
},
{
"context": "RateCompletionModal",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "zeta::ThumbsUp",
"shift-down": "zeta::NextEdit",
"shift-up": "zeta::PreviousEdit",
"right": "zeta::PreviewCompletion"
}
},
{
"context": "RateCompletionModal > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "zeta::FocusCompletions",
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
}
}
]

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,28 @@
mod active_thread;
mod assistant_panel;
mod assistant_settings;
mod context;
mod context_picker;
mod inline_assistant;
mod message_editor;
mod prompts;
mod streaming_diff;
mod thread;
mod thread_history;
mod thread_store;
mod ui;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs;
use gpui::{actions, AppContext};
use prompts::PromptLoadingParams;
use settings::Settings as _;
use util::ResultExt;
pub use crate::assistant_panel::AssistantPanel;
@@ -21,15 +33,37 @@ actions!(
NewThread,
ToggleModelSelector,
OpenHistory,
Chat
Chat,
ToggleInlineAssist,
CycleNextInlineAssist,
CyclePreviousInlineAssist
]
);
const NAMESPACE: &str = "assistant2";
/// Initializes the `assistant2` crate.
pub fn init(cx: &mut AppContext) {
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mut AppContext) {
AssistantSettings::register(cx);
assistant_panel::init(cx);
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
fs: fs.clone(),
repo_path: stdout_is_a_pty
.then(|| std::env::current_dir().log_err())
.flatten(),
cx,
}))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
feature_gate_assistant2_actions(cx);
}

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

View File

@@ -1,8 +1,20 @@
use gpui::SharedString;
use serde::{Deserialize, Serialize};
use util::post_inc;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ContextId(pub(crate) usize);
impl ContextId {
pub fn post_inc(&mut self) -> Self {
Self(post_inc(&mut self.0))
}
}
/// Some context attached to a message in a thread.
#[derive(Debug, Clone)]
pub struct Context {
pub id: ContextId,
pub name: SharedString,
pub kind: ContextKind,
pub text: SharedString,

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
use std::rc::Rc;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
@@ -10,7 +12,7 @@ use ui::{
PopoverMenuHandle, Tooltip,
};
use crate::context::{Context, ContextKind};
use crate::context::{Context, ContextId, ContextKind};
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
use crate::thread::{RequestKind, Thread};
use crate::ui::ContextPill;
@@ -20,19 +22,14 @@ pub struct MessageEditor {
thread: Model<Thread>,
editor: View<Editor>,
context: Vec<Context>,
next_context_id: ContextId,
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
use_tools: bool,
}
impl MessageEditor {
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
let mocked_context = vec![Context {
name: "shape.rs".into(),
kind: ContextKind::File,
text: "```rs\npub enum Shape {\n Circle,\n Square,\n Triangle,\n}".into(),
}];
Self {
let mut this = Self {
thread,
editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
@@ -40,10 +37,20 @@ impl MessageEditor {
editor
}),
context: mocked_context,
context: Vec::new(),
next_context_id: ContextId(0),
context_picker_handle: PopoverMenuHandle::default(),
use_tools: false,
}
};
this.context.push(Context {
id: this.next_context_id.post_inc(),
name: "shape.rs".into(),
kind: ContextKind::File,
text: "```rs\npub enum Shape {\n Circle,\n Square,\n Triangle,\n}".into(),
});
this
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
@@ -178,11 +185,15 @@ impl Render for MessageEditor {
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
))
.children(
self.context
.iter()
.map(|context| ContextPill::new(context.clone())),
)
.children(self.context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
Rc::new(cx.listener(move |this, _event, cx| {
this.context.retain(|other| other.id != context.id);
cx.notify();
}))
})
}))
.when(!self.context.is_empty(), |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,49 @@
use ui::prelude::*;
use std::rc::Rc;
use gpui::ClickEvent;
use ui::{prelude::*, IconButtonShape};
use crate::context::Context;
#[derive(IntoElement)]
pub struct ContextPill {
context: Context,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
}
impl ContextPill {
pub fn new(context: Context) -> Self {
Self { context }
Self {
context,
on_remove: None,
}
}
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
self.on_remove = Some(on_remove);
self
}
}
impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
div()
h_flex()
.gap_1()
.px_1()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
.when_some(self.on_remove, |parent, on_remove| {
parent.child(
IconButton::new("remove", IconName::Close)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.on_click({
let on_remove = on_remove.clone();
move |event, cx| on_remove(event, cx)
}),
)
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -296,7 +296,6 @@ mod tests {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
});
// When inserting, ensure autocompletion is favored over Copilot suggestions.
cx.set_state(indoc! {"
oneˇ
two
@@ -323,8 +322,9 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
// We want to show both: the inline completion and the completion menu
assert!(editor.context_menu_visible());
assert!(!editor.has_active_inline_completion());
assert!(editor.has_active_inline_completion());
// Confirming a completion inserts it and hides the context menu, without showing
// the copilot suggestion afterwards.
@@ -338,40 +338,7 @@ mod tests {
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
});
// Ensure Copilot suggestions are shown right away if no autocompletion is available.
cx.set_state(indoc! {"
oneˇ
two
three
"});
cx.simulate_keystroke(".");
drop(handle_completion_request(
&mut cx,
indoc! {"
one.|<>
two
three
"},
vec![],
));
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
});
// Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
// Reset editor and test that accepting completions works
cx.set_state(indoc! {"
oneˇ
two
@@ -399,17 +366,12 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.context_menu_visible());
assert!(!editor.has_active_inline_completion());
// When hiding the context menu, the Copilot suggestion becomes visible.
editor.cancel(&Default::default(), cx);
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
});
// Ensure existing completion is interpolated when inserting again.
// Ensure existing inline completion is interpolated when inserting again.
cx.simulate_keystroke("c");
executor.run_until_parked();
cx.update_editor(|editor, cx| {
@@ -880,7 +842,7 @@ mod tests {
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible(), "Even there are some completions available, those are not triggered when active copilot suggestion is present");
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
@@ -934,15 +896,9 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(
editor.context_menu_visible(),
"On completion trigger input, the completions should be fetched and visible"
);
assert!(
!editor.has_active_inline_completion(),
"On completion trigger input, copilot suggestion should be dismissed"
);
assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
assert!(editor.context_menu_visible());
assert!(editor.has_active_inline_completion(),);
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
});
}

View File

@@ -129,10 +129,10 @@ use multi_buffer::{
};
use parking_lot::RwLock;
use project::{
lsp_store::{FormatTarget, FormatTrigger},
lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle},
project_settings::{GitGutterSetting, ProjectSettings},
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
Project, ProjectItem, ProjectTransaction, TaskSourceKind,
LspStore, Project, ProjectItem, ProjectTransaction, TaskSourceKind,
};
use rand::prelude::*;
use rpc::{proto::*, ErrorExt};
@@ -663,6 +663,7 @@ pub struct Editor {
focused_block: Option<FocusedBlock>,
next_scroll_position: NextScrollCursorCenterTopBottom,
addons: HashMap<TypeId, Box<dyn Addon>>,
registered_buffers: HashMap<BufferId, OpenLspBufferHandle>,
_scroll_cursor_center_top_bottom_task: Task<()>,
}
@@ -1308,6 +1309,7 @@ impl Editor {
focused_block: None,
next_scroll_position: NextScrollCursorCenterTopBottom::default(),
addons: HashMap::default(),
registered_buffers: HashMap::default(),
_scroll_cursor_center_top_bottom_task: Task::ready(()),
text_style_refinement: None,
};
@@ -1325,6 +1327,17 @@ impl Editor {
this.git_blame_inline_enabled = true;
this.start_git_blame_inline(false, cx);
}
if let Some(buffer) = buffer.read(cx).as_singleton() {
if let Some(project) = this.project.as_ref() {
let lsp_store = project.read(cx).lsp_store();
let handle = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.register_buffer_with_language_servers(&buffer, cx)
});
this.registered_buffers
.insert(buffer.read(cx).remote_id(), handle);
}
}
}
this.report_editor_event("open", None, cx);
@@ -1635,6 +1648,22 @@ impl Editor {
self.collapse_matches = collapse_matches;
}
pub fn register_buffers_with_language_servers(&mut self, cx: &mut ViewContext<Self>) {
let buffers = self.buffer.read(cx).all_buffers();
let Some(lsp_store) = self.lsp_store(cx) else {
return;
};
lsp_store.update(cx, |lsp_store, cx| {
for buffer in buffers {
self.registered_buffers
.entry(buffer.read(cx).remote_id())
.or_insert_with(|| {
lsp_store.register_buffer_with_language_servers(&buffer, cx)
});
}
})
}
pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
if self.collapse_matches {
return range.start..range.start;
@@ -3687,16 +3716,13 @@ impl Editor {
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
*context_menu = Some(CodeContextMenu::Completions(menu));
drop(context_menu);
editor.discard_inline_completion(false, cx);
cx.notify();
} else if editor.completion_tasks.len() <= 1 {
// If there are no more completion tasks and the last menu was
// empty, we should hide it. If it was already hidden, we should
// also show the copilot completion when available.
drop(context_menu);
if editor.hide_context_menu(cx).is_none() {
editor.update_visible_inline_completion(cx);
}
editor.hide_context_menu(cx);
}
})?;
@@ -3732,6 +3758,7 @@ impl Editor {
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
use language::ToOffset as _;
self.discard_inline_completion(true, cx);
let completions_menu =
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
menu
@@ -4475,6 +4502,8 @@ impl Editor {
_: &AcceptInlineCompletion,
cx: &mut ViewContext<Self>,
) {
self.hide_context_menu(cx);
let Some(active_inline_completion) = self.active_inline_completion.as_ref() else {
return;
};
@@ -4629,9 +4658,7 @@ impl Editor {
let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
let excerpt_id = cursor.excerpt_id;
if self.context_menu.read().is_some()
|| (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())
|| !offset_selection.is_empty()
if !offset_selection.is_empty()
|| self
.active_inline_completion
.as_ref()
@@ -4978,11 +5005,7 @@ impl Editor {
fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<CodeContextMenu> {
cx.notify();
self.completion_tasks.clear();
let context_menu = self.context_menu.write().take();
if context_menu.is_some() {
self.update_visible_inline_completion(cx);
}
context_menu
self.context_menu.write().take()
}
fn show_snippet_choices(
@@ -9648,6 +9671,7 @@ impl Editor {
|theme| theme.editor_highlighted_line_background,
cx,
);
editor.register_buffers_with_language_servers(cx);
});
let item = Box::new(editor);
@@ -11844,6 +11868,12 @@ impl Editor {
cx.notify();
}
pub fn lsp_store(&self, cx: &AppContext) -> Option<Model<LspStore>> {
self.project
.as_ref()
.map(|project| project.read(cx).lsp_store())
}
fn on_buffer_changed(&mut self, _: Model<MultiBuffer>, cx: &mut ViewContext<Self>) {
cx.notify();
}
@@ -11857,6 +11887,7 @@ impl Editor {
match event {
multi_buffer::Event::Edited {
singleton_buffer_edited,
edited_buffer: buffer_edited,
} => {
self.scrollbar_marker_state.dirty = true;
self.active_indent_guides_state.dirty = true;
@@ -11865,6 +11896,19 @@ impl Editor {
if self.has_active_inline_completion() {
self.update_visible_inline_completion(cx);
}
if let Some(buffer) = buffer_edited {
let buffer_id = buffer.read(cx).remote_id();
if !self.registered_buffers.contains_key(&buffer_id) {
if let Some(lsp_store) = self.lsp_store(cx) {
lsp_store.update(cx, |lsp_store, cx| {
self.registered_buffers.insert(
buffer_id,
lsp_store.register_buffer_with_language_servers(&buffer, cx),
);
})
}
}
}
cx.emit(EditorEvent::BufferEdited);
cx.emit(SearchEvent::MatchesInvalidated);
if *singleton_buffer_edited {
@@ -11931,6 +11975,9 @@ impl Editor {
}
multi_buffer::Event::ExcerptsRemoved { ids } => {
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
let buffer = self.buffer.read(cx);
self.registered_buffers
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
}
multi_buffer::Event::ExcerptsEdited { ids } => {

View File

@@ -32,9 +32,12 @@ use project::{
project_settings::{LspSettings, ProjectSettings},
};
use serde_json::{self, json};
use std::sync::atomic::{self, AtomicUsize};
use std::{cell::RefCell, future::Future, iter, rc::Rc, time::Instant};
use test::editor_lsp_test_context::rust_lang;
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use std::{
iter,
sync::atomic::{self, AtomicUsize},
};
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
use unindent::Unindent;
use util::{
assert_set_eq,
@@ -6836,14 +6839,15 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
.await
.unwrap();
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let (editor, cx) =
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
assert!(cx.read(|cx| editor.is_dirty(cx)));
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let save = editor
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
.unwrap();
@@ -7117,6 +7121,7 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
assert!(!buffer.is_dirty());
assert_eq!(buffer.text(), sample_text_3,)
});
cx.executor().run_until_parked();
cx.executor().start_waiting();
let save = multi_buffer_editor
@@ -7188,14 +7193,15 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
.await
.unwrap();
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let (editor, cx) =
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
assert!(cx.read(|cx| editor.is_dirty(cx)));
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let save = editor
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
.unwrap();
@@ -7339,13 +7345,14 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
.await
.unwrap();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) =
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
let format = editor
.update(cx, |editor, cx| {
editor.perform_format(
@@ -10332,9 +10339,6 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
})
.await
.unwrap();
cx.executor().run_until_parked();
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let editor_handle = workspace
.update(cx, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -10345,6 +10349,9 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
.downcast::<Editor>()
.unwrap();
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
@@ -10434,7 +10441,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let _buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/a/main.rs", cx)
project.open_local_buffer_with_lsp("/a/main.rs", cx)
})
.await
.unwrap();

View File

@@ -50,7 +50,7 @@ use language::{
use lsp::DiagnosticSeverity;
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
MultiBufferSnapshot,
MultiBufferSnapshot, ToOffset,
};
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
@@ -1696,16 +1696,23 @@ impl EditorElement {
None
};
let offset_range_start = snapshot
.display_point_to_anchor(DisplayPoint::new(range.start, 0), Bias::Left)
.to_offset(&snapshot.buffer_snapshot);
let offset_range_end = snapshot
.display_point_to_anchor(DisplayPoint::new(range.end, 0), Bias::Right)
.to_offset(&snapshot.buffer_snapshot);
editor
.tasks
.iter()
.filter_map(|(_, tasks)| {
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
let display_row = multibuffer_point.to_display_point(snapshot).row();
if range.start > display_row || range.end < display_row {
if tasks.offset.0 < offset_range_start || tasks.offset.0 >= offset_range_end {
return None;
}
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
if snapshot.is_line_folded(multibuffer_row) {
// Skip folded indicators, unless it's the starting line of a fold.
if multibuffer_row
@@ -1718,6 +1725,7 @@ impl EditorElement {
return None;
}
}
let display_row = multibuffer_point.to_display_point(snapshot).row();
let button = editor.render_run_indicator(
&self.style,
Some(display_row) == active_task_indicator_row,
@@ -6653,7 +6661,6 @@ mod tests {
use language::language_settings;
use log::info;
use std::num::NonZeroU32;
use ui::Context;
use util::test::sample_text;
#[gpui::test]

View File

@@ -22,10 +22,7 @@ use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer}
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
use text::{OffsetRangeExt, ToPoint};
use theme::ActiveTheme;
use ui::{
div, h_flex, Color, Context, FluentBuilder, Icon, IconName, IntoElement, Label, LabelCommon,
ParentElement, SharedString, Styled, ViewContext, VisualContext, WindowContext,
};
use ui::prelude::*;
use util::{paths::compare_paths, ResultExt};
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
@@ -1099,136 +1096,290 @@ impl Render for ProjectDiffEditor {
#[cfg(test)]
mod tests {
use futures::future::join_all;
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
use project::buffer_store::BufferChangeSet;
use rand::{
distributions::{Alphanumeric, DistString},
prelude::*,
};
use serde_json::json;
use settings::SettingsStore;
use std::{
ops::Deref as _,
ffi::OsString,
ops::Not,
path::{Path, PathBuf},
};
use text::{Edit, Patch, Rope};
use super::*;
// TODO finish
// #[gpui::test]
// async fn randomized_tests(cx: &mut TestAppContext) {
// // Create a new project (how?? temp fs?),
// let fs = FakeFs::new(cx.executor());
// let project = Project::test(fs, [], cx).await;
struct TestFile {
name: OsString,
staged_text: String,
buffer: text::BufferSnapshot,
editor: View<Editor>,
patch: Patch<usize>,
}
// // create random files with random content
// // Commit it into git somehow (technically can do with "real" fs in a temp dir)
// //
// // Apply randomized changes to the project: select a random file, random change and apply to buffers
// }
#[gpui::test(iterations = 30)]
async fn simple_edit_test(cx: &mut TestAppContext) {
cx.executor().allow_parking();
#[gpui::test(iterations = 100)]
async fn random_edits(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
let rng = &mut rng;
let fs = fs::FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root",
json!({
".git": {},
"file_a": "This is file_a",
"file_b": "This is file_b",
}),
)
.await;
let project = Project::test(fs.clone(), [Path::new("/root")], cx).await;
let names = ["file0", "file1", "file2", "file3", "file4"].map(str::to_owned);
let fs = {
let fs = fs::FakeFs::new(cx.executor().clone());
let mut files = json!(names
.clone()
.into_iter()
.zip(std::iter::repeat_with(|| gen_file(rng).into()))
.collect::<serde_json::Map<_, _>>());
files
.as_object_mut()
.unwrap()
.insert(".git".to_owned(), json!({}));
fs.insert_tree("/project", files).await;
fs
};
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let file_a_editor = workspace
let (file_editors, project_diff_editor) = workspace
.update(cx, |workspace, cx| {
let file_a_editor =
workspace.open_abs_path(PathBuf::from("/root/file_a"), true, cx);
let file_editors = names.clone().map(|name| {
workspace.open_abs_path(PathBuf::from(format!("/project/{}", name)), true, cx)
});
ProjectDiffEditor::deploy(workspace, &Deploy, cx);
file_a_editor
})
.unwrap()
.await
.expect("did not open an item at all")
.downcast::<Editor>()
.expect("did not open an editor for file_a");
let project_diff_editor = workspace
.update(cx, |workspace, cx| {
workspace
let project_diff_editor = workspace
.active_pane()
.read(cx)
.items()
.find_map(|item| item.downcast::<ProjectDiffEditor>())
.expect("Didn't open project diff editor");
(file_editors, project_diff_editor)
})
.unwrap()
.expect("did not find a ProjectDiffEditor");
project_diff_editor.update(cx, |project_diff_editor, cx| {
assert!(
project_diff_editor.editor.read(cx).text(cx).is_empty(),
"Should have no changes after opening the diff on no git changes"
);
});
let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx));
let change = "an edit after git add";
file_a_editor
.update(cx, |file_a_editor, cx| {
file_a_editor.insert(change, cx);
file_a_editor.save(false, project.clone(), cx)
})
.unwrap();
let file_editors = join_all(file_editors)
.await
.expect("failed to save a file");
file_a_editor.update(cx, |file_a_editor, cx| {
let change_set = cx.new_model(|cx| {
BufferChangeSet::new_with_base_text(
old_text.clone(),
file_a_editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.text_snapshot(),
cx,
)
});
file_a_editor
.diff_map
.add_change_set(change_set.clone(), cx);
project.update(cx, |project, cx| {
project.buffer_store().update(cx, |buffer_store, cx| {
buffer_store.set_change_set(
file_a_editor
.into_iter()
.map(|result| {
result
.expect("Failed to open file editor")
.downcast::<Editor>()
.expect("Unexpected non-editor")
})
.collect::<Vec<_>>();
for file_editor in &file_editors {
file_editor
.update(cx, |editor, cx| editor.save(false, project.clone(), cx))
.await
.expect("Failed to save file");
}
let buffers = file_editors.clone().into_iter().map(|file_editor| {
file_editor.update(cx, |editor, cx| {
editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.text_snapshot()
})
});
let mut files = names
.into_iter()
.zip(file_editors)
.zip(buffers)
.map(|((name, editor), buffer)| {
let staged_text = buffer.text();
TestFile {
name: name.into(),
editor,
buffer,
staged_text,
patch: Patch::new(Vec::new()),
}
})
.collect::<Vec<_>>();
check(project_diff_editor.clone(), &files, cx);
for _ in 0..10 {
let file = files.choose_mut(rng).unwrap();
match rng.gen_range(0..5) {
0..3 => {
let old_text = file.buffer.as_rope().clone();
let new_edits = gen_edits(rng, &old_text);
file.editor
.update(cx, |editor, cx| {
editor.edit(
new_edits
.clone()
.into_iter()
.map(|(old, _, content)| (old, content)),
cx,
);
editor.save(false, project.clone(), cx)
})
.await
.expect("Failed to save file");
let snapshot = file.editor.update(cx, |editor, cx| {
editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.remote_id(),
change_set,
);
});
});
});
fs.set_status_for_repo_via_git_operation(
Path::new("/root/.git"),
&[(Path::new("file_a"), GitFileStatus::Modified)],
);
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
.text_snapshot()
});
let diff = file
.editor
.update(cx, |editor, cx| {
let change_set = cx.new_model(|cx| {
BufferChangeSet::new_with_base_text(
old_text.to_string(),
snapshot.clone(),
cx,
)
});
editor.diff_map.add_change_set_with_project(
project.clone(),
change_set,
cx,
);
let diff = BufferDiff::build(&old_text, &snapshot);
diff
})
.await;
let patch = std::mem::take(&mut file.patch);
file.patch = patch.compose(diff.hunks(&snapshot).map(|hunk| {
let new = hunk.buffer_range.to_offset(&snapshot);
let old = if hunk.diff_base_byte_range == (0..0) {
new_edits
.iter()
.find_map(|(old, edit_new, _)| (edit_new == &new).then_some(old))
.cloned()
.unwrap()
} else {
hunk.diff_base_byte_range
};
Edit { old, new }
}));
file.buffer = snapshot;
}
3 => {
file.staged_text = file.buffer.text();
file.patch = Patch::new(Vec::new());
}
4 => {
file.editor
.update(cx, |editor, cx| {
editor.set_text(file.staged_text.as_str(), cx);
editor.save(false, project.clone(), cx)
})
.await
.expect("Failed to save file");
file.buffer = file.editor.update(cx, |editor, cx| {
editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.text_snapshot()
});
file.patch = Patch::new(Vec::new());
}
_ => unreachable!(),
}
project_diff_editor.update(cx, |project_diff_editor, cx| {
assert_eq!(
// TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added)
project_diff_editor.editor.read(cx).text(cx),
format!("{change}{old_text}"),
"Should have a new change shown in the beginning, and the old text shown as deleted text afterwards"
fs.set_status_for_repo_via_git_operation(
Path::new("/project/.git"),
&files
.iter()
.filter_map(|file| {
file.patch
.is_empty()
.not()
.then_some((file.name.as_ref(), GitFileStatus::Modified))
})
.collect::<Vec<_>>(),
);
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
check(project_diff_editor.clone(), &files, cx);
}
}
fn check(
project_diff_editor: View<ProjectDiffEditor>,
files: &[TestFile],
cx: &mut VisualTestContext,
) {
for file in files {
assert_eq!(
file.patch.is_empty(),
file.staged_text == file.buffer.text(),
)
}
project_diff_editor.update(cx, |project_diff_editor, cx| {
let snapshot = project_diff_editor
.editor
.update(cx, |editor, cx| editor.snapshot(cx));
let hunks = snapshot
.diff_map
.diff_hunks(&snapshot.buffer_snapshot)
.map(|hunk| {
let buffer_snapshot = snapshot
.buffer_snapshot
.buffer_for_excerpt(hunk.excerpt_id)
.unwrap();
(
hunk.buffer_id,
hunk.diff_base_byte_range,
hunk.buffer_range.to_offset(buffer_snapshot),
)
})
.collect::<Vec<_>>();
let edit_hunks = files
.iter()
.flat_map(|file| {
file.patch.edits().iter().map(|Edit { old, new }| {
(file.buffer.remote_id(), old.clone(), new.clone())
})
})
.collect::<Vec<_>>();
assert_eq!(hunks.len(), edit_hunks.len());
for edit_hunk in edit_hunks {
assert!(hunks.iter().any(|hunk| hunk == &edit_hunk));
}
let mut hunks = hunks.into_iter().peekable();
for excerpt in snapshot.buffer_snapshot.all_excerpts() {
let Some((buffer_id, _, new)) = hunks.next() else {
panic!("Excerpt without a hunk")
};
let excerpt_buffer_id = excerpt.buffer().remote_id();
let excerpt_range = excerpt.buffer_range().to_offset(excerpt.buffer());
assert!(excerpt_buffer_id == buffer_id);
assert!(excerpt_range.start <= new.start);
assert!(excerpt_range.end >= new.end);
while let Some((_, _, new)) = hunks.peek().map_or(None, |hunk @ (id, _, new)| {
(id == &excerpt_buffer_id
&& (excerpt_range.end > new.start
|| (new.is_empty() && excerpt_range.end == new.start)))
.then_some(hunk)
}) {
assert!(excerpt_range.start <= new.start);
assert!(excerpt_range.end >= new.end);
hunks.next();
}
}
if let Some(hunk) = hunks.next() {
panic!("Hunk without an excerpt: {hunk:?}")
}
});
}
@@ -1251,4 +1402,71 @@ mod tests {
cx.set_staff(true);
});
}
fn gen_line(rng: &mut StdRng) -> String {
let len = rng.gen_range(0..20);
let mut s = Alphanumeric.sample_string(rng, len);
s.push('\n');
s
}
fn gen_file(rng: &mut StdRng) -> String {
let line_count = rng.gen_range(0..10);
(0..line_count).map(|_| gen_line(rng)).collect()
}
fn gen_edits(rng: &mut StdRng, old: &Rope) -> Vec<(Range<usize>, Range<usize>, String)> {
let mut old_lines = {
let mut old_lines = Vec::new();
let mut old_lines_iter = old.chunks().lines();
while let Some(line) = old_lines_iter.next() {
assert!(!line.ends_with("\n"));
old_lines.push(line.to_owned());
}
if old_lines.last().is_some_and(|line| line.is_empty()) {
old_lines.pop();
}
old_lines.into_iter()
};
let mut edits = Vec::new();
let unchanged_count = rng.gen_range(0..=old_lines.len());
let mut old_offset = old_lines
.by_ref()
.take(unchanged_count)
.map(|line| line.len() + 1)
.sum::<usize>();
let mut new_offset = old_offset;
while old_lines.len() > 0 {
let deleted_count = rng.gen_range(0..=old_lines.len());
let advance = old_lines
.by_ref()
.take(deleted_count)
.map(|line| line.len() + 1)
.sum::<usize>();
let deleted_range = old_offset..old_offset + advance;
old_offset += advance;
let minimum_added = if deleted_count == 0 { 1 } else { 0 };
let added_count = rng.gen_range(minimum_added..=5);
let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
let added_range = new_offset..new_offset + addition.len();
new_offset += addition.len();
edits.push((deleted_range, added_range, addition));
if old_lines.len() > 0 {
let blank_lines = old_lines.clone().take_while(|line| line.is_empty()).count();
if blank_lines == old_lines.len() {
break;
};
let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
let advance = old_lines
.by_ref()
.take(unchanged_count)
.map(|line| line.len() + 1)
.sum::<usize>();
old_offset += advance;
new_offset += advance;
}
}
edits
}
}

View File

@@ -80,6 +80,40 @@ impl DiffMap {
self.snapshot.clone()
}
#[cfg(test)]
pub fn add_change_set_with_project(
&mut self,
project: Model<project::Project>,
change_set: Model<BufferChangeSet>,
cx: &mut ViewContext<Editor>,
) {
let buffer_id = change_set.read(cx).buffer_id;
self.snapshot
.0
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
self.diff_bases.insert(
buffer_id,
DiffBaseState {
last_version: None,
_subscription: cx.observe(&change_set, move |editor, change_set, cx| {
editor
.diff_map
.snapshot
.0
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, cx);
}),
change_set: change_set.clone(),
},
);
project.update(cx, |project, cx| {
project.buffer_store().update(cx, |buffer_store, _cx| {
buffer_store.set_change_set(buffer_id, change_set);
});
});
}
pub fn add_change_set(
&mut self,
change_set: Model<BufferChangeSet>,
@@ -149,6 +183,7 @@ impl DiffMapSnapshot {
let end =
excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0));
MultiBufferDiffHunk {
excerpt_id: excerpt.id(),
row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row),
buffer_id,
buffer_range: hunk.buffer_range.clone(),
@@ -185,6 +220,7 @@ impl DiffMapSnapshot {
.map_point_from_buffer(Point::new(hunk.row_range.end, 0))
.row;
MultiBufferDiffHunk {
excerpt_id: excerpt.id(),
row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row),
buffer_id,
buffer_range: hunk.buffer_range.clone(),
@@ -1112,6 +1148,7 @@ pub(crate) fn to_diff_hunk(
.multi_buffer_range
.to_point(multi_buffer_snapshot);
Some(MultiBufferDiffHunk {
excerpt_id: hovered_hunk.multi_buffer_range.start.excerpt_id,
row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
buffer_id,
buffer_range,

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
use gpui::Model;
use gpui::{prelude::*, Model};
use indoc::indoc;
use inline_completion::InlineCompletionProvider;
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
use std::ops::Range;
use text::{Point, ToOffset};
use ui::Context;
use crate::{
editor_tests::init_test, test::editor_test_context::EditorTestContext, InlineCompletion,

View File

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

View File

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

View File

@@ -64,6 +64,11 @@ impl FeatureFlag for ZetaFeatureFlag {
const NAME: &'static str = "zeta";
}
pub struct GitUiFeatureFlag;
impl FeatureFlag for GitUiFeatureFlag {
const NAME: &'static str = "git-ui";
}
pub struct Remoting {}
impl FeatureFlag for Remoting {
const NAME: &'static str = "remoting";

View File

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

@@ -0,0 +1 @@
../../LICENSE-GPL

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

View File

@@ -0,0 +1 @@
pub mod git_panel;

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -204,7 +204,7 @@ impl Render for InlineCompletionButton {
}
div().child(
Button::new("zeta", "Zeta")
Button::new("zeta", "ζ")
.label_size(LabelSize::Small)
.on_click(cx.listener(|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {

View File

@@ -3,9 +3,8 @@ use crate::{
LanguageModelProviderState,
};
use collections::BTreeMap;
use gpui::{AppContext, EventEmitter, Global, Model, ModelContext};
use gpui::{prelude::*, AppContext, EventEmitter, Global, Model, ModelContext};
use std::sync::Arc;
use ui::Context;
pub fn init(cx: &mut AppContext) {
let registry = cx.new_model(|_cx| LanguageModelRegistry::default());

View File

@@ -22,11 +22,7 @@ use language_model::{
use settings::SettingsStore;
use std::time::Duration;
use strum::IntoEnumIterator;
use ui::{
div, h_flex, v_flex, Button, ButtonCommon, Clickable, Color, Context, FixedWidth, Icon,
IconName, IconPosition, IconSize, IntoElement, Label, LabelCommon, ParentElement, Styled,
ViewContext, VisualContext, WindowContext,
};
use ui::prelude::*;
use super::anthropic::count_anthropic_tokens;
use super::open_ai::count_open_ai_tokens;

View File

@@ -447,7 +447,7 @@ impl Render for ConfigurationView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let is_authenticated = self.state.read(cx).is_authenticated();
let ollama_intro = "Get up and running with Llama 3.2, Mistral, Gemma 2, and other large language models with Ollama.";
let ollama_intro = "Get up and running with Llama 3.3, Mistral, Gemma 2, and other large language models with Ollama.";
let ollama_reqs =
"Ollama must be running with at least one model installed to use it in the assistant.";

View File

@@ -57,7 +57,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
let _rust_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/the-root/test.rs", cx)
project.open_local_buffer_with_lsp("/the-root/test.rs", cx)
})
.await
.unwrap();

View File

@@ -1,6 +1,6 @@
name = "C++"
grammar = "cpp"
path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "cu", "cuh"]
path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "cu", "cuh", "C", "H"]
line_comments = ["// ", "/// ", "//! "]
autoclose_before = ";:.,=}])>"
brackets = [

View File

@@ -89,6 +89,7 @@ pub enum Event {
},
Edited {
singleton_buffer_edited: bool,
edited_buffer: Option<Model<Buffer>>,
},
TransactionUndone {
transaction_id: TransactionId,
@@ -114,6 +115,8 @@ pub struct MultiBufferDiffHunk {
pub row_range: Range<MultiBufferRow>,
/// The buffer ID that this hunk belongs to.
pub buffer_id: BufferId,
/// The ID of the excerpt where this hunk appears.
pub excerpt_id: ExcerptId,
/// The range of the underlying buffer that this hunk corresponds to.
pub buffer_range: Range<text::Anchor>,
/// The range within the buffer's diff base that this hunk corresponds to.
@@ -250,7 +253,7 @@ struct Excerpt {
///
/// Contains methods for getting the [`Buffer`] of the excerpt,
/// as well as mapping offsets to/from buffer and multibuffer coordinates.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct MultiBufferExcerpt<'a> {
excerpt: &'a Excerpt,
excerpt_offset: usize,
@@ -1485,6 +1488,7 @@ impl MultiBuffer {
}]);
cx.emit(Event::Edited {
singleton_buffer_edited: false,
edited_buffer: None,
});
cx.emit(Event::ExcerptsAdded {
buffer,
@@ -1512,6 +1516,7 @@ impl MultiBuffer {
}]);
cx.emit(Event::Edited {
singleton_buffer_edited: false,
edited_buffer: None,
});
cx.emit(Event::ExcerptsRemoved { ids });
cx.notify();
@@ -1753,6 +1758,7 @@ impl MultiBuffer {
self.subscriptions.publish_mut(edits);
cx.emit(Event::Edited {
singleton_buffer_edited: false,
edited_buffer: None,
});
cx.emit(Event::ExcerptsRemoved { ids });
cx.notify();
@@ -1816,6 +1822,7 @@ impl MultiBuffer {
cx.emit(match event {
language::BufferEvent::Edited => Event::Edited {
singleton_buffer_edited: true,
edited_buffer: Some(buffer.clone()),
},
language::BufferEvent::DirtyChanged => Event::DirtyChanged,
language::BufferEvent::Saved => Event::Saved,
@@ -1979,6 +1986,7 @@ impl MultiBuffer {
self.subscriptions.publish_mut(edits);
cx.emit(Event::Edited {
singleton_buffer_edited: false,
edited_buffer: None,
});
cx.emit(Event::ExcerptsExpanded { ids: vec![id] });
cx.notify();
@@ -2076,6 +2084,7 @@ impl MultiBuffer {
self.subscriptions.publish_mut(edits);
cx.emit(Event::Edited {
singleton_buffer_edited: false,
edited_buffer: None,
});
cx.emit(Event::ExcerptsExpanded { ids });
cx.notify();
@@ -3439,17 +3448,13 @@ impl MultiBufferSnapshot {
let mut cursor = self.excerpts.cursor::<(usize, Point)>(&());
cursor.seek(&range.start, Bias::Right, &());
cursor.prev(&());
iter::from_fn(move || {
let item = cursor.item()?;
let hit =
cursor.start().0 < range.end || (range.is_empty() && cursor.start().0 == range.end);
let excerpt = hit.then(|| MultiBufferExcerpt::new(item, *cursor.start()));
cursor.next(&());
if cursor.start().0 < range.end {
cursor
.item()
.map(|item| MultiBufferExcerpt::new(item, *cursor.start()))
} else {
None
}
excerpt
})
}
@@ -5363,13 +5368,16 @@ mod tests {
events.read().as_slice(),
&[
Event::Edited {
singleton_buffer_edited: false
singleton_buffer_edited: false,
edited_buffer: None,
},
Event::Edited {
singleton_buffer_edited: false
singleton_buffer_edited: false,
edited_buffer: None,
},
Event::Edited {
singleton_buffer_edited: false
singleton_buffer_edited: false,
edited_buffer: None,
}
]
);

View File

@@ -1,4 +1,5 @@
use crate::{
lsp_store::OpenLspBufferHandle,
search::SearchQuery,
worktree_store::{WorktreeStore, WorktreeStoreEvent},
ProjectItem as _, ProjectPath,
@@ -47,6 +48,7 @@ pub struct BufferStore {
struct SharedBuffer {
buffer: Model<Buffer>,
unstaged_changes: Option<Model<BufferChangeSet>>,
lsp_handle: Option<OpenLspBufferHandle>,
}
#[derive(Debug)]
@@ -1571,6 +1573,21 @@ impl BufferStore {
})?
}
pub fn register_shared_lsp_handle(
&mut self,
peer_id: proto::PeerId,
buffer_id: BufferId,
handle: OpenLspBufferHandle,
) {
if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id) {
if let Some(buffer) = shared_buffers.get_mut(&buffer_id) {
buffer.lsp_handle = Some(handle);
return;
}
}
debug_panic!("tried to register shared lsp handle, but buffer was not shared")
}
pub fn handle_synchronize_buffers(
&mut self,
envelope: TypedEnvelope<proto::SynchronizeBuffers>,
@@ -1597,6 +1614,7 @@ impl BufferStore {
.or_insert_with(|| SharedBuffer {
buffer: buffer.clone(),
unstaged_changes: None,
lsp_handle: None,
});
let buffer = buffer.read(cx);
@@ -2017,6 +2035,7 @@ impl BufferStore {
SharedBuffer {
buffer: buffer.clone(),
unstaged_changes: None,
lsp_handle: None,
},
);
@@ -2223,7 +2242,7 @@ impl BufferChangeSet {
cx: &mut ModelContext<Self>,
) -> oneshot::Receiver<()> {
LineEnding::normalize(&mut base_text);
self.recalculate_diff_internal(base_text, buffer_snapshot, true, cx)
self.recalculate_diff_internal(Rope::from(base_text), buffer_snapshot, true, cx)
}
pub fn unset_base_text(
@@ -2245,8 +2264,13 @@ impl BufferChangeSet {
buffer_snapshot: text::BufferSnapshot,
cx: &mut ModelContext<Self>,
) -> oneshot::Receiver<()> {
if let Some(base_text) = self.base_text.clone() {
self.recalculate_diff_internal(base_text.read(cx).text(), buffer_snapshot, false, cx)
if let Some(base_text) = self.base_text.as_ref() {
self.recalculate_diff_internal(
base_text.read(cx).as_rope().clone(),
buffer_snapshot,
false,
cx,
)
} else {
oneshot::channel().1
}
@@ -2254,7 +2278,7 @@ impl BufferChangeSet {
fn recalculate_diff_internal(
&mut self,
base_text: String,
base_text: Rope,
buffer_snapshot: text::BufferSnapshot,
base_text_changed: bool,
cx: &mut ModelContext<Self>,
@@ -2273,7 +2297,7 @@ impl BufferChangeSet {
if base_text_changed {
this.base_text_version += 1;
this.base_text = Some(cx.new_model(|cx| {
Buffer::local_normalized(Rope::from(base_text), LineEnding::default(), cx)
Buffer::local_normalized(base_text, LineEnding::default(), cx)
}));
}
this.diff_to_buffer = diff;

File diff suppressed because it is too large Load Diff

View File

@@ -1254,6 +1254,10 @@ impl Project {
self.buffer_store.read(cx).buffers().collect()
}
pub fn environment(&self) -> &Model<ProjectEnvironment> {
&self.environment
}
pub fn cli_environment(&self, cx: &AppContext) -> Option<HashMap<String, String>> {
self.environment.read(cx).get_cli_environment()
}
@@ -1843,6 +1847,19 @@ impl Project {
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn open_local_buffer_with_lsp(
&mut self,
abs_path: impl AsRef<Path>,
cx: &mut ModelContext<Self>,
) -> Task<Result<(Model<Buffer>, lsp_store::OpenLspBufferHandle)>> {
if let Some((worktree, relative_path)) = self.find_worktree(abs_path.as_ref(), cx) {
self.open_buffer_with_lsp((worktree.read(cx).id(), relative_path), cx)
} else {
Task::ready(Err(anyhow!("no such path")))
}
}
pub fn open_buffer(
&mut self,
path: impl Into<ProjectPath>,
@@ -1857,6 +1874,23 @@ impl Project {
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn open_buffer_with_lsp(
&mut self,
path: impl Into<ProjectPath>,
cx: &mut ModelContext<Self>,
) -> Task<Result<(Model<Buffer>, lsp_store::OpenLspBufferHandle)>> {
let buffer = self.open_buffer(path, cx);
let lsp_store = self.lsp_store().clone();
cx.spawn(|_, mut cx| async move {
let buffer = buffer.await?;
let handle = lsp_store.update(&mut cx, |lsp_store, cx| {
lsp_store.register_buffer_with_language_servers(&buffer, cx)
})?;
Ok((buffer, handle))
})
}
pub fn open_unstaged_changes(
&mut self,
buffer: Model<Buffer>,

View File

@@ -442,17 +442,17 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
);
// Open a buffer without an associated language server.
let toml_buffer = project
let (toml_buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer("/the-root/Cargo.toml", cx)
project.open_local_buffer_with_lsp("/the-root/Cargo.toml", cx)
})
.await
.unwrap();
// Open a buffer with an associated language server before the language for it has been loaded.
let rust_buffer = project
let (rust_buffer, _handle2) = project
.update(cx, |project, cx| {
project.open_local_buffer("/the-root/test.rs", cx)
project.open_local_buffer_with_lsp("/the-root/test.rs", cx)
})
.await
.unwrap();
@@ -513,9 +513,9 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
);
// Open a third buffer with a different associated language server.
let json_buffer = project
let (json_buffer, _json_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer("/the-root/package.json", cx)
project.open_local_buffer_with_lsp("/the-root/package.json", cx)
})
.await
.unwrap();
@@ -550,9 +550,9 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
// When opening another buffer whose language server is already running,
// it is also configured based on the existing language server's capabilities.
let rust_buffer2 = project
let (rust_buffer2, _handle4) = project
.update(cx, |project, cx| {
project.open_local_buffer("/the-root/test2.rs", cx)
project.open_local_buffer_with_lsp("/the-root/test2.rs", cx)
})
.await
.unwrap();
@@ -765,7 +765,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
);
// Close notifications are reported only to servers matching the buffer's language.
cx.update(|_| drop(json_buffer));
cx.update(|_| drop(_json_handle));
let close_message = lsp::DidCloseTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(
lsp::Url::from_file_path("/the-root/package.json").unwrap(),
@@ -827,9 +827,9 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
cx.executor().run_until_parked();
// Start the language server by opening a buffer with a compatible file extension.
let _buffer = project
let _ = project
.update(cx, |project, cx| {
project.open_local_buffer("/the-root/src/a.rs", cx)
project.open_local_buffer_with_lsp("/the-root/src/a.rs", cx)
})
.await
.unwrap();
@@ -1239,8 +1239,10 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
// Cause worktree to start the fake language server
let _buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
let _ = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/b.rs", cx)
})
.await
.unwrap();
@@ -1259,6 +1261,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
fake_server
.start_progress(format!("{}/0", progress_token))
.await;
assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
assert_eq!(
events.next().await.unwrap(),
Event::DiskBasedDiagnosticsStarted {
@@ -1365,8 +1368,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
})
.await
.unwrap();
@@ -1390,6 +1395,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
Some(worktree_id)
)
);
assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
fake_server.start_progress(progress_token).await;
assert_eq!(
events.next().await.unwrap(),
@@ -1438,8 +1444,10 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
let (buffer, _) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
})
.await
.unwrap();
@@ -1517,8 +1525,10 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
})
.await
.unwrap();
@@ -1565,8 +1575,10 @@ async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) {
},
);
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
})
.await
.unwrap();
@@ -1634,11 +1646,15 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
language_registry.add(js_lang());
let _rs_buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
})
.await
.unwrap();
let _js_buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx))
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/b.js", cx)
})
.await
.unwrap();
@@ -1734,6 +1750,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
fs.insert_tree("/dir", json!({ "a.rs": text })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
@@ -1750,6 +1767,10 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
.await
.unwrap();
let _handle = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.register_buffer_with_language_servers(&buffer, cx)
});
let mut fake_server = fake_servers.next().await.unwrap();
let open_notification = fake_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
@@ -2162,8 +2183,10 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
})
.await
.unwrap();
@@ -2533,8 +2556,10 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/b.rs", cx)
})
.await
.unwrap();
@@ -2638,8 +2663,8 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
},
);
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
let (buffer, _handle) = project
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
.await
.unwrap();
@@ -2730,8 +2755,8 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
},
);
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
let (buffer, _handle) = project
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
.await
.unwrap();
@@ -2793,8 +2818,8 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
},
);
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
let (buffer, _handle) = project
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
.await
.unwrap();
@@ -3984,7 +4009,7 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) {
let _ = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/one.rs", cx)
project.open_local_buffer_with_lsp("/dir/one.rs", cx)
})
.await
.unwrap();
@@ -4086,9 +4111,9 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
},
);
let buffer = project
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/one.rs", cx)
project.open_local_buffer_with_lsp("/dir/one.rs", cx)
})
.await
.unwrap();
@@ -4951,8 +4976,8 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
),
];
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
let (buffer, _handle) = project
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.tsx", cx))
.await
.unwrap();
cx.executor().run_until_parked();
@@ -5060,8 +5085,8 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
},
);
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
let (buffer, _handle) = project
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
.await
.unwrap();
cx.executor().run_until_parked();
@@ -5130,8 +5155,8 @@ async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) {
},
);
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
let (buffer, _handle) = project
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
.await
.unwrap();
cx.executor().run_until_parked();
@@ -5251,8 +5276,8 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
),
];
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
let (buffer, _handle) = project
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.tsx", cx))
.await
.unwrap();
cx.executor().run_until_parked();

View File

@@ -4262,7 +4262,6 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
use std::path::{Path, PathBuf};
use ui::Context;
use workspace::{
item::{Item, ProjectItem},
register_project_item, AppState,

View File

@@ -292,7 +292,7 @@ mod tests {
let _buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/test.rs", cx)
project.open_local_buffer_with_lsp("/dir/test.rs", cx)
})
.await
.unwrap();

View File

@@ -304,7 +304,9 @@ message Envelope {
InstallExtension install_extension = 287;
GetStagedText get_staged_text = 288;
GetStagedTextResponse get_staged_text_response = 289; // current max
GetStagedTextResponse get_staged_text_response = 289;
RegisterBufferWithLanguageServers register_buffer_with_language_servers = 290;
}
reserved 87 to 88;
@@ -2537,7 +2539,6 @@ message UpdateGitBranch {
string branch_name = 2;
ProjectPath repository = 3;
}
message GetPanicFiles {
}
@@ -2582,3 +2583,8 @@ message InstallExtension {
Extension extension = 1;
string tmp_dir = 2;
}
message RegisterBufferWithLanguageServers{
uint64 project_id = 1;
uint64 buffer_id = 2;
}

View File

@@ -373,6 +373,7 @@ messages!(
(SyncExtensions, Background),
(SyncExtensionsResponse, Background),
(InstallExtension, Background),
(RegisterBufferWithLanguageServers, Background),
);
request_messages!(
@@ -499,6 +500,7 @@ request_messages!(
(CancelLanguageServerWork, Ack),
(SyncExtensions, SyncExtensionsResponse),
(InstallExtension, Ack),
(RegisterBufferWithLanguageServers, Ack),
);
entity_messages!(
@@ -584,6 +586,7 @@ entity_messages!(
ActiveToolchain,
GetPathMetadata,
CancelLanguageServerWork,
RegisterBufferWithLanguageServers,
);
entity_messages!(

View File

@@ -440,9 +440,9 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
// Wait for the settings to synchronize
cx.run_until_parked();
let buffer = project
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
})
.await
.unwrap();
@@ -616,9 +616,9 @@ async fn test_remote_cancel_language_server_work(
cx.run_until_parked();
let buffer = project
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
})
.await
.unwrap();

View File

@@ -2,29 +2,27 @@ use crate::kernels::KernelSpecification;
use crate::repl_store::ReplStore;
use crate::KERNEL_DOCS_URL;
use editor::Editor;
use gpui::DismissEvent;
use gpui::FontWeight;
use gpui::WeakView;
use picker::Picker;
use picker::PickerDelegate;
use project::WorktreeId;
use ui::ButtonLike;
use ui::Tooltip;
use std::sync::Arc;
use ui::ListItemSpacing;
use gpui::SharedString;
use gpui::Task;
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle};
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
pub type OnSelect = Box<dyn Fn(KernelSpecification, &mut WindowContext)>;
type OnSelect = Box<dyn Fn(KernelSpecification, &mut WindowContext)>;
pub struct KernelSelector {
#[derive(IntoElement)]
pub struct KernelSelector<T: PopoverTrigger> {
handle: Option<PopoverMenuHandle<Picker<KernelPickerDelegate>>>,
editor: WeakView<Editor>,
on_select: OnSelect,
trigger: T,
info_text: Option<SharedString>,
worktree_id: WorktreeId,
}
@@ -34,7 +32,6 @@ pub struct KernelPickerDelegate {
filtered_kernels: Vec<KernelSpecification>,
selected_kernelspec: Option<KernelSpecification>,
on_select: OnSelect,
group: Group,
}
// Helper function to truncate long paths
@@ -47,15 +44,12 @@ fn truncate_path(path: &SharedString, max_length: usize) -> SharedString {
}
}
impl KernelSelector {
pub fn new(
worktree_id: WorktreeId,
editor: WeakView<Editor>,
_cx: &mut ViewContext<Self>,
) -> Self {
impl<T: PopoverTrigger> KernelSelector<T> {
pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T) -> Self {
KernelSelector {
editor,
on_select,
handle: None,
trigger,
info_text: None,
worktree_id,
}
@@ -72,14 +66,6 @@ impl KernelSelector {
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Group {
All,
Jupyter,
Python,
Remote,
}
impl PickerDelegate for KernelPickerDelegate {
type ListItem = ListItem;
@@ -218,75 +204,6 @@ impl PickerDelegate for KernelPickerDelegate {
)
}
fn render_header(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
let mode = Group::All;
Some(
h_flex()
.child(
div()
.id("all")
.px_2()
.py_1()
.cursor_pointer()
.border_b_2()
.when(mode == Group::All, |this| {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("All"))
.on_click(cx.listener(|this, _, cx| {
this.delegate.set_group(Group::All, cx);
})),
)
.child(
div()
.id("jupyter")
.px_2()
.py_1()
.cursor_pointer()
.border_b_2()
.when(mode == Group::Jupyter, |this| {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("Jupyter"))
.on_click(cx.listener(|this, _, cx| {
this.delegate.set_group(Group::Jupyter, cx);
})),
)
.child(
div()
.id("python")
.px_2()
.py_1()
.cursor_pointer()
.border_b_2()
.when(mode == Group::Python, |this| {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("Python"))
.on_click(cx.listener(|this, _, cx| {
this.delegate.set_group(Group::Python, cx);
})),
)
.child(
div()
.id("remote")
.px_2()
.py_1()
.cursor_pointer()
.border_b_2()
.when(mode == Group::Remote, |this| {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("Remote"))
.on_click(cx.listener(|this, _, cx| {
this.delegate.set_group(Group::Remote, cx);
})),
)
.into_any_element(),
)
}
fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
Some(
h_flex()
@@ -308,29 +225,8 @@ impl PickerDelegate for KernelPickerDelegate {
}
}
impl KernelPickerDelegate {
fn new(
on_select: OnSelect,
kernels: Vec<KernelSpecification>,
selected_kernelspec: Option<KernelSpecification>,
) -> Self {
Self {
on_select,
all_kernels: kernels.clone(),
filtered_kernels: kernels,
group: Group::All,
selected_kernelspec,
}
}
fn set_group(&mut self, group: Group, cx: &mut ViewContext<Picker<Self>>) {
self.group = group;
cx.notify();
}
}
impl Render for KernelSelector {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let store = ReplStore::global(cx).read(cx);
let all_kernels: Vec<KernelSpecification> = store
@@ -339,18 +235,13 @@ impl Render for KernelSelector {
.collect();
let selected_kernelspec = store.active_kernelspec(self.worktree_id, None, cx);
let current_kernel_name = selected_kernelspec.as_ref().map(|spec| spec.name()).clone();
let editor = self.editor.clone();
let on_select: OnSelect = Box::new(move |kernelspec, cx| {
crate::assign_kernelspec(kernelspec, editor.clone(), cx).ok();
});
let menu_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>> =
PopoverMenuHandle::default();
let delegate =
KernelPickerDelegate::new(on_select, all_kernels, selected_kernelspec.clone());
let delegate = KernelPickerDelegate {
on_select: self.on_select,
all_kernels: all_kernels.clone(),
filtered_kernels: all_kernels,
selected_kernelspec,
};
let picker_view = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx)
@@ -361,42 +252,8 @@ impl Render for KernelSelector {
PopoverMenu::new("kernel-switcher")
.menu(move |_cx| Some(picker_view.clone()))
.trigger(
ButtonLike::new("kernel-selector")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(
Label::new(if let Some(name) = current_kernel_name {
name
} else {
SharedString::from("Select Kernel")
})
.size(LabelSize::Small)
.color(if selected_kernelspec.is_some() {
Color::Default
} else {
Color::Placeholder
})
.into_any_element(),
),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::text("Select Kernel", cx)),
)
.trigger(self.trigger)
.attach(gpui::AnchorCorner::BottomLeft)
.with_handle(menu_handle)
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
}
}

View File

@@ -73,21 +73,27 @@ pub fn init(cx: &mut AppContext) {
return;
}
let project_path = editor
.buffer()
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).project_path(cx));
let buffer = editor.buffer().read(cx).as_singleton();
let language = buffer
.as_ref()
.and_then(|buffer| buffer.read(cx).language());
let project_path = buffer.and_then(|buffer| buffer.read(cx).project_path(cx));
let editor_handle = cx.view().downgrade();
if let (Some(project_path), Some(project)) = (project_path, project) {
let store = ReplStore::global(cx);
store.update(cx, |store, cx| {
store
.refresh_python_kernelspecs(project_path.worktree_id, &project, cx)
.detach_and_log_err(cx);
});
if let Some(language) = language {
if language.name() == "Python".into() {
if let (Some(project_path), Some(project)) = (project_path, project) {
let store = ReplStore::global(cx);
store.update(cx, |store, cx| {
store
.refresh_python_kernelspecs(project_path.worktree_id, &project, cx)
.detach_and_log_err(cx);
});
}
}
}
editor

View File

@@ -173,7 +173,7 @@ impl ReplStore {
let remote_kernel_specifications = self.get_remote_kernel_specifications(cx);
cx.spawn(|this, mut cx| async move {
let all_specs = cx.background_executor().spawn(async move {
let mut all_specs = local_kernel_specifications
.await?
.into_iter()
@@ -186,10 +186,21 @@ impl ReplStore {
}
}
this.update(&mut cx, |this, cx| {
this.kernel_specifications = all_specs;
cx.notify();
})
anyhow::Ok(all_specs)
});
cx.spawn(|this, mut cx| async move {
let all_specs = all_specs.await;
if let Ok(specs) = all_specs {
this.update(&mut cx, |this, cx| {
this.kernel_specifications = specs;
cx.notify();
})
.ok();
}
anyhow::Ok(())
})
}

View File

@@ -445,7 +445,7 @@ impl ComponentPreview for Button {
"A button allows users to take actions, and make choices, with a single tap."
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Styles",

View File

@@ -118,7 +118,7 @@ impl ComponentPreview for Checkbox {
"A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Default",
@@ -214,7 +214,7 @@ impl ComponentPreview for CheckboxWithLabel {
"A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![
single_example(
"Unselected",

View File

@@ -95,7 +95,7 @@ impl ComponentPreview for ContentGroup {
ExampleLabelSide::Bottom
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![
single_example(
"Default",

View File

@@ -3,6 +3,13 @@ use gpui::{Hsla, IntoElement};
use crate::prelude::*;
#[derive(Clone, Copy, PartialEq)]
enum DividerStyle {
Solid,
Dashed,
}
#[derive(Clone, Copy, PartialEq)]
enum DividerDirection {
Horizontal,
Vertical,
@@ -27,6 +34,7 @@ impl DividerColor {
#[derive(IntoElement)]
pub struct Divider {
style: DividerStyle,
direction: DividerDirection,
color: DividerColor,
inset: bool,
@@ -34,22 +42,17 @@ pub struct Divider {
impl RenderOnce for Divider {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
div()
.map(|this| match self.direction {
DividerDirection::Horizontal => {
this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
}
DividerDirection::Vertical => {
this.w_px().h_full().when(self.inset, |this| this.my_1p5())
}
})
.bg(self.color.hsla(cx))
match self.style {
DividerStyle::Solid => self.render_solid(cx).into_any_element(),
DividerStyle::Dashed => self.render_dashed(cx).into_any_element(),
}
}
}
impl Divider {
pub fn horizontal() -> Self {
Self {
style: DividerStyle::Solid,
direction: DividerDirection::Horizontal,
color: DividerColor::default(),
inset: false,
@@ -58,6 +61,25 @@ impl Divider {
pub fn vertical() -> Self {
Self {
style: DividerStyle::Solid,
direction: DividerDirection::Vertical,
color: DividerColor::default(),
inset: false,
}
}
pub fn horizontal_dashed() -> Self {
Self {
style: DividerStyle::Dashed,
direction: DividerDirection::Horizontal,
color: DividerColor::default(),
inset: false,
}
}
pub fn vertical_dashed() -> Self {
Self {
style: DividerStyle::Dashed,
direction: DividerDirection::Vertical,
color: DividerColor::default(),
inset: false,
@@ -73,4 +95,49 @@ impl Divider {
self.color = color;
self
}
pub fn render_solid(self, cx: &WindowContext) -> impl IntoElement {
div()
.map(|this| match self.direction {
DividerDirection::Horizontal => {
this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
}
DividerDirection::Vertical => {
this.w_px().h_full().when(self.inset, |this| this.my_1p5())
}
})
.bg(self.color.hsla(cx))
}
// TODO: Use canvas or a shader here
// This obviously is a short term approach
pub fn render_dashed(self, cx: &WindowContext) -> impl IntoElement {
let segment_count = 128;
let segment_count_f = segment_count as f32;
let segment_min_w = 6.;
let base = match self.direction {
DividerDirection::Horizontal => h_flex(),
DividerDirection::Vertical => v_flex(),
};
let (w, h) = match self.direction {
DividerDirection::Horizontal => (px(segment_min_w), px(1.)),
DividerDirection::Vertical => (px(1.), px(segment_min_w)),
};
let color = self.color.hsla(cx);
let total_min_w = segment_min_w * segment_count_f * 2.; // * 2 because of the gap
base.min_w(px(total_min_w))
.map(|this| {
if self.direction == DividerDirection::Horizontal {
this.w_full().h_px()
} else {
this.w_px().h_full()
}
})
.gap(px(segment_min_w))
.overflow_hidden()
.children(
(0..segment_count).map(|_| div().flex_grow().flex_shrink_0().w(w).h(h).bg(color)),
)
}
}

View File

@@ -67,7 +67,7 @@ impl ComponentPreview for Facepile {
\n\nFacepiles are used to display a group of people or things,\
such as a list of participants in a collaboration session."
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
let few_faces: [&'static str; 3] = [
"https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
"https://avatars.githubusercontent.com/u/67129314?s=60&v=4",

View File

@@ -178,6 +178,7 @@ pub enum IconName {
File,
FileCode,
FileDoc,
FileDiff,
FileGeneric,
FileGit,
FileLock,
@@ -199,6 +200,7 @@ pub enum IconName {
GenericRestore,
Github,
Globe,
GitBranch,
Hash,
HistoryRerun,
Indicator,
@@ -223,6 +225,8 @@ pub enum IconName {
Option,
PageDown,
PageUp,
PanelLeft,
PanelRight,
Pencil,
Person,
PhoneIncoming,
@@ -266,6 +270,9 @@ pub enum IconName {
SparkleFilled,
Spinner,
Split,
SquareDot,
SquareMinus,
SquarePlus,
Star,
StarFilled,
Stop,
@@ -278,6 +285,8 @@ pub enum IconName {
Tab,
Terminal,
TextSnippet,
ThumbsUp,
ThumbsDown,
Trash,
TrashAlt,
Triangle,
@@ -494,7 +503,7 @@ impl RenderOnce for IconDecoration {
}
impl ComponentPreview for IconDecoration {
fn examples(cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
let examples = all_kinds
@@ -536,7 +545,7 @@ impl RenderOnce for DecoratedIcon {
}
impl ComponentPreview for DecoratedIcon {
fn examples(cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
let icon_1 = Icon::new(IconName::FileDoc);
let icon_2 = Icon::new(IconName::FileDoc);
let icon_3 = Icon::new(IconName::FileDoc);
@@ -655,7 +664,7 @@ impl RenderOnce for IconWithIndicator {
}
impl ComponentPreview for Icon {
fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
let arrow_icons = vec![
IconName::ArrowDown,
IconName::ArrowLeft,

View File

@@ -89,7 +89,7 @@ impl ComponentPreview for Indicator {
"An indicator visually represents a status or state."
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Types",

View File

@@ -39,6 +39,7 @@ pub struct ListItem {
children: SmallVec<[AnyElement; 2]>,
selectable: bool,
overflow_x: bool,
focused: Option<bool>,
}
impl ListItem {
@@ -62,6 +63,7 @@ impl ListItem {
children: SmallVec::new(),
selectable: true,
overflow_x: false,
focused: None,
}
}
@@ -140,6 +142,11 @@ impl ListItem {
self.overflow_x = true;
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = Some(focused);
self
}
}
impl Disableable for ListItem {
@@ -177,9 +184,14 @@ impl RenderOnce for ListItem {
this
// TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| {
// this.border_1()
// .border_color(cx.theme().colors().border_focused)
// })
.when_some(self.focused, |this, focused| {
if focused {
this.border_1()
.border_color(cx.theme().colors().border_focused)
} else {
this.border_1()
}
})
.when(self.selectable, |this| {
this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
@@ -204,10 +216,15 @@ impl RenderOnce for ListItem {
.when(self.inset && !self.disabled, |this| {
this
// TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| {
// this.border_1()
// .border_color(cx.theme().colors().border_focused)
// })
//.when(self.state == InteractionState::Focused, |this| {
.when_some(self.focused, |this, focused| {
if focused {
this.border_1()
.border_color(cx.theme().colors().border_focused)
} else {
this.border_1()
}
})
.when(self.selectable, |this| {
this.hover(|style| {
style.bg(cx.theme().colors().ghost_element_hover)

View File

@@ -160,7 +160,7 @@ impl ComponentPreview for Table {
ExampleLabelSide::Top
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group(vec![
single_example(

View File

@@ -30,20 +30,20 @@ pub trait ComponentPreview: IntoElement {
ExampleLabelSide::default()
}
fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>>;
fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>>;
fn custom_example(_cx: &WindowContext) -> impl Into<Option<AnyElement>> {
None::<AnyElement>
}
fn component_previews(cx: &WindowContext) -> Vec<AnyElement> {
fn component_previews(cx: &mut WindowContext) -> Vec<AnyElement> {
Self::examples(cx)
.into_iter()
.map(|example| Self::render_example_group(example))
.collect()
}
fn render_component_previews(cx: &WindowContext) -> AnyElement {
fn render_component_previews(cx: &mut WindowContext) -> AnyElement {
let title = Self::title();
let (source, title) = title
.rsplit_once("::")

View File

@@ -108,147 +108,6 @@ impl ThemePreview {
cx.theme().colors().editor_background
}
fn render_avatars(&self, cx: &ViewContext<Self>) -> impl IntoElement {
v_flex()
.gap_1()
.child(
Headline::new("Avatars")
.size(HeadlineSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
.items_start()
.gap_4()
.child(Avatar::new(AVATAR_URL).size(px(24.)))
.child(Avatar::new(AVATAR_URL).size(px(24.)).grayscale(true))
.child(
Avatar::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)),
)
.child(
Avatar::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
)
.child(
Avatar::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
)
.child(
Avatar::new(AVATAR_URL)
.size(px(24.))
.indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
)
.child(
Facepile::empty()
.child(
Avatar::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
)
.child(
Avatar::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
)
.child(
Avatar::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
)
.child(
Avatar::new(AVATAR_URL)
.border_color(Self::preview_bg(cx))
.size(px(22.))
.into_any_element(),
),
),
)
}
fn render_buttons(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
v_flex()
.gap_1()
.child(
Headline::new("Buttons")
.size(HeadlineSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
.items_start()
.gap_px()
.child(
IconButton::new("icon_button_transparent", IconName::Check)
.style(ButtonStyle::Transparent),
)
.child(
IconButton::new("icon_button_subtle", IconName::Check)
.style(ButtonStyle::Subtle),
)
.child(
IconButton::new("icon_button_filled", IconName::Check)
.style(ButtonStyle::Filled),
)
.child(
IconButton::new("icon_button_selected_accent", IconName::Check)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(true),
)
.child(IconButton::new("icon_button_selected", IconName::Check).selected(true))
.child(
IconButton::new("icon_button_positive", IconName::Check)
.style(ButtonStyle::Tinted(TintColor::Positive)),
)
.child(
IconButton::new("icon_button_warning", IconName::Check)
.style(ButtonStyle::Tinted(TintColor::Warning)),
)
.child(
IconButton::new("icon_button_negative", IconName::Check)
.style(ButtonStyle::Tinted(TintColor::Negative)),
),
)
.child(
h_flex()
.gap_px()
.child(
Button::new("button_transparent", "Transparent")
.style(ButtonStyle::Transparent),
)
.child(Button::new("button_subtle", "Subtle").style(ButtonStyle::Subtle))
.child(Button::new("button_filled", "Filled").style(ButtonStyle::Filled))
.child(
Button::new("button_selected", "Selected")
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(true),
)
.child(
Button::new("button_selected_tinted", "Selected (Tinted)")
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(true),
)
.child(
Button::new("button_positive", "Tint::Positive")
.style(ButtonStyle::Tinted(TintColor::Positive)),
)
.child(
Button::new("button_warning", "Tint::Warning")
.style(ButtonStyle::Tinted(TintColor::Warning)),
)
.child(
Button::new("button_negative", "Tint::Negative")
.style(ButtonStyle::Tinted(TintColor::Negative)),
),
)
}
fn render_text(&self, layer: ElevationIndex, cx: &ViewContext<Self>) -> impl IntoElement {
let bg = layer.bg(cx);
@@ -502,7 +361,7 @@ impl ThemePreview {
)
}
fn render_components_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
fn render_components_page(&self, cx: &mut WindowContext) -> impl IntoElement {
let layer = ElevationIndex::Surface;
v_flex()
@@ -520,8 +379,6 @@ impl ThemePreview {
.child(Indicator::render_component_previews(cx))
.child(Icon::render_component_previews(cx))
.child(Table::render_component_previews(cx))
.child(self.render_avatars(cx))
.child(self.render_buttons(layer, cx))
}
fn render_page_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {

View File

@@ -92,11 +92,7 @@ use task::SpawnInTerminal;
use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui;
use ui::{
div, h_flex, px, BorrowAppContext, Context as _, Div, FluentBuilder, InteractiveElement as _,
IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext,
VisualContext as _, WindowContext,
};
use ui::prelude::*;
use util::{paths::SanitizedPath, ResultExt, TryFutureExt};
use uuid::Uuid;
pub use workspace_settings::{
@@ -597,7 +593,6 @@ impl AppState {
use node_runtime::NodeRuntime;
use session::Session;
use settings::SettingsStore;
use ui::Context as _;
if !cx.has_global::<SettingsStore>() {
let settings_store = SettingsStore::test(cx);
@@ -7856,7 +7851,7 @@ mod tests {
}
mod register_project_item_tests {
use ui::Context as _;
use gpui::Context as _;
use super::*;

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.166.0"
version = "0.167.0"
publish = false
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]
@@ -52,6 +52,7 @@ file_icons.workspace = true
fs.workspace = true
futures.workspace = true
git.workspace = true
git_ui.workspace = true
git_hosting_providers.workspace = true
go_to_line.workspace = true
gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] }
@@ -80,6 +81,7 @@ outline.workspace = true
outline_panel.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
profiling.workspace = true
project.workspace = true
project_panel.workspace = true

View File

@@ -406,7 +406,12 @@ fn main() {
stdout_is_a_pty(),
cx,
);
assistant2::init(cx);
assistant2::init(
app_state.fs.clone(),
app_state.client.clone(),
stdout_is_a_pty(),
cx,
);
assistant_tools::init(cx);
repl::init(
app_state.fs.clone(),
@@ -442,6 +447,7 @@ fn main() {
outline::init(cx);
project_symbols::init(cx);
project_panel::init(Assets, cx);
git_ui::git_panel::init(cx);
outline_panel::init(Assets, cx);
tasks_ui::init(cx);
snippets_ui::init(cx);
@@ -463,6 +469,7 @@ fn main() {
welcome::init(cx);
settings_ui::init(cx);
extensions_ui::init(cx);
zeta::init(cx);
cx.observe_global::<SettingsStore>({
let languages = app_state.languages.clone();

View File

@@ -216,7 +216,7 @@ pub fn initialize_workspace(
status_bar.add_left_item(activity_indicator, cx);
status_bar.add_right_item(inline_completion_button, cx);
status_bar.add_right_item(active_buffer_language, cx);
status_bar.add_right_item(active_toolchain_language, cx);
status_bar.add_right_item(active_toolchain_language, cx);
status_bar.add_right_item(vim_mode_indicator, cx);
status_bar.add_right_item(cursor_position, cx);
});
@@ -237,8 +237,11 @@ pub fn initialize_workspace(
let release_channel = ReleaseChannel::global(cx);
let assistant2_feature_flag = cx.wait_for_flag::<feature_flags::Assistant2FeatureFlag>();
let git_ui_feature_flag = cx.wait_for_flag::<feature_flags::GitUiFeatureFlag>();
let prompt_builder = prompt_builder.clone();
let is_staff = cx.is_staff();
cx.spawn(|workspace_handle, mut cx| async move {
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
@@ -276,13 +279,26 @@ pub fn initialize_workspace(
workspace.add_panel(chat_panel, cx);
workspace.add_panel(notification_panel, cx);
})?;
let git_ui_enabled = git_ui_feature_flag.await || is_staff;
let git_panel = if git_ui_enabled {
Some(git_ui::git_panel::GitPanel::load(workspace_handle.clone(), cx.clone()).await?)
} else {
None
};
workspace_handle.update(&mut cx, |workspace, cx| {
if let Some(git_panel) = git_panel {
workspace.add_panel(git_panel, cx);
}
})?;
let is_assistant2_enabled =
if cfg!(test) || release_channel != ReleaseChannel::Dev {
false
} else {
assistant2_feature_flag.await
}
;
};
let (assistant_panel, assistant2_panel) = if is_assistant2_enabled {
let assistant2_panel =
@@ -303,6 +319,7 @@ pub fn initialize_workspace(
if let Some(assistant2_panel) = assistant2_panel {
workspace.add_panel(assistant2_panel, cx);
}
})
})
.detach();

View File

@@ -165,7 +165,7 @@ fn assign_inline_completion_provider(
}
}
language::language_settings::InlineCompletionProvider::Zeta => {
if cx.has_flag::<ZetaFeatureFlag>() {
if cx.has_flag::<ZetaFeatureFlag>() || cfg!(debug_assertions) {
let zeta = zeta::Zeta::register(client.clone(), cx);
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
if buffer.read(cx).file().is_some() {

View File

@@ -13,8 +13,6 @@ use gpui::{
Action, AnchorCorner, ClickEvent, ElementId, EventEmitter, FocusHandle, FocusableView,
InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView,
};
use repl::worktree_id_for_editor;
use repl_menu::ReplMenu;
use search::{buffer_search, BufferSearchBar};
use settings::{Settings, SettingsStore};
use ui::{
@@ -35,7 +33,6 @@ pub struct QuickActionBar {
toggle_selections_handle: PopoverMenuHandle<ContextMenu>,
toggle_settings_handle: PopoverMenuHandle<ContextMenu>,
workspace: WeakView<Workspace>,
repl_menu: Option<View<ReplMenu>>,
}
impl QuickActionBar {
@@ -52,7 +49,6 @@ impl QuickActionBar {
toggle_selections_handle: Default::default(),
toggle_settings_handle: Default::default(),
workspace: workspace.weak_handle(),
repl_menu: None,
};
this.apply_settings(cx);
cx.observe_global::<SettingsStore>(|this, cx| this.apply_settings(cx))
@@ -354,7 +350,7 @@ impl Render for QuickActionBar {
h_flex()
.id("quick action bar")
.gap(DynamicSpacing::Base06.rems(cx))
.children(self.repl_menu.clone())
.children(self.render_repl_menu(cx))
.children(self.render_toggle_markdown_preview(self.workspace.clone(), cx))
.children(search_button)
.when(
@@ -429,15 +425,7 @@ impl ToolbarItemView for QuickActionBar {
if let Some(active_item) = active_pane_item {
self._inlay_hints_enabled_subscription.take();
let editor = active_item.downcast::<Editor>();
let work_tree_id = active_item
.downcast::<Editor>()
.and_then(|editor| worktree_id_for_editor(editor.downgrade(), cx));
if let (Some(editor), Some(work_tree_id)) = (editor, work_tree_id) {
self.repl_menu =
Some(cx.new_view(|cx| ReplMenu::new(work_tree_id, editor.downgrade(), cx)));
if let Some(editor) = active_item.downcast::<Editor>() {
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
self._inlay_hints_enabled_subscription =
@@ -453,8 +441,6 @@ impl ToolbarItemView for QuickActionBar {
cx.notify()
}
}));
} else {
self.repl_menu = None
}
}
self.get_toolbar_item_location()

View File

@@ -1,22 +1,24 @@
use std::time::Duration;
use editor::Editor;
use gpui::ElementId;
use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View};
use gpui::{ElementId, WeakView};
use project::WorktreeId;
use picker::Picker;
use repl::{
components::KernelSelector, ExecutionState, JupyterSettings, Kernel, KernelSpecification,
components::{KernelPickerDelegate, KernelSelector},
worktree_id_for_editor, ExecutionState, JupyterSettings, Kernel, KernelSpecification,
KernelStatus, Session, SessionSupport,
};
use ui::{
prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu,
Tooltip,
PopoverMenuHandle, Tooltip,
};
use util::ResultExt;
use super::QuickActionBar;
const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl";
struct ReplSessionState {
struct ReplMenuState {
tooltip: SharedString,
icon: IconName,
icon_color: Color,
@@ -29,73 +31,47 @@ struct ReplSessionState {
kernel_language: SharedString,
}
pub struct ReplMenu {
active_editor: WeakView<Editor>,
kernel_menu: View<KernelSelector>,
}
impl ReplMenu {
pub fn new(
work_tree_id: WorktreeId,
editor: WeakView<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
kernel_menu: cx.new_view(|cx| KernelSelector::new(work_tree_id, editor.clone(), cx)),
active_editor: editor.clone(),
}
}
}
impl Render for ReplMenu {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
impl QuickActionBar {
pub fn render_repl_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
if !JupyterSettings::enabled(cx) {
return div().into_any_element();
return None;
}
let editor = self.active_editor.clone();
let editor = self.active_editor()?;
let is_local_project = editor
.upgrade()
.as_ref()
.map(|editor| {
editor
.read(cx)
.workspace()
.map(|workspace| workspace.read(cx).project().read(cx).is_local())
.unwrap_or(false)
})
.read(cx)
.workspace()
.map(|workspace| workspace.read(cx).project().read(cx).is_local())
.unwrap_or(false);
if !is_local_project {
return div().into_any_element();
return None;
}
let has_nonempty_selection = {
editor
.update(cx, |this, cx| {
this.selections
.count()
.ne(&0)
.then(|| {
let latest = this.selections.newest_display(cx);
!latest.is_empty()
})
.unwrap_or_default()
})
.unwrap_or(false)
editor.update(cx, |this, cx| {
this.selections
.count()
.ne(&0)
.then(|| {
let latest = this.selections.newest_display(cx);
!latest.is_empty()
})
.unwrap_or_default()
})
};
let session = repl::session(editor.clone(), cx);
let session = repl::session(editor.downgrade(), cx);
let session = match session {
SessionSupport::ActiveSession(session) => session,
SessionSupport::Inactive(spec) => {
return self.render_repl_launch_menu(spec, cx).into_any_element();
return self.render_repl_launch_menu(spec, cx);
}
SessionSupport::RequiresSetup(language) => {
return self.render_repl_setup(&language.0, cx).into_any_element();
return self.render_repl_setup(&language.0, cx);
}
SessionSupport::Unsupported => return div().into_any_element(),
SessionSupport::Unsupported => return None,
};
let menu_state = session_state(session.clone(), cx);
@@ -104,7 +80,7 @@ impl Render for ReplMenu {
let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into());
let editor = editor.clone();
let editor = editor.downgrade();
let dropdown_menu = PopoverMenu::new(element_id("menu"))
.menu(move |cx| {
let editor = editor.clone();
@@ -269,85 +245,138 @@ impl Render for ReplMenu {
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
.into_any_element();
h_flex()
.child(self.kernel_menu.clone())
.child(button)
.child(dropdown_menu)
.into_any_element()
Some(
h_flex()
.child(self.render_kernel_selector(cx))
.child(button)
.child(dropdown_menu)
.into_any_element(),
)
}
}
impl ReplMenu {
pub fn render_repl_launch_menu(
&self,
kernel_specification: KernelSpecification,
_cx: &mut ViewContext<Self>,
) -> impl IntoElement {
cx: &mut ViewContext<Self>,
) -> Option<AnyElement> {
let tooltip: SharedString =
SharedString::from(format!("Start REPL for {}", kernel_specification.name()));
h_flex().child(self.kernel_menu.clone()).child(
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
.size(ButtonSize::Compact)
.icon_color(Color::Muted)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {}))),
Some(
h_flex()
.child(self.render_kernel_selector(cx))
.child(
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
.size(ButtonSize::Compact)
.icon_color(Color::Muted)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {}))),
)
.into_any_element(),
)
}
pub fn render_repl_setup(&self, language: &str, _cx: &mut ViewContext<Self>) -> AnyElement {
pub fn render_kernel_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let editor = if let Some(editor) = self.active_editor() {
editor
} else {
return div().into_any_element();
};
let Some(worktree_id) = worktree_id_for_editor(editor.downgrade(), cx) else {
return div().into_any_element();
};
let session = repl::session(editor.downgrade(), cx);
let current_kernelspec = match session {
SessionSupport::ActiveSession(view) => Some(view.read(cx).kernel_specification.clone()),
SessionSupport::Inactive(kernel_specification) => Some(kernel_specification),
SessionSupport::RequiresSetup(_language_name) => None,
SessionSupport::Unsupported => None,
};
let current_kernel_name = current_kernelspec.as_ref().map(|spec| spec.name());
let menu_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>> =
PopoverMenuHandle::default();
KernelSelector::new(
{
Box::new(move |kernelspec, cx| {
repl::assign_kernelspec(kernelspec, editor.downgrade(), cx).ok();
})
},
worktree_id,
ButtonLike::new("kernel-selector")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(
Label::new(if let Some(name) = current_kernel_name {
name
} else {
SharedString::from("Select Kernel")
})
.size(LabelSize::Small)
.color(if current_kernelspec.is_some() {
Color::Default
} else {
Color::Placeholder
})
.into_any_element(),
),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::text("Select Kernel", cx)),
)
.with_handle(menu_handle.clone())
.into_any_element()
}
pub fn render_repl_setup(
&self,
language: &str,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement> {
let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language));
h_flex()
.child(self.kernel_menu.clone())
.child(
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
.size(ButtonSize::Compact)
.icon_color(Color::Muted)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
.on_click(|_, cx| {
cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION))
}),
)
.into_any_element()
Some(
h_flex()
.child(self.render_kernel_selector(cx))
.child(
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)
.size(ButtonSize::Compact)
.icon_color(Color::Muted)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
.on_click(|_, cx| {
cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION))
}),
)
.into_any_element(),
)
}
}
// struct KernelMenu {
// menu_handle: PopoverMenuHandle<Picker<KernelPickerDelegate>>,
// editor: WeakView<Editor>,
// }
// impl KernelMenu {
// pub fn new(editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Self {
// Self {
// editor,
// menu_handle,
// }
// }
// }
// impl Render for KernelMenu {
// fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
// let Some(worktree_id) = worktree_id_for_editor(self.editor.clone(), cx) else {
// return div().into_any_element();
// };
// KernelSelector::new(self.editor.clone(), worktree_id)
// .with_handle(self.menu_handle.clone())
// .into_any_element()
// }
// }
fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState {
fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
let session = session.read(cx);
let kernel_name = session.kernel_specification.name();
let kernel_language: SharedString = session.kernel_specification.language();
let fill_fields = || {
ReplSessionState {
ReplMenuState {
tooltip: "Nothing running".into(),
icon: IconName::ReplNeutral,
icon_color: Color::Default,
@@ -363,7 +392,7 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState
};
match &session.kernel {
Kernel::Restarting => ReplSessionState {
Kernel::Restarting => ReplMenuState {
tooltip: format!("Restarting {}", kernel_name).into(),
icon_is_animating: true,
popover_disabled: true,
@@ -373,13 +402,13 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState
..fill_fields()
},
Kernel::RunningKernel(kernel) => match &kernel.execution_state() {
ExecutionState::Idle => ReplSessionState {
ExecutionState::Idle => ReplMenuState {
tooltip: format!("Run code on {} ({})", kernel_name, kernel_language).into(),
indicator: Some(Indicator::dot().color(Color::Success)),
status: session.kernel.status(),
..fill_fields()
},
ExecutionState::Busy => ReplSessionState {
ExecutionState::Busy => ReplMenuState {
tooltip: format!("Interrupt {} ({})", kernel_name, kernel_language).into(),
icon_is_animating: true,
popover_disabled: false,
@@ -388,7 +417,7 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState
..fill_fields()
},
},
Kernel::StartingKernel(_) => ReplSessionState {
Kernel::StartingKernel(_) => ReplMenuState {
tooltip: format!("{} is starting", kernel_name).into(),
icon_is_animating: true,
popover_disabled: true,
@@ -397,14 +426,14 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState
status: session.kernel.status(),
..fill_fields()
},
Kernel::ErroredLaunch(e) => ReplSessionState {
Kernel::ErroredLaunch(e) => ReplMenuState {
tooltip: format!("Error with kernel {}: {}", kernel_name, e).into(),
popover_disabled: false,
indicator: Some(Indicator::dot().color(Color::Error)),
status: session.kernel.status(),
..fill_fields()
},
Kernel::ShuttingDown => ReplSessionState {
Kernel::ShuttingDown => ReplMenuState {
tooltip: format!("{} is shutting down", kernel_name).into(),
popover_disabled: true,
icon_color: Color::Muted,
@@ -412,7 +441,7 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplSessionState
status: session.kernel.status(),
..fill_fields()
},
Kernel::Shutdown => ReplSessionState {
Kernel::Shutdown => ReplMenuState {
tooltip: "Nothing running".into(),
icon: IconName::ReplNeutral,
icon_color: Color::Default,

View File

@@ -13,6 +13,9 @@ workspace = true
path = "src/zeta.rs"
doctest = false
[features]
test-support = []
[dependencies]
anyhow.workspace = true
client.workspace = true
@@ -21,6 +24,7 @@ editor.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
indoc.workspace = true
inline_completion.workspace = true
language.workspace = true
language_models.workspace = true
@@ -32,8 +36,8 @@ settings.workspace = true
similar.workspace = true
telemetry_events.workspace = true
theme.workspace = true
util.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true

View File

@@ -1,18 +1,44 @@
use crate::{InlineCompletion, InlineCompletionRating, Zeta};
use editor::Editor;
use gpui::{
prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, HighlightStyle,
Model, StyledText, TextStyle, View, ViewContext,
actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
HighlightStyle, Model, StyledText, TextStyle, View, ViewContext,
};
use language::{language_settings, OffsetRangeExt};
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, ListItem, ListItemSpacing};
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, TintColor, Tooltip};
use workspace::{ModalView, Workspace};
actions!(
zeta,
[
RateCompletions,
ThumbsUp,
ThumbsDown,
ThumbsUpActiveCompletion,
ThumbsDownActiveCompletion,
NextEdit,
PreviousEdit,
FocusCompletions,
PreviewCompletion,
]
);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(move |workspace: &mut Workspace, _cx| {
workspace.register_action(|workspace, _: &RateCompletions, cx| {
RateCompletionModal::toggle(workspace, cx);
});
})
.detach();
}
pub struct RateCompletionModal {
zeta: Model<Zeta>,
active_completion: Option<ActiveCompletion>,
selected_index: usize,
focus_handle: FocusHandle,
_subscription: gpui::Subscription,
}
@@ -33,6 +59,7 @@ impl RateCompletionModal {
let subscription = cx.observe(&zeta, |_, _, cx| cx.notify());
Self {
zeta,
selected_index: 0,
focus_handle: cx.focus_handle(),
active_completion: None,
_subscription: subscription,
@@ -43,15 +70,194 @@ impl RateCompletionModal {
cx.emit(DismissEvent);
}
fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
self.selected_index += 1;
self.selected_index = usize::min(
self.selected_index,
self.zeta.read(cx).recent_completions().count(),
);
cx.notify();
}
fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
self.selected_index = self.selected_index.saturating_sub(1);
cx.notify();
}
fn select_next_edit(&mut self, _: &NextEdit, cx: &mut ViewContext<Self>) {
let next_index = self
.zeta
.read(cx)
.recent_completions()
.skip(self.selected_index)
.enumerate()
.skip(1) // Skip straight to the next item
.find(|(_, completion)| !completion.edits.is_empty())
.map(|(ix, _)| ix + self.selected_index);
if let Some(next_index) = next_index {
self.selected_index = next_index;
cx.notify();
}
}
fn select_prev_edit(&mut self, _: &PreviousEdit, cx: &mut ViewContext<Self>) {
let zeta = self.zeta.read(cx);
let completions_len = zeta.recent_completions_len();
let prev_index = self
.zeta
.read(cx)
.recent_completions()
.rev()
.skip((completions_len - 1) - self.selected_index)
.enumerate()
.skip(1) // Skip straight to the previous item
.find(|(_, completion)| !completion.edits.is_empty())
.map(|(ix, _)| self.selected_index - ix);
if let Some(prev_index) = prev_index {
self.selected_index = prev_index;
cx.notify();
}
cx.notify();
}
fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
self.selected_index = 0;
cx.notify();
}
fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
self.selected_index = self.zeta.read(cx).recent_completions_len() - 1;
cx.notify();
}
fn thumbs_up(&mut self, _: &ThumbsUp, cx: &mut ViewContext<Self>) {
self.zeta.update(cx, |zeta, cx| {
let completion = zeta
.recent_completions()
.skip(self.selected_index)
.next()
.cloned();
if let Some(completion) = completion {
zeta.rate_completion(
&completion,
InlineCompletionRating::Positive,
"".to_string(),
cx,
);
}
});
self.select_next_edit(&Default::default(), cx);
cx.notify();
}
fn thumbs_up_active(&mut self, _: &ThumbsUpActiveCompletion, cx: &mut ViewContext<Self>) {
self.zeta.update(cx, |zeta, cx| {
if let Some(active) = &self.active_completion {
zeta.rate_completion(
&active.completion,
InlineCompletionRating::Positive,
active.feedback_editor.read(cx).text(cx),
cx,
);
}
});
let current_completion = self
.active_completion
.as_ref()
.map(|completion| completion.completion.clone());
self.select_completion(current_completion, false, cx);
self.select_next_edit(&Default::default(), cx);
self.confirm(&Default::default(), cx);
cx.notify();
}
fn thumbs_down_active(&mut self, _: &ThumbsDownActiveCompletion, cx: &mut ViewContext<Self>) {
if let Some(active) = &self.active_completion {
if active.feedback_editor.read(cx).text(cx).is_empty() {
return;
}
self.zeta.update(cx, |zeta, cx| {
zeta.rate_completion(
&active.completion,
InlineCompletionRating::Negative,
active.feedback_editor.read(cx).text(cx),
cx,
);
});
}
let current_completion = self
.active_completion
.as_ref()
.map(|completion| completion.completion.clone());
self.select_completion(current_completion, false, cx);
self.select_next_edit(&Default::default(), cx);
self.confirm(&Default::default(), cx);
cx.notify();
}
fn focus_completions(&mut self, _: &FocusCompletions, cx: &mut ViewContext<Self>) {
cx.focus_self();
cx.notify();
}
fn preview_completion(&mut self, _: &PreviewCompletion, cx: &mut ViewContext<Self>) {
let completion = self
.zeta
.read(cx)
.recent_completions()
.skip(self.selected_index)
.take(1)
.next()
.cloned();
self.select_completion(completion, false, cx);
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let completion = self
.zeta
.read(cx)
.recent_completions()
.skip(self.selected_index)
.take(1)
.next()
.cloned();
self.select_completion(completion, true, cx);
}
pub fn select_completion(
&mut self,
completion: Option<InlineCompletion>,
focus: bool,
cx: &mut ViewContext<Self>,
) {
// Avoid resetting completion rating if it's already selected.
if let Some(completion) = completion.as_ref() {
self.selected_index = self
.zeta
.read(cx)
.recent_completions()
.enumerate()
.find(|(_, completion_b)| completion.id == completion_b.id)
.map(|(ix, _)| ix)
.unwrap_or(self.selected_index);
cx.notify();
if let Some(prev_completion) = self.active_completion.as_ref() {
if completion.id == prev_completion.completion.id {
if focus {
cx.focus_view(&prev_completion.feedback_editor);
}
return;
}
}
@@ -69,10 +275,14 @@ impl RateCompletionModal {
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_show_inline_completions(Some(false), cx);
editor.set_placeholder_text("Your feedback about this completion...", cx);
editor.set_placeholder_text("Add your feedback", cx);
if focus {
cx.focus_self();
}
editor
}),
});
cx.notify();
}
fn render_active_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
@@ -134,94 +344,131 @@ impl RateCompletionModal {
};
let rated = self.zeta.read(cx).is_completion_rated(completion_id);
let feedback_empty = active_completion
.feedback_editor
.read(cx)
.text(cx)
.is_empty();
let border_color = cx.theme().colors().border;
let bg_color = cx.theme().colors().editor_background;
let label_container = || h_flex().pl_1().gap_1p5();
Some(
v_flex()
.flex_1()
.size_full()
.gap_2()
.child(h_flex().justify_center().children(if rated {
Some(
Label::new("This completion was already rated")
.color(Color::Muted)
.size(LabelSize::Large),
)
} else if active_completion.completion.edits.is_empty() {
Some(
Label::new("This completion didn't produce any edits")
.color(Color::Warning)
.size(LabelSize::Large),
)
} else {
None
}))
.overflow_hidden()
.child(
v_flex()
div()
.id("diff")
.flex_1()
.flex_basis(relative(0.75))
.bg(cx.theme().colors().editor_background)
.overflow_y_scroll()
.p_2()
.border_color(cx.theme().colors().border)
.border_1()
.rounded_lg()
.py_4()
.px_6()
.size_full()
.bg(bg_color)
.overflow_scroll()
.child(StyledText::new(diff).with_highlights(&text_style, diff_highlights)),
)
.child(
h_flex()
.p_2()
.gap_2()
.border_y_1()
.border_color(border_color)
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted)
)
.child(
Label::new("Ensure you explain why this completion is negative or positive. In case it's negative, report what you expected instead.")
.size(LabelSize::Small)
.color(Color::Muted)
)
)
.child(
div()
.flex_1()
.flex_basis(relative(0.25))
.bg(cx.theme().colors().editor_background)
.border_color(cx.theme().colors().border)
.border_1()
.rounded_lg()
.h_40()
.pt_1()
.bg(bg_color)
.child(active_completion.feedback_editor.clone()),
)
.child(
h_flex()
.gap_2()
.justify_end()
.p_1()
.h_8()
.border_t_1()
.border_color(border_color)
.max_w_full()
.justify_between()
.children(if rated {
Some(
label_container()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Rated completion").color(Color::Muted)),
)
} else if active_completion.completion.edits.is_empty() {
Some(
label_container()
.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new("No edits produced").color(Color::Muted)),
)
} else {
Some(label_container())
})
.child(
Button::new("bad", "👎 Bad Completion")
.size(ButtonSize::Large)
.disabled(rated)
.label_size(LabelSize::Large)
.color(Color::Error)
.on_click({
let completion = active_completion.completion.clone();
let feedback_editor = active_completion.feedback_editor.clone();
cx.listener(move |this, _, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.rate_completion(
&completion,
InlineCompletionRating::Negative,
feedback_editor.read(cx).text(cx),
cx,
)
h_flex()
.gap_1()
.child(
Button::new("bad", "Bad Completion")
.key_binding(KeyBinding::for_action_in(
&ThumbsDown,
&self.focus_handle(cx),
cx,
))
.style(ButtonStyle::Tinted(TintColor::Negative))
.icon(IconName::ThumbsDown)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.icon_color(Color::Error)
.disabled(rated || feedback_empty)
.when(feedback_empty, |this| {
this.tooltip(|cx| {
Tooltip::text("Explain why this completion is bad before reporting it", cx)
})
})
})
}),
)
.child(
Button::new("good", "👍 Good Completion")
.size(ButtonSize::Large)
.disabled(rated)
.label_size(LabelSize::Large)
.color(Color::Success)
.on_click({
let completion = active_completion.completion.clone();
let feedback_editor = active_completion.feedback_editor.clone();
cx.listener(move |this, _, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.rate_completion(
&completion,
InlineCompletionRating::Positive,
feedback_editor.read(cx).text(cx),
.on_click(cx.listener(move |this, _, cx| {
this.thumbs_down_active(
&ThumbsDownActiveCompletion,
cx,
)
})
})
}),
);
})),
)
.child(
Button::new("good", "Good Completion")
.key_binding(KeyBinding::for_action_in(
&ThumbsUp,
&self.focus_handle(cx),
cx,
))
.style(ButtonStyle::Tinted(TintColor::Positive))
.icon(IconName::ThumbsUp)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.icon_color(Color::Success)
.disabled(rated)
.on_click(cx.listener(move |this, _, cx| {
this.thumbs_up_active(&ThumbsUpActiveCompletion, cx);
})),
),
),
),
)
@@ -230,30 +477,53 @@ impl RateCompletionModal {
impl Render for RateCompletionModal {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let border_color = cx.theme().colors().border;
h_flex()
.gap_2()
.bg(cx.theme().colors().elevated_surface_background)
.w(cx.viewport_size().width - px(256.))
.h(cx.viewport_size().height - px(256.))
.rounded_lg()
.shadow_lg()
.p_2()
.key_context("RateCompletionModal")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::select_prev_edit))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_next_edit))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::thumbs_up))
.on_action(cx.listener(Self::thumbs_up_active))
.on_action(cx.listener(Self::thumbs_down_active))
.on_action(cx.listener(Self::focus_completions))
.on_action(cx.listener(Self::preview_completion))
.bg(cx.theme().colors().elevated_surface_background)
.border_1()
.border_color(border_color)
.w(cx.viewport_size().width - px(320.))
.h(cx.viewport_size().height - px(300.))
.rounded_lg()
.shadow_lg()
.child(
div()
.id("completion_list")
.border_r_1()
.border_color(border_color)
.w_96()
.h_full()
.p_0p5()
.overflow_y_scroll()
.child(
ui::List::new()
List::new()
.empty_message(
"No completions, use the editor to generate some and rate them!",
div()
.p_2()
.child(
Label::new("No completions yet. Use the editor to generate some and rate them!")
.color(Color::Muted),
)
.into_any_element(),
)
.children(self.zeta.read(cx).recent_completions().cloned().map(
|completion| {
.children(self.zeta.read(cx).recent_completions().cloned().enumerate().map(
|(index, completion)| {
let selected =
self.active_completion.as_ref().map_or(false, |selected| {
selected.completion.id == completion.id
@@ -261,25 +531,30 @@ impl Render for RateCompletionModal {
let rated =
self.zeta.read(cx).is_completion_rated(completion.id);
ListItem::new(completion.id)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.focused(index == self.selected_index)
.selected(selected)
.end_slot(if rated {
.start_slot(if rated {
Icon::new(IconName::Check).color(Color::Success)
} else if completion.edits.is_empty() {
Icon::new(IconName::Ellipsis).color(Color::Muted)
Icon::new(IconName::File).color(Color::Muted).size(IconSize::Small)
} else {
Icon::new(IconName::Diff).color(Color::Muted)
Icon::new(IconName::FileDiff).color(Color::Accent).size(IconSize::Small)
})
.child(Label::new(
completion.path.to_string_lossy().to_string(),
))
).size(LabelSize::Small))
.child(
Label::new(format!("({})", completion.id))
.color(Color::Muted)
.size(LabelSize::XSmall),
div()
.overflow_hidden()
.text_ellipsis()
.child(Label::new(format!("({})", completion.id))
.color(Color::Muted)
.size(LabelSize::XSmall)),
)
.on_click(cx.listener(move |this, _, cx| {
this.select_completion(Some(completion.clone()), cx);
this.select_completion(Some(completion.clone()), true, cx);
}))
},
)),

View File

@@ -6,7 +6,10 @@ use anyhow::{anyhow, Context as _, Result};
use client::Client;
use collections::{HashMap, HashSet, VecDeque};
use futures::AsyncReadExt;
use gpui::{actions, AppContext, Context, Global, Model, ModelContext, Subscription, Task};
use gpui::{
actions, AppContext, AsyncAppContext, Context, EntityId, Global, Model, ModelContext,
Subscription, Task,
};
use http_client::{HttpClient, Method};
use language::{
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, OffsetRangeExt,
@@ -18,6 +21,7 @@ use std::{
borrow::Cow,
cmp,
fmt::Write,
future::Future,
mem,
ops::Range,
path::Path,
@@ -253,12 +257,17 @@ impl Zeta {
}
}
pub fn request_completion(
pub fn request_completion_impl<F, R>(
&mut self,
buffer: &Model<Buffer>,
position: language::Anchor,
cx: &mut ModelContext<Self>,
) -> Task<Result<InlineCompletion>> {
perform_predict_edits: F,
) -> Task<Result<InlineCompletion>>
where
F: FnOnce(Arc<Client>, LlmApiToken, PredictEditsParams) -> R + 'static,
R: Future<Output = Result<PredictEditsResponse>> + Send + 'static,
{
let snapshot = self.report_changes_for_buffer(buffer, cx);
let point = position.to_point(&snapshot);
let offset = point.to_offset(&snapshot);
@@ -292,7 +301,7 @@ impl Zeta {
input_excerpt: input_excerpt.clone(),
};
let response = Self::perform_predict_edits(&client, llm_token, body).await?;
let response = perform_predict_edits(client, llm_token, body).await?;
let output_excerpt = response.output_excerpt;
log::debug!("prediction took: {:?}", start.elapsed());
@@ -305,7 +314,9 @@ impl Zeta {
path,
input_events,
input_excerpt,
)?;
&cx,
)
.await?;
this.update(&mut cx, |this, cx| {
this.recent_completions
@@ -320,50 +331,210 @@ impl Zeta {
})
}
async fn perform_predict_edits(
client: &Arc<Client>,
// Generates several example completions of various states to fill the Zeta completion modal
#[cfg(any(test, feature = "test-support"))]
pub fn fill_with_fake_completions(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
let test_buffer_text = indoc::indoc! {r#"a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
"#};
let buffer = cx.new_model(|cx| Buffer::local(test_buffer_text, cx));
let position = buffer.read(cx).anchor_before(Point::new(1, 0));
let completion_tasks = vec![
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!("{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
[here's an edit]
And maybe a short line
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
", ),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
[and another edit]
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
[a third completion]
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
and then another
[fourth completion example]
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
[fifth and final completion]
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
];
cx.spawn(|zeta, mut cx| async move {
for task in completion_tasks {
task.await.unwrap();
}
zeta.update(&mut cx, |zeta, _cx| {
zeta.recent_completions.get_mut(2).unwrap().edits = Arc::new([]);
zeta.recent_completions.get_mut(3).unwrap().edits = Arc::new([]);
})
.ok();
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake_completion(
&mut self,
buffer: &Model<Buffer>,
position: language::Anchor,
response: PredictEditsResponse,
cx: &mut ModelContext<Self>,
) -> Task<Result<InlineCompletion>> {
use std::future::ready;
self.request_completion_impl(buffer, position, cx, |_, _, _| ready(Ok(response)))
}
pub fn request_completion(
&mut self,
buffer: &Model<Buffer>,
position: language::Anchor,
cx: &mut ModelContext<Self>,
) -> Task<Result<InlineCompletion>> {
self.request_completion_impl(buffer, position, cx, Self::perform_predict_edits)
}
fn perform_predict_edits(
client: Arc<Client>,
llm_token: LlmApiToken,
body: PredictEditsParams,
) -> Result<PredictEditsResponse> {
let http_client = client.http_client();
let mut token = llm_token.acquire(client).await?;
let mut did_retry = false;
) -> impl Future<Output = Result<PredictEditsResponse>> {
async move {
let http_client = client.http_client();
let mut token = llm_token.acquire(&client).await?;
let mut did_retry = false;
loop {
let request_builder = http_client::Request::builder();
let request = request_builder
.method(Method::POST)
.uri(
http_client
.build_zed_llm_url("/predict_edits", &[])?
.as_ref(),
)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", token))
.body(serde_json::to_string(&body)?.into())?;
loop {
let request_builder = http_client::Request::builder();
let request = request_builder
.method(Method::POST)
.uri(
http_client
.build_zed_llm_url("/predict_edits", &[])?
.as_ref(),
)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", token))
.body(serde_json::to_string(&body)?.into())?;
let mut response = http_client.send(request).await?;
let mut response = http_client.send(request).await?;
if response.status().is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
return Ok(serde_json::from_str(&body)?);
} else if !did_retry
&& response
.headers()
.get(EXPIRED_LLM_TOKEN_HEADER_NAME)
.is_some()
{
did_retry = true;
token = llm_token.refresh(client).await?;
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
return Err(anyhow!(
"error predicting edits.\nStatus: {:?}\nBody: {}",
response.status(),
body
));
if response.status().is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
return Ok(serde_json::from_str(&body)?);
} else if !did_retry
&& response
.headers()
.get(EXPIRED_LLM_TOKEN_HEADER_NAME)
.is_some()
{
did_retry = true;
token = llm_token.refresh(&client).await?;
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
return Err(anyhow!(
"error predicting edits.\nStatus: {:?}\nBody: {}",
response.status(),
body
));
}
}
}
}
@@ -375,41 +546,45 @@ impl Zeta {
path: Arc<Path>,
input_events: String,
input_excerpt: String,
) -> Result<InlineCompletion> {
let content = output_excerpt.replace(CURSOR_MARKER, "");
cx: &AsyncAppContext,
) -> Task<Result<InlineCompletion>> {
let snapshot = snapshot.clone();
cx.background_executor().spawn(async move {
let content = output_excerpt.replace(CURSOR_MARKER, "");
let codefence_start = content
.find(EDITABLE_REGION_START_MARKER)
.context("could not find start marker")?;
let content = &content[codefence_start..];
let codefence_start = content
.find(EDITABLE_REGION_START_MARKER)
.context("could not find start marker")?;
let content = &content[codefence_start..];
let newline_ix = content.find('\n').context("could not find newline")?;
let content = &content[newline_ix + 1..];
let newline_ix = content.find('\n').context("could not find newline")?;
let content = &content[newline_ix + 1..];
let codefence_end = content
.rfind(&format!("\n{EDITABLE_REGION_END_MARKER}"))
.context("could not find end marker")?;
let new_text = &content[..codefence_end];
let codefence_end = content
.rfind(&format!("\n{EDITABLE_REGION_END_MARKER}"))
.context("could not find end marker")?;
let new_text = &content[..codefence_end];
let old_text = snapshot
.text_for_range(excerpt_range.clone())
.collect::<String>();
let old_text = snapshot
.text_for_range(excerpt_range.clone())
.collect::<String>();
let edits = Self::compute_edits(old_text, new_text, excerpt_range.start, snapshot);
let edits = Self::compute_edits(old_text, new_text, excerpt_range.start, &snapshot);
Ok(InlineCompletion {
id: InlineCompletionId::new(),
path,
excerpt_range,
edits: edits.into(),
snapshot: snapshot.clone(),
input_events: input_events.into(),
input_excerpt: input_excerpt.into(),
output_excerpt: output_excerpt.into(),
Ok(InlineCompletion {
id: InlineCompletionId::new(),
path,
excerpt_range,
edits: edits.into(),
snapshot: snapshot.clone(),
input_events: input_events.into(),
input_excerpt: input_excerpt.into(),
output_excerpt: output_excerpt.into(),
})
})
}
fn compute_edits(
pub fn compute_edits(
old_text: String,
new_text: &str,
offset: usize,
@@ -500,10 +675,14 @@ impl Zeta {
cx.notify();
}
pub fn recent_completions(&self) -> impl Iterator<Item = &InlineCompletion> {
pub fn recent_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
self.recent_completions.iter()
}
pub fn recent_completions_len(&self) -> usize {
self.recent_completions.len()
}
fn report_changes_for_buffer(
&mut self,
buffer: &Model<Buffer>,
@@ -665,9 +844,14 @@ impl Event {
}
}
struct CurrentInlineCompletion {
buffer_id: EntityId,
completion: InlineCompletion,
}
pub struct ZetaInlineCompletionProvider {
zeta: Model<Zeta>,
current_completion: Option<InlineCompletion>,
current_completion: Option<CurrentInlineCompletion>,
pending_refresh: Task<()>,
}
@@ -708,28 +892,34 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
debounce: bool,
cx: &mut ModelContext<Self>,
) {
self.pending_refresh = cx.spawn(|this, mut cx| async move {
if debounce {
cx.background_executor().timer(Self::DEBOUNCE_TIMEOUT).await;
}
self.pending_refresh =
cx.spawn(|this, mut cx| async move {
if debounce {
cx.background_executor().timer(Self::DEBOUNCE_TIMEOUT).await;
}
let completion_request = this.update(&mut cx, |this, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.request_completion(&buffer, position, cx)
let completion_request = this.update(&mut cx, |this, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.request_completion(&buffer, position, cx)
})
});
let mut completion = None;
if let Ok(completion_request) = completion_request {
completion = completion_request.await.log_err().map(|completion| {
CurrentInlineCompletion {
buffer_id: buffer.entity_id(),
completion,
}
});
}
this.update(&mut cx, |this, cx| {
this.current_completion = completion;
cx.notify();
})
.ok();
});
let mut completion = None;
if let Ok(completion_request) = completion_request {
completion = completion_request.await.log_err();
}
this.update(&mut cx, |this, cx| {
this.current_completion = completion;
cx.notify();
})
.ok();
});
}
fn cycle(
@@ -754,7 +944,16 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
cursor_position: language::Anchor,
cx: &mut ModelContext<Self>,
) -> Option<inline_completion::InlineCompletion> {
let completion = self.current_completion.as_mut()?;
let CurrentInlineCompletion {
buffer_id,
completion,
} = self.current_completion.as_mut()?;
// Invalidate previous completion if it was generated for a different buffer.
if *buffer_id != buffer.entity_id() {
self.current_completion.take();
return None;
}
let buffer = buffer.read(cx);
let Some(edits) = completion.interpolate(buffer.snapshot()) else {

View File

@@ -14,6 +14,16 @@ CompileFlags:
Add: [-xc]
```
By default clang and gcc by will recognize `*.C` and `*.H` (uppercase extensions) as C++ and not C and so Zed too follows this convention. If you are working with a C-only project (perhaps one with legacy uppercase pathing like `FILENAME.C`) you can override this behavior by adding this to your settings:
```json
{
"file_types": {
"C": ["C", "H"]
}
}
```
## Formatting
By default Zed will use the `clangd` language server for formatting C code. The Clangd is the same as the `clang-format` CLI tool. To configure this you can add a `.clang-format` file. For example:

View File

@@ -167,6 +167,7 @@ if [[ -n $pacman ]]; then
deps=(
gcc
clang
musl
cmake
alsa-lib
fontconfig