Compare commits
54 Commits
gui
...
zeta-exten
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57ca62b694 | ||
|
|
5318f529de | ||
|
|
096bbfead5 | ||
|
|
0b4495a920 | ||
|
|
636c28b652 | ||
|
|
6ceec5d9a2 | ||
|
|
9f0f63f92b | ||
|
|
b38e9e44d6 | ||
|
|
e0cbbf8d06 | ||
|
|
4eaa1c2514 | ||
|
|
b3de19a6bd | ||
|
|
241b14eeaf | ||
|
|
72d8f2e595 | ||
|
|
3f6ac53856 | ||
|
|
74d7ce2d2b | ||
|
|
6a37307302 | ||
|
|
8dd1c23b92 | ||
|
|
57874717c1 | ||
|
|
bab6a79ab6 | ||
|
|
9a806f98e6 | ||
|
|
e778635487 | ||
|
|
5de0bcc990 | ||
|
|
9143fd2924 | ||
|
|
d7eba54016 | ||
|
|
52c0d712a6 | ||
|
|
111e844753 | ||
|
|
0eb992219b | ||
|
|
573e096fc5 | ||
|
|
ee6f834028 | ||
|
|
b4c8e04544 | ||
|
|
bcf8a2f9fc | ||
|
|
77d066200a | ||
|
|
5d0e75dd73 | ||
|
|
181af7804b | ||
|
|
ad4c4aff13 | ||
|
|
91b02a6259 | ||
|
|
1f296d8f3b | ||
|
|
c204b0d01a | ||
|
|
8e0ae441f3 | ||
|
|
02fbad18ce | ||
|
|
227f21f035 | ||
|
|
543a3ef5e4 | ||
|
|
cc97e682d5 | ||
|
|
59afc27f03 | ||
|
|
611abcadc0 | ||
|
|
fff12ec1e5 | ||
|
|
13a81e454a | ||
|
|
de89f8cf83 | ||
|
|
c594ccb0af | ||
|
|
937186da12 | ||
|
|
b3ffbea376 | ||
|
|
124e63d07c | ||
|
|
dd66a20d78 | ||
|
|
e8c72d91c3 |
4
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -26,8 +26,8 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
|
||||
description: Drag issues into the text input below
|
||||
label: If applicable, add screenshots or screencasts of the incorrect state / behavior
|
||||
description: Drag images / videos into the text input below
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,3 +1,4 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Language Request
|
||||
|
||||
1211
Cargo.lock
generated
1211
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,11 @@
|
||||
"gitignore": "vcs",
|
||||
"gitkeep": "vcs",
|
||||
"gitmodules": "vcs",
|
||||
"TAG_EDITMSG": "vcs",
|
||||
"MERGE_MSG": "vcs",
|
||||
"COMMIT_EDITMSG": "vcs",
|
||||
"NOTES_EDITMSG": "vcs",
|
||||
"EDIT_DESCRIPTION": "vcs",
|
||||
"gleam": "gleam",
|
||||
"go": "go",
|
||||
"gql": "graphql",
|
||||
@@ -108,6 +113,7 @@
|
||||
"mdf": "storage",
|
||||
"mdx": "document",
|
||||
"metadata": "code",
|
||||
"metal": "metal",
|
||||
"mjs": "javascript",
|
||||
"mka": "audio",
|
||||
"mkv": "video",
|
||||
@@ -317,6 +323,9 @@
|
||||
"lua": {
|
||||
"icon": "icons/file_icons/lua.svg"
|
||||
},
|
||||
"metal": {
|
||||
"icon": "icons/file_icons/metal.svg"
|
||||
},
|
||||
"nim": {
|
||||
"icon": "icons/file_icons/nim.svg"
|
||||
},
|
||||
|
||||
1
assets/icons/file_icons/metal.svg
Normal file
1
assets/icons/file_icons/metal.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.56 4.502 3.25 3.027V11.5h1.5V6.973l2.69 3.025 1.31 1.475V7.918l3.306 3.582h2.042L8.55 5.491 7.25 4.081V7.528L4.56 4.502Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 269 B |
4
assets/icons/zed_predict.svg
Normal file
4
assets/icons/zed_predict.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 8.9V11C5.93097 11 5.06903 11 3 11V10.4L8 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M11 5L13 8L11 11" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 268 B |
@@ -468,13 +468,21 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"context": "Editor && inline_completion",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
|
||||
@@ -541,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"
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"ctrl-a": "editor::MoveToBeginningOfLine",
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-d": "editor::Delete",
|
||||
@@ -53,6 +55,14 @@
|
||||
"shift shift": "file_finder::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-r": "search::SelectPrevMatch",
|
||||
"ctrl-g": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"ctrl-a": "editor::MoveToBeginningOfLine",
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-d": "editor::Delete",
|
||||
@@ -53,6 +55,14 @@
|
||||
"shift shift": "file_finder::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-r": "search::SelectPrevMatch",
|
||||
"ctrl-g": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
// 4. Highlight the full line (default):
|
||||
// "all"
|
||||
"current_line_highlight": "all",
|
||||
// The debounce delay before querying highlights from the language
|
||||
// server based on the current cursor location.
|
||||
"lsp_highlight_debounce": 75,
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
@@ -471,6 +474,14 @@
|
||||
// Default width of the chat panel.
|
||||
"default_width": 240
|
||||
},
|
||||
"git_panel": {
|
||||
// Whether to show the git panel button in the status bar.
|
||||
"button": true,
|
||||
// Where to the git panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Default width of the git panel.
|
||||
"default_width": 360
|
||||
},
|
||||
"message_editor": {
|
||||
// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
// For example: typing `:wave:` gets replaced with `👋`.
|
||||
|
||||
@@ -55,7 +55,7 @@ use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model_selector::{LanguageModelPickerDelegate, LanguageModelSelector};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::lsp_store::LocalLspAdapterDelegate;
|
||||
@@ -143,7 +143,7 @@ pub struct AssistantPanel {
|
||||
languages: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
subscriptions: Vec<Subscription>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
model_summary_editor: View<Editor>,
|
||||
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
|
||||
configuration_subscription: Option<Subscription>,
|
||||
@@ -341,11 +341,12 @@ impl AssistantPanel {
|
||||
) -> Self {
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
let model_summary_editor = cx.new_view(Editor::single_line);
|
||||
let context_editor_toolbar = cx.new_view(|_| {
|
||||
let context_editor_toolbar = cx.new_view(|cx| {
|
||||
ContextEditorToolbarItem::new(
|
||||
workspace,
|
||||
model_selector_menu_handle.clone(),
|
||||
model_summary_editor.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -4455,23 +4456,36 @@ impl FollowableItem for ContextEditor {
|
||||
}
|
||||
|
||||
pub struct ContextEditorToolbarItem {
|
||||
fs: Arc<dyn Fs>,
|
||||
active_context_editor: Option<WeakView<ContextEditor>>,
|
||||
model_summary_editor: View<Editor>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
}
|
||||
|
||||
impl ContextEditorToolbarItem {
|
||||
pub fn new(
|
||||
workspace: &Workspace,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
model_summary_editor: View<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
fs: workspace.app_state().fs.clone(),
|
||||
active_context_editor: None,
|
||||
model_summary_editor,
|
||||
model_selector_menu_handle,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
language_model_selector_menu_handle: model_selector_menu_handle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4560,17 +4574,8 @@ impl Render for ContextEditorToolbarItem {
|
||||
// .map(|remaining_items| format!("Files to scan: {}", remaining_items))
|
||||
// })
|
||||
.child(
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}
|
||||
},
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
@@ -4616,7 +4621,7 @@ impl Render for ContextEditorToolbarItem {
|
||||
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
|
||||
}),
|
||||
)
|
||||
.with_handle(self.model_selector_menu_handle.clone()),
|
||||
.with_handle(self.language_model_selector_menu_handle.clone()),
|
||||
)
|
||||
.children(self.render_remaining_tokens(cx));
|
||||
|
||||
@@ -5113,9 +5118,11 @@ fn make_lsp_adapter_delegate(
|
||||
return Ok(None::<Arc<dyn LspAdapterDelegate>>);
|
||||
};
|
||||
let http_client = project.client().http_client().clone();
|
||||
project.lsp_store().update(cx, |lsp_store, cx| {
|
||||
project.lsp_store().update(cx, |_, cx| {
|
||||
Ok(Some(LocalLspAdapterDelegate::new(
|
||||
lsp_store,
|
||||
project.languages().clone(),
|
||||
project.environment(),
|
||||
cx.weak_model(),
|
||||
&worktree,
|
||||
http_client,
|
||||
project.fs().clone(),
|
||||
|
||||
@@ -17,7 +17,7 @@ use futures::{
|
||||
channel::mpsc,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use parking_lot::Mutex;
|
||||
@@ -35,7 +35,7 @@ use std::{
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
|
||||
use ui::{Context as _, IconName, WindowContext};
|
||||
use ui::{IconName, WindowContext};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
|
||||
@@ -33,7 +33,7 @@ use language_model::{
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelTextStream, Role,
|
||||
};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_models::report_assistant_event;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
@@ -1358,8 +1358,8 @@ enum PromptEditorEvent {
|
||||
|
||||
struct PromptEditor {
|
||||
id: InlineAssistId,
|
||||
fs: Arc<dyn Fs>,
|
||||
editor: View<Editor>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
edited_since_done: bool,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
prompt_history: VecDeque<String>,
|
||||
@@ -1500,43 +1500,27 @@ impl Render for PromptEditor {
|
||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}
|
||||
},
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.info_text(
|
||||
"Inline edits use context\n\
|
||||
from the currently selected\n\
|
||||
assistant panel tab.",
|
||||
),
|
||||
)
|
||||
.child(LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
))
|
||||
.map(|el| {
|
||||
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
|
||||
return el;
|
||||
@@ -1642,6 +1626,19 @@ impl PromptEditor {
|
||||
let mut this = Self {
|
||||
id,
|
||||
editor: prompt_editor,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
gutter_dimensions,
|
||||
prompt_history,
|
||||
@@ -1650,7 +1647,6 @@ impl PromptEditor {
|
||||
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
|
||||
editor_subscriptions: Vec::new(),
|
||||
codegen,
|
||||
fs,
|
||||
pending_token_count: Task::ready(Ok(())),
|
||||
token_counts: None,
|
||||
_token_count_subscriptions: token_count_subscriptions,
|
||||
|
||||
@@ -20,7 +20,7 @@ use language::Buffer;
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_models::report_assistant_event;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use std::{
|
||||
@@ -476,9 +476,9 @@ enum PromptEditorEvent {
|
||||
|
||||
struct PromptEditor {
|
||||
id: TerminalInlineAssistId,
|
||||
fs: Arc<dyn Fs>,
|
||||
height_in_lines: u8,
|
||||
editor: View<Editor>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
edited_since_done: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_history_ix: Option<usize>,
|
||||
@@ -614,17 +614,8 @@ impl Render for PromptEditor {
|
||||
.w_12()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(LanguageModelSelector::new(
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}
|
||||
},
|
||||
.child(LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -718,6 +709,19 @@ impl PromptEditor {
|
||||
id,
|
||||
height_in_lines: 1,
|
||||
editor: prompt_editor,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
prompt_history,
|
||||
prompt_history_ix: None,
|
||||
@@ -725,7 +729,6 @@ impl PromptEditor {
|
||||
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
|
||||
editor_subscriptions: Vec::new(),
|
||||
codegen,
|
||||
fs,
|
||||
pending_token_count: Task::ready(Ok(())),
|
||||
token_count: None,
|
||||
_token_count_subscriptions: token_count_subscriptions,
|
||||
|
||||
@@ -13,30 +13,53 @@ 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
|
||||
fuzzy.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
|
||||
terminal.workspace = true
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
@@ -45,3 +68,8 @@ unindent.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rand.workspace = true
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
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 terminal_inline_assistant;
|
||||
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 +34,43 @@ 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,
|
||||
);
|
||||
terminal_inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
feature_gate_assistant2_actions(cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,13 +88,13 @@ impl AssistantPanel {
|
||||
thread: cx.new_view(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
workspace,
|
||||
workspace.clone(),
|
||||
language_registry,
|
||||
tools.clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
message_editor: cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)),
|
||||
message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)),
|
||||
tools,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
@@ -123,7 +123,8 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
|
||||
self.message_editor =
|
||||
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
}
|
||||
|
||||
@@ -145,7 +146,8 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
|
||||
self.message_editor =
|
||||
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
}
|
||||
|
||||
|
||||
485
crates/assistant2/src/assistant_settings.rs
Normal file
485
crates/assistant2/src/assistant_settings.rs
Normal file
@@ -0,0 +1,485 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use gpui::Pixels;
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
#[default]
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContentV1 {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev { default_model: Option<CloudModel> },
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
api_url: Option<String>,
|
||||
available_models: Option<Vec<OpenAiModel>>,
|
||||
},
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic {
|
||||
default_model: Option<AnthropicModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
#[serde(rename = "ollama")]
|
||||
Ollama {
|
||||
default_model: Option<OllamaModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AssistantSettings {
|
||||
pub enabled: bool,
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AssistantSettingsContent {
|
||||
Versioned(VersionedAssistantSettingsContent),
|
||||
Legacy(LegacyAssistantSettingsContent),
|
||||
}
|
||||
|
||||
impl JsonSchema for AssistantSettingsContent {
|
||||
fn schema_name() -> String {
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
VersionedAssistantSettingsContent::is_referenceable()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::Versioned(VersionedAssistantSettingsContent::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantSettingsContent {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(_) => true,
|
||||
VersionedAssistantSettingsContent::V2(_) => false,
|
||||
},
|
||||
AssistantSettingsContent::Legacy(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_width,
|
||||
default_model: settings
|
||||
.provider
|
||||
.clone()
|
||||
.and_then(|provider| match provider {
|
||||
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "zed.dev".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "openai".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "anthropic".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "ollama".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "openai".to_string(),
|
||||
model: settings
|
||||
.default_open_ai_model
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.id()
|
||||
.to_string(),
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
|
||||
let model = language_model.id().0.to_string();
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(tag = "version")]
|
||||
pub enum VersionedAssistantSettingsContent {
|
||||
#[serde(rename = "1")]
|
||||
V1(AssistantSettingsContentV1),
|
||||
#[serde(rename = "2")]
|
||||
V2(AssistantSettingsContentV2),
|
||||
}
|
||||
|
||||
impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
default_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV2 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The default model to use when creating new chats.
|
||||
default_model: Option<LanguageModelSelection>,
|
||||
/// Additional models with which to generate alternatives when performing inline assists.
|
||||
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
||||
/// Enable experimental live diffs in the assistant panel.
|
||||
///
|
||||
/// Default: false
|
||||
enable_experimental_live_diffs: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct LanguageModelSelection {
|
||||
#[schemars(schema_with = "providers_schema")]
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"google".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
impl Default for LanguageModelSelection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: "openai".to_string(),
|
||||
model: "gpt-4".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV1 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The provider of the assistant service.
|
||||
///
|
||||
/// This can be "openai", "anthropic", "ollama", "zed.dev"
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AssistantProviderContentV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct LegacyAssistantSettingsContent {
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
pub default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
pub default_height: Option<f32>,
|
||||
/// The default OpenAI model to use when creating new chats.
|
||||
///
|
||||
/// Default: gpt-4-1106-preview
|
||||
pub default_open_ai_model: Option<OpenAiModel>,
|
||||
/// OpenAI API base URL to use when creating new chats.
|
||||
///
|
||||
/// Default: https://api.openai.com/v1
|
||||
pub openai_api_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut settings = AssistantSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.is_version_outdated() {
|
||||
settings.using_outdated_settings_version = true;
|
||||
}
|
||||
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
merge(
|
||||
&mut settings.default_width,
|
||||
value.default_width.map(Into::into),
|
||||
);
|
||||
merge(
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model);
|
||||
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||
merge(
|
||||
&mut settings.enable_experimental_live_diffs,
|
||||
value.enable_experimental_live_diffs,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::Fs;
|
||||
use gpui::{ReadGlobal, TestAppContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let test_settings = settings::SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
AssistantSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: "claude-3-5-sonnet".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AssistantSettingsContent::Versioned(
|
||||
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
}),
|
||||
)
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
|
||||
assert!(raw_settings_value.contains(r#""version": "2""#));
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AssistantSettingsTest {
|
||||
assistant: AssistantSettingsContent,
|
||||
}
|
||||
|
||||
let assistant_settings: AssistantSettingsTest =
|
||||
serde_json_lenient::from_str(&raw_settings_value).unwrap();
|
||||
|
||||
assert!(!assistant_settings.assistant.is_version_outdated());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,20 @@
|
||||
use gpui::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::post_inc;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct ContextId(pub(crate) usize);
|
||||
|
||||
impl ContextId {
|
||||
pub fn post_inc(&mut self) -> Self {
|
||||
Self(post_inc(&mut self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Some context attached to a message in a thread.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub id: ContextId,
|
||||
pub name: SharedString,
|
||||
pub kind: ContextKind,
|
||||
pub text: SharedString,
|
||||
|
||||
@@ -1,15 +1,93 @@
|
||||
mod file_context_picker;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{DismissEvent, SharedString, Task, WeakView};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
|
||||
use gpui::{
|
||||
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
|
||||
WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||
use crate::message_editor::MessageEditor;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct ContextPicker<T: PopoverTrigger> {
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
trigger: T,
|
||||
#[derive(Debug, Clone)]
|
||||
enum ContextPickerMode {
|
||||
Default,
|
||||
File(View<FileContextPicker>),
|
||||
}
|
||||
|
||||
pub(super) struct ContextPicker {
|
||||
mode: ContextPickerMode,
|
||||
picker: View<Picker<ContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl ContextPicker {
|
||||
pub fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = ContextPickerDelegate {
|
||||
context_picker: cx.view().downgrade(),
|
||||
workspace: workspace.clone(),
|
||||
message_editor: message_editor.clone(),
|
||||
entries: vec![
|
||||
ContextPickerEntry {
|
||||
name: "directory".into(),
|
||||
description: "Insert any directory".into(),
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "file".into(),
|
||||
description: "Insert any file".into(),
|
||||
icon: IconName::File,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "web".into(),
|
||||
description: "Fetch content from URL".into(),
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
],
|
||||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
|
||||
});
|
||||
|
||||
ContextPicker {
|
||||
mode: ContextPickerMode::Default,
|
||||
picker,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_mode(&mut self) {
|
||||
self.mode = ContextPickerMode::Default;
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ContextPicker {}
|
||||
|
||||
impl FocusableView for ContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
match &self.mode {
|
||||
ContextPickerMode::Default => self.picker.focus_handle(cx),
|
||||
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex().min_w(px(400.)).map(|parent| match &self.mode {
|
||||
ContextPickerMode::Default => parent.child(self.picker.clone()),
|
||||
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -20,26 +98,18 @@ struct ContextPickerEntry {
|
||||
}
|
||||
|
||||
pub(crate) struct ContextPickerDelegate {
|
||||
all_entries: Vec<ContextPickerEntry>,
|
||||
filtered_entries: Vec<ContextPickerEntry>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
entries: Vec<ContextPickerEntry>,
|
||||
selected_ix: usize,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> ContextPicker<T> {
|
||||
pub(crate) fn new(message_editor: WeakView<MessageEditor>, trigger: T) -> Self {
|
||||
ContextPicker {
|
||||
message_editor,
|
||||
trigger,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.filtered_entries.len()
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
@@ -47,7 +117,7 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
|
||||
self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -55,52 +125,41 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
"Select a context source…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let all_commands = self.all_entries.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let filtered_commands = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
if query.is_empty() {
|
||||
all_commands
|
||||
} else {
|
||||
all_commands
|
||||
.into_iter()
|
||||
.filter(|model_info| {
|
||||
model_info
|
||||
.name
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.filtered_entries = filtered_commands;
|
||||
this.delegate.set_selected_index(0, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
|
||||
self.message_editor
|
||||
.update(cx, |_message_editor, _cx| {
|
||||
println!("Insert context from {}", entry.name);
|
||||
if let Some(entry) = self.entries.get(self.selected_ix) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
match entry.name.to_string().as_str() {
|
||||
"file" => {
|
||||
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
|
||||
FileContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.message_editor.clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cx.focus_self();
|
||||
})
|
||||
.ok();
|
||||
cx.emit(DismissEvent);
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn editor_position(&self) -> PickerEditorPosition {
|
||||
PickerEditorPosition::End
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| match this.mode {
|
||||
ContextPickerMode::Default => cx.emit(DismissEvent),
|
||||
ContextPickerMode::File(_) => {}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
@@ -109,7 +168,7 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let entry = self.filtered_entries.get(ix)?;
|
||||
let entry = &self.entries[ix];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
@@ -148,50 +207,3 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let entries = vec![
|
||||
ContextPickerEntry {
|
||||
name: "directory".into(),
|
||||
description: "Insert any directory".into(),
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "file".into(),
|
||||
description: "Insert any file".into(),
|
||||
icon: IconName::File,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "web".into(),
|
||||
description: "Fetch content from URL".into(),
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
];
|
||||
|
||||
let delegate = ContextPickerDelegate {
|
||||
all_entries: entries.clone(),
|
||||
message_editor: self.message_editor.clone(),
|
||||
filtered_entries: entries,
|
||||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let picker =
|
||||
cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
|
||||
|
||||
let handle = self
|
||||
.message_editor
|
||||
.update(cx, |this, _| this.context_picker_handle.clone())
|
||||
.ok();
|
||||
PopoverMenu::new("context-picker")
|
||||
.menu(move |_cx| Some(picker.clone()))
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::AnchorCorner::TopLeft)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
})
|
||||
.when_some(handle, |this, handle| this.with_handle(handle))
|
||||
}
|
||||
}
|
||||
|
||||
289
crates/assistant2/src/context_picker/file_context_picker.rs
Normal file
289
crates/assistant2/src/context_picker/file_context_picker.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
use std::fmt::Write as _;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, WorktreeId};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::message_editor::MessageEditor;
|
||||
|
||||
pub struct FileContextPicker {
|
||||
picker: View<Picker<FileContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl FileContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = FileContextPickerDelegate::new(context_picker, workspace, message_editor);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for FileContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for FileContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
matches: Vec<PathMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl FileContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
message_editor,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn search(
|
||||
&mut self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
|
||||
let entries = entries
|
||||
.into_iter()
|
||||
.map(|entries| entries.0)
|
||||
.chain(project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let id = worktree.id();
|
||||
worktree
|
||||
.child_entries(Path::new(""))
|
||||
.filter(|entry| entry.kind.is_file())
|
||||
.map(move |entry| project::ProjectPath {
|
||||
worktree_id: id,
|
||||
path: entry.path.clone(),
|
||||
})
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
|
||||
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
|
||||
full_path.push(&entry.path);
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: entry.worktree_id.to_usize(),
|
||||
path: full_path.into(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: true,
|
||||
candidates: project::Candidates::Files,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.as_str(),
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for FileContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Search files…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
// TODO: This should be probably be run in the background.
|
||||
let paths = search_task.await;
|
||||
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.delegate.matches = paths;
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let mat = &self.matches[self.selected_index];
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let path = mat.path.clone();
|
||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let Some(open_buffer_task) = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, path.clone()), cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
|
||||
let buffer = open_buffer_task.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate
|
||||
.message_editor
|
||||
.update(cx, |message_editor, cx| {
|
||||
let mut text = String::new();
|
||||
text.push_str(&codeblock_fence_for_path(Some(&path), None));
|
||||
text.push_str(&buffer.read(cx).text());
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
text.push_str("```\n");
|
||||
|
||||
message_editor.insert_context(
|
||||
ContextKind::File,
|
||||
path.to_string_lossy().to_string(),
|
||||
text,
|
||||
);
|
||||
})
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let mat = &self.matches[ix];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(mat.path.to_string_lossy().to_string()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<RangeInclusive<u32>>) -> String {
|
||||
let mut text = String::new();
|
||||
write!(text, "```").unwrap();
|
||||
|
||||
if let Some(path) = path {
|
||||
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
|
||||
write!(text, "{} ", extension).unwrap();
|
||||
}
|
||||
|
||||
write!(text, "{}", path.display()).unwrap();
|
||||
} else {
|
||||
write!(text, "untitled").unwrap();
|
||||
}
|
||||
|
||||
if let Some(row_range) = row_range {
|
||||
write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
|
||||
}
|
||||
|
||||
text.push('\n');
|
||||
text
|
||||
}
|
||||
3848
crates/assistant2/src/inline_assistant.rs
Normal file
3848
crates/assistant2/src/inline_assistant.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,19 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use picker::Picker;
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
|
||||
PopoverMenuHandle, Tooltip,
|
||||
PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::{Context, ContextKind};
|
||||
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
|
||||
use crate::context::{Context, ContextId, ContextKind};
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::ui::ContextPill;
|
||||
use crate::{Chat, ToggleModelSelector};
|
||||
@@ -20,18 +22,20 @@ pub struct MessageEditor {
|
||||
thread: Model<Thread>,
|
||||
editor: View<Editor>,
|
||||
context: Vec<Context>,
|
||||
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
|
||||
next_context_id: ContextId,
|
||||
context_picker: View<ContextPicker>,
|
||||
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
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(),
|
||||
}];
|
||||
|
||||
pub fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
thread: Model<Thread>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let weak_self = cx.view().downgrade();
|
||||
Self {
|
||||
thread,
|
||||
editor: cx.new_view(|cx| {
|
||||
@@ -40,12 +44,36 @@ impl MessageEditor {
|
||||
|
||||
editor
|
||||
}),
|
||||
context: mocked_context,
|
||||
context: Vec::new(),
|
||||
next_context_id: ContextId(0),
|
||||
context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)),
|
||||
context_picker_handle: PopoverMenuHandle::default(),
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
|model, _cx| {
|
||||
println!("Selected {:?}", model.name());
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
use_tools: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_context(
|
||||
&mut self,
|
||||
kind: ContextKind,
|
||||
name: impl Into<SharedString>,
|
||||
text: impl Into<SharedString>,
|
||||
) {
|
||||
self.context.push(Context {
|
||||
id: self.next_context_id.post_inc(),
|
||||
name: name.into(),
|
||||
kind,
|
||||
text: text.into(),
|
||||
});
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
|
||||
self.send_to_model(RequestKind::Chat, cx);
|
||||
}
|
||||
@@ -101,10 +129,8 @@ impl MessageEditor {
|
||||
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
|
||||
LanguageModelSelector::new(
|
||||
|model, _cx| {
|
||||
println!("Selected {:?}", model.name());
|
||||
},
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
@@ -160,6 +186,7 @@ impl Render for MessageEditor {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let context_picker = self.context_picker.clone();
|
||||
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
@@ -172,17 +199,31 @@ impl Render for MessageEditor {
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_2()
|
||||
.child(ContextPicker::new(
|
||||
cx.view().downgrade(),
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small),
|
||||
))
|
||||
.children(
|
||||
self.context
|
||||
.iter()
|
||||
.map(|context| ContextPill::new(context.clone())),
|
||||
.child(
|
||||
PopoverMenu::new("context-picker")
|
||||
.menu(move |_cx| Some(context_picker.clone()))
|
||||
.trigger(
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small),
|
||||
)
|
||||
.attach(gpui::AnchorCorner::TopLeft)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
})
|
||||
.with_handle(self.context_picker_handle.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)
|
||||
|
||||
312
crates/assistant2/src/prompts.rs
Normal file
312
crates/assistant2/src/prompts.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn generate_terminal_assistant_prompt(
|
||||
&self,
|
||||
user_prompt: &str,
|
||||
shell: Option<&str>,
|
||||
working_directory: Option<&str>,
|
||||
latest_output: &[String],
|
||||
) -> Result<String, RenderError> {
|
||||
let context = TerminalAssistantPromptContext {
|
||||
os: std::env::consts::OS.to_string(),
|
||||
arch: std::env::consts::ARCH.to_string(),
|
||||
shell: shell.map(|s| s.to_string()),
|
||||
working_directory: working_directory.map(|s| s.to_string()),
|
||||
latest_output: latest_output.to_vec(),
|
||||
user_prompt: user_prompt.to_string(),
|
||||
};
|
||||
|
||||
self.handlebars
|
||||
.lock()
|
||||
.render("terminal_assistant_prompt", &context)
|
||||
}
|
||||
}
|
||||
1102
crates/assistant2/src/streaming_diff.rs
Normal file
1102
crates/assistant2/src/streaming_diff.rs
Normal file
File diff suppressed because it is too large
Load Diff
1062
crates/assistant2/src/terminal_inline_assistant.rs
Normal file
1062
crates/assistant2/src/terminal_inline_assistant.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct CallSettingsContent {
|
||||
|
||||
/// Whether your current project should be shared when joining an empty channel.
|
||||
///
|
||||
/// Default: true
|
||||
/// Default: false
|
||||
pub share_on_join: Option<bool>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1288,6 +1288,12 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn muted_by_user(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.map_or(false, |live_kit| live_kit.muted_by_user)
|
||||
}
|
||||
|
||||
pub fn is_speaking(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
|
||||
@@ -1307,6 +1307,12 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn muted_by_user(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.map_or(false, |live_kit| live_kit.muted_by_user)
|
||||
}
|
||||
|
||||
pub fn is_speaking(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
|
||||
@@ -310,6 +310,9 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetStagedText>)
|
||||
.add_request_handler(
|
||||
forward_mutating_project_request::<proto::RegisterBufferWithLanguageServers>,
|
||||
)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::UpdateGitBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
|
||||
.add_request_handler(
|
||||
|
||||
@@ -994,10 +994,12 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
|
||||
|
||||
let _buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_local_buffer_with_lsp("/dir/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1587,7 +1589,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
@@ -1597,6 +1598,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
|
||||
// Set up the language server to return an additional inlay hint on each request.
|
||||
let edits_made = Arc::new(AtomicUsize::new(0));
|
||||
let closure_edits_made = Arc::clone(&edits_made);
|
||||
|
||||
@@ -3891,13 +3891,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
// Cause the language server to start.
|
||||
let _buffer = project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.open_buffer(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Path::new("other.rs").into(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
project.open_local_buffer_with_lsp("/a/other.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4176,7 +4170,9 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
// Join the project as client B and open all three files.
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
|
||||
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
|
||||
project_b.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, file_name), cx)
|
||||
})
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4230,7 +4226,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
cx.subscribe(&project_b, move |_, _, event, cx| {
|
||||
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
|
||||
disk_based_diagnostics_finished.store(true, SeqCst);
|
||||
for buffer in &guest_buffers {
|
||||
for (buffer, _) in &guest_buffers {
|
||||
assert_eq!(
|
||||
buffer
|
||||
.read(cx)
|
||||
@@ -4351,7 +4347,6 @@ async fn test_formatting_buffer(
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
executor.allow_parking();
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
@@ -4379,10 +4374,16 @@ async fn test_formatting_buffer(
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let lsp_store_b = project_b.update(cx_b, |p, _| p.lsp_store());
|
||||
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _handle = lsp_store_b.update(cx_b, |lsp_store, cx| {
|
||||
lsp_store.register_buffer_with_language_servers(&buffer_b, cx)
|
||||
});
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
|
||||
Ok(Some(vec![
|
||||
@@ -4431,6 +4432,8 @@ async fn test_formatting_buffer(
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
executor.allow_parking();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.format(
|
||||
@@ -4503,8 +4506,12 @@ async fn test_prettier_formatting_buffer(
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
@@ -4620,8 +4627,12 @@ async fn test_definition(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Request the definition of a symbol as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -4765,8 +4776,12 @@ async fn test_references(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Request references to a symbol as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -5012,8 +5027,12 @@ async fn test_document_highlights(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Request document highlights as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -5130,8 +5149,12 @@ async fn test_lsp_hover(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file as the guest
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut servers_with_hover_requests = HashMap::default();
|
||||
for i in 0..language_server_names.len() {
|
||||
@@ -5306,9 +5329,12 @@ async fn test_project_symbols(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Cause the language server to start.
|
||||
let open_buffer_task =
|
||||
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
|
||||
let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
|
||||
let _buffer = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
|
||||
@@ -5400,8 +5426,12 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
|
||||
let (buffer_b1, _lsp) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
|
||||
@@ -5417,13 +5447,22 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
let buffer_b2;
|
||||
if rng.gen() {
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
|
||||
buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
|
||||
(buffer_b2, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
|
||||
(buffer_b2, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
|
||||
}
|
||||
|
||||
let buffer_b2 = buffer_b2.await.unwrap();
|
||||
let definitions = definitions.await.unwrap();
|
||||
assert_eq!(definitions.len(), 1);
|
||||
assert_eq!(definitions[0].target.buffer, buffer_b2);
|
||||
|
||||
@@ -426,8 +426,10 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
executor.run_until_parked();
|
||||
|
||||
// Opens the buffer and formats it
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
|
||||
})
|
||||
.await
|
||||
.expect("user B opens buffer for formatting");
|
||||
|
||||
|
||||
@@ -6,13 +6,12 @@ use anyhow::{anyhow, Result};
|
||||
use chrono::DateTime;
|
||||
use fs::Fs;
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Global};
|
||||
use gpui::{prelude::*, AppContext, AsyncAppContext, Global};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use paths::home_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::watch_config_file;
|
||||
use strum::EnumIter;
|
||||
use ui::Context;
|
||||
|
||||
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
|
||||
pub const COPILOT_CHAT_AUTH_URL: &str = "https://api.github.com/copilot_internal/v2/token";
|
||||
|
||||
@@ -7,7 +7,7 @@ use language::{
|
||||
Buffer, OffsetRangeExt, ToOffset,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, time::Duration};
|
||||
use std::{ops::Range, path::Path, time::Duration};
|
||||
|
||||
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
|
||||
@@ -76,6 +76,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
_visible_range: Option<Range<usize>>,
|
||||
debounce: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
@@ -296,7 +297,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 +323,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 +339,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 +367,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 +843,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 +897,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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -251,6 +251,7 @@ gpui::actions!(
|
||||
DisplayCursorNames,
|
||||
DuplicateLineDown,
|
||||
DuplicateLineUp,
|
||||
DuplicateSelection,
|
||||
ExpandAllHunkDiffs,
|
||||
ExpandMacroRecursively,
|
||||
FindAllReferences,
|
||||
|
||||
@@ -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};
|
||||
@@ -176,7 +176,7 @@ use workspace::{
|
||||
};
|
||||
use workspace::{Item as WorkspaceItem, OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
|
||||
|
||||
use crate::hover_links::find_url;
|
||||
use crate::hover_links::{find_url, find_url_from_range};
|
||||
use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
|
||||
|
||||
pub const FILE_HEADER_HEIGHT: u32 = 2;
|
||||
@@ -190,8 +190,6 @@ const MAX_SELECTION_HISTORY_LEN: usize = 1024;
|
||||
pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
|
||||
#[doc(hidden)]
|
||||
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
#[doc(hidden)]
|
||||
pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
|
||||
pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
@@ -572,6 +570,7 @@ pub struct Editor {
|
||||
collaboration_hub: Option<Box<dyn CollaborationHub>>,
|
||||
blink_manager: Model<BlinkManager>,
|
||||
show_cursor_names: bool,
|
||||
visible_range: Option<Range<Anchor>>,
|
||||
hovered_cursors: HashMap<HoveredCursor, Task<()>>,
|
||||
pub show_local_selections: bool,
|
||||
mode: EditorMode,
|
||||
@@ -663,6 +662,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,8 +1308,10 @@ 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,
|
||||
visible_range: None,
|
||||
};
|
||||
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
@@ -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);
|
||||
@@ -1391,6 +1404,15 @@ impl Editor {
|
||||
key_context.add("inline_completion");
|
||||
}
|
||||
|
||||
if !self
|
||||
.selections
|
||||
.disjoint
|
||||
.iter()
|
||||
.all(|selection| selection.start == selection.end)
|
||||
{
|
||||
key_context.add("selection");
|
||||
}
|
||||
|
||||
key_context
|
||||
}
|
||||
|
||||
@@ -1635,6 +1657,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 +3725,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 +3767,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
|
||||
@@ -4284,10 +4320,10 @@ impl Editor {
|
||||
if cursor_buffer != tail_buffer {
|
||||
return None;
|
||||
}
|
||||
|
||||
let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce;
|
||||
self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT)
|
||||
.timer(Duration::from_millis(debounce))
|
||||
.await;
|
||||
|
||||
let highlights = if let Some(highlights) = cx
|
||||
@@ -4391,8 +4427,18 @@ impl Editor {
|
||||
return None;
|
||||
}
|
||||
|
||||
let excerpt_id = cursor.excerpt_id;
|
||||
let visible_range = self.visible_range.as_ref().and_then(|visible_range| {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.range_to_buffer_ranges(visible_range.clone(), cx)
|
||||
.into_iter()
|
||||
.find(|(_, _, buffer_excerpt_id)| *buffer_excerpt_id == excerpt_id)
|
||||
.map(|(_, range, _)| range)
|
||||
});
|
||||
|
||||
self.update_visible_inline_completion(cx);
|
||||
provider.refresh(buffer, cursor_buffer_position, debounce, cx);
|
||||
provider.refresh(buffer, cursor_buffer_position, visible_range, debounce, cx);
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -4475,6 +4521,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 +4677,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 +5024,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(
|
||||
@@ -6089,6 +6131,28 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn duplicate_selection(&mut self, _: &DuplicateSelection, cx: &mut ViewContext<Self>) {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
|
||||
let mut edits = Vec::new();
|
||||
for selection in selections.iter() {
|
||||
let start = selection.start;
|
||||
let end = selection.end;
|
||||
let text = buffer.text_for_range(start..end).collect::<String>();
|
||||
edits.push((selection.end..selection.end, text));
|
||||
}
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, None, cx);
|
||||
});
|
||||
|
||||
this.request_autoscroll(Autoscroll::fit(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn duplicate_line(&mut self, upwards: bool, cx: &mut ViewContext<Self>) {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
@@ -9241,23 +9305,42 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn open_url(&mut self, _: &OpenUrl, cx: &mut ViewContext<Self>) {
|
||||
let position = self.selections.newest_anchor().head();
|
||||
let Some((buffer, buffer_position)) =
|
||||
self.buffer.read(cx).text_anchor_for_position(position, cx)
|
||||
let selection = self.selections.newest_anchor();
|
||||
let head = selection.head();
|
||||
let tail = selection.tail();
|
||||
|
||||
let Some((buffer, start_position)) =
|
||||
self.buffer.read(cx).text_anchor_for_position(head, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
if let Some((_, url)) = find_url(&buffer, buffer_position, cx.clone()) {
|
||||
let end_position = if head != tail {
|
||||
let Some((_, pos)) = self.buffer.read(cx).text_anchor_for_position(tail, cx) else {
|
||||
return;
|
||||
};
|
||||
Some(pos)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let url_finder = cx.spawn(|editor, mut cx| async move {
|
||||
let url = if let Some(end_pos) = end_position {
|
||||
find_url_from_range(&buffer, start_position..end_pos, cx.clone())
|
||||
} else {
|
||||
find_url(&buffer, start_position, cx.clone()).map(|(_, url)| url)
|
||||
};
|
||||
|
||||
if let Some(url) = url {
|
||||
editor.update(&mut cx, |_, cx| {
|
||||
cx.open_url(&url);
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
url_finder.detach();
|
||||
}
|
||||
|
||||
pub fn open_file(&mut self, _: &OpenFile, cx: &mut ViewContext<Self>) {
|
||||
@@ -9648,6 +9731,7 @@ impl Editor {
|
||||
|theme| theme.editor_highlighted_line_background,
|
||||
cx,
|
||||
);
|
||||
editor.register_buffers_with_language_servers(cx);
|
||||
});
|
||||
|
||||
let item = Box::new(editor);
|
||||
@@ -10643,6 +10727,10 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_visible_range(&mut self, range: Range<Anchor>) {
|
||||
self.visible_range = Some(range);
|
||||
}
|
||||
|
||||
pub fn insert_blocks(
|
||||
&mut self,
|
||||
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
|
||||
@@ -11844,6 +11932,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 +11951,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 +11960,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 +12039,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 } => {
|
||||
|
||||
@@ -9,6 +9,7 @@ pub struct EditorSettings {
|
||||
pub cursor_blink: bool,
|
||||
pub cursor_shape: Option<CursorShape>,
|
||||
pub current_line_highlight: CurrentLineHighlight,
|
||||
pub lsp_highlight_debounce: u64,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub toolbar: Toolbar,
|
||||
pub scrollbar: Scrollbar,
|
||||
@@ -185,6 +186,11 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: all
|
||||
pub current_line_highlight: Option<CurrentLineHighlight>,
|
||||
/// The debounce delay before querying highlights from the language
|
||||
/// server based on the current cursor location.
|
||||
///
|
||||
/// Default: 75
|
||||
pub lsp_highlight_debounce: Option<u64>,
|
||||
/// Whether to show the informational hover box when moving the mouse
|
||||
/// over symbols in the editor.
|
||||
///
|
||||
|
||||
@@ -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,
|
||||
@@ -3892,6 +3895,28 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
let view = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
|
||||
build_editor(buffer, cx)
|
||||
});
|
||||
_ = view.update(cx, |view, cx| {
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
|
||||
DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
|
||||
])
|
||||
});
|
||||
view.duplicate_selection(&DuplicateSelection, cx);
|
||||
assert_eq!(view.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
vec![
|
||||
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
|
||||
DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -6836,14 +6861,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 +7143,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 +7215,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 +7367,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 +10361,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 +10371,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 +10463,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();
|
||||
|
||||
@@ -218,6 +218,7 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::cut_to_end_of_line);
|
||||
register_action(view, cx, Editor::duplicate_line_up);
|
||||
register_action(view, cx, Editor::duplicate_line_down);
|
||||
register_action(view, cx, Editor::duplicate_selection);
|
||||
register_action(view, cx, Editor::move_line_up);
|
||||
register_action(view, cx, Editor::move_line_down);
|
||||
register_action(view, cx, Editor::transpose);
|
||||
@@ -2754,22 +2755,34 @@ impl EditorElement {
|
||||
|
||||
match &active_inline_completion.completion {
|
||||
InlineCompletion::Move(target_position) => {
|
||||
let container_element = div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
let tab_kbd = h_flex()
|
||||
.px_0p5()
|
||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_size(TextSize::XSmall.rems(cx))
|
||||
.text_color(cx.theme().colors().text.opacity(0.8))
|
||||
.child("tab");
|
||||
|
||||
let icon_container = div().mt(px(2.5)); // For optical alignment
|
||||
|
||||
let container_element = h_flex()
|
||||
.items_center()
|
||||
.py_0p5()
|
||||
.px_1()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_subheader_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_color(cx.theme().colors().text_accent.opacity(0.2))
|
||||
.rounded_md()
|
||||
.px_1();
|
||||
.shadow_sm();
|
||||
|
||||
let target_display_point = target_position.to_display_point(editor_snapshot);
|
||||
if target_display_point.row().as_f32() < scroll_top {
|
||||
let mut element = container_element
|
||||
.child(tab_kbd)
|
||||
.child(Label::new("Jump to Edit").size(LabelSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Tab))
|
||||
.child(Label::new("Jump to Edit"))
|
||||
.child(Icon::new(IconName::ArrowUp)),
|
||||
icon_container
|
||||
.child(Icon::new(IconName::ArrowUp).size(IconSize::Small)),
|
||||
)
|
||||
.into_any();
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
@@ -2778,12 +2791,11 @@ impl EditorElement {
|
||||
Some(element)
|
||||
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
|
||||
let mut element = container_element
|
||||
.child(tab_kbd)
|
||||
.child(Label::new("Jump to Edit").size(LabelSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Tab))
|
||||
.child(Label::new("Jump to Edit"))
|
||||
.child(Icon::new(IconName::ArrowDown)),
|
||||
icon_container
|
||||
.child(Icon::new(IconName::ArrowDown).size(IconSize::Small)),
|
||||
)
|
||||
.into_any();
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
@@ -2795,12 +2807,8 @@ impl EditorElement {
|
||||
Some(element)
|
||||
} else {
|
||||
let mut element = container_element
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Tab))
|
||||
.child(Label::new("Jump to Edit")),
|
||||
)
|
||||
.child(tab_kbd)
|
||||
.child(Label::new("Jump to Edit").size(LabelSize::Small))
|
||||
.into_any();
|
||||
|
||||
let target_line_end = DisplayPoint::new(
|
||||
@@ -5387,6 +5395,10 @@ impl Element for EditorElement {
|
||||
)
|
||||
};
|
||||
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.set_visible_range(start_anchor..end_anchor);
|
||||
});
|
||||
|
||||
let highlighted_rows = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.highlighted_display_rows(cx));
|
||||
@@ -6661,7 +6673,6 @@ mod tests {
|
||||
use language::language_settings;
|
||||
use log::info;
|
||||
use std::num::NonZeroU32;
|
||||
use ui::Context;
|
||||
use util::test::sample_text;
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -22,10 +22,7 @@ use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer}
|
||||
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
|
||||
use text::{OffsetRangeExt, ToPoint};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
div, h_flex, Color, Context, FluentBuilder, Icon, IconName, IntoElement, Label, LabelCommon,
|
||||
ParentElement, SharedString, Styled, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::{paths::compare_paths, ResultExt};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
|
||||
@@ -694,6 +694,65 @@ pub(crate) fn find_url(
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn find_url_from_range(
|
||||
buffer: &Model<language::Buffer>,
|
||||
range: Range<text::Anchor>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Option<String> {
|
||||
const LIMIT: usize = 2048;
|
||||
|
||||
let Ok(snapshot) = buffer.update(&mut cx, |buffer, _| buffer.snapshot()) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let start_offset = range.start.to_offset(&snapshot);
|
||||
let end_offset = range.end.to_offset(&snapshot);
|
||||
|
||||
let mut token_start = start_offset.min(end_offset);
|
||||
let mut token_end = start_offset.max(end_offset);
|
||||
|
||||
let range_len = token_end - token_start;
|
||||
|
||||
if range_len >= LIMIT {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Skip leading whitespace
|
||||
for ch in snapshot.chars_at(token_start).take(range_len) {
|
||||
if !ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
token_start += ch.len_utf8();
|
||||
}
|
||||
|
||||
// Skip trailing whitespace
|
||||
for ch in snapshot.reversed_chars_at(token_end).take(range_len) {
|
||||
if !ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
token_end -= ch.len_utf8();
|
||||
}
|
||||
|
||||
if token_start >= token_end {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = snapshot
|
||||
.text_for_range(token_start..token_end)
|
||||
.collect::<String>();
|
||||
|
||||
let mut finder = LinkFinder::new();
|
||||
finder.kinds(&[LinkKind::Url]);
|
||||
|
||||
if let Some(link) = finder.links(&text).next() {
|
||||
if link.start() == 0 && link.end() == text.len() {
|
||||
return Some(link.as_str().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn find_file(
|
||||
buffer: &Model<language::Buffer>,
|
||||
project: Option<Model<Project>>,
|
||||
|
||||
@@ -359,6 +359,7 @@ fn show_hover(
|
||||
let mut base_text_style = cx.text_style();
|
||||
base_text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(settings.ui_font.family.clone()),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: Some(settings.ui_font_size.into()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(gpui::transparent_black()),
|
||||
@@ -547,11 +548,14 @@ async fn parse_blocks(
|
||||
.new_view(|cx| {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let ui_font_family = settings.ui_font.family.clone();
|
||||
let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
|
||||
let buffer_font_family = settings.buffer_font.family.clone();
|
||||
let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
|
||||
|
||||
let mut base_text_style = cx.text_style();
|
||||
base_text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(ui_font_family.clone()),
|
||||
font_fallbacks: ui_font_fallbacks,
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -562,6 +566,7 @@ async fn parse_blocks(
|
||||
inline_code: TextStyleRefinement {
|
||||
background_color: Some(cx.theme().colors().background),
|
||||
font_family: Some(buffer_font_family),
|
||||
font_fallbacks: buffer_font_fallbacks,
|
||||
..Default::default()
|
||||
},
|
||||
rule_color: cx.theme().colors().border,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
@@ -331,6 +330,7 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
|
||||
&mut self,
|
||||
_buffer: gpui::Model<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_visible_range: Option<Range<usize>>,
|
||||
_debounce: bool,
|
||||
_cx: &mut gpui::ModelContext<Self>,
|
||||
) {
|
||||
|
||||
@@ -6,8 +6,8 @@ use collections::BTreeMap;
|
||||
use futures::Future;
|
||||
use git::diff::DiffHunkStatus;
|
||||
use gpui::{
|
||||
AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View, ViewContext,
|
||||
VisualTestContext, WindowHandle,
|
||||
prelude::*, AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View,
|
||||
ViewContext, VisualTestContext, WindowHandle,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry};
|
||||
@@ -23,8 +23,6 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use ui::Context;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
|
||||
@@ -623,9 +623,9 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
None,
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(project_dir.join("test.gleam"), cx)
|
||||
project.open_local_buffer_with_lsp(project_dir.join("test.gleam"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -64,6 +64,11 @@ impl FeatureFlag for ZetaFeatureFlag {
|
||||
const NAME: &'static str = "zeta";
|
||||
}
|
||||
|
||||
pub struct GitUiFeatureFlag;
|
||||
impl FeatureFlag for GitUiFeatureFlag {
|
||||
const NAME: &'static str = "git-ui";
|
||||
}
|
||||
|
||||
pub struct Remoting {}
|
||||
impl FeatureFlag for Remoting {
|
||||
const NAME: &'static str = "remoting";
|
||||
|
||||
@@ -21,6 +21,7 @@ git.workspace = true
|
||||
git2.workspace = true
|
||||
gpui.workspace = true
|
||||
libc.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
rope.workspace = true
|
||||
|
||||
@@ -695,10 +695,13 @@ impl Fs for RealFs {
|
||||
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
|
||||
let watcher = Arc::new(linux_watcher::LinuxWatcher::new(tx, pending_paths.clone()));
|
||||
|
||||
watcher.add(&path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
|
||||
if let Some(parent) = path.parent() {
|
||||
// watch the parent dir so we can tell when settings.json is created
|
||||
watcher.add(parent).log_err();
|
||||
if watcher.add(path).is_err() {
|
||||
// If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
|
||||
if let Some(parent) = path.parent() {
|
||||
if let Err(e) = watcher.add(parent) {
|
||||
log::warn!("Failed to watch: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if path is a symlink and follow the target parent
|
||||
@@ -777,7 +780,10 @@ impl Fs for RealFs {
|
||||
}
|
||||
|
||||
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
|
||||
let repo = git2::Repository::open(dotgit_path).log_err()?;
|
||||
// with libgit2, we can open git repo from an existing work dir
|
||||
// https://libgit2.org/docs/reference/main/repository/git_repository_open.html
|
||||
let workdir_root = dotgit_path.parent()?;
|
||||
let repo = git2::Repository::open(workdir_root).log_err()?;
|
||||
Some(Arc::new(RealGitRepository::new(
|
||||
repo,
|
||||
self.git_binary_path.clone(),
|
||||
|
||||
@@ -13,22 +13,23 @@ name = "git_ui"
|
||||
path = "src/git_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
db.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools = { workspace = true, optional = true }
|
||||
menu.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
workspace.workspace = true
|
||||
ui.workspace = true
|
||||
project.workspace = true
|
||||
smallvec.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
git.workspace = true
|
||||
editor.workspace = true
|
||||
collections.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
stories = ["dep:itertools"]
|
||||
|
||||
1
crates/git_ui/LICENSE-GPL
Symbolic link
1
crates/git_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
45
crates/git_ui/TODO.md
Normal file
45
crates/git_ui/TODO.md
Normal file
@@ -0,0 +1,45 @@
|
||||
### General
|
||||
|
||||
- [x] Disable staging and committing actions for read-only projects
|
||||
|
||||
### List
|
||||
|
||||
- [x] Add uniform list
|
||||
- [x] Git status item
|
||||
- [ ] Directory item
|
||||
- [x] Scrollbar
|
||||
- [ ] Add indent size setting
|
||||
- [ ] Add tree settings
|
||||
|
||||
### List Items
|
||||
|
||||
- [x] Checkbox for staging
|
||||
- [x] Git status icon
|
||||
- [ ] Context menu
|
||||
- [ ] Discard Changes
|
||||
- ---
|
||||
- [ ] Ignore
|
||||
- [ ] Ignore directory
|
||||
- ---
|
||||
- [ ] Copy path
|
||||
- [ ] Copy relative path
|
||||
- ---
|
||||
- [ ] Reveal in Finder
|
||||
|
||||
### Commit Editor
|
||||
|
||||
- [ ] Add commit editor
|
||||
- [ ] Add commit message placeholder & add commit message to store
|
||||
- [ ] Add a way to get the current collaborators & automatically add them to the commit message as co-authors
|
||||
- [ ] Add action to clear commit message
|
||||
- [x] Swap commit button between "Commit" and "Commit All" based on modifier key
|
||||
|
||||
### Component Updates
|
||||
|
||||
- [ ] ChangedLineCount (new)
|
||||
- takes `lines_added: usize, lines_removed: usize`, returns a added/removed badge
|
||||
- [x] GitStatusIcon (new)
|
||||
- [ ] Checkbox
|
||||
- update checkbox design
|
||||
- [ ] ScrollIndicator
|
||||
- shows a gradient overlay when more content is available to be scrolled
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,949 +1,53 @@
|
||||
use editor::Editor;
|
||||
use ::settings::Settings;
|
||||
use git::repository::GitFileStatus;
|
||||
use gpui::*;
|
||||
use ui::{prelude::*, ElevationIndex, IconButtonShape};
|
||||
use ui::{Disclosure, Divider};
|
||||
use workspace::item::TabContentParams;
|
||||
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
|
||||
use gpui::{actions, AppContext, Hsla};
|
||||
use settings::GitPanelSettings;
|
||||
use ui::{Color, Icon, IconName, IntoElement};
|
||||
|
||||
pub mod git_panel;
|
||||
mod settings;
|
||||
|
||||
actions!(
|
||||
vcs_status,
|
||||
git_ui,
|
||||
[
|
||||
Deploy,
|
||||
DiscardAll,
|
||||
StageAll,
|
||||
DiscardSelected,
|
||||
StageSelected,
|
||||
UnstageSelected,
|
||||
UnstageAll,
|
||||
FilesChanged
|
||||
DiscardAll,
|
||||
CommitStagedChanges,
|
||||
CommitAllChanges
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChangedFile {
|
||||
pub staged: bool,
|
||||
pub file_path: SharedString,
|
||||
pub lines_added: usize,
|
||||
pub lines_removed: usize,
|
||||
pub status: GitFileStatus,
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
GitPanelSettings::register(cx);
|
||||
}
|
||||
|
||||
pub struct GitLines {
|
||||
pub added: usize,
|
||||
pub removed: usize,
|
||||
}
|
||||
const ADDED_COLOR: Hsla = Hsla {
|
||||
h: 142. / 360.,
|
||||
s: 0.68,
|
||||
l: 0.45,
|
||||
a: 1.0,
|
||||
};
|
||||
const MODIFIED_COLOR: Hsla = Hsla {
|
||||
h: 48. / 360.,
|
||||
s: 0.76,
|
||||
l: 0.47,
|
||||
a: 1.0,
|
||||
};
|
||||
const REMOVED_COLOR: Hsla = Hsla {
|
||||
h: 355. / 360.,
|
||||
s: 0.65,
|
||||
l: 0.65,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ChangedFileHeader {
|
||||
id: ElementId,
|
||||
file: ChangedFile,
|
||||
is_selected: bool,
|
||||
}
|
||||
|
||||
impl ChangedFileHeader {
|
||||
fn new(id: impl Into<ElementId>, file: ChangedFile, is_selected: bool) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
file,
|
||||
is_selected,
|
||||
// todo!(): Add updated status colors to theme
|
||||
pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
|
||||
match status {
|
||||
GitFileStatus::Added => Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR)),
|
||||
GitFileStatus::Modified => {
|
||||
Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_for_status(&self) -> impl IntoElement {
|
||||
let (icon_name, color) = match self.file.status {
|
||||
GitFileStatus::Added => (IconName::SquarePlus, Color::Created),
|
||||
GitFileStatus::Modified => (IconName::SquareDot, Color::Modified),
|
||||
GitFileStatus::Conflict => (IconName::SquareMinus, Color::Conflict),
|
||||
};
|
||||
|
||||
Icon::new(icon_name).size(IconSize::Small).color(color)
|
||||
GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ChangedFileHeader {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let disclosure_id = ElementId::Name(format!("{}-file-disclosure", self.id.clone()).into());
|
||||
let file_path = self.file.file_path.clone();
|
||||
|
||||
h_flex()
|
||||
.id(self.id.clone())
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.when(!self.is_selected, |this| {
|
||||
this.hover(|this| this.bg(cx.theme().colors().ghost_element_hover))
|
||||
})
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.when(self.is_selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_active)
|
||||
})
|
||||
.group("")
|
||||
.rounded_sm()
|
||||
.px_2()
|
||||
.py_1p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Disclosure::new(disclosure_id, false))
|
||||
.child(self.icon_for_status())
|
||||
.child(Label::new(file_path).size(LabelSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(self.file.lines_added > 0, |this| {
|
||||
this.child(
|
||||
Label::new(format!("+{}", self.file.lines_added))
|
||||
.color(Color::Created)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
.when(self.file.lines_removed > 0, |this| {
|
||||
this.child(
|
||||
Label::new(format!("-{}", self.file.lines_removed))
|
||||
.color(Color::Deleted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
IconButton::new("more-menu", IconName::EllipsisVertical)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("remove-file", IconName::X)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Error)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::Background)
|
||||
.on_click(move |_, cx| cx.dispatch_action(Box::new(DiscardSelected))),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("check-file", IconName::Check)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::Background)
|
||||
.on_click(move |_, cx| {
|
||||
if self.file.staged {
|
||||
cx.dispatch_action(Box::new(UnstageSelected))
|
||||
} else {
|
||||
cx.dispatch_action(Box::new(StageSelected))
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct GitProjectOverview {
|
||||
id: ElementId,
|
||||
project_status: Model<GitProjectStatus>,
|
||||
}
|
||||
|
||||
impl GitProjectOverview {
|
||||
pub fn new(id: impl Into<ElementId>, project_status: Model<GitProjectStatus>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
project_status,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_file_list(&self, cx: &mut WindowContext) {
|
||||
self.project_status.update(cx, |status, cx| {
|
||||
status.show_list = !status.show_list;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for GitProjectOverview {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let status = self.project_status.read(cx);
|
||||
|
||||
let changed_files: SharedString =
|
||||
format!("{} Changed files", status.changed_file_count()).into();
|
||||
|
||||
let added_label: Option<SharedString> = (status.lines_changed.added > 0)
|
||||
.then(|| format!("+{}", status.lines_changed.added).into());
|
||||
let removed_label: Option<SharedString> = (status.lines_changed.removed > 0)
|
||||
.then(|| format!("-{}", status.lines_changed.removed).into());
|
||||
let total_label: SharedString = "total lines changed".into();
|
||||
|
||||
h_flex()
|
||||
.id(self.id.clone())
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.px_2()
|
||||
.py_2p5()
|
||||
.gap_2()
|
||||
.child(
|
||||
IconButton::new("open-sidebar", IconName::PanelLeft)
|
||||
.selected(self.project_status.read(cx).show_list)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(move |_, cx| self.toggle_file_list(cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.child(Label::new(changed_files).size(LabelSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(added_label.is_some(), |this| {
|
||||
this.child(
|
||||
Label::new(added_label.unwrap())
|
||||
.color(Color::Created)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
.when(removed_label.is_some(), |this| {
|
||||
this.child(
|
||||
Label::new(removed_label.unwrap())
|
||||
.color(Color::Deleted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Label::new(total_label)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct GitStagingControls {
|
||||
id: ElementId,
|
||||
project_status: Model<GitProjectStatus>,
|
||||
is_staged: bool,
|
||||
is_selected: bool,
|
||||
}
|
||||
|
||||
impl GitStagingControls {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
project_status: Model<GitProjectStatus>,
|
||||
is_staged: bool,
|
||||
is_selected: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
project_status,
|
||||
is_staged,
|
||||
is_selected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for GitStagingControls {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let status = self.project_status.read(cx);
|
||||
|
||||
let (staging_type, count) = if self.is_staged {
|
||||
("Staged", status.staged_count())
|
||||
} else {
|
||||
("Unstaged", status.unstaged_count())
|
||||
};
|
||||
|
||||
let is_expanded = if self.is_staged {
|
||||
status.staged_expanded
|
||||
} else {
|
||||
status.unstaged_expanded
|
||||
};
|
||||
|
||||
let label: SharedString = format!("{} Changes: {}", staging_type, count).into();
|
||||
|
||||
h_flex()
|
||||
.id(self.id.clone())
|
||||
.hover(|this| this.bg(cx.theme().colors().ghost_element_hover))
|
||||
.on_click(move |_, cx| {
|
||||
self.project_status.update(cx, |status, cx| {
|
||||
if self.is_staged {
|
||||
status.staged_expanded = !status.staged_expanded;
|
||||
} else {
|
||||
status.unstaged_expanded = !status.unstaged_expanded;
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.map(|this| {
|
||||
if self.is_selected {
|
||||
this.bg(cx.theme().colors().ghost_element_active)
|
||||
} else {
|
||||
this.bg(cx.theme().colors().elevated_surface_background)
|
||||
}
|
||||
})
|
||||
.px_3()
|
||||
.py_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Disclosure::new(self.id.clone(), is_expanded))
|
||||
.child(Label::new(label).size(LabelSize::Small)),
|
||||
)
|
||||
.child(h_flex().gap_2().map(|this| {
|
||||
if !self.is_staged {
|
||||
this.child(
|
||||
Button::new(
|
||||
ElementId::Name(format!("{}-discard", self.id.clone()).into()),
|
||||
"Discard All",
|
||||
)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ui::ElevationIndex::ModalSurface)
|
||||
.size(ButtonSize::Compact)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::X)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(status.changed_file_count() == 0)
|
||||
.on_click(move |_, cx| cx.dispatch_action(Box::new(DiscardAll))),
|
||||
)
|
||||
.child(
|
||||
Button::new(
|
||||
ElementId::Name(format!("{}-stage", self.id.clone()).into()),
|
||||
"Stage All",
|
||||
)
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Compact)
|
||||
.label_size(LabelSize::Small)
|
||||
.layer(ui::ElevationIndex::ModalSurface)
|
||||
.icon(IconName::Check)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(status.no_unstaged())
|
||||
.on_click(move |_, cx| cx.dispatch_action(Box::new(StageAll))),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
Button::new(
|
||||
ElementId::Name(format!("{}-unstage", self.id.clone()).into()),
|
||||
"Unstage All",
|
||||
)
|
||||
.layer(ui::ElevationIndex::ModalSurface)
|
||||
.icon(IconName::Check)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(status.no_staged())
|
||||
.on_click(move |_, cx| cx.dispatch_action(Box::new(UnstageAll))),
|
||||
)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GitProjectStatus {
|
||||
unstaged_files: Vec<ChangedFile>,
|
||||
staged_files: Vec<ChangedFile>,
|
||||
lines_changed: GitLines,
|
||||
staged_expanded: bool,
|
||||
unstaged_expanded: bool,
|
||||
show_list: bool,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl GitProjectStatus {
|
||||
fn new(changed_files: Vec<ChangedFile>) -> Self {
|
||||
let (unstaged_files, staged_files): (Vec<_>, Vec<_>) =
|
||||
changed_files.into_iter().partition(|f| !f.staged);
|
||||
|
||||
let lines_changed = GitLines {
|
||||
added: unstaged_files
|
||||
.iter()
|
||||
.chain(staged_files.iter())
|
||||
.map(|f| f.lines_added)
|
||||
.sum(),
|
||||
removed: unstaged_files
|
||||
.iter()
|
||||
.chain(staged_files.iter())
|
||||
.map(|f| f.lines_removed)
|
||||
.sum(),
|
||||
};
|
||||
|
||||
Self {
|
||||
unstaged_files,
|
||||
staged_files,
|
||||
lines_changed,
|
||||
staged_expanded: true,
|
||||
unstaged_expanded: true,
|
||||
show_list: false,
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn changed_file_count(&self) -> usize {
|
||||
self.unstaged_files.len() + self.staged_files.len()
|
||||
}
|
||||
|
||||
fn unstaged_count(&self) -> usize {
|
||||
self.unstaged_files.len()
|
||||
}
|
||||
|
||||
fn staged_count(&self) -> usize {
|
||||
self.staged_files.len()
|
||||
}
|
||||
|
||||
fn total_item_count(&self) -> usize {
|
||||
self.changed_file_count() + 2 // +2 for the two controls
|
||||
}
|
||||
|
||||
fn no_unstaged(&self) -> bool {
|
||||
self.unstaged_files.is_empty()
|
||||
}
|
||||
|
||||
fn all_unstaged(&self) -> bool {
|
||||
self.staged_files.is_empty()
|
||||
}
|
||||
|
||||
fn no_staged(&self) -> bool {
|
||||
self.staged_files.is_empty()
|
||||
}
|
||||
|
||||
fn all_staged(&self) -> bool {
|
||||
self.unstaged_files.is_empty()
|
||||
}
|
||||
|
||||
fn update_lines_changed(&mut self) {
|
||||
self.lines_changed = GitLines {
|
||||
added: self
|
||||
.unstaged_files
|
||||
.iter()
|
||||
.chain(self.staged_files.iter())
|
||||
.map(|f| f.lines_added)
|
||||
.sum(),
|
||||
removed: self
|
||||
.unstaged_files
|
||||
.iter()
|
||||
.chain(self.staged_files.iter())
|
||||
.map(|f| f.lines_removed)
|
||||
.sum(),
|
||||
};
|
||||
}
|
||||
|
||||
fn discard_all(&mut self) {
|
||||
self.unstaged_files.clear();
|
||||
self.staged_files.clear();
|
||||
self.update_lines_changed();
|
||||
}
|
||||
|
||||
fn stage_all(&mut self) {
|
||||
self.staged_files.extend(self.unstaged_files.drain(..));
|
||||
self.update_lines_changed();
|
||||
}
|
||||
|
||||
fn unstage_all(&mut self) {
|
||||
self.unstaged_files.extend(self.staged_files.drain(..));
|
||||
self.update_lines_changed();
|
||||
}
|
||||
|
||||
fn discard_selected(&mut self) {
|
||||
let total_len = self.unstaged_files.len() + self.staged_files.len();
|
||||
if self.selected_index > 0 && self.selected_index <= total_len {
|
||||
if self.selected_index <= self.unstaged_files.len() {
|
||||
self.unstaged_files.remove(self.selected_index - 1);
|
||||
} else {
|
||||
self.staged_files
|
||||
.remove(self.selected_index - 1 - self.unstaged_files.len());
|
||||
}
|
||||
self.update_lines_changed();
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_selected(&mut self) {
|
||||
if self.selected_index > 0 && self.selected_index <= self.unstaged_files.len() {
|
||||
let file = self.unstaged_files.remove(self.selected_index - 1);
|
||||
self.staged_files.push(file);
|
||||
self.update_lines_changed();
|
||||
}
|
||||
}
|
||||
|
||||
fn unstage_selected(&mut self) {
|
||||
let unstaged_len = self.unstaged_files.len();
|
||||
if self.selected_index > unstaged_len && self.selected_index <= self.total_item_count() - 2
|
||||
{
|
||||
let file = self
|
||||
.staged_files
|
||||
.remove(self.selected_index - 1 - unstaged_len);
|
||||
self.unstaged_files.push(file);
|
||||
self.update_lines_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProjectStatusTab {
|
||||
id: ElementId,
|
||||
focus_handle: FocusHandle,
|
||||
status: Model<GitProjectStatus>,
|
||||
list_state: ListState,
|
||||
}
|
||||
|
||||
impl ProjectStatusTab {
|
||||
pub fn new(id: impl Into<ElementId>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let changed_files = static_changed_files();
|
||||
let status = cx.new_model(|_| GitProjectStatus::new(changed_files));
|
||||
|
||||
let status_clone = status.clone();
|
||||
let list_state = ListState::new(
|
||||
status.read(cx).total_item_count(),
|
||||
gpui::ListAlignment::Top,
|
||||
px(10.),
|
||||
move |ix, cx| {
|
||||
let status = status_clone.read(cx);
|
||||
let is_selected = status.selected_index == ix;
|
||||
if ix == 0 {
|
||||
GitStagingControls::new(
|
||||
"unstaged-controls",
|
||||
status_clone.clone(),
|
||||
false,
|
||||
is_selected,
|
||||
)
|
||||
.into_any_element()
|
||||
} else if ix == status.total_item_count() - 1 {
|
||||
GitStagingControls::new(
|
||||
"staged-controls",
|
||||
status_clone.clone(),
|
||||
true,
|
||||
is_selected,
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
let file_ix = ix - 1;
|
||||
let file = if file_ix < status.unstaged_count() {
|
||||
status.unstaged_files.get(file_ix)
|
||||
} else {
|
||||
status.staged_files.get(file_ix - status.unstaged_count())
|
||||
};
|
||||
|
||||
file.map(|file| {
|
||||
ChangedFileHeader::new(
|
||||
ElementId::Name(format!("file-{}", file_ix).into()),
|
||||
file.clone(),
|
||||
is_selected,
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or_else(|| div().into_any_element())
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
id: id.into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
status,
|
||||
list_state,
|
||||
}
|
||||
}
|
||||
|
||||
fn recreate_list_state(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let status = self.status.read(cx);
|
||||
let status_clone = self.status.clone();
|
||||
|
||||
self.list_state = ListState::new(
|
||||
status.total_item_count(),
|
||||
gpui::ListAlignment::Top,
|
||||
px(10.),
|
||||
move |ix, cx| {
|
||||
let is_selected = status_clone.read(cx).selected_index == ix;
|
||||
if ix == 0 {
|
||||
GitStagingControls::new(
|
||||
"unstaged-controls",
|
||||
status_clone.clone(),
|
||||
false,
|
||||
is_selected,
|
||||
)
|
||||
.into_any_element()
|
||||
} else if ix == status_clone.read(cx).total_item_count() - 1 {
|
||||
GitStagingControls::new(
|
||||
"staged-controls",
|
||||
status_clone.clone(),
|
||||
true,
|
||||
is_selected,
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
let file_ix = ix - 1;
|
||||
let status = status_clone.read(cx);
|
||||
let file = if file_ix < status.unstaged_count() {
|
||||
status.unstaged_files.get(file_ix)
|
||||
} else {
|
||||
status.staged_files.get(file_ix - status.unstaged_count())
|
||||
};
|
||||
|
||||
file.map(|file| {
|
||||
ChangedFileHeader::new(
|
||||
ElementId::Name(format!("file-{}", file_ix).into()),
|
||||
file.clone(),
|
||||
is_selected,
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or_else(|| div().into_any_element())
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectStatusTab>(cx) {
|
||||
workspace.activate_item(&existing, true, true, cx);
|
||||
} else {
|
||||
let status_tab = cx.new_view(|cx| Self::new("project-status-tab", cx));
|
||||
workspace.add_item_to_active_pane(Box::new(status_tab), None, true, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn discard_all(&mut self, _: &DiscardAll, cx: &mut ViewContext<Self>) {
|
||||
self.status.update(cx, |status, _| {
|
||||
status.discard_all();
|
||||
});
|
||||
self.recreate_list_state(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn stage_all(&mut self, _: &StageAll, cx: &mut ViewContext<Self>) {
|
||||
self.status.update(cx, |status, _| {
|
||||
status.stage_all();
|
||||
});
|
||||
self.recreate_list_state(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn unstage_all(&mut self, _: &UnstageAll, cx: &mut ViewContext<Self>) {
|
||||
self.status.update(cx, |status, _| {
|
||||
status.unstage_all();
|
||||
});
|
||||
self.recreate_list_state(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn discard_selected(&mut self, _: &DiscardSelected, cx: &mut ViewContext<Self>) {
|
||||
self.status.update(cx, |status, _| {
|
||||
status.discard_selected();
|
||||
});
|
||||
self.recreate_list_state(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn stage_selected(&mut self, _: &StageSelected, cx: &mut ViewContext<Self>) {
|
||||
self.status.update(cx, |status, _| {
|
||||
status.stage_selected();
|
||||
});
|
||||
self.recreate_list_state(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn unstage_selected(&mut self, _: &UnstageSelected, cx: &mut ViewContext<Self>) {
|
||||
self.status.update(cx, |status, _| {
|
||||
status.unstage_selected();
|
||||
});
|
||||
self.recreate_list_state(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn selected_index(&self, cx: &WindowContext) -> usize {
|
||||
self.status.read(cx).selected_index
|
||||
}
|
||||
|
||||
pub fn set_selected_index(
|
||||
&mut self,
|
||||
index: usize,
|
||||
jump_to_index: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.status.update(cx, |status, _| {
|
||||
status.selected_index = index.min(status.total_item_count() - 1);
|
||||
});
|
||||
|
||||
if jump_to_index {
|
||||
self.jump_to_cell(index, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
|
||||
let current_index = self.status.read(cx).selected_index;
|
||||
let total_count = self.status.read(cx).total_item_count();
|
||||
let new_index = (current_index + 1).min(total_count - 1);
|
||||
self.set_selected_index(new_index, true, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn select_previous(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
let current_index = self.status.read(cx).selected_index;
|
||||
let new_index = current_index.saturating_sub(1);
|
||||
self.set_selected_index(new_index, true, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
|
||||
self.set_selected_index(0, true, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
|
||||
let total_count = self.status.read(cx).total_item_count();
|
||||
self.set_selected_index(total_count - 1, true, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn jump_to_cell(&mut self, index: usize, _cx: &mut ViewContext<Self>) {
|
||||
self.list_state.scroll_to_reveal_item(index);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectStatusTab {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let project_status = self.status.read(cx);
|
||||
|
||||
h_flex()
|
||||
.id(self.id.clone())
|
||||
.key_context("vcs_status")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::discard_all))
|
||||
.on_action(cx.listener(Self::stage_all))
|
||||
.on_action(cx.listener(Self::unstage_all))
|
||||
.on_action(cx.listener(Self::discard_selected))
|
||||
.on_action(cx.listener(Self::stage_selected))
|
||||
.on_action(cx.listener(Self::unstage_selected))
|
||||
.on_action(cx.listener(|this, &FilesChanged, cx| this.recreate_list_state(cx)))
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.when(project_status.show_list, |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.bg(ElevationIndex::Surface.bg(cx))
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.w(px(280.))
|
||||
.flex_none()
|
||||
.h_full()
|
||||
.child("sidebar"),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.bg(ElevationIndex::Surface.bg(cx))
|
||||
.child(GitProjectOverview::new(
|
||||
"project-overview",
|
||||
self.status.clone(),
|
||||
))
|
||||
.child(Divider::horizontal_dashed())
|
||||
.child(list(self.list_state.clone()).size_full()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for ProjectStatusTab {}
|
||||
|
||||
impl FocusableView for ProjectStatusTab {
|
||||
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl workspace::Item for ProjectStatusTab {
|
||||
type Event = ();
|
||||
|
||||
fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
|
||||
|
||||
fn tab_content(&self, _params: TabContentParams, _cx: &WindowContext) -> AnyElement {
|
||||
Label::new("Project Status").into_any_element()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GitStatusIndicator {
|
||||
active_editor: Option<WeakView<Editor>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
current_status: Option<GitProjectStatus>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl Render for GitStatusIndicator {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
h_flex().h(rems(1.375)).gap_2().child(
|
||||
IconButton::new("git-status-indicator", IconName::GitBranch).on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectStatusTab::deploy(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
},
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl GitStatusIndicator {
|
||||
pub fn new(workspace: &Workspace, _: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
active_editor: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
current_status: None,
|
||||
_observe_active_editor: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for GitStatusIndicator {}
|
||||
|
||||
impl StatusItemView for GitStatusIndicator {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
|
||||
self.active_editor = Some(editor.downgrade());
|
||||
} else {
|
||||
self.active_editor = None;
|
||||
self.current_status = None;
|
||||
self._observe_active_editor = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn static_changed_files() -> Vec<ChangedFile> {
|
||||
vec![
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file1".into(),
|
||||
lines_added: 10,
|
||||
lines_removed: 5,
|
||||
status: GitFileStatus::Modified,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file2".into(),
|
||||
lines_added: 8,
|
||||
lines_removed: 0,
|
||||
status: GitFileStatus::Added,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file3".into(),
|
||||
lines_added: 15,
|
||||
lines_removed: 20,
|
||||
status: GitFileStatus::Modified,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file4".into(),
|
||||
lines_added: 5,
|
||||
lines_removed: 0,
|
||||
status: GitFileStatus::Added,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file5".into(),
|
||||
lines_added: 12,
|
||||
lines_removed: 7,
|
||||
status: GitFileStatus::Modified,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file6".into(),
|
||||
lines_added: 0,
|
||||
lines_removed: 12,
|
||||
status: GitFileStatus::Modified,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file7".into(),
|
||||
lines_added: 7,
|
||||
lines_removed: 3,
|
||||
status: GitFileStatus::Modified,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file8".into(),
|
||||
lines_added: 2,
|
||||
lines_removed: 0,
|
||||
status: GitFileStatus::Added,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file9".into(),
|
||||
lines_added: 18,
|
||||
lines_removed: 15,
|
||||
status: GitFileStatus::Modified,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file10".into(),
|
||||
lines_added: 22,
|
||||
lines_removed: 0,
|
||||
status: GitFileStatus::Added,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file11".into(),
|
||||
lines_added: 5,
|
||||
lines_removed: 5,
|
||||
status: GitFileStatus::Modified,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file12".into(),
|
||||
lines_added: 7,
|
||||
lines_removed: 0,
|
||||
status: GitFileStatus::Added,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file13".into(),
|
||||
lines_added: 3,
|
||||
lines_removed: 11,
|
||||
status: GitFileStatus::Modified,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file14".into(),
|
||||
lines_added: 30,
|
||||
lines_removed: 0,
|
||||
status: GitFileStatus::Added,
|
||||
},
|
||||
ChangedFile {
|
||||
staged: false,
|
||||
file_path: "path/to/changed_file15".into(),
|
||||
lines_added: 12,
|
||||
lines_removed: 22,
|
||||
status: GitFileStatus::Modified,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
41
crates/git_ui/src/settings.rs
Normal file
41
crates/git_ui/src/settings.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use gpui::Pixels;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use workspace::dock::DockPosition;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GitPanelSettings {
|
||||
pub button: bool,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct PanelSettingsContent {
|
||||
/// Whether to show the panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Where to dock the panel.
|
||||
///
|
||||
/// Default: left
|
||||
pub dock: Option<DockPosition>,
|
||||
/// Default width of the panel in pixels.
|
||||
///
|
||||
/// Default: 360
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
impl Settings for GitPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("git_panel");
|
||||
|
||||
type FileContent = PanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
@@ -48,8 +48,17 @@ impl CursorPosition {
|
||||
) {
|
||||
let editor = editor.downgrade();
|
||||
self.update_position = cx.spawn(|cursor_position, mut cx| async move {
|
||||
if let Some(debounce) = debounce {
|
||||
cx.background_executor().timer(debounce).await;
|
||||
let is_singleton = editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
editor.buffer().read(cx).is_singleton()
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or(true);
|
||||
|
||||
if !is_singleton {
|
||||
if let Some(debounce) = debounce {
|
||||
cx.background_executor().timer(debounce).await;
|
||||
}
|
||||
}
|
||||
|
||||
editor
|
||||
|
||||
254
crates/gpui/examples/gradient.rs
Normal file
254
crates/gpui/examples/gradient.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use gpui::{
|
||||
canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, size, App, AppContext,
|
||||
Bounds, ColorSpace, Half, Render, ViewContext, WindowOptions,
|
||||
};
|
||||
|
||||
struct GradientViewer {
|
||||
color_space: ColorSpace,
|
||||
}
|
||||
|
||||
impl GradientViewer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
color_space: ColorSpace::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GradientViewer {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let color_space = self.color_space;
|
||||
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child("Gradient Examples")
|
||||
.child(
|
||||
div().flex().gap_2().items_center().child(
|
||||
div()
|
||||
.id("method")
|
||||
.flex()
|
||||
.px_3()
|
||||
.py_1()
|
||||
.text_sm()
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.child(format!("{}", color_space))
|
||||
.active(|this| this.opacity(0.8))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.color_space = match this.color_space {
|
||||
ColorSpace::Oklab => ColorSpace::Srgb,
|
||||
ColorSpace::Srgb => ColorSpace::Oklab,
|
||||
};
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.rounded_xl()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(gpui::red())
|
||||
.text_color(gpui::white())
|
||||
.child("Solid Color"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.rounded_xl()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(gpui::blue())
|
||||
.text_color(gpui::white())
|
||||
.child("Solid Color"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.h_24()
|
||||
.text_color(gpui::white())
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
45.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
135.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::green(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
225.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
315.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::yellow(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.h_24()
|
||||
.text_color(gpui::white())
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
360.,
|
||||
linear_color_stop(gpui::yellow(), 0.),
|
||||
linear_color_stop(gpui::white(), 1.),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(gpui::green(), 0.05),
|
||||
linear_color_stop(gpui::yellow(), 0.95),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.05),
|
||||
linear_color_stop(gpui::red(), 0.95),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.gap_3()
|
||||
.child(
|
||||
div().flex().flex_1().gap_3().child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(gpui::blue(), 0.5),
|
||||
linear_color_stop(gpui::red(), 0.5),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().rounded_xl().bg(linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::green(), 0.),
|
||||
linear_color_stop(gpui::blue(), 0.5),
|
||||
)
|
||||
.color_space(color_space)),
|
||||
),
|
||||
)
|
||||
.child(div().h_24().child(canvas(
|
||||
move |_, _| {},
|
||||
move |bounds, _, cx| {
|
||||
let size = size(bounds.size.width * 0.8, px(80.));
|
||||
let square_bounds = Bounds {
|
||||
origin: point(
|
||||
bounds.size.width.half() - size.width.half(),
|
||||
bounds.origin.y,
|
||||
),
|
||||
size,
|
||||
};
|
||||
let height = square_bounds.size.height;
|
||||
let horizontal_offset = height;
|
||||
let vertical_offset = px(30.);
|
||||
let mut path = gpui::Path::new(square_bounds.lower_left());
|
||||
path.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset));
|
||||
path.line_to(
|
||||
square_bounds.upper_right() + point(-horizontal_offset, vertical_offset),
|
||||
);
|
||||
path.line_to(square_bounds.lower_right());
|
||||
path.line_to(square_bounds.lower_left());
|
||||
cx.paint_path(
|
||||
path,
|
||||
linear_gradient(
|
||||
180.,
|
||||
linear_color_stop(gpui::red(), 0.),
|
||||
linear_color_stop(gpui::blue(), 1.),
|
||||
)
|
||||
.color_space(color_space),
|
||||
);
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
focus: true,
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|_| GradientViewer::new()),
|
||||
)
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
@@ -548,6 +548,164 @@ impl<'de> Deserialize<'de> for Hsla {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) enum BackgroundTag {
|
||||
Solid = 0,
|
||||
LinearGradient = 1,
|
||||
}
|
||||
|
||||
/// A color space for color interpolation.
|
||||
///
|
||||
/// References:
|
||||
/// - https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method
|
||||
/// - https://www.w3.org/TR/css-color-4/#typedef-color-space
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
#[repr(C)]
|
||||
pub enum ColorSpace {
|
||||
#[default]
|
||||
/// The sRGB color space.
|
||||
Srgb = 0,
|
||||
/// The Oklab color space.
|
||||
Oklab = 1,
|
||||
}
|
||||
|
||||
impl Display for ColorSpace {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ColorSpace::Srgb => write!(f, "sRGB"),
|
||||
ColorSpace::Oklab => write!(f, "Oklab"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A background color, which can be either a solid color or a linear gradient.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct Background {
|
||||
pub(crate) tag: BackgroundTag,
|
||||
pub(crate) color_space: ColorSpace,
|
||||
pub(crate) solid: Hsla,
|
||||
pub(crate) angle: f32,
|
||||
pub(crate) colors: [LinearColorStop; 2],
|
||||
/// Padding for alignment for repr(C) layout.
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
impl Eq for Background {}
|
||||
impl Default for Background {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tag: BackgroundTag::Solid,
|
||||
solid: Hsla::default(),
|
||||
color_space: ColorSpace::default(),
|
||||
angle: 0.0,
|
||||
colors: [LinearColorStop::default(), LinearColorStop::default()],
|
||||
pad: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a LinearGradient background color.
|
||||
///
|
||||
/// The gradient line's angle of direction. A value of `0.` is equivalent to to top; increasing values rotate clockwise from there.
|
||||
///
|
||||
/// The `angle` is in degrees value in the range 0.0 to 360.0.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient
|
||||
pub fn linear_gradient(
|
||||
angle: f32,
|
||||
from: impl Into<LinearColorStop>,
|
||||
to: impl Into<LinearColorStop>,
|
||||
) -> Background {
|
||||
Background {
|
||||
tag: BackgroundTag::LinearGradient,
|
||||
angle,
|
||||
colors: [from.into(), to.into()],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A color stop in a linear gradient.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#linear-color-stop
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct LinearColorStop {
|
||||
/// The color of the color stop.
|
||||
pub color: Hsla,
|
||||
/// The percentage of the gradient, in the range 0.0 to 1.0.
|
||||
pub percentage: f32,
|
||||
}
|
||||
|
||||
/// Creates a new linear color stop.
|
||||
///
|
||||
/// The percentage of the gradient, in the range 0.0 to 1.0.
|
||||
pub fn linear_color_stop(color: impl Into<Hsla>, percentage: f32) -> LinearColorStop {
|
||||
LinearColorStop {
|
||||
color: color.into(),
|
||||
percentage,
|
||||
}
|
||||
}
|
||||
|
||||
impl LinearColorStop {
|
||||
/// Returns a new color stop with the same color, but with a modified alpha value.
|
||||
pub fn opacity(&self, factor: f32) -> Self {
|
||||
Self {
|
||||
percentage: self.percentage,
|
||||
color: self.color.opacity(factor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Background {
|
||||
/// Use specified color space for color interpolation.
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method
|
||||
pub fn color_space(mut self, color_space: ColorSpace) -> Self {
|
||||
self.color_space = color_space;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value.
|
||||
pub fn opacity(&self, factor: f32) -> Self {
|
||||
let mut background = *self;
|
||||
background.solid = background.solid.opacity(factor);
|
||||
background.colors = [
|
||||
self.colors[0].opacity(factor),
|
||||
self.colors[1].opacity(factor),
|
||||
];
|
||||
background
|
||||
}
|
||||
|
||||
/// Returns whether the background color is transparent.
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
match self.tag {
|
||||
BackgroundTag::Solid => self.solid.is_transparent(),
|
||||
BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsla> for Background {
|
||||
fn from(value: Hsla) -> Self {
|
||||
Background {
|
||||
tag: BackgroundTag::Solid,
|
||||
solid: value,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<Rgba> for Background {
|
||||
fn from(value: Rgba) -> Self {
|
||||
Background {
|
||||
tag: BackgroundTag::Solid,
|
||||
solid: Hsla::from(value),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
@@ -595,4 +753,32 @@ mod tests {
|
||||
|
||||
assert_eq!(actual, rgba(0xdeadbeef))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_background_solid() {
|
||||
let color = Hsla::from(rgba(0xff0099ff));
|
||||
let mut background = Background::from(color);
|
||||
assert_eq!(background.tag, BackgroundTag::Solid);
|
||||
assert_eq!(background.solid, color);
|
||||
|
||||
assert_eq!(background.opacity(0.5).solid, color.opacity(0.5));
|
||||
assert_eq!(background.is_transparent(), false);
|
||||
background.solid = hsla(0.0, 0.0, 0.0, 0.0);
|
||||
assert_eq!(background.is_transparent(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_background_linear_gradient() {
|
||||
let from = linear_color_stop(rgba(0xff0099ff), 0.0);
|
||||
let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
|
||||
let background = linear_gradient(90.0, from, to);
|
||||
assert_eq!(background.tag, BackgroundTag::LinearGradient);
|
||||
assert_eq!(background.colors[0], from);
|
||||
assert_eq!(background.colors[1], to);
|
||||
|
||||
assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
|
||||
assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
|
||||
assert_eq!(background.is_transparent(), false);
|
||||
assert_eq!(background.opacity(0.0).is_transparent(), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use super::{BladeAtlas, PATH_TEXTURE_FORMAT};
|
||||
use crate::{
|
||||
AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, GPUSpecs, Hsla,
|
||||
AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, GPUSpecs,
|
||||
MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
|
||||
ScaledPixels, Scene, Shadow, Size, Underline,
|
||||
};
|
||||
@@ -174,7 +174,7 @@ struct ShaderSurfacesData {
|
||||
#[repr(C)]
|
||||
struct PathSprite {
|
||||
bounds: Bounds<ScaledPixels>,
|
||||
color: Hsla,
|
||||
color: Background,
|
||||
tile: AtlasTile,
|
||||
}
|
||||
|
||||
|
||||
@@ -15,18 +15,21 @@ struct Bounds {
|
||||
origin: vec2<f32>,
|
||||
size: vec2<f32>,
|
||||
}
|
||||
|
||||
struct Corners {
|
||||
top_left: f32,
|
||||
top_right: f32,
|
||||
bottom_right: f32,
|
||||
bottom_left: f32,
|
||||
}
|
||||
|
||||
struct Edges {
|
||||
top: f32,
|
||||
right: f32,
|
||||
bottom: f32,
|
||||
left: f32,
|
||||
}
|
||||
|
||||
struct Hsla {
|
||||
h: f32,
|
||||
s: f32,
|
||||
@@ -34,6 +37,24 @@ struct Hsla {
|
||||
a: f32,
|
||||
}
|
||||
|
||||
struct LinearColorStop {
|
||||
color: Hsla,
|
||||
percentage: f32,
|
||||
}
|
||||
|
||||
struct Background {
|
||||
// 0u is Solid
|
||||
// 1u is LinearGradient
|
||||
tag: u32,
|
||||
// 0u is sRGB linear color
|
||||
// 1u is Oklab color
|
||||
color_space: u32,
|
||||
solid: Hsla,
|
||||
angle: f32,
|
||||
colors: array<LinearColorStop, 2>,
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
struct AtlasTextureId {
|
||||
index: u32,
|
||||
kind: u32,
|
||||
@@ -43,6 +64,7 @@ struct AtlasBounds {
|
||||
origin: vec2<i32>,
|
||||
size: vec2<i32>,
|
||||
}
|
||||
|
||||
struct AtlasTile {
|
||||
texture_id: AtlasTextureId,
|
||||
tile_id: u32,
|
||||
@@ -96,6 +118,24 @@ fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
fn linear_to_srgb(linear: vec3<f32>) -> vec3<f32> {
|
||||
let cutoff = linear < vec3<f32>(0.0031308);
|
||||
let higher = vec3<f32>(1.055) * pow(linear, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
|
||||
let lower = linear * vec3<f32>(12.92);
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
/// Convert a linear color to sRGBA space.
|
||||
fn linear_to_srgba(color: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(linear_to_srgb(color.rgb), color.a);
|
||||
}
|
||||
|
||||
/// Convert a sRGBA color to linear space.
|
||||
fn srgba_to_linear(color: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(srgb_to_linear(color.rgb), color.a);
|
||||
}
|
||||
|
||||
/// Hsla to linear RGBA conversion.
|
||||
fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
|
||||
let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
|
||||
let s = hsla.s;
|
||||
@@ -135,6 +175,43 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
|
||||
return vec4<f32>(linear, a);
|
||||
}
|
||||
|
||||
/// Convert a linear sRGB to Oklab space.
|
||||
/// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
|
||||
fn linear_srgb_to_oklab(color: vec4<f32>) -> vec4<f32> {
|
||||
let l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
|
||||
let m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
|
||||
let s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
|
||||
|
||||
let l_ = pow(l, 1.0 / 3.0);
|
||||
let m_ = pow(m, 1.0 / 3.0);
|
||||
let s_ = pow(s, 1.0 / 3.0);
|
||||
|
||||
return vec4<f32>(
|
||||
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
|
||||
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
|
||||
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
|
||||
color.a
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert an Oklab color to linear sRGB space.
|
||||
fn oklab_to_linear_srgb(color: vec4<f32>) -> vec4<f32> {
|
||||
let l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
|
||||
let m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
|
||||
let s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
|
||||
|
||||
let l = l_ * l_ * l_;
|
||||
let m = m_ * m_ * m_;
|
||||
let s = s_ * s_ * s_;
|
||||
|
||||
return vec4<f32>(
|
||||
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
||||
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
||||
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
|
||||
color.a
|
||||
);
|
||||
}
|
||||
|
||||
fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
|
||||
let alpha = above.a + below.a * (1.0 - above.a);
|
||||
let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
|
||||
@@ -197,6 +274,94 @@ fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
|
||||
return vec4<f32>(color.rgb * multiplier, alpha);
|
||||
}
|
||||
|
||||
|
||||
struct GradientColor {
|
||||
solid: vec4<f32>,
|
||||
color0: vec4<f32>,
|
||||
color1: vec4<f32>,
|
||||
}
|
||||
|
||||
fn prepare_gradient_color(tag: u32, color_space: u32,
|
||||
solid: Hsla, colors: array<LinearColorStop, 2>) -> GradientColor {
|
||||
var result = GradientColor();
|
||||
|
||||
if (tag == 0u) {
|
||||
result.solid = hsla_to_rgba(solid);
|
||||
} else if (tag == 1u) {
|
||||
// The hsla_to_rgba is returns a linear sRGB color
|
||||
result.color0 = hsla_to_rgba(colors[0].color);
|
||||
result.color1 = hsla_to_rgba(colors[1].color);
|
||||
|
||||
// Prepare color space in vertex for avoid conversion
|
||||
// in fragment shader for performance reasons
|
||||
if (color_space == 0u) {
|
||||
// sRGB
|
||||
result.color0 = linear_to_srgba(result.color0);
|
||||
result.color1 = linear_to_srgba(result.color1);
|
||||
} else if (color_space == 1u) {
|
||||
// Oklab
|
||||
result.color0 = linear_srgb_to_oklab(result.color0);
|
||||
result.color1 = linear_srgb_to_oklab(result.color1);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn gradient_color(background: Background, position: vec2<f32>, bounds: Bounds,
|
||||
sold_color: vec4<f32>, color0: vec4<f32>, color1: vec4<f32>) -> vec4<f32> {
|
||||
var background_color = vec4<f32>(0.0);
|
||||
|
||||
switch (background.tag) {
|
||||
default: {
|
||||
return sold_color;
|
||||
}
|
||||
case 1u: {
|
||||
// Linear gradient background.
|
||||
// -90 degrees to match the CSS gradient angle.
|
||||
let radians = (background.angle % 360.0 - 90.0) * M_PI_F / 180.0;
|
||||
var direction = vec2<f32>(cos(radians), sin(radians));
|
||||
let stop0_percentage = background.colors[0].percentage;
|
||||
let stop1_percentage = background.colors[1].percentage;
|
||||
|
||||
// Expand the short side to be the same as the long side
|
||||
if (bounds.size.x > bounds.size.y) {
|
||||
direction.y *= bounds.size.y / bounds.size.x;
|
||||
} else {
|
||||
direction.x *= bounds.size.x / bounds.size.y;
|
||||
}
|
||||
|
||||
// Get the t value for the linear gradient with the color stop percentages.
|
||||
let half_size = bounds.size / 2.0;
|
||||
let center = bounds.origin + half_size;
|
||||
let center_to_point = position - center;
|
||||
var t = dot(center_to_point, direction) / length(direction);
|
||||
// Check the direct to determine the use x or y
|
||||
if (abs(direction.x) > abs(direction.y)) {
|
||||
t = (t + half_size.x) / bounds.size.x;
|
||||
} else {
|
||||
t = (t + half_size.y) / bounds.size.y;
|
||||
}
|
||||
|
||||
// Adjust t based on the stop percentages
|
||||
t = (t - stop0_percentage) / (stop1_percentage - stop0_percentage);
|
||||
t = clamp(t, 0.0, 1.0);
|
||||
|
||||
switch (background.color_space) {
|
||||
default: {
|
||||
background_color = srgba_to_linear(mix(color0, color1, t));
|
||||
}
|
||||
case 1u: {
|
||||
let oklab_color = mix(color0, color1, t);
|
||||
background_color = oklab_to_linear_srgb(oklab_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return background_color;
|
||||
}
|
||||
|
||||
// --- quads --- //
|
||||
|
||||
struct Quad {
|
||||
@@ -204,7 +369,7 @@ struct Quad {
|
||||
pad: u32,
|
||||
bounds: Bounds,
|
||||
content_mask: Bounds,
|
||||
background: Hsla,
|
||||
background: Background,
|
||||
border_color: Hsla,
|
||||
corner_radii: Corners,
|
||||
border_widths: Edges,
|
||||
@@ -213,11 +378,13 @@ var<storage, read> b_quads: array<Quad>;
|
||||
|
||||
struct QuadVarying {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) @interpolate(flat) background_color: vec4<f32>,
|
||||
@location(1) @interpolate(flat) border_color: vec4<f32>,
|
||||
@location(2) @interpolate(flat) quad_id: u32,
|
||||
//TODO: use `clip_distance` once Naga supports it
|
||||
@location(3) clip_distances: vec4<f32>,
|
||||
@location(0) @interpolate(flat) border_color: vec4<f32>,
|
||||
@location(1) @interpolate(flat) quad_id: u32,
|
||||
// TODO: use `clip_distance` once Naga supports it
|
||||
@location(2) clip_distances: vec4<f32>,
|
||||
@location(3) @interpolate(flat) background_solid: vec4<f32>,
|
||||
@location(4) @interpolate(flat) background_color0: vec4<f32>,
|
||||
@location(5) @interpolate(flat) background_color1: vec4<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
@@ -227,7 +394,16 @@ fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
|
||||
|
||||
var out = QuadVarying();
|
||||
out.position = to_device_position(unit_vertex, quad.bounds);
|
||||
out.background_color = hsla_to_rgba(quad.background);
|
||||
|
||||
let gradient = prepare_gradient_color(
|
||||
quad.background.tag,
|
||||
quad.background.color_space,
|
||||
quad.background.solid,
|
||||
quad.background.colors
|
||||
);
|
||||
out.background_solid = gradient.solid;
|
||||
out.background_color0 = gradient.color0;
|
||||
out.background_color1 = gradient.color1;
|
||||
out.border_color = hsla_to_rgba(quad.border_color);
|
||||
out.quad_id = instance_id;
|
||||
out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
|
||||
@@ -242,21 +418,23 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
}
|
||||
|
||||
let quad = b_quads[input.quad_id];
|
||||
let half_size = quad.bounds.size / 2.0;
|
||||
let center = quad.bounds.origin + half_size;
|
||||
let center_to_point = input.position.xy - center;
|
||||
|
||||
let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
|
||||
input.background_solid, input.background_color0, input.background_color1);
|
||||
|
||||
// Fast path when the quad is not rounded and doesn't have any border.
|
||||
if (quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 &&
|
||||
quad.corner_radii.top_right == 0.0 &&
|
||||
quad.corner_radii.bottom_right == 0.0 && quad.border_widths.top == 0.0 &&
|
||||
quad.border_widths.left == 0.0 && quad.border_widths.right == 0.0 &&
|
||||
quad.border_widths.bottom == 0.0) {
|
||||
return blend_color(input.background_color, 1.0);
|
||||
return blend_color(background_color, 1.0);
|
||||
}
|
||||
|
||||
let half_size = quad.bounds.size / 2.0;
|
||||
let center = quad.bounds.origin + half_size;
|
||||
let center_to_point = input.position.xy - center;
|
||||
|
||||
let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
|
||||
|
||||
let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
|
||||
let distance =
|
||||
length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
|
||||
@@ -277,13 +455,13 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
border_width = vertical_border;
|
||||
}
|
||||
|
||||
var color = input.background_color;
|
||||
var color = background_color;
|
||||
if (border_width > 0.0) {
|
||||
let inset_distance = distance + border_width;
|
||||
// Blend the border on top of the background and then linearly interpolate
|
||||
// between the two as we slide inside the background.
|
||||
let blended_border = over(input.background_color, input.border_color);
|
||||
color = mix(blended_border, input.background_color,
|
||||
let blended_border = over(background_color, input.border_color);
|
||||
color = mix(blended_border, background_color,
|
||||
saturate(0.5 - inset_distance));
|
||||
}
|
||||
|
||||
@@ -408,7 +586,7 @@ fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 {
|
||||
|
||||
struct PathSprite {
|
||||
bounds: Bounds,
|
||||
color: Hsla,
|
||||
color: Background,
|
||||
tile: AtlasTile,
|
||||
}
|
||||
var<storage, read> b_path_sprites: array<PathSprite>;
|
||||
@@ -416,7 +594,10 @@ var<storage, read> b_path_sprites: array<PathSprite>;
|
||||
struct PathVarying {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) tile_position: vec2<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
@location(1) @interpolate(flat) instance_id: u32,
|
||||
@location(2) @interpolate(flat) color_solid: vec4<f32>,
|
||||
@location(3) @interpolate(flat) color0: vec4<f32>,
|
||||
@location(4) @interpolate(flat) color1: vec4<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
@@ -428,7 +609,17 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
|
||||
var out = PathVarying();
|
||||
out.position = to_device_position(unit_vertex, sprite.bounds);
|
||||
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
|
||||
out.color = hsla_to_rgba(sprite.color);
|
||||
out.instance_id = instance_id;
|
||||
|
||||
let gradient = prepare_gradient_color(
|
||||
sprite.color.tag,
|
||||
sprite.color.color_space,
|
||||
sprite.color.solid,
|
||||
sprite.color.colors
|
||||
);
|
||||
out.color_solid = gradient.solid;
|
||||
out.color0 = gradient.color0;
|
||||
out.color1 = gradient.color1;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -436,7 +627,11 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
|
||||
fn fs_path(input: PathVarying) -> @location(0) vec4<f32> {
|
||||
let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
|
||||
let mask = 1.0 - abs(1.0 - sample % 2.0);
|
||||
return blend_color(input.color, mask);
|
||||
let sprite = b_path_sprites[input.instance_id];
|
||||
let background = sprite.color;
|
||||
let color = gradient_color(background, input.position.xy, sprite.bounds,
|
||||
input.color_solid, input.color0, input.color1);
|
||||
return blend_color(color, mask);
|
||||
}
|
||||
|
||||
// --- underlines --- //
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::metal_atlas::MetalAtlas;
|
||||
use crate::{
|
||||
point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels,
|
||||
Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
|
||||
point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask,
|
||||
DevicePixels, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
|
||||
PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -1242,7 +1242,7 @@ enum PathRasterizationInputIndex {
|
||||
#[repr(C)]
|
||||
pub struct PathSprite {
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub color: Hsla,
|
||||
pub color: Background,
|
||||
pub tile: AtlasTile,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
using namespace metal;
|
||||
|
||||
float4 hsla_to_rgba(Hsla hsla);
|
||||
float3 srgb_to_linear(float3 color);
|
||||
float3 linear_to_srgb(float3 color);
|
||||
float4 srgb_to_oklab(float4 color);
|
||||
float4 oklab_to_srgb(float4 color);
|
||||
float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
|
||||
constant Size_DevicePixels *viewport_size);
|
||||
float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds,
|
||||
@@ -21,20 +25,34 @@ float2 erf(float2 x);
|
||||
float blur_along_x(float x, float y, float sigma, float corner,
|
||||
float2 half_size);
|
||||
float4 over(float4 below, float4 above);
|
||||
float radians(float degrees);
|
||||
float4 gradient_color(Background background, float2 position, Bounds_ScaledPixels bounds,
|
||||
float4 solid_color, float4 color0, float4 color1);
|
||||
|
||||
struct GradientColor {
|
||||
float4 solid;
|
||||
float4 color0;
|
||||
float4 color1;
|
||||
};
|
||||
GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid, Hsla color0, Hsla color1);
|
||||
|
||||
struct QuadVertexOutput {
|
||||
float4 position [[position]];
|
||||
float4 background_color [[flat]];
|
||||
float4 border_color [[flat]];
|
||||
uint quad_id [[flat]];
|
||||
float4 position [[position]];
|
||||
float4 border_color [[flat]];
|
||||
float4 background_solid [[flat]];
|
||||
float4 background_color0 [[flat]];
|
||||
float4 background_color1 [[flat]];
|
||||
float clip_distance [[clip_distance]][4];
|
||||
};
|
||||
|
||||
struct QuadFragmentInput {
|
||||
float4 position [[position]];
|
||||
float4 background_color [[flat]];
|
||||
float4 border_color [[flat]];
|
||||
uint quad_id [[flat]];
|
||||
float4 position [[position]];
|
||||
float4 border_color [[flat]];
|
||||
float4 background_solid [[flat]];
|
||||
float4 background_color0 [[flat]];
|
||||
float4 background_color1 [[flat]];
|
||||
};
|
||||
|
||||
vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
|
||||
@@ -51,13 +69,23 @@ vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
|
||||
to_device_position(unit_vertex, quad.bounds, viewport_size);
|
||||
float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds,
|
||||
quad.content_mask.bounds);
|
||||
float4 background_color = hsla_to_rgba(quad.background);
|
||||
float4 border_color = hsla_to_rgba(quad.border_color);
|
||||
|
||||
GradientColor gradient = prepare_gradient_color(
|
||||
quad.background.tag,
|
||||
quad.background.color_space,
|
||||
quad.background.solid,
|
||||
quad.background.colors[0].color,
|
||||
quad.background.colors[1].color
|
||||
);
|
||||
|
||||
return QuadVertexOutput{
|
||||
device_position,
|
||||
background_color,
|
||||
border_color,
|
||||
quad_id,
|
||||
device_position,
|
||||
border_color,
|
||||
gradient.solid,
|
||||
gradient.color0,
|
||||
gradient.color1,
|
||||
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
|
||||
}
|
||||
|
||||
@@ -65,6 +93,11 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
constant Quad *quads
|
||||
[[buffer(QuadInputIndex_Quads)]]) {
|
||||
Quad quad = quads[input.quad_id];
|
||||
float2 half_size = float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
|
||||
float2 center = float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
|
||||
float2 center_to_point = input.position.xy - center;
|
||||
float4 color = gradient_color(quad.background, input.position.xy, quad.bounds,
|
||||
input.background_solid, input.background_color0, input.background_color1);
|
||||
|
||||
// Fast path when the quad is not rounded and doesn't have any border.
|
||||
if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. &&
|
||||
@@ -72,14 +105,9 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. &&
|
||||
quad.border_widths.left == 0. && quad.border_widths.right == 0. &&
|
||||
quad.border_widths.bottom == 0.) {
|
||||
return input.background_color;
|
||||
return color;
|
||||
}
|
||||
|
||||
float2 half_size =
|
||||
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
|
||||
float2 center =
|
||||
float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
|
||||
float2 center_to_point = input.position.xy - center;
|
||||
float corner_radius;
|
||||
if (center_to_point.x < 0.) {
|
||||
if (center_to_point.y < 0.) {
|
||||
@@ -118,15 +146,12 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
border_width = vertical_border;
|
||||
}
|
||||
|
||||
float4 color;
|
||||
if (border_width == 0.) {
|
||||
color = input.background_color;
|
||||
} else {
|
||||
if (border_width != 0.) {
|
||||
float inset_distance = distance + border_width;
|
||||
// Blend the border on top of the background and then linearly interpolate
|
||||
// between the two as we slide inside the background.
|
||||
float4 blended_border = over(input.background_color, input.border_color);
|
||||
color = mix(blended_border, input.background_color,
|
||||
float4 blended_border = over(color, input.border_color);
|
||||
color = mix(blended_border, color,
|
||||
saturate(0.5 - inset_distance));
|
||||
}
|
||||
|
||||
@@ -437,7 +462,10 @@ fragment float4 path_rasterization_fragment(PathRasterizationFragmentInput input
|
||||
struct PathSpriteVertexOutput {
|
||||
float4 position [[position]];
|
||||
float2 tile_position;
|
||||
float4 color [[flat]];
|
||||
uint sprite_id [[flat]];
|
||||
float4 solid_color [[flat]];
|
||||
float4 color0 [[flat]];
|
||||
float4 color1 [[flat]];
|
||||
};
|
||||
|
||||
vertex PathSpriteVertexOutput path_sprite_vertex(
|
||||
@@ -456,8 +484,23 @@ vertex PathSpriteVertexOutput path_sprite_vertex(
|
||||
float4 device_position =
|
||||
to_device_position(unit_vertex, sprite.bounds, viewport_size);
|
||||
float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
|
||||
float4 color = hsla_to_rgba(sprite.color);
|
||||
return PathSpriteVertexOutput{device_position, tile_position, color};
|
||||
|
||||
GradientColor gradient = prepare_gradient_color(
|
||||
sprite.color.tag,
|
||||
sprite.color.color_space,
|
||||
sprite.color.solid,
|
||||
sprite.color.colors[0].color,
|
||||
sprite.color.colors[1].color
|
||||
);
|
||||
|
||||
return PathSpriteVertexOutput{
|
||||
device_position,
|
||||
tile_position,
|
||||
sprite_id,
|
||||
gradient.solid,
|
||||
gradient.color0,
|
||||
gradient.color1
|
||||
};
|
||||
}
|
||||
|
||||
fragment float4 path_sprite_fragment(
|
||||
@@ -469,7 +512,10 @@ fragment float4 path_sprite_fragment(
|
||||
float4 sample =
|
||||
atlas_texture.sample(atlas_texture_sampler, input.tile_position);
|
||||
float mask = 1. - abs(1. - fmod(sample.r, 2.));
|
||||
float4 color = input.color;
|
||||
PathSprite sprite = sprites[input.sprite_id];
|
||||
Background background = sprite.color;
|
||||
float4 color = gradient_color(background, input.position.xy, sprite.bounds,
|
||||
input.solid_color, input.color0, input.color1);
|
||||
color.a *= mask;
|
||||
return color;
|
||||
}
|
||||
@@ -574,6 +620,56 @@ float4 hsla_to_rgba(Hsla hsla) {
|
||||
return rgba;
|
||||
}
|
||||
|
||||
float3 srgb_to_linear(float3 color) {
|
||||
return pow(color, float3(2.2));
|
||||
}
|
||||
|
||||
float3 linear_to_srgb(float3 color) {
|
||||
return pow(color, float3(1.0 / 2.2));
|
||||
}
|
||||
|
||||
// Converts a sRGB color to the Oklab color space.
|
||||
// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
|
||||
float4 srgb_to_oklab(float4 color) {
|
||||
// Convert non-linear sRGB to linear sRGB
|
||||
color = float4(srgb_to_linear(color.rgb), color.a);
|
||||
|
||||
float l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
|
||||
float m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
|
||||
float s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
|
||||
|
||||
float l_ = pow(l, 1.0/3.0);
|
||||
float m_ = pow(m, 1.0/3.0);
|
||||
float s_ = pow(s, 1.0/3.0);
|
||||
|
||||
return float4(
|
||||
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
|
||||
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
|
||||
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
|
||||
color.a
|
||||
);
|
||||
}
|
||||
|
||||
// Converts an Oklab color to the sRGB color space.
|
||||
float4 oklab_to_srgb(float4 color) {
|
||||
float l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
|
||||
float m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
|
||||
float s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
|
||||
|
||||
float l = l_ * l_ * l_;
|
||||
float m = m_ * m_ * m_;
|
||||
float s = s_ * s_ * s_;
|
||||
|
||||
float3 linear_rgb = float3(
|
||||
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
||||
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
||||
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
|
||||
);
|
||||
|
||||
// Convert linear sRGB to non-linear sRGB
|
||||
return float4(linear_to_srgb(linear_rgb), color.a);
|
||||
}
|
||||
|
||||
float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
|
||||
constant Size_DevicePixels *input_viewport_size) {
|
||||
float2 position =
|
||||
@@ -691,3 +787,81 @@ float4 over(float4 below, float4 above) {
|
||||
result.a = alpha;
|
||||
return result;
|
||||
}
|
||||
|
||||
GradientColor prepare_gradient_color(uint tag, uint color_space, Hsla solid,
|
||||
Hsla color0, Hsla color1) {
|
||||
GradientColor out;
|
||||
if (tag == 0) {
|
||||
out.solid = hsla_to_rgba(solid);
|
||||
} else if (tag == 1) {
|
||||
out.color0 = hsla_to_rgba(color0);
|
||||
out.color1 = hsla_to_rgba(color1);
|
||||
|
||||
// Prepare color space in vertex for avoid conversion
|
||||
// in fragment shader for performance reasons
|
||||
if (color_space == 1) {
|
||||
// Oklab
|
||||
out.color0 = srgb_to_oklab(out.color0);
|
||||
out.color1 = srgb_to_oklab(out.color1);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
float4 gradient_color(Background background,
|
||||
float2 position,
|
||||
Bounds_ScaledPixels bounds,
|
||||
float4 solid_color, float4 color0, float4 color1) {
|
||||
float4 color;
|
||||
|
||||
switch (background.tag) {
|
||||
case 0:
|
||||
color = solid_color;
|
||||
break;
|
||||
case 1: {
|
||||
// -90 degrees to match the CSS gradient angle.
|
||||
float radians = (fmod(background.angle, 360.0) - 90.0) * (M_PI_F / 180.0);
|
||||
float2 direction = float2(cos(radians), sin(radians));
|
||||
|
||||
// Expand the short side to be the same as the long side
|
||||
if (bounds.size.width > bounds.size.height) {
|
||||
direction.y *= bounds.size.height / bounds.size.width;
|
||||
} else {
|
||||
direction.x *= bounds.size.width / bounds.size.height;
|
||||
}
|
||||
|
||||
// Get the t value for the linear gradient with the color stop percentages.
|
||||
float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.;
|
||||
float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
|
||||
float2 center_to_point = position - center;
|
||||
float t = dot(center_to_point, direction) / length(direction);
|
||||
// Check the direct to determine the use x or y
|
||||
if (abs(direction.x) > abs(direction.y)) {
|
||||
t = (t + half_size.x) / bounds.size.width;
|
||||
} else {
|
||||
t = (t + half_size.y) / bounds.size.height;
|
||||
}
|
||||
|
||||
// Adjust t based on the stop percentages
|
||||
t = (t - background.colors[0].percentage)
|
||||
/ (background.colors[1].percentage
|
||||
- background.colors[0].percentage);
|
||||
t = clamp(t, 0.0, 1.0);
|
||||
|
||||
switch (background.color_space) {
|
||||
case 0:
|
||||
color = mix(color0, color1, t);
|
||||
break;
|
||||
case 1: {
|
||||
float4 oklab_color = mix(color0, color1, t);
|
||||
color = oklab_to_srgb(oklab_color);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
@@ -1068,7 +1068,7 @@ unsafe extern "system" fn wnd_proc(
|
||||
let weak = Box::new(Rc::downgrade(creation_result.as_ref().unwrap()));
|
||||
unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) };
|
||||
ctx.inner = Some(creation_result);
|
||||
return LRESULT(1);
|
||||
return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
|
||||
}
|
||||
let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsWindowStatePtr>;
|
||||
if ptr.is_null() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! application to avoid having to import each trait individually.
|
||||
|
||||
pub use crate::{
|
||||
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
|
||||
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context as _, Element, FocusableElement,
|
||||
InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
|
||||
StatefulInteractiveElement, Styled, StyledImage, VisualContext,
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#![cfg_attr(windows, allow(dead_code))]
|
||||
|
||||
use crate::{
|
||||
bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges,
|
||||
Hsla, Pixels, Point, Radians, ScaledPixels, Size,
|
||||
bounds_tree::BoundsTree, point, AtlasTextureId, AtlasTile, Background, Bounds, ContentMask,
|
||||
Corners, Edges, Hsla, Pixels, Point, Radians, ScaledPixels, Size,
|
||||
};
|
||||
use std::{fmt::Debug, iter::Peekable, ops::Range, slice};
|
||||
|
||||
@@ -458,7 +458,7 @@ pub(crate) struct Quad {
|
||||
pub pad: u32, // align to 8 bytes
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub background: Hsla,
|
||||
pub background: Background,
|
||||
pub border_color: Hsla,
|
||||
pub corner_radii: Corners<ScaledPixels>,
|
||||
pub border_widths: Edges<ScaledPixels>,
|
||||
@@ -671,7 +671,7 @@ pub struct Path<P: Clone + Default + Debug> {
|
||||
pub(crate) bounds: Bounds<P>,
|
||||
pub(crate) content_mask: ContentMask<P>,
|
||||
pub(crate) vertices: Vec<PathVertex<P>>,
|
||||
pub(crate) color: Hsla,
|
||||
pub(crate) color: Background,
|
||||
start: Point<P>,
|
||||
current: Point<P>,
|
||||
contour_count: usize,
|
||||
|
||||
@@ -5,10 +5,11 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
black, phi, point, quad, rems, size, AbsoluteLength, Bounds, ContentMask, Corners,
|
||||
CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font,
|
||||
FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point,
|
||||
PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
|
||||
black, phi, point, quad, rems, size, AbsoluteLength, Background, BackgroundTag, Bounds,
|
||||
ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges,
|
||||
EdgesRefinement, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length,
|
||||
Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun,
|
||||
WindowContext,
|
||||
};
|
||||
use collections::HashSet;
|
||||
use refineable::Refineable;
|
||||
@@ -572,7 +573,17 @@ impl Style {
|
||||
|
||||
let background_color = self.background.as_ref().and_then(Fill::color);
|
||||
if background_color.map_or(false, |color| !color.is_transparent()) {
|
||||
let mut border_color = background_color.unwrap_or_default();
|
||||
let mut border_color = match background_color {
|
||||
Some(color) => match color.tag {
|
||||
BackgroundTag::Solid => color.solid,
|
||||
BackgroundTag::LinearGradient => color
|
||||
.colors
|
||||
.first()
|
||||
.map(|stop| stop.color)
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
None => Hsla::default(),
|
||||
};
|
||||
border_color.a = 0.;
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
@@ -737,12 +748,14 @@ pub struct StrikethroughStyle {
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Fill {
|
||||
/// A solid color fill.
|
||||
Color(Hsla),
|
||||
Color(Background),
|
||||
}
|
||||
|
||||
impl Fill {
|
||||
/// Unwrap this fill into a solid color, if it is one.
|
||||
pub fn color(&self) -> Option<Hsla> {
|
||||
///
|
||||
/// If the fill is not a solid color, this method returns `None`.
|
||||
pub fn color(&self) -> Option<Background> {
|
||||
match self {
|
||||
Fill::Color(color) => Some(*color),
|
||||
}
|
||||
@@ -751,13 +764,13 @@ impl Fill {
|
||||
|
||||
impl Default for Fill {
|
||||
fn default() -> Self {
|
||||
Self::Color(Hsla::default())
|
||||
Self::Color(Background::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsla> for Fill {
|
||||
fn from(color: Hsla) -> Self {
|
||||
Self::Color(color)
|
||||
Self::Color(color.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,6 +780,12 @@ impl From<Rgba> for Fill {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Background> for Fill {
|
||||
fn from(background: Background) -> Self {
|
||||
Self::Color(background)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TextStyle> for HighlightStyle {
|
||||
fn from(other: TextStyle) -> Self {
|
||||
Self::from(&other)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip,
|
||||
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow,
|
||||
Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
|
||||
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Background, Bounds,
|
||||
BoxShadow, Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
|
||||
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
|
||||
FileDropEvent, Flatten, FontId, GPUSpecs, Global, GlobalElementId, GlyphId, Hsla, InputHandler,
|
||||
IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent,
|
||||
@@ -2325,7 +2325,7 @@ impl<'a> WindowContext<'a> {
|
||||
/// Paint the given `Path` into the scene for the next frame at the current z-index.
|
||||
///
|
||||
/// This method should only be called as part of the paint phase of element drawing.
|
||||
pub fn paint_path(&mut self, mut path: Path<Pixels>, color: impl Into<Hsla>) {
|
||||
pub fn paint_path(&mut self, mut path: Path<Pixels>, color: impl Into<Background>) {
|
||||
debug_assert_eq!(
|
||||
self.window.draw_phase,
|
||||
DrawPhase::Paint,
|
||||
@@ -2336,7 +2336,8 @@ impl<'a> WindowContext<'a> {
|
||||
let content_mask = self.content_mask();
|
||||
let opacity = self.element_opacity();
|
||||
path.content_mask = content_mask;
|
||||
path.color = color.into().opacity(opacity);
|
||||
let color: Background = color.into();
|
||||
path.color = color.opacity(opacity);
|
||||
self.window
|
||||
.next_frame
|
||||
.scene
|
||||
@@ -4980,7 +4981,7 @@ pub struct PaintQuad {
|
||||
/// The radii of the quad's corners.
|
||||
pub corner_radii: Corners<Pixels>,
|
||||
/// The background color of the quad.
|
||||
pub background: Hsla,
|
||||
pub background: Background,
|
||||
/// The widths of the quad's borders.
|
||||
pub border_widths: Edges<Pixels>,
|
||||
/// The color of the quad's borders.
|
||||
@@ -5013,7 +5014,7 @@ impl PaintQuad {
|
||||
}
|
||||
|
||||
/// Sets the background color of the quad.
|
||||
pub fn background(self, background: impl Into<Hsla>) -> Self {
|
||||
pub fn background(self, background: impl Into<Background>) -> Self {
|
||||
PaintQuad {
|
||||
background: background.into(),
|
||||
..self
|
||||
@@ -5025,7 +5026,7 @@ impl PaintQuad {
|
||||
pub fn quad(
|
||||
bounds: Bounds<Pixels>,
|
||||
corner_radii: impl Into<Corners<Pixels>>,
|
||||
background: impl Into<Hsla>,
|
||||
background: impl Into<Background>,
|
||||
border_widths: impl Into<Edges<Pixels>>,
|
||||
border_color: impl Into<Hsla>,
|
||||
) -> PaintQuad {
|
||||
@@ -5039,7 +5040,7 @@ pub fn quad(
|
||||
}
|
||||
|
||||
/// Creates a filled quad with the given bounds and background color.
|
||||
pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Hsla>) -> PaintQuad {
|
||||
pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Background>) -> PaintQuad {
|
||||
PaintQuad {
|
||||
bounds: bounds.into(),
|
||||
corner_radii: (0.).into(),
|
||||
@@ -5054,7 +5055,7 @@ pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>)
|
||||
PaintQuad {
|
||||
bounds: bounds.into(),
|
||||
corner_radii: (0.).into(),
|
||||
background: transparent_black(),
|
||||
background: transparent_black().into(),
|
||||
border_widths: (1.).into(),
|
||||
border_color: border_color.into(),
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
visible_range: Option<Range<usize>>,
|
||||
debounce: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
);
|
||||
@@ -61,6 +62,7 @@ pub trait InlineCompletionProviderHandle {
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
visible_range: Option<Range<usize>>,
|
||||
debounce: bool,
|
||||
cx: &mut AppContext,
|
||||
);
|
||||
@@ -102,11 +104,12 @@ where
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
visible_range: Option<Range<usize>>,
|
||||
debounce: bool,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.refresh(buffer, cursor_position, debounce, cx)
|
||||
this.refresh(buffer, cursor_position, visible_range, debounce, cx)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ use editor::{scroll::Autoscroll, Editor};
|
||||
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
|
||||
Render, Subscription, View, ViewContext, WeakView, WindowContext,
|
||||
actions, div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement,
|
||||
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{
|
||||
@@ -16,7 +16,6 @@ use language::{
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use supermaven::{AccountStatus, Supermaven};
|
||||
use ui::{Button, LabelSize};
|
||||
use workspace::{
|
||||
create_and_open_local_file,
|
||||
item::ItemHandle,
|
||||
@@ -29,6 +28,8 @@ use workspace::{
|
||||
use zed_actions::OpenBrowser;
|
||||
use zeta::RateCompletionModal;
|
||||
|
||||
actions!(zeta, [RateCompletions]);
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
|
||||
struct CopilotErrorToast;
|
||||
@@ -204,16 +205,22 @@ impl Render for InlineCompletionButton {
|
||||
}
|
||||
|
||||
div().child(
|
||||
Button::new("zeta", "ζ")
|
||||
.label_size(LabelSize::Small)
|
||||
IconButton::new("zeta", IconName::ZedPredict)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Zed Predict",
|
||||
Some(&RateCompletions),
|
||||
"Click to rate completions",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
RateCompletionModal::toggle(workspace, cx)
|
||||
});
|
||||
}
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Rate Completions", cx)),
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,11 +427,6 @@ pub trait LocalFile: File {
|
||||
|
||||
/// Loads the file's contents from disk.
|
||||
fn load_bytes(&self, cx: &AppContext) -> Task<Result<Vec<u8>>>;
|
||||
|
||||
/// Returns true if the file should not be shared with collaborators.
|
||||
fn is_private(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The auto-indent behavior associated with an editing operation.
|
||||
|
||||
@@ -147,7 +147,7 @@ pub trait LanguageModel: Send + Sync {
|
||||
let events = self.stream_completion(request, cx);
|
||||
|
||||
async move {
|
||||
let mut events = events.await?;
|
||||
let mut events = events.await?.fuse();
|
||||
let mut message_id = None;
|
||||
let mut first_item_text = None;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use feature_flags::ZedPro;
|
||||
use gpui::{Action, AnyElement, AppContext, DismissEvent, SharedString, Task};
|
||||
use gpui::{
|
||||
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Task,
|
||||
View, WeakView,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use proto::Plan;
|
||||
@@ -12,19 +15,101 @@ const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
|
||||
|
||||
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &AppContext) + 'static>;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct LanguageModelSelector<T: PopoverTrigger> {
|
||||
handle: Option<PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>>,
|
||||
on_model_changed: OnModelChanged,
|
||||
trigger: T,
|
||||
info_text: Option<SharedString>,
|
||||
pub struct LanguageModelSelector {
|
||||
picker: View<Picker<LanguageModelPickerDelegate>>,
|
||||
}
|
||||
|
||||
pub struct LanguageModelPickerDelegate {
|
||||
on_model_changed: OnModelChanged,
|
||||
all_models: Vec<ModelInfo>,
|
||||
filtered_models: Vec<ModelInfo>,
|
||||
selected_index: usize,
|
||||
impl LanguageModelSelector {
|
||||
pub fn new(
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &AppContext) + 'static,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let on_model_changed = Arc::new(on_model_changed);
|
||||
|
||||
let all_models = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.flat_map(|provider| {
|
||||
let icon = provider.icon();
|
||||
|
||||
provider.provided_models(cx).into_iter().map(move |model| {
|
||||
let model = model.clone();
|
||||
let icon = model.icon().unwrap_or(icon);
|
||||
|
||||
ModelInfo {
|
||||
model: model.clone(),
|
||||
icon,
|
||||
availability: model.availability(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let delegate = LanguageModelPickerDelegate {
|
||||
language_model_selector: cx.view().downgrade(),
|
||||
on_model_changed: on_model_changed.clone(),
|
||||
all_models: all_models.clone(),
|
||||
filtered_models: all_models,
|
||||
selected_index: 0,
|
||||
};
|
||||
|
||||
let picker =
|
||||
cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
|
||||
|
||||
LanguageModelSelector { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for LanguageModelSelector {}
|
||||
|
||||
impl FocusableView for LanguageModelSelector {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for LanguageModelSelector {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct LanguageModelSelectorPopoverMenu<T>
|
||||
where
|
||||
T: PopoverTrigger,
|
||||
{
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
trigger: T,
|
||||
handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> LanguageModelSelectorPopoverMenu<T> {
|
||||
pub fn new(language_model_selector: View<LanguageModelSelector>, trigger: T) -> Self {
|
||||
Self {
|
||||
language_model_selector,
|
||||
trigger,
|
||||
handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_handle(mut self, handle: PopoverMenuHandle<LanguageModelSelector>) -> Self {
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for LanguageModelSelectorPopoverMenu<T> {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let language_model_selector = self.language_model_selector.clone();
|
||||
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_cx| Some(language_model_selector.clone()))
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::AnchorCorner::BottomLeft)
|
||||
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -32,34 +117,14 @@ struct ModelInfo {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
icon: IconName,
|
||||
availability: LanguageModelAvailability,
|
||||
is_selected: bool,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> LanguageModelSelector<T> {
|
||||
pub fn new(
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &AppContext) + 'static,
|
||||
trigger: T,
|
||||
) -> Self {
|
||||
LanguageModelSelector {
|
||||
handle: None,
|
||||
on_model_changed: Arc::new(on_model_changed),
|
||||
trigger,
|
||||
info_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_handle(
|
||||
mut self,
|
||||
handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
|
||||
) -> Self {
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn info_text(mut self, text: impl Into<SharedString>) -> Self {
|
||||
self.info_text = Some(text.into());
|
||||
self
|
||||
}
|
||||
pub struct LanguageModelPickerDelegate {
|
||||
language_model_selector: WeakView<LanguageModelSelector>,
|
||||
on_model_changed: OnModelChanged,
|
||||
all_models: Vec<ModelInfo>,
|
||||
filtered_models: Vec<ModelInfo>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
@@ -142,23 +207,15 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
let model = model_info.model.clone();
|
||||
(self.on_model_changed)(model.clone(), cx);
|
||||
|
||||
// Update the selection status
|
||||
let selected_model_id = model_info.model.id();
|
||||
let selected_provider_id = model_info.model.provider_id();
|
||||
for model in &mut self.all_models {
|
||||
model.is_selected = model.model.id() == selected_model_id
|
||||
&& model.model.provider_id() == selected_provider_id;
|
||||
}
|
||||
for model in &mut self.filtered_models {
|
||||
model.is_selected = model.model.id() == selected_model_id
|
||||
&& model.model.provider_id() == selected_provider_id;
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.language_model_selector
|
||||
.update(cx, |_this, cx| cx.emit(DismissEvent))
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_header(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
|
||||
let configured_models_count = LanguageModelRegistry::global(cx)
|
||||
@@ -195,6 +252,17 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
let model_info = self.filtered_models.get(ix)?;
|
||||
let provider_name: String = model_info.model.provider_name().0.clone().into();
|
||||
|
||||
let active_provider_id = LanguageModelRegistry::read_global(cx)
|
||||
.active_provider()
|
||||
.map(|m| m.id());
|
||||
|
||||
let active_model_id = LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|m| m.id());
|
||||
|
||||
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
|
||||
&& Some(model_info.model.id()) == active_model_id;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
@@ -235,7 +303,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.end_slot(div().when(model_info.is_selected, |this| {
|
||||
.end_slot(div().when(is_selected, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent)
|
||||
@@ -296,58 +364,3 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for LanguageModelSelector<T> {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let selected_provider = LanguageModelRegistry::read_global(cx)
|
||||
.active_provider()
|
||||
.map(|m| m.id());
|
||||
|
||||
let selected_model = LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|m| m.id());
|
||||
|
||||
let all_models = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.flat_map(|provider| {
|
||||
let provider_id = provider.id();
|
||||
let icon = provider.icon();
|
||||
let selected_model = selected_model.clone();
|
||||
let selected_provider = selected_provider.clone();
|
||||
|
||||
provider.provided_models(cx).into_iter().map(move |model| {
|
||||
let model = model.clone();
|
||||
let icon = model.icon().unwrap_or(icon);
|
||||
|
||||
ModelInfo {
|
||||
model: model.clone(),
|
||||
icon,
|
||||
availability: model.availability(),
|
||||
is_selected: selected_model.as_ref() == Some(&model.id())
|
||||
&& selected_provider.as_ref() == Some(&provider_id),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let delegate = LanguageModelPickerDelegate {
|
||||
on_model_changed: self.on_model_changed.clone(),
|
||||
all_models: all_models.clone(),
|
||||
filtered_models: all_models,
|
||||
selected_index: 0,
|
||||
};
|
||||
|
||||
let picker_view = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
|
||||
picker
|
||||
});
|
||||
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_cx| Some(picker_view.clone()))
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::AnchorCorner::BottomLeft)
|
||||
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,7 @@ use language_model::{
|
||||
use settings::SettingsStore;
|
||||
use std::time::Duration;
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::{
|
||||
div, h_flex, v_flex, Button, ButtonCommon, Clickable, Color, Context, FixedWidth, Icon,
|
||||
IconName, IconPosition, IconSize, IntoElement, Label, LabelCommon, ParentElement, Styled,
|
||||
ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
|
||||
use super::anthropic::count_anthropic_tokens;
|
||||
use super::open_ai::count_open_ai_tokens;
|
||||
|
||||
@@ -447,7 +447,7 @@ impl Render for ConfigurationView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let is_authenticated = self.state.read(cx).is_authenticated();
|
||||
|
||||
let ollama_intro = "Get up and running with Llama 3.2, Mistral, Gemma 2, and other large language models with Ollama.";
|
||||
let ollama_intro = "Get up and running with Llama 3.3, Mistral, Gemma 2, and other large language models with Ollama.";
|
||||
let ollama_reqs =
|
||||
"Ollama must be running with at least one model installed to use it in the assistant.";
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
|
||||
|
||||
let _rust_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/test.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/test.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
(comment) @comment
|
||||
|
||||
[
|
||||
(addition)
|
||||
(new_file)
|
||||
@@ -12,4 +14,35 @@
|
||||
|
||||
(location) @attribute
|
||||
|
||||
(command) @function
|
||||
(command
|
||||
"diff" @function
|
||||
(argument) @variable.parameter)
|
||||
|
||||
(filename) @string.special.path
|
||||
|
||||
(mode) @number
|
||||
|
||||
([
|
||||
".."
|
||||
"+"
|
||||
"++"
|
||||
"+++"
|
||||
"++++"
|
||||
"-"
|
||||
"--"
|
||||
"---"
|
||||
"----"
|
||||
] @punctuation.special)
|
||||
|
||||
[
|
||||
(binary_change)
|
||||
(similarity)
|
||||
(file_change)
|
||||
] @label
|
||||
|
||||
(index
|
||||
"index" @keyword)
|
||||
|
||||
(similarity
|
||||
(score) @number
|
||||
"%" @number)
|
||||
|
||||
@@ -608,6 +608,10 @@ impl LanguageServer {
|
||||
root_uri: Some(root_uri.clone()),
|
||||
initialization_options: None,
|
||||
capabilities: ClientCapabilities {
|
||||
general: Some(GeneralClientCapabilities {
|
||||
position_encodings: Some(vec![PositionEncodingKind::UTF16]),
|
||||
..Default::default()
|
||||
}),
|
||||
workspace: Some(WorkspaceClientCapabilities {
|
||||
configuration: Some(true),
|
||||
did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities {
|
||||
@@ -644,6 +648,7 @@ impl LanguageServer {
|
||||
will_rename: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
apply_edit: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
text_document: Some(TextDocumentClientCapabilities {
|
||||
@@ -760,9 +765,11 @@ impl LanguageServer {
|
||||
})),
|
||||
window: Some(WindowClientCapabilities {
|
||||
work_done_progress: Some(true),
|
||||
show_message: Some(ShowMessageRequestClientCapabilities {
|
||||
message_action_item: None,
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
general: None,
|
||||
},
|
||||
trace: None,
|
||||
workspace_folders: Some(vec![WorkspaceFolder {
|
||||
@@ -776,6 +783,7 @@ impl LanguageServer {
|
||||
}
|
||||
}),
|
||||
locale: None,
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ pub enum Event {
|
||||
},
|
||||
Edited {
|
||||
singleton_buffer_edited: bool,
|
||||
edited_buffer: Option<Model<Buffer>>,
|
||||
},
|
||||
TransactionUndone {
|
||||
transaction_id: TransactionId,
|
||||
@@ -1485,6 +1486,7 @@ impl MultiBuffer {
|
||||
}]);
|
||||
cx.emit(Event::Edited {
|
||||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
});
|
||||
cx.emit(Event::ExcerptsAdded {
|
||||
buffer,
|
||||
@@ -1512,6 +1514,7 @@ impl MultiBuffer {
|
||||
}]);
|
||||
cx.emit(Event::Edited {
|
||||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
});
|
||||
cx.emit(Event::ExcerptsRemoved { ids });
|
||||
cx.notify();
|
||||
@@ -1753,6 +1756,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 +1820,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 +1984,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 +2082,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();
|
||||
@@ -5363,13 +5370,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,
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
@@ -58,6 +58,7 @@ impl Prettier {
|
||||
"prettier.config.js",
|
||||
"prettier.config.cjs",
|
||||
".editorconfig",
|
||||
".prettierignore",
|
||||
];
|
||||
|
||||
pub async fn locate_prettier_installation(
|
||||
@@ -134,6 +135,101 @@ impl Prettier {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn locate_prettier_ignore(
|
||||
fs: &dyn Fs,
|
||||
prettier_ignores: &HashSet<PathBuf>,
|
||||
locate_from: &Path,
|
||||
) -> anyhow::Result<ControlFlow<(), Option<PathBuf>>> {
|
||||
let mut path_to_check = locate_from
|
||||
.components()
|
||||
.take_while(|component| component.as_os_str().to_string_lossy() != "node_modules")
|
||||
.collect::<PathBuf>();
|
||||
if path_to_check != locate_from {
|
||||
log::debug!(
|
||||
"Skipping prettier ignore location for path {path_to_check:?} that is inside node_modules"
|
||||
);
|
||||
return Ok(ControlFlow::Break(()));
|
||||
}
|
||||
|
||||
let path_to_check_metadata = fs
|
||||
.metadata(&path_to_check)
|
||||
.await
|
||||
.with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))?
|
||||
.with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?;
|
||||
if !path_to_check_metadata.is_dir {
|
||||
path_to_check.pop();
|
||||
}
|
||||
|
||||
let mut closest_package_json_path = None;
|
||||
loop {
|
||||
if prettier_ignores.contains(&path_to_check) {
|
||||
log::debug!("Found prettier ignore at {path_to_check:?}");
|
||||
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
||||
} else if let Some(package_json_contents) =
|
||||
read_package_json(fs, &path_to_check).await?
|
||||
{
|
||||
let ignore_path = path_to_check.join(".prettierignore");
|
||||
if let Some(metadata) = fs
|
||||
.metadata(&ignore_path)
|
||||
.await
|
||||
.with_context(|| format!("fetching metadata for {ignore_path:?}"))?
|
||||
{
|
||||
if !metadata.is_dir && !metadata.is_symlink {
|
||||
log::info!("Found prettier ignore at {ignore_path:?}");
|
||||
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
||||
}
|
||||
}
|
||||
match &closest_package_json_path {
|
||||
None => closest_package_json_path = Some(path_to_check.clone()),
|
||||
Some(closest_package_json_path) => {
|
||||
if let Some(serde_json::Value::Array(workspaces)) =
|
||||
package_json_contents.get("workspaces")
|
||||
{
|
||||
let subproject_path = closest_package_json_path
|
||||
.strip_prefix(&path_to_check)
|
||||
.expect("traversing path parents, should be able to strip prefix");
|
||||
|
||||
if workspaces
|
||||
.iter()
|
||||
.filter_map(|value| {
|
||||
if let serde_json::Value::String(s) = value {
|
||||
Some(s.clone())
|
||||
} else {
|
||||
log::warn!(
|
||||
"Skipping non-string 'workspaces' value: {value:?}"
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.any(|workspace_definition| {
|
||||
workspace_definition == subproject_path.to_string_lossy()
|
||||
|| PathMatcher::new(&[workspace_definition])
|
||||
.ok()
|
||||
.map_or(false, |path_matcher| {
|
||||
path_matcher.is_match(subproject_path)
|
||||
})
|
||||
})
|
||||
{
|
||||
let workspace_ignore = path_to_check.join(".prettierignore");
|
||||
if let Some(metadata) = fs.metadata(&workspace_ignore).await? {
|
||||
if !metadata.is_dir {
|
||||
log::info!("Found prettier ignore at workspace root {workspace_ignore:?}");
|
||||
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !path_to_check.pop() {
|
||||
log::debug!("Found no prettier ignore in ancestors of {locate_from:?}");
|
||||
return Ok(ControlFlow::Continue(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn start(
|
||||
_: LanguageServerId,
|
||||
@@ -201,6 +297,7 @@ impl Prettier {
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_path: Option<PathBuf>,
|
||||
ignore_dir: Option<PathBuf>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> anyhow::Result<Diff> {
|
||||
match self {
|
||||
@@ -315,11 +412,17 @@ impl Prettier {
|
||||
|
||||
}
|
||||
|
||||
let ignore_path = ignore_dir.and_then(|dir| {
|
||||
let ignore_file = dir.join(".prettierignore");
|
||||
ignore_file.is_file().then_some(ignore_file)
|
||||
});
|
||||
|
||||
log::debug!(
|
||||
"Formatting file {:?} with prettier, plugins :{:?}, options: {:?}",
|
||||
"Formatting file {:?} with prettier, plugins :{:?}, options: {:?}, ignore_path: {:?}",
|
||||
buffer.file().map(|f| f.full_path(cx)),
|
||||
plugins,
|
||||
prettier_options,
|
||||
ignore_path,
|
||||
);
|
||||
|
||||
anyhow::Ok(FormatParams {
|
||||
@@ -329,6 +432,7 @@ impl Prettier {
|
||||
plugins,
|
||||
path: buffer_path,
|
||||
prettier_options,
|
||||
ignore_path,
|
||||
},
|
||||
})
|
||||
})?
|
||||
@@ -449,6 +553,7 @@ struct FormatOptions {
|
||||
#[serde(rename = "filepath")]
|
||||
path: Option<PathBuf>,
|
||||
prettier_options: Option<HashMap<String, serde_json::Value>>,
|
||||
ignore_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
@@ -840,4 +945,150 @@ mod tests {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prettier_ignore_with_editor_prettier(cx: &mut gpui::TestAppContext) {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"project": {
|
||||
"src": {
|
||||
"index.js": "// index.js file contents",
|
||||
"ignored.js": "// this file should be ignored",
|
||||
},
|
||||
".prettierignore": "ignored.js",
|
||||
"package.json": r#"{
|
||||
"name": "test-project"
|
||||
}"#
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
Prettier::locate_prettier_ignore(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/project/src/index.js"),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
ControlFlow::Continue(Some(PathBuf::from("/root/project"))),
|
||||
"Should find prettierignore in project root"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prettier_ignore_in_monorepo_with_only_child_ignore(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"monorepo": {
|
||||
"node_modules": {
|
||||
"prettier": {
|
||||
"index.js": "// Dummy prettier package file",
|
||||
}
|
||||
},
|
||||
"packages": {
|
||||
"web": {
|
||||
"src": {
|
||||
"index.js": "// index.js contents",
|
||||
"ignored.js": "// this should be ignored",
|
||||
},
|
||||
".prettierignore": "ignored.js",
|
||||
"package.json": r#"{
|
||||
"name": "web-package"
|
||||
}"#
|
||||
}
|
||||
},
|
||||
"package.json": r#"{
|
||||
"workspaces": ["packages/*"],
|
||||
"devDependencies": {
|
||||
"prettier": "^2.0.0"
|
||||
}
|
||||
}"#
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
Prettier::locate_prettier_ignore(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/monorepo/packages/web/src/index.js"),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
|
||||
"Should find prettierignore in child package"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prettier_ignore_in_monorepo_with_root_and_child_ignores(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"monorepo": {
|
||||
"node_modules": {
|
||||
"prettier": {
|
||||
"index.js": "// Dummy prettier package file",
|
||||
}
|
||||
},
|
||||
".prettierignore": "main.js",
|
||||
"packages": {
|
||||
"web": {
|
||||
"src": {
|
||||
"main.js": "// this should not be ignored",
|
||||
"ignored.js": "// this should be ignored",
|
||||
},
|
||||
".prettierignore": "ignored.js",
|
||||
"package.json": r#"{
|
||||
"name": "web-package"
|
||||
}"#
|
||||
}
|
||||
},
|
||||
"package.json": r#"{
|
||||
"workspaces": ["packages/*"],
|
||||
"devDependencies": {
|
||||
"prettier": "^2.0.0"
|
||||
}
|
||||
}"#
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
Prettier::locate_prettier_ignore(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/monorepo/packages/web/src/main.js"),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
|
||||
"Should find child package prettierignore first"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Prettier::locate_prettier_ignore(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/monorepo/packages/web/src/ignored.js"),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
|
||||
"Should find child package prettierignore first"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ class Prettier {
|
||||
process.exit(1);
|
||||
}
|
||||
process.stderr.write(
|
||||
`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`,
|
||||
`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(
|
||||
config,
|
||||
)}\n`,
|
||||
);
|
||||
process.stdin.resume();
|
||||
handleBuffer(new Prettier(prettierPath, prettier, config));
|
||||
@@ -68,7 +70,9 @@ async function handleBuffer(prettier) {
|
||||
sendResponse({
|
||||
id: message.id,
|
||||
...makeError(
|
||||
`error during message '${JSON.stringify(errorMessage)}' handling: ${e}`,
|
||||
`error during message '${JSON.stringify(
|
||||
errorMessage,
|
||||
)}' handling: ${e}`,
|
||||
),
|
||||
});
|
||||
});
|
||||
@@ -189,6 +193,22 @@ async function handleMessage(message, prettier) {
|
||||
if (params.options.filepath) {
|
||||
resolvedConfig =
|
||||
(await prettier.prettier.resolveConfig(params.options.filepath)) || {};
|
||||
|
||||
if (params.options.ignorePath) {
|
||||
const fileInfo = await prettier.prettier.getFileInfo(
|
||||
params.options.filepath,
|
||||
{
|
||||
ignorePath: params.options.ignorePath,
|
||||
},
|
||||
);
|
||||
if (fileInfo.ignored) {
|
||||
process.stderr.write(
|
||||
`Ignoring file '${params.options.filepath}' based on rules in '${params.options.ignorePath}'\n`,
|
||||
);
|
||||
sendResponse({ id, result: { text: params.text } });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Marking the params.options.filepath as undefined makes
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,7 @@ pub struct PrettierStore {
|
||||
worktree_store: Model<WorktreeStore>,
|
||||
default_prettier: DefaultPrettier,
|
||||
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
|
||||
prettier_ignores_per_worktree: HashMap<WorktreeId, HashSet<PathBuf>>,
|
||||
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
||||
}
|
||||
|
||||
@@ -65,11 +66,13 @@ impl PrettierStore {
|
||||
worktree_store,
|
||||
default_prettier: DefaultPrettier::default(),
|
||||
prettiers_per_worktree: HashMap::default(),
|
||||
prettier_ignores_per_worktree: HashMap::default(),
|
||||
prettier_instances: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
|
||||
self.prettier_ignores_per_worktree.remove(&id_to_remove);
|
||||
let mut prettier_instances_to_clean = FuturesUnordered::new();
|
||||
if let Some(prettier_paths) = self.prettiers_per_worktree.remove(&id_to_remove) {
|
||||
for path in prettier_paths.iter().flatten() {
|
||||
@@ -211,6 +214,65 @@ impl PrettierStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn prettier_ignore_for_buffer(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Option<PathBuf>> {
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_file = buffer.file();
|
||||
if buffer.language().is_none() {
|
||||
return Task::ready(None);
|
||||
}
|
||||
match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) {
|
||||
Some((worktree_id, buffer_path)) => {
|
||||
let fs = Arc::clone(&self.fs);
|
||||
let prettier_ignores = self
|
||||
.prettier_ignores_per_worktree
|
||||
.get(&worktree_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
cx.spawn(|lsp_store, mut cx| async move {
|
||||
match cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
Prettier::locate_prettier_ignore(
|
||||
fs.as_ref(),
|
||||
&prettier_ignores,
|
||||
&buffer_path,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(ControlFlow::Break(())) => None,
|
||||
Ok(ControlFlow::Continue(None)) => None,
|
||||
Ok(ControlFlow::Continue(Some(ignore_dir))) => {
|
||||
log::debug!("Found prettier ignore in {ignore_dir:?}");
|
||||
lsp_store
|
||||
.update(&mut cx, |store, _| {
|
||||
store
|
||||
.prettier_ignores_per_worktree
|
||||
.entry(worktree_id)
|
||||
.or_default()
|
||||
.insert(ignore_dir.clone());
|
||||
})
|
||||
.ok();
|
||||
Some(ignore_dir)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to determine prettier ignore path for buffer: {e:#}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
None => Task::ready(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_prettier(
|
||||
node: NodeRuntime,
|
||||
prettier_dir: PathBuf,
|
||||
@@ -654,6 +716,13 @@ pub(super) async fn format_with_prettier(
|
||||
.ok()?
|
||||
.await;
|
||||
|
||||
let ignore_dir = prettier_store
|
||||
.update(cx, |prettier_store, cx| {
|
||||
prettier_store.prettier_ignore_for_buffer(buffer, cx)
|
||||
})
|
||||
.ok()?
|
||||
.await;
|
||||
|
||||
let (prettier_path, prettier_task) = prettier_instance?;
|
||||
|
||||
let prettier_description = match prettier_path.as_ref() {
|
||||
@@ -671,7 +740,7 @@ pub(super) async fn format_with_prettier(
|
||||
.flatten();
|
||||
|
||||
let format_result = prettier
|
||||
.format(buffer, buffer_path, cx)
|
||||
.format(buffer, buffer_path, ignore_dir, cx)
|
||||
.await
|
||||
.map(crate::lsp_store::FormatOperation::Prettier)
|
||||
.with_context(|| format!("{} failed to format buffer", prettier_description));
|
||||
|
||||
@@ -1254,6 +1254,10 @@ impl Project {
|
||||
self.buffer_store.read(cx).buffers().collect()
|
||||
}
|
||||
|
||||
pub fn environment(&self) -> &Model<ProjectEnvironment> {
|
||||
&self.environment
|
||||
}
|
||||
|
||||
pub fn cli_environment(&self, cx: &AppContext) -> Option<HashMap<String, String>> {
|
||||
self.environment.read(cx).get_cli_environment()
|
||||
}
|
||||
@@ -1843,6 +1847,19 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn open_local_buffer_with_lsp(
|
||||
&mut self,
|
||||
abs_path: impl AsRef<Path>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<(Model<Buffer>, lsp_store::OpenLspBufferHandle)>> {
|
||||
if let Some((worktree, relative_path)) = self.find_worktree(abs_path.as_ref(), cx) {
|
||||
self.open_buffer_with_lsp((worktree.read(cx).id(), relative_path), cx)
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no such path")))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_buffer(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
@@ -1857,6 +1874,23 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn open_buffer_with_lsp(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<(Model<Buffer>, lsp_store::OpenLspBufferHandle)>> {
|
||||
let buffer = self.open_buffer(path, cx);
|
||||
let lsp_store = self.lsp_store().clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let buffer = buffer.await?;
|
||||
let handle = lsp_store.update(&mut cx, |lsp_store, cx| {
|
||||
lsp_store.register_buffer_with_language_servers(&buffer, cx)
|
||||
})?;
|
||||
Ok((buffer, handle))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_unstaged_changes(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
|
||||
@@ -442,17 +442,17 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
|
||||
// Open a buffer without an associated language server.
|
||||
let toml_buffer = project
|
||||
let (toml_buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/Cargo.toml", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/Cargo.toml", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Open a buffer with an associated language server before the language for it has been loaded.
|
||||
let rust_buffer = project
|
||||
let (rust_buffer, _handle2) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/test.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/test.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -513,9 +513,9 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
|
||||
// Open a third buffer with a different associated language server.
|
||||
let json_buffer = project
|
||||
let (json_buffer, _json_handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/package.json", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/package.json", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -550,9 +550,9 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
// When opening another buffer whose language server is already running,
|
||||
// it is also configured based on the existing language server's capabilities.
|
||||
let rust_buffer2 = project
|
||||
let (rust_buffer2, _handle4) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/test2.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/test2.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -765,7 +765,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
|
||||
// Close notifications are reported only to servers matching the buffer's language.
|
||||
cx.update(|_| drop(json_buffer));
|
||||
cx.update(|_| drop(_json_handle));
|
||||
let close_message = lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(
|
||||
lsp::Url::from_file_path("/the-root/package.json").unwrap(),
|
||||
@@ -827,9 +827,9 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Start the language server by opening a buffer with a compatible file extension.
|
||||
let _buffer = project
|
||||
let _ = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/src/a.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/the-root/src/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1239,8 +1239,10 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
|
||||
let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
|
||||
|
||||
// Cause worktree to start the fake language server
|
||||
let _buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
|
||||
let _ = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/b.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1259,6 +1261,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
|
||||
fake_server
|
||||
.start_progress(format!("{}/0", progress_token))
|
||||
.await;
|
||||
assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
|
||||
assert_eq!(
|
||||
events.next().await.unwrap(),
|
||||
Event::DiskBasedDiagnosticsStarted {
|
||||
@@ -1365,8 +1368,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
|
||||
|
||||
let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1390,6 +1395,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
|
||||
Some(worktree_id)
|
||||
)
|
||||
);
|
||||
assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
|
||||
fake_server.start_progress(progress_token).await;
|
||||
assert_eq!(
|
||||
events.next().await.unwrap(),
|
||||
@@ -1438,8 +1444,10 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
|
||||
language_registry.add(rust_lang());
|
||||
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
let (buffer, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1517,8 +1525,10 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
|
||||
language_registry.add(rust_lang());
|
||||
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1565,8 +1575,10 @@ async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1634,11 +1646,15 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
|
||||
language_registry.add(js_lang());
|
||||
|
||||
let _rs_buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let _js_buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx))
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/b.js", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1734,6 +1750,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
fs.insert_tree("/dir", json!({ "a.rs": text })).await;
|
||||
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
|
||||
language_registry.add(rust_lang());
|
||||
@@ -1750,6 +1767,10 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _handle = lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.register_buffer_with_language_servers(&buffer, cx)
|
||||
});
|
||||
|
||||
let mut fake_server = fake_servers.next().await.unwrap();
|
||||
let open_notification = fake_server
|
||||
.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
@@ -2162,8 +2183,10 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
|
||||
language_registry.add(rust_lang());
|
||||
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2533,8 +2556,10 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
|
||||
language_registry.add(rust_lang());
|
||||
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/b.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2638,8 +2663,8 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2730,8 +2755,8 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2793,8 +2818,8 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -3984,7 +4009,7 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let _ = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/one.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/dir/one.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4086,9 +4111,9 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/one.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/dir/one.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4951,8 +4976,8 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
|
||||
),
|
||||
];
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.tsx", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
@@ -5060,8 +5085,8 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
@@ -5130,8 +5155,8 @@ async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
@@ -5251,8 +5276,8 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
|
||||
),
|
||||
];
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.tsx", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
@@ -281,6 +281,7 @@ impl ProjectPanel {
|
||||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus(&focus_handle, Self::focus_in).detach();
|
||||
cx.on_focus_out(&focus_handle, |this, _, cx| {
|
||||
this.focus_out(cx);
|
||||
this.hide_scrollbar(cx);
|
||||
})
|
||||
.detach();
|
||||
@@ -595,6 +596,12 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.focus_handle.is_focused(cx) {
|
||||
self.confirm(&Confirm, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn deploy_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
@@ -3140,6 +3147,8 @@ impl ProjectPanel {
|
||||
details: EntryDetails,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Stateful<Div> {
|
||||
const GROUP_NAME: &str = "project_entry";
|
||||
|
||||
let kind = details.kind;
|
||||
let settings = ProjectPanelSettings::get_global(cx);
|
||||
let show_editor = details.is_editing && !details.is_processing;
|
||||
@@ -3185,8 +3194,37 @@ impl ProjectPanel {
|
||||
marked_selections: selections,
|
||||
};
|
||||
|
||||
let default_color = if is_marked || is_active {
|
||||
item_colors.marked_active
|
||||
} else {
|
||||
item_colors.default
|
||||
};
|
||||
|
||||
let bg_hover_color = if self.mouse_down {
|
||||
item_colors.marked_active
|
||||
} else {
|
||||
item_colors.hover
|
||||
};
|
||||
|
||||
let border_color =
|
||||
if !self.mouse_down && is_active && self.focus_handle.contains_focused(cx) {
|
||||
item_colors.focused
|
||||
} else if self.mouse_down && is_marked || is_active {
|
||||
item_colors.marked_active
|
||||
} else {
|
||||
item_colors.default
|
||||
};
|
||||
|
||||
div()
|
||||
.id(entry_id.to_proto() as usize)
|
||||
.group(GROUP_NAME)
|
||||
.cursor_pointer()
|
||||
.rounded_none()
|
||||
.bg(default_color)
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.border_color(border_color)
|
||||
.hover(|style| style.bg(bg_hover_color))
|
||||
.when(is_local, |div| {
|
||||
div.on_drag_move::<ExternalPaths>(cx.listener(
|
||||
move |this, event: &DragMoveEvent<ExternalPaths>, cx| {
|
||||
@@ -3322,12 +3360,11 @@ impl ProjectPanel {
|
||||
this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
|
||||
}
|
||||
}))
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
ListItem::new(entry_id.to_proto() as usize)
|
||||
.indent_level(depth)
|
||||
.indent_step_size(px(settings.indent_size))
|
||||
.selected(is_marked || is_active)
|
||||
.selectable(false)
|
||||
.when_some(canonical_path, |this, path| {
|
||||
this.end_slot::<AnyElement>(
|
||||
div()
|
||||
@@ -3367,13 +3404,11 @@ impl ProjectPanel {
|
||||
} else {
|
||||
IconDecorationKind::Dot
|
||||
},
|
||||
if is_marked || is_active {
|
||||
item_colors.marked_active
|
||||
} else {
|
||||
item_colors.default
|
||||
},
|
||||
default_color,
|
||||
cx,
|
||||
)
|
||||
.group_name(Some(GROUP_NAME.into()))
|
||||
.knockout_hover_color(bg_hover_color)
|
||||
.color(decoration_color.color(cx))
|
||||
.position(Point {
|
||||
x: px(-2.),
|
||||
@@ -3489,26 +3524,6 @@ impl ProjectPanel {
|
||||
))
|
||||
.overflow_x(),
|
||||
)
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.rounded_none()
|
||||
.hover(|style| {
|
||||
if is_active {
|
||||
style
|
||||
} else {
|
||||
style.bg(item_colors.hover).border_color(item_colors.hover)
|
||||
}
|
||||
})
|
||||
.when(is_marked || is_active, |this| {
|
||||
this.when(is_marked, |this| {
|
||||
this.bg(item_colors.marked_active)
|
||||
.border_color(item_colors.marked_active)
|
||||
})
|
||||
})
|
||||
.when(
|
||||
!self.mouse_down && is_active && self.focus_handle.contains_focused(cx),
|
||||
|this| this.border_color(item_colors.focused),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
|
||||
@@ -4262,7 +4277,6 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::path::{Path, PathBuf};
|
||||
use ui::Context;
|
||||
use workspace::{
|
||||
item::{Item, ProjectItem},
|
||||
register_project_item, AppState,
|
||||
|
||||
@@ -292,7 +292,7 @@ mod tests {
|
||||
|
||||
let _buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/test.rs", cx)
|
||||
project.open_local_buffer_with_lsp("/dir/test.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -304,7 +304,9 @@ message Envelope {
|
||||
InstallExtension install_extension = 287;
|
||||
|
||||
GetStagedText get_staged_text = 288;
|
||||
GetStagedTextResponse get_staged_text_response = 289; // current max
|
||||
GetStagedTextResponse get_staged_text_response = 289;
|
||||
|
||||
RegisterBufferWithLanguageServers register_buffer_with_language_servers = 290;
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
@@ -2537,7 +2539,6 @@ message UpdateGitBranch {
|
||||
string branch_name = 2;
|
||||
ProjectPath repository = 3;
|
||||
}
|
||||
|
||||
message GetPanicFiles {
|
||||
}
|
||||
|
||||
@@ -2582,3 +2583,8 @@ message InstallExtension {
|
||||
Extension extension = 1;
|
||||
string tmp_dir = 2;
|
||||
}
|
||||
|
||||
message RegisterBufferWithLanguageServers{
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
}
|
||||
|
||||
@@ -373,6 +373,7 @@ messages!(
|
||||
(SyncExtensions, Background),
|
||||
(SyncExtensionsResponse, Background),
|
||||
(InstallExtension, Background),
|
||||
(RegisterBufferWithLanguageServers, Background),
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
@@ -499,6 +500,7 @@ request_messages!(
|
||||
(CancelLanguageServerWork, Ack),
|
||||
(SyncExtensions, SyncExtensionsResponse),
|
||||
(InstallExtension, Ack),
|
||||
(RegisterBufferWithLanguageServers, Ack),
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
@@ -584,6 +586,7 @@ entity_messages!(
|
||||
ActiveToolchain,
|
||||
GetPathMetadata,
|
||||
CancelLanguageServerWork,
|
||||
RegisterBufferWithLanguageServers,
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
||||
@@ -440,9 +440,9 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
|
||||
// Wait for the settings to synchronize
|
||||
cx.run_until_parked();
|
||||
|
||||
let buffer = project
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
|
||||
project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -616,9 +616,9 @@ async fn test_remote_cancel_language_server_work(
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let buffer = project
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
|
||||
project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -16,6 +16,7 @@ doctest = false
|
||||
alacritty_terminal.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-dispatcher.workspace = true
|
||||
async-tungstenite = { workspace = true, features = ["async-std", "async-tls"] }
|
||||
base64.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
|
||||
@@ -3,6 +3,11 @@ use gpui::{Task, View, WindowContext};
|
||||
use http_client::{AsyncBody, HttpClient, Request};
|
||||
use jupyter_protocol::{ExecutionState, JupyterKernelspec, JupyterMessage, KernelInfoReply};
|
||||
|
||||
use async_tungstenite::{
|
||||
async_std::connect_async,
|
||||
tungstenite::{client::IntoClientRequest, http::HeaderValue},
|
||||
};
|
||||
|
||||
use futures::StreamExt;
|
||||
use smol::io::AsyncReadExt as _;
|
||||
|
||||
@@ -11,8 +16,8 @@ use crate::Session;
|
||||
use super::RunningKernel;
|
||||
use anyhow::Result;
|
||||
use jupyter_websocket_client::{
|
||||
JupyterWebSocketReader, JupyterWebSocketWriter, KernelLaunchRequest, KernelSpecsResponse,
|
||||
RemoteServer,
|
||||
JupyterWebSocket, JupyterWebSocketReader, JupyterWebSocketWriter, KernelLaunchRequest,
|
||||
KernelSpecsResponse, RemoteServer,
|
||||
};
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
@@ -151,7 +156,31 @@ impl RemoteRunningKernel {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (kernel_socket, _response) = remote_server.connect_to_kernel(&kernel_id).await?;
|
||||
let ws_url = format!(
|
||||
"{}/api/kernels/{}/channels?token={}",
|
||||
remote_server.base_url.replace("http", "ws"),
|
||||
kernel_id,
|
||||
remote_server.token
|
||||
);
|
||||
|
||||
let mut req: Request<()> = ws_url.into_client_request()?;
|
||||
let headers = req.headers_mut();
|
||||
|
||||
headers.insert(
|
||||
"User-Agent",
|
||||
HeaderValue::from_str(&format!(
|
||||
"Zed/{} ({}; {})",
|
||||
"repl",
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH
|
||||
))?,
|
||||
);
|
||||
|
||||
let response = connect_async(req).await;
|
||||
|
||||
let (ws_stream, _response) = response?;
|
||||
|
||||
let kernel_socket = JupyterWebSocket { inner: ws_stream };
|
||||
|
||||
let (mut w, mut r): (JupyterWebSocketWriter, JupyterWebSocketReader) =
|
||||
kernel_socket.split();
|
||||
|
||||
@@ -209,6 +209,7 @@ impl Render for BufferSearchBar {
|
||||
|
||||
let input_base_styles = || {
|
||||
h_flex()
|
||||
.min_w_32()
|
||||
.w(input_width)
|
||||
.h_8()
|
||||
.px_2()
|
||||
@@ -529,6 +530,11 @@ impl BufferSearchBar {
|
||||
this.toggle_whole_word(action, cx);
|
||||
}
|
||||
}));
|
||||
registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, cx| {
|
||||
if this.supported_options().regex {
|
||||
this.toggle_regex(action, cx);
|
||||
}
|
||||
}));
|
||||
registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, cx| {
|
||||
if this.supported_options().selection {
|
||||
this.toggle_selection(action, cx);
|
||||
|
||||
@@ -1595,6 +1595,7 @@ impl Render for ProjectSearchBar {
|
||||
|
||||
let input_base_styles = || {
|
||||
h_flex()
|
||||
.min_w_32()
|
||||
.w(input_width)
|
||||
.h_8()
|
||||
.px_2()
|
||||
|
||||
@@ -114,6 +114,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||
&mut self,
|
||||
buffer_handle: Model<Buffer>,
|
||||
cursor_position: Anchor,
|
||||
_visible_range: Option<Range<usize>>,
|
||||
debounce: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
|
||||
@@ -292,6 +292,7 @@ impl TitleBar {
|
||||
let is_local = project.is_local() || project.is_via_ssh();
|
||||
let is_shared = is_local && project.is_shared();
|
||||
let is_muted = room.is_muted();
|
||||
let muted_by_user = room.muted_by_user();
|
||||
let is_deafened = room.is_deafened().unwrap_or(false);
|
||||
let is_screen_sharing = room.is_screen_sharing();
|
||||
let can_use_microphone = room.can_use_microphone(cx);
|
||||
@@ -362,14 +363,20 @@ impl TitleBar {
|
||||
},
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if is_muted {
|
||||
"Unmute microphone"
|
||||
if is_muted {
|
||||
if is_deafened {
|
||||
Tooltip::with_meta(
|
||||
"Unmute Microphone",
|
||||
None,
|
||||
"Audio will be unmuted",
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
"Mute microphone"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
Tooltip::text("Unmute Microphone", cx)
|
||||
}
|
||||
} else {
|
||||
Tooltip::text("Mute Microphone", cx)
|
||||
}
|
||||
})
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -395,7 +402,23 @@ impl TitleBar {
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_deafened)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx)
|
||||
if is_deafened {
|
||||
let label = "Unmute Audio";
|
||||
|
||||
if !muted_by_user {
|
||||
Tooltip::with_meta(label, None, "Microphone will be unmuted", cx)
|
||||
} else {
|
||||
Tooltip::text(label, cx)
|
||||
}
|
||||
} else {
|
||||
let label = "Mute Audio";
|
||||
|
||||
if !muted_by_user {
|
||||
Tooltip::with_meta(label, None, "Microphone will be muted", cx)
|
||||
} else {
|
||||
Tooltip::text(label, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_click(move |_, cx| toggle_deafen(&Default::default(), cx))
|
||||
.into_any_element(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user