Compare commits

..

8 Commits

Author SHA1 Message Date
Bennet Bo Fenner
7a8965ccc7 cleanup 2024-12-12 12:38:55 +01:00
Bennet Bo Fenner
3364cd9889 remove unrelated changes 2024-12-12 12:26:59 +01:00
Bennet Bo Fenner
b6ac26b1e2 fix tests 2024-12-12 12:23:39 +01:00
Bennet Bo Fenner
9ab17f5b32 Merge branch 'main' into zeta-syntax-highlighting-diff 2024-12-12 12:09:04 +01:00
Bennet Bo Fenner
d5f5a615e8 move branching buffer to editor 2024-12-12 12:07:25 +01:00
Bennet Bo Fenner
f4f618c9cb Show existing text until the end of the last line 2024-12-12 11:54:15 +01:00
Bennet Bo Fenner
71b4d98367 WIP 2024-12-12 10:43:46 +01:00
Bennet Bo Fenner
77328e8300 WIP 2024-12-12 00:57:56 +01:00
147 changed files with 1651 additions and 5210 deletions

View File

@@ -26,8 +26,8 @@ body:
required: true
- type: textarea
attributes:
label: If applicable, add screenshots or screencasts of the incorrect state / behavior
description: Drag images / videos into the text input below
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
description: Drag issues into the text input below
validations:
required: false
- type: textarea

View File

@@ -8,7 +8,7 @@ on:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
if: github.repository == 'zed-industries/zed'
if: github.repository_owner == 'zed-industries'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv

View File

@@ -8,7 +8,7 @@ on:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
if: github.repository == 'zed-industries/zed'
if: github.repository_owner == 'zed-industries'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv

View File

@@ -140,7 +140,7 @@ jobs:
name: Create a Linux *.tar.gz bundle for ARM
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
- hosted-linux-arm-1
needs: tests
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}

35
Cargo.lock generated
View File

@@ -469,11 +469,8 @@ dependencies = [
"feature_flags",
"fs",
"futures 0.3.31",
"fuzzy",
"gpui",
"handlebars 4.5.0",
"html_to_markdown",
"http_client",
"indoc",
"language",
"language_model",
@@ -502,7 +499,6 @@ dependencies = [
"similar",
"smol",
"telemetry_events",
"terminal",
"terminal_view",
"text",
"theme",
@@ -5032,7 +5028,6 @@ name = "fuzzy"
version = "0.1.0"
dependencies = [
"gpui",
"log",
"util",
]
@@ -5179,22 +5174,9 @@ dependencies = [
name = "git_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"db",
"editor",
"git",
"gpui",
"language",
"project",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"theme",
"ui",
"util",
"windows 0.58.0",
"workspace",
]
@@ -10372,7 +10354,6 @@ dependencies = [
"alacritty_terminal",
"anyhow",
"async-dispatcher",
"async-tungstenite 0.28.1",
"base64 0.22.1",
"client",
"collections",
@@ -13979,6 +13960,7 @@ dependencies = [
"futures-lite 1.13.0",
"git2",
"globset",
"itertools 0.13.0",
"log",
"rand 0.8.5",
"regex",
@@ -16146,7 +16128,7 @@ dependencies = [
name = "zed_elixir"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.1.0",
]
[[package]]
@@ -16190,17 +16172,6 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "zed_extension_api"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fd16b8b30a9dc920fc1678ff852f696b5bdf5b5843bc745a128be0aac29859e"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
[[package]]
name = "zed_glsl"
version = "0.1.0"
@@ -16429,7 +16400,6 @@ name = "zeta"
version = "0.1.0"
dependencies = [
"anyhow",
"arrayvec",
"call",
"client",
"clock",
@@ -16456,6 +16426,7 @@ dependencies = [
"tree-sitter-go",
"tree-sitter-rust",
"ui",
"util",
"uuid",
"workspace",
"worktree",

View File

@@ -59,11 +59,6 @@
"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",
@@ -113,7 +108,6 @@
"mdf": "storage",
"mdx": "document",
"metadata": "code",
"metal": "metal",
"mjs": "javascript",
"mka": "audio",
"mkv": "video",
@@ -323,9 +317,6 @@
"lua": {
"icon": "icons/file_icons/lua.svg"
},
"metal": {
"icon": "icons/file_icons/metal.svg"
},
"nim": {
"icon": "icons/file_icons/nim.svg"
},

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 269 B

View File

@@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 268 B

View File

@@ -15,10 +15,8 @@
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"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 }],
"ctrl-a": "editor::MoveToBeginningOfLine",
"ctrl-e": "editor::MoveToEndOfLine",
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
@@ -55,14 +53,6 @@
"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": {

View File

@@ -12,7 +12,7 @@
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-d": "editor::DuplicateSelection",
"ctrl-d": "editor::DuplicateLineDown",
"ctrl-y": "editor::DeleteLine",
"ctrl-m": "editor::ScrollCursorCenter",
"ctrl-pagedown": "editor::MovePageDown",

View File

@@ -15,7 +15,7 @@
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"ctrl-shift-d": "editor::DuplicateSelection",
"ctrl-shift-d": "editor::DuplicateLineDown",
"alt-f3": "editor::SelectAllMatches", // find_all_under
"f12": "editor::GoToDefinition",
"ctrl-f12": "editor::GoToDefinitionSplit",

View File

@@ -15,10 +15,8 @@
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"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 }],
"ctrl-a": "editor::MoveToBeginningOfLine",
"ctrl-e": "editor::MoveToEndOfLine",
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
@@ -55,14 +53,6 @@
"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": {

View File

@@ -11,7 +11,7 @@
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"ctrl-shift-j": "editor::JoinLines",
"cmd-d": "editor::DuplicateSelection",
"cmd-d": "editor::DuplicateLineDown",
"cmd-backspace": "editor::DeleteLine",
"cmd-pagedown": "editor::MovePageDown",
"cmd-pageup": "editor::MovePageUp",

View File

@@ -18,7 +18,7 @@
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"cmd-shift-l": "editor::SplitSelectionIntoLines",
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
"cmd-shift-d": "editor::DuplicateSelection",
"cmd-shift-d": "editor::DuplicateLineDown",
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
"shift-f12": "editor::FindAllReferences",
"alt-cmd-down": "editor::GoToDefinition",

View File

@@ -144,9 +144,6 @@
// 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,
@@ -474,14 +471,6 @@
// 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 `👋`.

View File

@@ -55,7 +55,7 @@ use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
ZED_CLOUD_PROVIDER_ID,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_model_selector::{LanguageModelPickerDelegate, LanguageModelSelector};
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<LanguageModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
model_summary_editor: View<Editor>,
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
configuration_subscription: Option<Subscription>,
@@ -305,7 +305,7 @@ impl PickerDelegate for SavedContextPickerDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.child(item),
)
}
@@ -341,12 +341,11 @@ 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(|cx| {
let context_editor_toolbar = cx.new_view(|_| {
ContextEditorToolbarItem::new(
workspace,
model_selector_menu_handle.clone(),
model_summary_editor.clone(),
cx,
)
});
@@ -442,7 +441,7 @@ impl AssistantPanel {
)
}
})
.toggle_state(
.selected(
pane.active_item()
.map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
);
@@ -4456,36 +4455,23 @@ impl FollowableItem for ContextEditor {
}
pub struct ContextEditorToolbarItem {
fs: Arc<dyn Fs>,
active_context_editor: Option<WeakView<ContextEditor>>,
model_summary_editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
}
impl ContextEditorToolbarItem {
pub fn new(
workspace: &Workspace,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
model_summary_editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
fs: workspace.app_state().fs.clone(),
active_context_editor: None,
model_summary_editor,
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,
model_selector_menu_handle,
}
}
@@ -4574,8 +4560,17 @@ impl Render for ContextEditorToolbarItem {
// .map(|remaining_items| format!("Files to scan: {}", remaining_items))
// })
.child(
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
LanguageModelSelector::new(
{
let fs = self.fs.clone();
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
}
},
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
@@ -4621,7 +4616,7 @@ impl Render for ContextEditorToolbarItem {
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
}),
)
.with_handle(self.language_model_selector_menu_handle.clone()),
.with_handle(self.model_selector_menu_handle.clone()),
)
.children(self.render_remaining_tokens(cx));
@@ -4956,7 +4951,7 @@ fn render_slash_command_output_toggle(
("slash-command-output-fold-indicator", row.0 as u64),
!is_folded,
)
.toggle_state(is_folded)
.selected(is_folded)
.on_click(move |_e, cx| fold(!is_folded, cx))
.into_any_element()
}
@@ -4971,7 +4966,7 @@ fn fold_toggle(
) -> AnyElement {
move |row, is_folded, fold, _cx| {
Disclosure::new((name, row.0 as u64), !is_folded)
.toggle_state(is_folded)
.selected(is_folded)
.on_click(move |_e, cx| fold(!is_folded, cx))
.into_any_element()
}
@@ -5013,7 +5008,7 @@ fn render_quote_selection_output_toggle(
_cx: &mut WindowContext,
) -> AnyElement {
Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
.toggle_state(is_folded)
.selected(is_folded)
.on_click(move |_e, cx| fold(!is_folded, cx))
.into_any_element()
}
@@ -5036,7 +5031,7 @@ fn render_pending_slash_command_gutter_decoration(
icon = icon.icon_color(Color::Muted);
}
PendingSlashCommandStatus::Running { .. } => {
icon = icon.toggle_state(true);
icon = icon.selected(true);
}
PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
}

View File

@@ -33,7 +33,7 @@ use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelTextStream, Role,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_model_selector::LanguageModelSelector;
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,27 +1500,43 @@ impl Render for PromptEditor {
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2()
.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,
)
}),
))
.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.",
),
)
.map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
return el;
@@ -1534,7 +1550,7 @@ impl Render for PromptEditor {
v_flex()
.child(
IconButton::new("rate-limit-error", IconName::XCircle)
.toggle_state(self.show_rate_limit_notice)
.selected(self.show_rate_limit_notice)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(cx.listener(Self::toggle_rate_limit_notice)),
@@ -1626,19 +1642,6 @@ 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,
@@ -1647,6 +1650,7 @@ 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,
@@ -2133,15 +2137,15 @@ impl PromptEditor {
"dont-show-again",
Label::new("Don't show again"),
if dismissed_rate_limit_notice() {
ui::ToggleState::Selected
ui::Selection::Selected
} else {
ui::ToggleState::Unselected
ui::Selection::Unselected
},
|selection, cx| {
let is_dismissed = match selection {
ui::ToggleState::Unselected => false,
ui::ToggleState::Indeterminate => return,
ui::ToggleState::Selected => true,
ui::Selection::Unselected => false,
ui::Selection::Indeterminate => return,
ui::Selection::Selected => true,
};
set_rate_limit_notice_dismissed(is_dismissed, cx)

View File

@@ -11,8 +11,8 @@ use futures::{
use fuzzy::StringMatchCandidate;
use gpui::{
actions, point, size, transparent_black, Action, AppContext, BackgroundExecutor, Bounds,
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TextStyle, TitlebarOptions,
UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
};
use heed::{
types::{SerdeBincode, SerdeJson, Str},
@@ -232,13 +232,13 @@ impl PickerDelegate for PromptPickerDelegate {
let element = ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
prompt.title.clone().unwrap_or("Untitled".into()),
)))
.end_slot::<IconButton>(default.then(|| {
IconButton::new("toggle-default-prompt", IconName::SparkleFilled)
.toggle_state(true)
.selected(true)
.icon_color(Color::Accent)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
@@ -274,7 +274,7 @@ impl PickerDelegate for PromptPickerDelegate {
})
.child(
IconButton::new("toggle-default-prompt", IconName::Sparkle)
.toggle_state(default)
.selected(default)
.selected_icon(IconName::SparkleFilled)
.icon_color(if default { Color::Accent } else { Color::Muted })
.shape(IconButtonShape::Square)
@@ -928,8 +928,10 @@ impl PromptLibrary {
status: cx.theme().status().clone(),
inlay_hints_style:
editor::make_inlay_hints_style(cx),
inline_completion_styles:
editor::make_suggestion_styles(cx),
suggestions_style: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
..EditorStyle::default()
},
)),
@@ -1053,7 +1055,7 @@ impl PromptLibrary {
IconName::Sparkle,
)
.style(ButtonStyle::Transparent)
.toggle_state(prompt_metadata.default)
.selected(prompt_metadata.default)
.selected_icon(IconName::SparkleFilled)
.icon_color(if prompt_metadata.default {
Color::Accent

View File

@@ -176,7 +176,7 @@ impl PickerDelegate for SlashCommandDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.toggle_state(selected)
.selected(selected)
.tooltip({
let description = info.description.clone();
move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into()
@@ -229,7 +229,7 @@ impl PickerDelegate for SlashCommandDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.toggle_state(selected)
.selected(selected)
.child(renderer(cx)),
),
}

View File

@@ -20,7 +20,7 @@ use language::Buffer;
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_model_selector::LanguageModelSelector;
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,8 +614,17 @@ impl Render for PromptEditor {
.w_12()
.justify_center()
.gap_2()
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
.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)
@@ -709,19 +718,6 @@ 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,
@@ -729,6 +725,7 @@ 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,

View File

@@ -28,11 +28,8 @@ editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
handlebars.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
@@ -61,7 +58,6 @@ 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

View File

@@ -7,7 +7,6 @@ mod inline_assistant;
mod message_editor;
mod prompts;
mod streaming_diff;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
@@ -64,12 +63,6 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mu
client.telemetry().clone(),
cx,
);
terminal_inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
feature_gate_assistant2_actions(cx);
}

View File

@@ -88,13 +88,13 @@ impl AssistantPanel {
thread: cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
workspace.clone(),
workspace,
language_registry,
tools.clone(),
cx,
)
}),
message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)),
message_editor: cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)),
tools,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
@@ -123,8 +123,7 @@ impl AssistantPanel {
cx,
)
});
self.message_editor =
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
@@ -146,8 +145,7 @@ impl AssistantPanel {
cx,
)
});
self.message_editor =
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}

View File

@@ -23,5 +23,4 @@ pub struct Context {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
FetchedUrl,
}

View File

@@ -1,101 +1,15 @@
mod fetch_context_picker;
mod file_context_picker;
use std::sync::Arc;
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 gpui::{DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
use crate::message_editor::MessageEditor;
#[derive(Debug, Clone)]
enum ContextPickerMode {
Default,
File(View<FileContextPicker>),
Fetch(View<FetchContextPicker>),
}
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: "fetch".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),
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
}
}
}
impl Render for ContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.w(px(400.))
.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()),
ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
})
}
#[derive(IntoElement)]
pub(super) struct ContextPicker<T: PopoverTrigger> {
message_editor: WeakView<MessageEditor>,
trigger: T,
}
#[derive(Clone)]
@@ -106,18 +20,26 @@ struct ContextPickerEntry {
}
pub(crate) struct ContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
all_entries: Vec<ContextPickerEntry>,
filtered_entries: Vec<ContextPickerEntry>,
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.entries.len()
self.filtered_entries.len()
}
fn selected_index(&self) -> usize {
@@ -125,7 +47,7 @@ impl PickerDelegate for ContextPickerDelegate {
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
cx.notify();
}
@@ -133,51 +55,52 @@ impl PickerDelegate for ContextPickerDelegate {
"Select a context source…".into()
}
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
Task::ready(())
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 confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
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,
)
}));
}
"fetch" => {
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
FetchContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.message_editor.clone(),
cx,
)
}));
}
_ => {}
}
cx.focus_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);
})
.log_err();
.ok();
cx.emit(DismissEvent);
}
}
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(_) | ContextPickerMode::Fetch(_) => {}
})
.log_err();
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
}
fn render_match(
@@ -186,13 +109,13 @@ impl PickerDelegate for ContextPickerDelegate {
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry = &self.entries[ix];
let entry = self.filtered_entries.get(ix)?;
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.toggle_state(selected)
.selected(selected)
.tooltip({
let description = entry.description.clone();
move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
@@ -225,3 +148,50 @@ 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))
}
}

View File

@@ -1,218 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::{bail, Context as _, Result};
use futures::AsyncReadExt as _;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClientWithUrl};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ListItemSpacing, ViewContext};
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::message_editor::MessageEditor;
pub struct FetchContextPicker {
picker: View<Picker<FetchContextPickerDelegate>>,
}
impl FetchContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, message_editor);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
}
}
impl FocusableView for FetchContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FetchContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
Html,
Plaintext,
Json,
}
pub struct FetchContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
url: String,
}
impl FetchContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
message_editor,
url: String::new(),
}
}
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let mut url = url.to_owned();
if !url.starts_with("https://") && !url.starts_with("http://") {
url = format!("https://{url}");
}
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
}
}
impl PickerDelegate for FetchContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
1
}
fn selected_index(&self) -> usize {
0
}
fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
"Enter a URL…".into()
}
fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
self.url = query;
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let http_client = workspace.read(cx).client().http_client().clone();
let url = self.url.clone();
cx.spawn(|this, mut cx| async move {
let text = Self::build_message(http_client, &url).await?;
this.update(&mut cx, |this, cx| {
this.delegate
.message_editor
.update(cx, |message_editor, _cx| {
message_editor.insert_context(ContextKind::FetchedUrl, url, 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);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(self.url.clone()),
)
}
}

View File

@@ -1,289 +0,0 @@
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);
})
.ok();
}
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)
.toggle_state(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
}

View File

@@ -2,7 +2,6 @@ use crate::{
assistant_settings::AssistantSettings,
prompts::PromptBuilder,
streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff},
terminal_inline_assistant::TerminalInlineAssistant,
CycleNextInlineAssist, CyclePreviousInlineAssist, ToggleInlineAssist,
};
use anyhow::{Context as _, Result};
@@ -31,7 +30,7 @@ use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelTextStream, Role,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_model_selector::LanguageModelSelector;
use language_models::report_assistant_event;
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
@@ -208,16 +207,16 @@ impl InlineAssistant {
.map_or(false, |provider| provider.is_authenticated(cx))
};
let handle_assist = |cx: &mut ViewContext<Workspace>| match inline_assist_target {
InlineAssistTarget::Editor(active_editor) => {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_editor, Some(cx.view().downgrade()), cx)
})
}
InlineAssistTarget::Terminal(active_terminal) => {
TerminalInlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_terminal, Some(cx.view().downgrade()), cx)
})
let handle_assist = |cx: &mut ViewContext<Workspace>| {
match inline_assist_target {
InlineAssistTarget::Editor(active_editor) => {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_editor, Some(cx.view().downgrade()), cx)
})
}
InlineAssistTarget::Terminal(_active_terminal) => {
// TODO show the terminal inline assistant
}
}
};
@@ -1454,8 +1453,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>,
@@ -1589,27 +1588,43 @@ impl Render for PromptEditor {
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2()
.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,
)
}),
))
.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.",
),
)
.map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
return el;
@@ -1623,7 +1638,7 @@ impl Render for PromptEditor {
v_flex()
.child(
IconButton::new("rate-limit-error", IconName::XCircle)
.toggle_state(self.show_rate_limit_notice)
.selected(self.show_rate_limit_notice)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(cx.listener(Self::toggle_rate_limit_notice)),
@@ -1698,19 +1713,6 @@ 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,
@@ -1719,6 +1721,7 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(),
codegen,
fs,
show_rate_limit_notice: false,
};
this.subscribe_to_editor(cx);
@@ -2065,15 +2068,15 @@ impl PromptEditor {
"dont-show-again",
Label::new("Don't show again"),
if dismissed_rate_limit_notice() {
ui::ToggleState::Selected
ui::Selection::Selected
} else {
ui::ToggleState::Unselected
ui::Selection::Unselected
},
|selection, cx| {
let is_dismissed = match selection {
ui::ToggleState::Unselected => false,
ui::ToggleState::Indeterminate => return,
ui::ToggleState::Selected => true,
ui::Selection::Unselected => false,
ui::Selection::Indeterminate => return,
ui::Selection::Selected => true,
};
set_rate_limit_notice_dismissed(is_dismissed, cx)

View File

@@ -1,19 +1,19 @@
use std::rc::Rc;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_model_selector::LanguageModelSelector;
use picker::Picker;
use settings::Settings;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
PopoverMenu, PopoverMenuHandle, Tooltip,
PopoverMenuHandle, Tooltip,
};
use workspace::Workspace;
use crate::context::{Context, ContextId, ContextKind};
use crate::context_picker::ContextPicker;
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
use crate::thread::{RequestKind, Thread};
use crate::ui::ContextPill;
use crate::{Chat, ToggleModelSelector};
@@ -23,20 +23,13 @@ pub struct MessageEditor {
editor: View<Editor>,
context: Vec<Context>,
next_context_id: ContextId,
context_picker: View<ContextPicker>,
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
use_tools: bool,
}
impl MessageEditor {
pub fn new(
workspace: WeakView<Workspace>,
thread: Model<Thread>,
cx: &mut ViewContext<Self>,
) -> Self {
let weak_self = cx.view().downgrade();
Self {
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
let mut this = Self {
thread,
editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
@@ -46,32 +39,18 @@ impl MessageEditor {
}),
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(),
this.context.push(Context {
id: this.next_context_id.post_inc(),
name: "shape.rs".into(),
kind: ContextKind::File,
text: "```rs\npub enum Shape {\n Circle,\n Square,\n Triangle,\n}".into(),
});
this
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
@@ -129,8 +108,10 @@ impl MessageEditor {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
LanguageModelSelector::new(
|model, _cx| {
println!("Selected {:?}", model.name());
},
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
@@ -186,7 +167,6 @@ 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")
@@ -199,22 +179,12 @@ impl Render for MessageEditor {
h_flex()
.flex_wrap()
.gap_2()
.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()),
)
.child(ContextPicker::new(
cx.view().downgrade(),
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
))
.children(self.context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
@@ -268,8 +238,8 @@ impl Render for MessageEditor {
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
};
}),
)))

View File

@@ -288,25 +288,4 @@ impl PromptBuilder {
};
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)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -193,19 +193,12 @@ impl Thread {
if let Some(context) = self.context_for_message(message.id) {
let mut file_context = String::new();
let mut fetch_context = String::new();
for context in context.iter() {
match context.kind {
ContextKind::File => {
file_context.push_str(&context.text);
file_context.push('\n');
}
ContextKind::FetchedUrl => {
fetch_context.push_str(&context.name);
fetch_context.push('\n');
fetch_context.push_str(&context.text);
fetch_context.push('\n');
file_context.push_str("\n");
}
}
}
@@ -216,11 +209,6 @@ impl Thread {
context_text.push_str(&file_context);
}
if !fetch_context.is_empty() {
context_text.push_str("The following fetched results are available\n");
context_text.push_str(&fetch_context);
}
request_message
.content
.push(MessageContent::Text(context_text))

View File

@@ -20,7 +20,7 @@ pub struct CallSettingsContent {
/// Whether your current project should be shared when joining an empty channel.
///
/// Default: false
/// Default: true
pub share_on_join: Option<bool>,
}

View File

@@ -1288,12 +1288,6 @@ 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()

View File

@@ -1307,12 +1307,6 @@ 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()

View File

@@ -841,7 +841,7 @@ impl CollabPanel {
ListItem::new(SharedString::from(user.github_login.clone()))
.start_slot(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone()))
.toggle_state(is_selected)
.selected(is_selected)
.end_slot(if is_pending {
Label::new("Calling").color(Color::Muted).into_any_element()
} else if is_current_user {
@@ -894,7 +894,7 @@ impl CollabPanel {
.into();
ListItem::new(project_id as usize)
.toggle_state(is_selected)
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
this.workspace
.update(cx, |workspace, cx| {
@@ -924,7 +924,7 @@ impl CollabPanel {
let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
ListItem::new(("screen", id))
.toggle_state(is_selected)
.selected(is_selected)
.start_slot(
h_flex()
.gap_1()
@@ -964,7 +964,7 @@ impl CollabPanel {
let channel_store = self.channel_store.read(cx);
let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);
ListItem::new("channel-notes")
.toggle_state(is_selected)
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
this.open_channel_notes(channel_id, cx);
}))
@@ -996,7 +996,7 @@ impl CollabPanel {
let channel_store = self.channel_store.read(cx);
let has_messages_notification = channel_store.has_new_messages(channel_id);
ListItem::new("channel-chat")
.toggle_state(is_selected)
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
this.join_channel_chat(channel_id, cx);
}))
@@ -2253,7 +2253,7 @@ impl CollabPanel {
})
.inset(true)
.end_slot::<AnyElement>(button)
.toggle_state(is_selected),
.selected(is_selected),
)
}
@@ -2270,7 +2270,7 @@ impl CollabPanel {
let item = ListItem::new(github_login.clone())
.indent_level(1)
.indent_step_size(px(20.))
.toggle_state(is_selected)
.selected(is_selected)
.child(
h_flex()
.w_full()
@@ -2381,7 +2381,7 @@ impl CollabPanel {
ListItem::new(github_login.clone())
.indent_level(1)
.indent_step_size(px(20.))
.toggle_state(is_selected)
.selected(is_selected)
.child(
h_flex()
.w_full()
@@ -2425,7 +2425,7 @@ impl CollabPanel {
];
ListItem::new(("channel-invite", channel.id.0 as usize))
.toggle_state(is_selected)
.selected(is_selected)
.child(
h_flex()
.w_full()
@@ -2448,7 +2448,7 @@ impl CollabPanel {
ListItem::new("contact-placeholder")
.child(Icon::new(IconName::Plus))
.child(Label::new("Add a Contact"))
.toggle_state(is_selected)
.selected(is_selected)
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
}
@@ -2547,7 +2547,7 @@ impl CollabPanel {
// Add one level of depth for the disclosure arrow.
.indent_level(depth + 1)
.indent_step_size(px(20.))
.toggle_state(is_selected || is_active)
.selected(is_selected || is_active)
.toggle(disclosed)
.on_toggle(
cx.listener(move |this, _, cx| {

View File

@@ -89,15 +89,15 @@ impl ChannelModal {
cx.notify()
}
fn set_channel_visibility(&mut self, selection: &ToggleState, cx: &mut ViewContext<Self>) {
fn set_channel_visibility(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
self.channel_store.update(cx, |channel_store, cx| {
channel_store
.set_channel_visibility(
self.channel_id,
match selection {
ToggleState::Unselected => ChannelVisibility::Members,
ToggleState::Selected => ChannelVisibility::Public,
ToggleState::Indeterminate => return,
Selection::Unselected => ChannelVisibility::Members,
Selection::Selected => ChannelVisibility::Public,
Selection::Indeterminate => return,
},
cx,
)
@@ -159,9 +159,9 @@ impl Render for ChannelModal {
"is-public",
Label::new("Public").size(LabelSize::Small),
if visibility == ChannelVisibility::Public {
ui::ToggleState::Selected
ui::Selection::Selected
} else {
ui::ToggleState::Unselected
ui::Selection::Unselected
},
cx.listener(Self::set_channel_visibility),
))
@@ -386,7 +386,7 @@ impl PickerDelegate for ChannelModalDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.start_slot(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone()))
.end_slot(h_flex().gap_2().map(|slot| {

View File

@@ -151,7 +151,7 @@ impl PickerDelegate for ContactFinderDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.start_slot(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone()))
.end_slot::<Icon>(icon_path.map(Icon::from_path)),

View File

@@ -397,7 +397,7 @@ impl PickerDelegate for CommandPaletteDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.child(
h_flex()
.w_full()

View File

@@ -251,7 +251,6 @@ gpui::actions!(
DisplayCursorNames,
DuplicateLineDown,
DuplicateLineUp,
DuplicateSelection,
ExpandAllHunkDiffs,
ExpandMacroRecursively,
FindAllReferences,

View File

@@ -1,4 +1,9 @@
use std::{cell::Cell, cmp::Reverse, ops::Range, sync::Arc};
use std::{
cell::Cell,
cmp::{min, Reverse},
ops::Range,
sync::Arc,
};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
@@ -11,13 +16,14 @@ use language::{CodeLabel, Documentation};
use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use parking_lot::RwLock;
use parking_lot::{Mutex, RwLock};
use project::{CodeAction, Completion, TaskSourceKind};
use std::iter;
use task::ResolvedTask;
use ui::{
h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover,
StatefulInteractiveElement as _, Styled, StyledExt as _, Toggleable as _,
Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover, Selectable as _,
StatefulInteractiveElement as _, Styled, StyledExt as _,
};
use util::ResultExt as _;
use workspace::Workspace;
@@ -145,6 +151,7 @@ pub struct CompletionsMenu {
resolve_completions: bool,
pub aside_was_displayed: Cell<bool>,
show_completion_documentation: bool,
last_rendered_range: Arc<Mutex<Option<Range<usize>>>>,
}
impl CompletionsMenu {
@@ -173,7 +180,6 @@ impl CompletionsMenu {
sort_completions,
initial_position,
buffer,
show_completion_documentation,
completions: Arc::new(RwLock::new(completions)),
match_candidates,
matches: Vec::new().into(),
@@ -181,6 +187,8 @@ impl CompletionsMenu {
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
aside_was_displayed: Cell::new(aside_was_displayed),
show_completion_documentation,
last_rendered_range: Arc::new(Mutex::new(None)),
}
}
@@ -236,6 +244,7 @@ impl CompletionsMenu {
resolve_completions: false,
aside_was_displayed: Cell::new(false),
show_completion_documentation: false,
last_rendered_range: Arc::new(Mutex::new(None)),
}
}
@@ -244,11 +253,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = 0;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(0, provider, cx);
}
fn select_prev(
@@ -256,15 +261,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item > 0 {
self.selected_item -= 1;
} else {
self.selected_item = self.matches.len() - 1;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(self.prev_match_index(), provider, cx);
}
fn select_next(
@@ -272,15 +269,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item + 1 < self.matches.len() {
self.selected_item += 1;
} else {
self.selected_item = 0;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(self.next_match_index(), provider, cx);
}
fn select_last(
@@ -288,14 +277,41 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = self.matches.len() - 1;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(self.matches.len() - 1, provider, cx);
}
pub fn resolve_selected_completion(
fn update_selection_index(
&mut self,
match_index: usize,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item != match_index {
self.selected_item = match_index;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_visible_completions(provider, cx);
cx.notify();
}
}
fn prev_match_index(&self) -> usize {
if self.selected_item > 0 {
self.selected_item - 1
} else {
self.matches.len() - 1
}
}
fn next_match_index(&self) -> usize {
if self.selected_item + 1 < self.matches.len() {
self.selected_item + 1
} else {
0
}
}
pub fn resolve_visible_completions(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
@@ -307,10 +323,59 @@ impl CompletionsMenu {
return;
};
let completion_index = self.matches[self.selected_item].candidate_id;
// Attempt to resolve completions for every item that will be displayed. This matters
// because single line documentation may be displayed inline with the completion.
//
// When navigating to the very beginning or end of completions, `last_rendered_range` may
// have no overlap with the completions that will be displayed, so instead use a range based
// on the last rendered count.
const APPROXIMATE_VISIBLE_COUNT: usize = 12;
let last_rendered_range = self.last_rendered_range.lock().clone();
let visible_count = last_rendered_range
.clone()
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
let matches_range = if self.selected_item == 0 {
0..min(visible_count, self.matches.len())
} else if self.selected_item == self.matches.len() - 1 {
self.matches.len().saturating_sub(visible_count)..self.matches.len()
} else {
last_rendered_range.unwrap_or_else(|| self.selected_item..self.selected_item + 1)
};
// Expand the range to resolve more completions than are predicted to be visible, to reduce
// jank on navigation.
const EXTRA_TO_RESOLVE: usize = 4;
let matches_indices = util::iterate_expanded_and_wrapped_usize_range(
matches_range.clone(),
EXTRA_TO_RESOLVE,
EXTRA_TO_RESOLVE,
self.matches.len(),
);
// Avoid work by sometimes filtering out completions that already have documentation.
// This filtering doesn't happen if the completions are currently being updated.
let candidate_ids = matches_indices.map(|i| self.matches[i].candidate_id);
let candidate_ids = match self.completions.try_read() {
None => candidate_ids.collect::<Vec<usize>>(),
Some(completions) => candidate_ids
.filter(|i| completions[*i].documentation.is_none())
.collect::<Vec<usize>>(),
};
// Current selection is always resolved even if it already has documentation, to handle
// out-of-spec language servers that return more results later.
let selected_candidate_id = self.matches[self.selected_item].candidate_id;
let candidate_ids = iter::once(selected_candidate_id)
.chain(
candidate_ids
.into_iter()
.filter(|id| *id != selected_candidate_id),
)
.collect::<Vec<usize>>();
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
vec![completion_index],
candidate_ids,
self.completions.clone(),
cx,
);
@@ -406,11 +471,14 @@ impl CompletionsMenu {
.occlude()
});
let last_rendered_range = self.last_rendered_range.clone();
let list = uniform_list(
cx.view().clone(),
"completions",
matches.len(),
move |_editor, range, cx| {
last_rendered_range.lock().replace(range.clone());
let start_ix = range.start;
let completions_guard = completions.read();
@@ -473,7 +541,7 @@ impl CompletionsMenu {
div().min_w(px(220.)).max_w(px(540.)).child(
ListItem::new(mat.candidate_id)
.inset(true)
.toggle_state(item_ix == selected_item)
.selected(item_ix == selected_item)
.on_click(cx.listener(move |editor, _event, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_completion(

View File

@@ -535,16 +535,10 @@ pub(crate) struct Highlights<'a> {
pub styles: HighlightStyles,
}
#[derive(Clone, Copy, Debug)]
pub struct InlineCompletionStyles {
pub insertion: HighlightStyle,
pub whitespace: HighlightStyle,
}
#[derive(Default, Debug, Clone, Copy)]
pub struct HighlightStyles {
pub inlay_hint: Option<HighlightStyle>,
pub inline_completion: Option<InlineCompletionStyles>,
pub suggestion: Option<HighlightStyle>,
}
#[derive(Clone)]
@@ -865,7 +859,7 @@ impl DisplaySnapshot {
language_aware,
HighlightStyles {
inlay_hint: Some(editor_style.inlay_hints_style),
inline_completion: Some(editor_style.inline_completion_styles),
suggestion: Some(editor_style.suggestions_style),
},
)
.flat_map(|chunk| {

View File

@@ -62,9 +62,9 @@ impl Inlay {
}
}
pub fn inline_completion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
pub fn suggestion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
Self {
id: InlayId::InlineCompletion(id),
id: InlayId::Suggestion(id),
position,
text: text.into(),
}
@@ -346,15 +346,7 @@ impl<'a> Iterator for InlayChunks<'a> {
}
let mut highlight_style = match inlay.id {
InlayId::InlineCompletion(_) => {
self.highlight_styles.inline_completion.map(|s| {
if inlay.text.chars().all(|c| c.is_whitespace()) {
s.whitespace
} else {
s.insertion
}
})
}
InlayId::Suggestion(_) => self.highlight_styles.suggestion,
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
};
let next_inlay_highlight_endpoint;
@@ -701,7 +693,7 @@ impl InlayMap {
let inlay_id = if i % 2 == 0 {
InlayId::Hint(post_inc(next_inlay_id))
} else {
InlayId::InlineCompletion(post_inc(next_inlay_id))
InlayId::Suggestion(post_inc(next_inlay_id))
};
log::info!(
"creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}",
@@ -1397,7 +1389,7 @@ mod tests {
text: "|123|".into(),
},
Inlay {
id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
position: buffer.read(cx).snapshot(cx).anchor_after(3),
text: "|456|".into(),
},
@@ -1613,7 +1605,7 @@ mod tests {
text: "|456|".into(),
},
Inlay {
id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
position: buffer.read(cx).snapshot(cx).anchor_before(7),
text: "\n|567|\n".into(),
},

View File

@@ -176,7 +176,7 @@ use workspace::{
};
use workspace::{Item as WorkspaceItem, OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
use crate::hover_links::{find_url, find_url_from_range};
use crate::hover_links::find_url;
use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
pub const FILE_HEADER_HEIGHT: u32 = 2;
@@ -190,6 +190,8 @@ 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);
@@ -259,14 +261,14 @@ pub fn render_parsed_markdown(
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) enum InlayId {
InlineCompletion(usize),
Suggestion(usize),
Hint(usize),
}
impl InlayId {
fn id(&self) -> usize {
match self {
Self::InlineCompletion(id) => *id,
Self::Suggestion(id) => *id,
Self::Hint(id) => *id,
}
}
@@ -405,7 +407,7 @@ pub struct EditorStyle {
pub syntax: Arc<SyntaxTheme>,
pub status: StatusColors,
pub inlay_hints_style: HighlightStyle,
pub inline_completion_styles: InlineCompletionStyles,
pub suggestions_style: HighlightStyle,
pub unnecessary_code_fade: f32,
}
@@ -422,10 +424,7 @@ impl Default for EditorStyle {
// style and retrieve them directly from the theme.
status: StatusColors::dark(),
inlay_hints_style: HighlightStyle::default(),
inline_completion_styles: InlineCompletionStyles {
insertion: HighlightStyle::default(),
whitespace: HighlightStyle::default(),
},
suggestions_style: HighlightStyle::default(),
unnecessary_code_fade: Default::default(),
}
}
@@ -443,23 +442,13 @@ pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle {
}
}
pub fn make_suggestion_styles(cx: &WindowContext) -> InlineCompletionStyles {
InlineCompletionStyles {
insertion: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
whitespace: HighlightStyle {
background_color: Some(cx.theme().status().created_background),
..HighlightStyle::default()
},
}
}
type CompletionId = usize;
enum InlineCompletion {
Edit(Vec<(Range<Anchor>, String)>),
Edit {
edits: Vec<(Range<Anchor>, String)>,
applied_edits_buffer: Model<Buffer>,
},
Move(Anchor),
}
@@ -1418,15 +1407,6 @@ impl Editor {
key_context.add("inline_completion");
}
if !self
.selections
.disjoint
.iter()
.all(|selection| selection.start == selection.end)
{
key_context.add("selection");
}
key_context
}
@@ -3736,7 +3716,7 @@ impl Editor {
if editor.focus_handle.is_focused(cx) && menu.is_some() {
let mut menu = menu.unwrap();
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
*context_menu = Some(CodeContextMenu::Completions(menu));
drop(context_menu);
cx.notify();
@@ -4334,10 +4314,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(Duration::from_millis(debounce))
.timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT)
.await;
let highlights = if let Some(highlights) = cx
@@ -4540,7 +4520,7 @@ impl Editor {
selections.select_anchor_ranges([position..position]);
});
}
InlineCompletion::Edit(edits) => {
InlineCompletion::Edit { edits, .. } => {
if let Some(provider) = self.inline_completion_provider() {
provider.accept(cx);
}
@@ -4587,7 +4567,7 @@ impl Editor {
selections.select_anchor_ranges([position..position]);
});
}
InlineCompletion::Edit(edits) => {
InlineCompletion::Edit { edits, .. } => {
if edits.len() == 1 && edits[0].0.start == edits[0].0.end {
let text = edits[0].1.as_str();
let mut partial_completion = text
@@ -4704,6 +4684,7 @@ impl Editor {
let completion = provider.suggest(&buffer, cursor_buffer_position, cx)?;
let edits = completion
.edits
.clone()
.into_iter()
.map(|(range, new_text)| {
(
@@ -4737,13 +4718,13 @@ impl Editor {
let mut inlay_ids = Vec::new();
let invalidation_row_range;
let completion;
let editor_completion;
if cursor_row < edit_start_row {
invalidation_row_range = cursor_row..edit_end_row;
completion = InlineCompletion::Move(first_edit_start);
editor_completion = InlineCompletion::Move(first_edit_start);
} else if cursor_row > edit_end_row {
invalidation_row_range = edit_start_row..cursor_row;
completion = InlineCompletion::Move(first_edit_start);
editor_completion = InlineCompletion::Move(first_edit_start);
} else {
if edits
.iter()
@@ -4751,7 +4732,7 @@ impl Editor {
{
let mut inlays = Vec::new();
for (range, new_text) in &edits {
let inlay = Inlay::inline_completion(
let inlay = Inlay::suggestion(
post_inc(&mut self.next_inlay_id),
range.start,
new_text.as_str(),
@@ -4774,7 +4755,15 @@ impl Editor {
}
invalidation_row_range = edit_start_row..edit_end_row;
completion = InlineCompletion::Edit(edits);
let applied_edits_buffer = buffer.update(cx, |buffer, cx| buffer.branch(cx));
applied_edits_buffer.update(cx, |buffer, cx| {
buffer.edit(completion.edits, None, cx);
});
editor_completion = InlineCompletion::Edit {
edits,
applied_edits_buffer,
};
};
let invalidation_range = multibuffer
@@ -4786,7 +4775,7 @@ impl Editor {
self.active_inline_completion = Some(InlineCompletionState {
inlay_ids,
completion,
completion: editor_completion,
invalidation_range,
});
cx.notify();
@@ -4811,7 +4800,7 @@ impl Editor {
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.toggle_state(is_active)
.selected(is_active)
.tooltip({
let focus_handle = self.focus_handle.clone();
move |cx| {
@@ -4988,7 +4977,7 @@ impl Editor {
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.toggle_state(is_active)
.selected(is_active)
.on_click(cx.listener(move |editor, _e, cx| {
editor.focus(cx);
editor.toggle_code_actions(
@@ -6135,7 +6124,7 @@ impl Editor {
});
}
pub fn duplicate(&mut self, upwards: bool, whole_lines: bool, cx: &mut ViewContext<Self>) {
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;
let selections = self.selections.all::<Point>(cx);
@@ -6143,44 +6132,36 @@ impl Editor {
let mut edits = Vec::new();
let mut selections_iter = selections.iter().peekable();
while let Some(selection) = selections_iter.next() {
// Avoid duplicating the same lines twice.
let mut rows = selection.spanned_rows(false, &display_map);
// duplicate line-wise
if whole_lines || selection.start == selection.end {
// Avoid duplicating the same lines twice.
while let Some(next_selection) = selections_iter.peek() {
let next_rows = next_selection.spanned_rows(false, &display_map);
if next_rows.start < rows.end {
rows.end = next_rows.end;
selections_iter.next().unwrap();
} else {
break;
}
}
// Copy the text from the selected row region and splice it either at the start
// or end of the region.
let start = Point::new(rows.start.0, 0);
let end = Point::new(
rows.end.previous_row().0,
buffer.line_len(rows.end.previous_row()),
);
let text = buffer
.text_for_range(start..end)
.chain(Some("\n"))
.collect::<String>();
let insert_location = if upwards {
Point::new(rows.end.0, 0)
while let Some(next_selection) = selections_iter.peek() {
let next_rows = next_selection.spanned_rows(false, &display_map);
if next_rows.start < rows.end {
rows.end = next_rows.end;
selections_iter.next().unwrap();
} else {
start
};
edits.push((insert_location..insert_location, text));
} else {
// duplicate character-wise
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));
break;
}
}
// Copy the text from the selected row region and splice it either at the start
// or end of the region.
let start = Point::new(rows.start.0, 0);
let end = Point::new(
rows.end.previous_row().0,
buffer.line_len(rows.end.previous_row()),
);
let text = buffer
.text_for_range(start..end)
.chain(Some("\n"))
.collect::<String>();
let insert_location = if upwards {
Point::new(rows.end.0, 0)
} else {
start
};
edits.push((insert_location..insert_location, text));
}
self.transact(cx, |this, cx| {
@@ -6193,15 +6174,11 @@ impl Editor {
}
pub fn duplicate_line_up(&mut self, _: &DuplicateLineUp, cx: &mut ViewContext<Self>) {
self.duplicate(true, true, cx);
self.duplicate_line(true, cx);
}
pub fn duplicate_line_down(&mut self, _: &DuplicateLineDown, cx: &mut ViewContext<Self>) {
self.duplicate(false, true, cx);
}
pub fn duplicate_selection(&mut self, _: &DuplicateSelection, cx: &mut ViewContext<Self>) {
self.duplicate(false, false, cx);
self.duplicate_line(false, cx);
}
pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext<Self>) {
@@ -9299,42 +9276,23 @@ impl Editor {
}
pub fn open_url(&mut self, _: &OpenUrl, cx: &mut ViewContext<Self>) {
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)
let position = self.selections.newest_anchor().head();
let Some((buffer, buffer_position)) =
self.buffer.read(cx).text_anchor_for_position(position, cx)
else {
return;
};
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 {
cx.spawn(|editor, mut cx| async move {
if let Some((_, url)) = find_url(&buffer, buffer_position, cx.clone()) {
editor.update(&mut cx, |_, cx| {
cx.open_url(&url);
})
} else {
Ok(())
}
});
url_finder.detach();
})
.detach();
}
pub fn open_file(&mut self, _: &OpenFile, cx: &mut ViewContext<Self>) {
@@ -9907,9 +9865,10 @@ impl Editor {
font_weight: Some(FontWeight::BOLD),
..make_inlay_hints_style(cx)
},
inline_completion_styles: make_suggestion_styles(
cx,
),
suggestions_style: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
..EditorStyle::default()
},
))
@@ -13762,7 +13721,7 @@ impl EditorSnapshot {
if folded || (is_foldable && (row_contains_cursor || self.gutter_hovered)) {
Some(
Disclosure::new(("gutter_crease", buffer_row.0), !folded)
.toggle_state(folded)
.selected(folded)
.on_click(cx.listener_for(&editor, move |this, _e, cx| {
if folded {
this.unfold_at(&UnfoldAt { buffer_row }, cx);
@@ -13910,7 +13869,10 @@ impl Render for Editor {
syntax: cx.theme().syntax().clone(),
status: cx.theme().status().clone(),
inlay_hints_style: make_inlay_hints_style(cx),
inline_completion_styles: make_suggestion_styles(cx),
suggestions_style: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade,
},
)

View File

@@ -9,7 +9,6 @@ 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,
@@ -186,11 +185,6 @@ 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.
///

View File

@@ -265,8 +265,8 @@ impl RenderOnce for BufferFontLigaturesControl {
|selection, cx| {
Self::write(
match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
},
cx,
);
@@ -318,8 +318,8 @@ impl RenderOnce for InlineGitBlameControl {
|selection, cx| {
Self::write(
match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
},
cx,
);
@@ -371,8 +371,8 @@ impl RenderOnce for LineNumbersControl {
|selection, cx| {
Self::write(
match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
},
cx,
);

View File

@@ -25,14 +25,18 @@ use language::{
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use multi_buffer::MultiBufferIndentGuide;
use parking_lot::Mutex;
use pretty_assertions::{assert_eq, assert_ne};
use project::{buffer_store::BufferChangeSet, FakeFs};
use project::{
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
project_settings::{LspSettings, ProjectSettings},
};
use serde_json::{self, json};
use std::sync::atomic::{self, AtomicBool, AtomicUsize};
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::{
@@ -3891,28 +3895,6 @@ 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]
@@ -10742,6 +10724,62 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let item_0 = lsp::CompletionItem {
label: "abs".into(),
insert_text: Some("abs".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "abs".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
};
let items = iter::once(item_0.clone())
.chain((11..51).map(|i| lsp::CompletionItem {
label: format!("item_{}", i),
insert_text: Some(format!("item_{}", i)),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
}))
.collect::<Vec<_>>();
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "default": "data"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
let item_0_out = lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
insert_text_format: Some(default_insert_text_format),
..item_0
};
let items_out = iter::once(item_0_out)
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
data: Some(default_data.clone()),
insert_text_mode: Some(default_insert_text_mode),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: item.label.clone(),
})),
..item.clone()
}))
.collect::<Vec<lsp::CompletionItem>>();
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
@@ -10758,138 +10796,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke(".");
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "very": "special"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
let expect_first_item = Arc::new(AtomicBool::new(true));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_default_data = default_data.clone();
let closure_resolve_requests_number = resolve_requests_number.clone();
let closure_expect_first_item = expect_first_item.clone();
let closure_default_commit_characters = default_commit_characters.clone();
move |item_to_resolve, _| {
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
let default_data = closure_default_data.clone();
let default_commit_characters = closure_default_commit_characters.clone();
let expect_first_item = closure_expect_first_item.clone();
async move {
if expect_first_item.load(atomic::Ordering::Acquire) {
assert_eq!(
item_to_resolve.label, "Some(2)",
"Should have selected the first item"
);
assert_eq!(
item_to_resolve.data,
Some(json!({ "very": "special"})),
"First item should bring its own data for resolving"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"First item had no own commit characters and should inherit the default ones"
);
assert!(
matches!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
),
"First item should bring its own edit range for resolving"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(default_insert_text_format),
"First item had no own insert text format and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
"First item should bring its own insert text mode for resolving"
);
Ok(item_to_resolve)
} else {
assert_eq!(
item_to_resolve.label, "vec![2]",
"Should have selected the last item"
);
assert_eq!(
item_to_resolve.data,
Some(default_data),
"Last item has no own resolve data and should inherit the default one"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"Last item had no own commit characters and should inherit the default ones"
);
assert_eq!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: "vec![2]".to_string()
})),
"Last item had no own edit range and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(lsp::InsertTextFormat::PLAIN_TEXT),
"Last item should bring its own insert text format for resolving"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(default_insert_text_mode),
"Last item had no own insert text mode and should inherit the default one"
);
Ok(item_to_resolve)
}
}
}
}).detach();
let completion_data = default_data.clone();
let completion_characters = default_commit_characters.clone();
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let default_data = completion_data.clone();
let default_commit_characters = completion_characters.clone();
let items = items.clone();
async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
items: vec![
lsp::CompletionItem {
label: "Some(2)".into(),
insert_text: Some("Some(2)".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "Some(2)".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
},
lsp::CompletionItem {
label: "vec![2]".into(),
insert_text: Some("vec![2]".into()),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
},
],
items,
item_defaults: Some(lsp::CompletionListItemDefaults {
data: Some(default_data.clone()),
commit_characters: Some(default_commit_characters.clone()),
@@ -10906,6 +10821,21 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
.next()
.await;
let resolved_items = Arc::new(Mutex::new(Vec::new()));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_resolved_items = resolved_items.clone();
move |item_to_resolve, _| {
let closure_resolved_items = closure_resolved_items.clone();
async move {
closure_resolved_items.lock().push(item_to_resolve.clone());
Ok(item_to_resolve)
}
}
})
.detach();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.run_until_parked();
@@ -10917,40 +10847,50 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
completions_menu
.matches
.iter()
.map(|c| c.string.as_str())
.collect::<Vec<_>>(),
vec!["Some(2)", "vec![2]"]
.map(|c| c.string.clone())
.collect::<Vec<String>>(),
items_out
.iter()
.map(|completion| completion.label.clone())
.collect::<Vec<String>>()
);
}
CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
}
});
// Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
// with 4 from the end.
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
1,
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
*resolved_items.lock(),
[
&items_out[0..16],
&items_out[items_out.len() - 4..items_out.len()]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
cx.update_editor(|editor, cx| {
editor.context_menu_first(&ContextMenuFirst, cx);
editor.context_menu_prev(&ContextMenuPrev, cx);
});
cx.run_until_parked();
// Completions that have already been resolved are skipped.
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
2,
"After re-selecting the first item, another resolve request should have been sent"
);
expect_first_item.store(false, atomic::Ordering::Release);
cx.update_editor(|editor, cx| {
editor.context_menu_last(&ContextMenuLast, cx);
});
cx.run_until_parked();
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
3,
"After selecting the other item, another resolve request should have been sent"
*resolved_items.lock(),
[
// Selected item is always resolved even if it was resolved before.
&items_out[items_out.len() - 1..items_out.len()],
&items_out[items_out.len() - 16..items_out.len() - 4]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
}
#[gpui::test]

View File

@@ -33,10 +33,10 @@ use gpui::{
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
WeakView, WindowContext,
Model, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString,
Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View,
ViewContext, WeakView, WindowContext,
};
use gpui::{ClickEvent, Subscription};
use itertools::Itertools;
@@ -45,7 +45,7 @@ use language::{
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
ShowWhitespaceSetting,
},
ChunkRendererContext,
Buffer, ChunkRendererContext,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
@@ -218,7 +218,6 @@ 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);
@@ -2755,34 +2754,22 @@ impl EditorElement {
match &active_inline_completion.completion {
InlineCompletion::Move(target_position) => {
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)
let container_element = div()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().text_accent.opacity(0.2))
.border_color(cx.theme().colors().border)
.rounded_md()
.shadow_sm();
.px_1();
let target_display_point = target_position.to_display_point(editor_snapshot);
if target_display_point.row().as_f32() < scroll_top {
let mut element = container_element
.child(tab_kbd)
.child(Label::new("Jump to Edit").size(LabelSize::Small))
.child(
icon_container
.child(Icon::new(IconName::ArrowUp).size(IconSize::Small)),
h_flex()
.gap_1()
.child(Icon::new(IconName::Tab))
.child(Label::new("Jump to Edit"))
.child(Icon::new(IconName::ArrowUp)),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
@@ -2791,11 +2778,12 @@ 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(
icon_container
.child(Icon::new(IconName::ArrowDown).size(IconSize::Small)),
h_flex()
.gap_1()
.child(Icon::new(IconName::Tab))
.child(Label::new("Jump to Edit"))
.child(Icon::new(IconName::ArrowDown)),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
@@ -2807,8 +2795,12 @@ impl EditorElement {
Some(element)
} else {
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")),
)
.into_any();
let target_line_end = DisplayPoint::new(
@@ -2826,7 +2818,10 @@ impl EditorElement {
Some(element)
}
}
InlineCompletion::Edit(edits) => {
InlineCompletion::Edit {
edits,
applied_edits_buffer,
} => {
let edit_start = edits
.first()
.unwrap()
@@ -2850,7 +2845,12 @@ impl EditorElement {
return None;
}
let (text, highlights) = inline_completion_popover_text(editor_snapshot, edits, cx);
let (text, highlights) = inline_completion_popover_text(
editor_snapshot,
edits,
applied_edits_buffer.clone(),
cx,
);
let longest_row =
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
@@ -4317,54 +4317,89 @@ impl EditorElement {
fn inline_completion_popover_text(
editor_snapshot: &EditorSnapshot,
edits: &Vec<(Range<Anchor>, String)>,
applied_edits_buffer: Model<Buffer>,
cx: &WindowContext,
) -> (String, Vec<(Range<usize>, HighlightStyle)>) {
let edit_start = edits
use text::{ToOffset as _, ToPoint as _};
let mut text = String::new();
let mut highlights = Vec::new();
let snapshot = applied_edits_buffer.read(cx).snapshot();
let mut start = edits
.first()
.unwrap()
.0
.start
.to_display_point(editor_snapshot);
let mut text = String::new();
let mut offset = DisplayPoint::new(edit_start.row(), 0).to_offset(editor_snapshot, Bias::Left);
let mut highlights = Vec::new();
.bias_left(&editor_snapshot.buffer_snapshot)
.text_anchor
.to_point(&snapshot);
start.column = 0;
let mut offset = text::ToOffset::to_offset(&start, &snapshot);
for (old_range, new_text) in edits {
let old_offset_range = old_range.to_offset(&editor_snapshot.buffer_snapshot);
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(offset..old_offset_range.start, false)
.map(|chunk| chunk.text),
);
offset = old_offset_range.end;
let edit_new_start = old_range
.start
.bias_left(&editor_snapshot.buffer_snapshot)
.text_anchor
.to_offset(&snapshot);
for chunk in snapshot.chunks(offset..edit_new_start, true) {
let start = text.len();
text.push_str(chunk.text);
let end = text.len();
let start = text.len();
text.push_str(new_text);
let end = text.len();
highlights.push((
start..end,
HighlightStyle {
if let Some(highlight_style) = chunk
.syntax_highlight_id
.and_then(|id| id.style(cx.theme().syntax()))
{
highlights.push((start..end, highlight_style));
}
}
let end_offset = edit_new_start + new_text.len();
for chunk in snapshot.chunks(edit_new_start..end_offset, true) {
let start = text.len();
text.push_str(chunk.text);
let end = text.len();
let mut highlight_style = HighlightStyle {
background_color: Some(cx.theme().status().created_background),
..Default::default()
},
));
};
if let Some(syntax_highlight_style) = chunk
.syntax_highlight_id
.and_then(|id| id.style(cx.theme().syntax()))
{
highlight_style.highlight(syntax_highlight_style);
}
highlights.push((start..end, highlight_style));
}
offset = end_offset;
}
let edit_end = edits
let mut end = edits
.last()
.unwrap()
.0
.end
.to_display_point(editor_snapshot);
let end_of_line = DisplayPoint::new(edit_end.row(), editor_snapshot.line_len(edit_end.row()))
.to_offset(editor_snapshot, Bias::Right);
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(offset..end_of_line, false)
.map(|chunk| chunk.text),
);
.bias_left(&editor_snapshot.buffer_snapshot)
.text_anchor
.to_point(&snapshot);
end.column = snapshot.line_len(end.row);
let end = text::ToOffset::to_offset(&end, &snapshot);
for chunk in snapshot.chunks(offset..end, true) {
let start = text.len();
text.push_str(chunk.text);
let end = text.len();
if let Some(highlight_style) = chunk
.syntax_highlight_id
.and_then(|id| id.style(cx.theme().syntax()))
{
highlights.push((start..end, highlight_style));
}
}
(text, highlights)
}
@@ -6665,7 +6700,7 @@ mod tests {
editor_tests::{init_test, update_test_language_settings},
Editor, MultiBuffer,
};
use gpui::{TestAppContext, VisualTestContext};
use gpui::{AppContext, TestAppContext, VisualTestContext};
use language::language_settings;
use log::info;
use std::num::NonZeroU32;
@@ -7135,7 +7170,12 @@ mod tests {
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
let edits = vec![(edit_range, " beautiful".to_string())];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
let (text, highlights) = inline_completion_popover_text(
&snapshot,
&edits,
create_applied_edits_buffer(editor, edits.clone(), cx),
cx,
);
assert_eq!(text, "Hello, beautiful world!");
assert_eq!(highlights.len(), 1);
@@ -7165,7 +7205,12 @@ mod tests {
"That".to_string(),
)];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
let (text, highlights) = inline_completion_popover_text(
&snapshot,
&edits,
create_applied_edits_buffer(editor, edits.clone(), cx),
cx,
);
assert_eq!(text, "That is a test.");
assert_eq!(highlights.len(), 1);
@@ -7202,7 +7247,12 @@ mod tests {
),
];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
let (text, highlights) = inline_completion_popover_text(
&snapshot,
&edits,
create_applied_edits_buffer(editor, edits.clone(), cx),
cx,
);
assert_eq!(text, "Greetings, world and universe!");
assert_eq!(highlights.len(), 2);
@@ -7252,7 +7302,12 @@ mod tests {
),
];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
let (text, highlights) = inline_completion_popover_text(
&snapshot,
&edits,
create_applied_edits_buffer(editor, edits.clone(), cx),
cx,
);
assert_eq!(text, "Second modified\nNew third line\nFourth updated line");
assert_eq!(highlights.len(), 3);
@@ -7271,6 +7326,29 @@ mod tests {
}
}
fn create_applied_edits_buffer(
editor: &Editor,
edits: Vec<(Range<Anchor>, String)>,
cx: &mut AppContext,
) -> Model<Buffer> {
let buffer = editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.update(cx, |buffer, cx| buffer.branch(cx));
buffer.update(cx, |buffer, cx| {
buffer.edit(
edits
.into_iter()
.map(|(range, text)| (range.start.text_anchor..range.end.text_anchor, text)),
None,
cx,
)
});
buffer
}
fn collect_invisibles_from_new_editor(
cx: &mut TestAppContext,
editor_mode: EditorMode,

View File

@@ -694,65 +694,6 @@ 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>>,

View File

@@ -359,7 +359,6 @@ 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()),
@@ -548,14 +547,11 @@ 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()
});
@@ -566,7 +562,6 @@ 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,

View File

@@ -726,7 +726,7 @@ impl Editor {
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.toggle_state(
.selected(
hunk_controls_menu_handle
.is_deployed(),
)

View File

@@ -237,7 +237,7 @@ fn assert_editor_active_edit_completion(
.as_ref()
.expect("editor has no active completion");
if let InlineCompletion::Edit(edits) = &completion_state.completion {
if let InlineCompletion::Edit { edits, .. } = &completion_state.completion {
assert(editor.buffer().read(cx).snapshot(cx), edits);
} else {
panic!("expected edit completion");

View File

@@ -841,12 +841,12 @@ mod tests {
.flat_map(|offset| {
[
Inlay {
id: InlayId::InlineCompletion(post_inc(&mut id)),
id: InlayId::Suggestion(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Left),
text: "test".into(),
},
Inlay {
id: InlayId::InlineCompletion(post_inc(&mut id)),
id: InlayId::Suggestion(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Right),
text: "test".into(),
},

View File

@@ -210,7 +210,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.disabled(disabled)
.child(
HighlightedLabel::new(

View File

@@ -933,7 +933,7 @@ impl ExtensionsPage {
fn update_settings<T: Settings>(
&mut self,
selection: &ToggleState,
selection: &Selection,
cx: &mut ViewContext<Self>,
callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
) {
@@ -942,8 +942,8 @@ impl ExtensionsPage {
let selection = *selection;
settings::update_settings_file::<T>(fs, cx, move |settings, _| {
let value = match selection {
ToggleState::Unselected => false,
ToggleState::Selected => true,
Selection::Unselected => false,
Selection::Selected => true,
_ => return,
};
@@ -998,9 +998,9 @@ impl ExtensionsPage {
"enable-vim",
Label::new("Enable vim mode"),
if VimModeSetting::get_global(cx).0 {
ui::ToggleState::Selected
ui::Selection::Selected
} else {
ui::ToggleState::Unselected
ui::Selection::Unselected
},
cx.listener(move |this, selection, cx| {
this.telemetry
@@ -1090,7 +1090,7 @@ impl Render for ExtensionsPage {
ToggleButton::new("filter-all", "All")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.toggle_state(self.filter == ExtensionFilter::All)
.selected(self.filter == ExtensionFilter::All)
.on_click(cx.listener(|this, _event, cx| {
this.filter = ExtensionFilter::All;
this.filter_extension_entries(cx);
@@ -1104,7 +1104,7 @@ impl Render for ExtensionsPage {
ToggleButton::new("filter-installed", "Installed")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.toggle_state(self.filter == ExtensionFilter::Installed)
.selected(self.filter == ExtensionFilter::Installed)
.on_click(cx.listener(|this, _event, cx| {
this.filter = ExtensionFilter::Installed;
this.filter_extension_entries(cx);
@@ -1118,9 +1118,7 @@ impl Render for ExtensionsPage {
ToggleButton::new("filter-not-installed", "Not Installed")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.toggle_state(
self.filter == ExtensionFilter::NotInstalled,
)
.selected(self.filter == ExtensionFilter::NotInstalled)
.on_click(cx.listener(|this, _event, cx| {
this.filter = ExtensionFilter::NotInstalled;
this.filter_extension_entries(cx);

View File

@@ -1228,7 +1228,7 @@ impl PickerDelegate for FileFinderDelegate {
.start_slot::<Icon>(file_icon)
.end_slot::<AnyElement>(history_icon)
.inset(true)
.toggle_state(selected)
.selected(selected)
.child(
h_flex()
.gap_2()

View File

@@ -414,7 +414,7 @@ impl PickerDelegate for NewPathDelegate {
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.inset(true)
.toggle_state(selected)
.selected(selected)
.child(LabelLike::new().child(m.styled_text(self.project.read(cx), cx))),
)
}

View File

@@ -283,7 +283,7 @@ impl PickerDelegate for OpenPathDelegate {
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.inset(true)
.toggle_state(selected)
.selected(selected)
.child(LabelLike::new().child(candidate.string.clone())),
)
}

View File

@@ -780,10 +780,7 @@ impl Fs for RealFs {
}
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
// 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()?;
let repo = git2::Repository::open(dotgit_path).log_err()?;
Some(Arc::new(RealGitRepository::new(
repo,
self.git_binary_path.clone(),

View File

@@ -15,4 +15,3 @@ doctest = false
[dependencies]
gpui.workspace = true
util.workspace = true
log.workspace = true

View File

@@ -61,23 +61,9 @@ impl StringMatch {
let mut positions = self.positions.iter().peekable();
iter::from_fn(move || {
if let Some(start) = positions.next().copied() {
if start >= self.string.len() {
log::error!(
"Invariant violation: Index {start} out of range in string {:?}",
self.string
);
return None;
}
let mut end = start + self.char_len_at_index(start);
while let Some(next_start) = positions.peek() {
if end == **next_start {
if end >= self.string.len() {
log::error!(
"Invariant violation: Index {end} out of range in string {:?}",
self.string
);
return None;
}
end += self.char_len_at_index(end);
positions.next();
} else {

View File

@@ -13,23 +13,10 @@ name = "git_ui"
path = "src/git_ui.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
git.workspace = true
gpui.workspace = true
language.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
ui.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -1,45 +0,0 @@
### 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

View File

@@ -1,40 +1,8 @@
use anyhow::Context;
use collections::HashMap;
use editor::Editor;
use language::Buffer;
use std::{
cell::OnceCell,
collections::HashSet,
ffi::OsStr,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use theme::ThemeSettings;
use git::repository::GitFileStatus;
use util::{ResultExt, TryFutureExt};
use db::kvp::KEY_VALUE_STORE;
use gpui::*;
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use ui::{
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
};
use ui::{prelude::*, Checkbox, Divider, DividerColor, ElevationIndex};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use crate::{git_status_icon, settings::GitPanelSettings, GitState};
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
actions!(git_panel, [ToggleFocus]);
const GIT_PANEL_KEY: &str = "GitPanel";
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
@@ -46,54 +14,12 @@ pub fn init(cx: &mut AppContext) {
.detach();
}
#[derive(Debug)]
pub enum Event {
Focus,
}
pub struct GitStatusEntry {}
#[derive(Debug, PartialEq, Eq, Clone)]
struct EntryDetails {
filename: String,
display_name: String,
path: Arc<Path>,
kind: EntryKind,
depth: usize,
is_expanded: bool,
status: Option<GitFileStatus>,
}
impl EntryDetails {
pub fn is_dir(&self) -> bool {
self.kind.is_dir()
}
}
#[derive(Serialize, Deserialize)]
struct SerializedGitPanel {
width: Option<Pixels>,
}
actions!(git_panel, [Deploy, ToggleFocus]);
#[derive(Clone)]
pub struct GitPanel {
_workspace: WeakView<Workspace>,
current_modifiers: Modifiers,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
hide_scrollbar_task: Option<Task<()>>,
pending_serialization: Task<Option<()>>,
project: Model<Project>,
scroll_handle: UniformListScrollHandle,
scrollbar_state: ScrollbarState,
selected_item: Option<usize>,
show_scrollbar: bool,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
git_state: Model<GitState>,
editor: View<Editor>,
// The entries that are currently shown in the panel, aka
// not hidden by folding or such
visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
width: Option<Pixels>,
}
@@ -103,449 +29,23 @@ impl GitPanel {
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
// Clippy incorrectly classifies this as a redundant closure
#[allow(clippy::redundant_closure)]
workspace.update(&mut cx, |workspace, cx| Self::new(workspace, cx))
workspace.update(&mut cx, |workspace, cx| {
let workspace_handle = workspace.weak_handle();
cx.new_view(|cx| Self::new(workspace_handle, cx))
})
})
}
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
let git_state = GitState::get_global(cx);
let fs = workspace.app_state().fs.clone();
let weak_workspace = workspace.weak_handle();
let project = workspace.project().clone();
let language_registry = workspace.app_state().languages.clone();
let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
let state = git_state.read(cx);
let current_commit_message = state.commit_message.clone();
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, |this, _, cx| {
this.hide_scrollbar(cx);
})
.detach();
cx.subscribe(&project, |this, _project, event, cx| match event {
project::Event::WorktreeRemoved(id) => {
this.expanded_dir_ids.remove(id);
this.update_visible_entries(None, cx);
cx.notify();
}
project::Event::WorktreeUpdatedEntries(_, _)
| project::Event::WorktreeAdded(_)
| project::Event::WorktreeOrderChanged => {
this.update_visible_entries(None, cx);
cx.notify();
}
_ => {}
})
.detach();
let editor = cx.new_view(|cx| {
let theme = ThemeSettings::get_global(cx);
let mut text_style = cx.text_style();
let refinement = TextStyleRefinement {
font_family: Some(theme.buffer_font.family.clone()),
font_features: Some(FontFeatures::disable_ligatures()),
font_size: Some(px(12.).into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(gpui::transparent_black()),
..Default::default()
};
text_style.refine(&refinement);
let mut editor = Editor::auto_height(10, cx);
if let Some(message) = current_commit_message {
editor.set_text(message, cx);
} else {
editor.set_text("", cx);
}
// editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_use_autoclose(false);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_text_style_refinement(refinement);
editor.set_placeholder_text("Enter commit message", cx);
editor
});
let buffer = editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("commit editor must be singleton");
cx.subscribe(&buffer, Self::on_buffer_event).detach();
let markdown = language_registry.language_for_name("Markdown");
cx.spawn(|_, mut cx| async move {
let markdown = markdown.await.context("failed to load Markdown language")?;
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx)
})
})
.detach_and_log_err(cx);
let scroll_handle = UniformListScrollHandle::new();
let mut this = Self {
_workspace: weak_workspace,
focus_handle: cx.focus_handle(),
fs,
pending_serialization: Task::ready(None),
project,
visible_entries: Vec::new(),
current_modifiers: cx.modifiers(),
expanded_dir_ids: Default::default(),
git_state,
editor,
width: Some(px(360.)),
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
scroll_handle,
selected_item: None,
show_scrollbar: !Self::should_autohide_scrollbar(cx),
hide_scrollbar_task: None,
};
this.update_visible_entries(None, cx);
this
});
git_panel
}
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
let width = self.width;
self.pending_serialization = cx.background_executor().spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
GIT_PANEL_KEY.into(),
serde_json::to_string(&SerializedGitPanel { width })?,
)
.await?;
anyhow::Ok(())
}
.log_err(),
);
}
fn dispatch_context(&self) -> KeyContext {
let mut dispatch_context = KeyContext::new_with_defaults();
dispatch_context.add("GitPanel");
dispatch_context.add("menu");
dispatch_context
}
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
if !self.focus_handle.contains_focused(cx) {
cx.emit(Event::Focus);
pub fn new(workspace: WeakView<Workspace>, cx: &mut ViewContext<Self>) -> Self {
Self {
_workspace: workspace,
focus_handle: cx.focus_handle(),
width: Some(px(360.)),
}
}
fn should_show_scrollbar(_cx: &AppContext) -> bool {
// TODO: plug into settings
true
}
fn should_autohide_scrollbar(_cx: &AppContext) -> bool {
// TODO: plug into settings
true
}
fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
if !Self::should_autohide_scrollbar(cx) {
return;
}
self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(&mut cx, |panel, cx| {
panel.show_scrollbar = false;
cx.notify();
})
.log_err();
}))
}
fn handle_modifiers_changed(
&mut self,
event: &ModifiersChangedEvent,
cx: &mut ViewContext<Self>,
) {
self.current_modifiers = event.modifiers;
cx.notify();
}
fn calculate_depth_and_difference(
entry: &Entry,
visible_worktree_entries: &HashSet<Arc<Path>>,
) -> (usize, usize) {
let (depth, difference) = entry
.path
.ancestors()
.skip(1) // Skip the entry itself
.find_map(|ancestor| {
if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
let entry_path_components_count = entry.path.components().count();
let parent_path_components_count = parent_entry.components().count();
let difference = entry_path_components_count - parent_path_components_count;
let depth = parent_entry
.ancestors()
.skip(1)
.filter(|ancestor| visible_worktree_entries.contains(*ancestor))
.count();
Some((depth + 1, difference))
} else {
None
}
})
.unwrap_or((0, 0));
(depth, difference)
}
}
impl GitPanel {
fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext<Self>) {
// TODO: Implement stage all
println!("Stage all triggered");
}
fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext<Self>) {
// TODO: Implement unstage all
println!("Unstage all triggered");
}
fn discard_all(&mut self, _: &DiscardAll, _cx: &mut ViewContext<Self>) {
// TODO: Implement discard all
println!("Discard all triggered");
}
fn clear_message(&mut self, cx: &mut ViewContext<Self>) {
let git_state = self.git_state.clone();
git_state.update(cx, |state, _cx| state.clear_message());
self.editor.update(cx, |editor, cx| editor.set_text("", cx));
}
/// Commit all staged changes
fn commit_staged_changes(&mut self, _: &CommitStagedChanges, cx: &mut ViewContext<Self>) {
self.clear_message(cx);
// TODO: Implement commit all staged
println!("Commit staged changes triggered");
}
/// Commit all changes, regardless of whether they are staged or not
fn commit_all_changes(&mut self, _: &CommitAllChanges, cx: &mut ViewContext<Self>) {
self.clear_message(cx);
// TODO: Implement commit all changes
println!("Commit all changes triggered");
}
fn all_staged(&self) -> bool {
// TODO: Implement all_staged
true
}
fn no_entries(&self) -> bool {
self.visible_entries.is_empty()
}
fn entry_count(&self) -> usize {
self.visible_entries
.iter()
.map(|(_, entries, _)| {
entries
.iter()
.filter(|entry| entry.git_status.is_some())
.count()
})
.sum()
}
fn for_each_visible_entry(
&self,
range: Range<usize>,
cx: &mut ViewContext<Self>,
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),
) {
let mut ix = 0;
for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
if ix >= range.end {
return;
}
if ix + visible_worktree_entries.len() <= range.start {
ix += visible_worktree_entries.len();
continue;
}
let end_ix = range.end.min(ix + visible_worktree_entries.len());
// let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name());
let expanded_entry_ids = self
.expanded_dir_ids
.get(&snapshot.id())
.map(Vec::as_slice)
.unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
let entries = entries_paths.get_or_init(|| {
visible_worktree_entries
.iter()
.map(|e| (e.path.clone()))
.collect()
});
for entry in visible_worktree_entries[entry_range].iter() {
let status = entry.git_status;
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);
let filename = match difference {
diff if diff > 1 => entry
.path
.iter()
.skip(entry.path.components().count() - diff)
.collect::<PathBuf>()
.to_str()
.unwrap_or_default()
.to_string(),
_ => entry
.path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
};
let display_name = entry.path.to_string_lossy().into_owned();
let details = EntryDetails {
filename,
display_name,
kind: entry.kind,
is_expanded,
path: entry.path.clone(),
status,
depth,
};
callback(entry.id, details, cx);
}
}
ix = end_ix;
}
}
// TODO: Update expanded directory state
fn update_visible_entries(
&mut self,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
cx: &mut ViewContext<Self>,
) {
let project = self.project.read(cx);
self.visible_entries.clear();
for worktree in project.visible_worktrees(cx) {
let snapshot = worktree.read(cx).snapshot();
let worktree_id = snapshot.id();
let mut visible_worktree_entries = Vec::new();
let mut entry_iter = snapshot.entries(true, 0);
while let Some(entry) = entry_iter.entry() {
// Only include entries with a git status
if entry.git_status.is_some() {
visible_worktree_entries.push(entry.clone());
}
entry_iter.advance();
}
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
project::sort_worktree_entries(&mut visible_worktree_entries);
if !visible_worktree_entries.is_empty() {
self.visible_entries
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
}
}
if let Some((worktree_id, entry_id)) = new_selected_entry {
self.selected_item = self.visible_entries.iter().enumerate().find_map(
|(worktree_index, (id, entries, _))| {
if *id == worktree_id {
entries
.iter()
.position(|entry| entry.id == entry_id)
.map(|entry_index| worktree_index * entries.len() + entry_index)
} else {
None
}
},
);
}
cx.notify();
}
fn on_buffer_event(
&mut self,
_buffer: Model<Buffer>,
event: &language::BufferEvent,
cx: &mut ViewContext<Self>,
) {
if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
let commit_message = self.editor.update(cx, |editor, cx| editor.text(cx));
self.git_state.update(cx, |state, _cx| {
state.commit_message = Some(commit_message.into());
});
cx.notify();
}
}
}
impl GitPanel {
pub fn panel_button(
&self,
id: impl Into<SharedString>,
label: impl Into<SharedString>,
) -> Button {
let id = id.into().clone();
let label = label.into().clone();
Button::new(id, label)
.label_size(LabelSize::Small)
.layer(ElevationIndex::ElevatedSurface)
.size(ButtonSize::Compact)
.style(ButtonStyle::Filled)
}
pub fn render_divider(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex()
.items_center()
.h(px(8.))
.child(Divider::horizontal_dashed().color(DividerColor::Border))
}
pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx).clone();
let changes_string = format!("{} changes", self.entry_count());
h_flex()
.h(px(32.))
.items_center()
@@ -553,277 +53,84 @@ impl GitPanel {
.bg(ElevationIndex::Surface.bg(cx))
.child(
h_flex()
.gap_2()
.gap_1()
.child(Checkbox::new("all-changes", true.into()).disabled(true))
.child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)),
.child(div().text_buffer(cx).text_ui_sm(cx).child("0 changes")),
)
.child(div().flex_grow())
.child(
h_flex()
.gap_2()
.gap_1()
.child(
IconButton::new("discard-changes", IconName::Undo)
.tooltip(move |cx| {
let focus_handle = focus_handle.clone();
Tooltip::for_action_in(
"Discard all changes",
&DiscardAll,
&focus_handle,
cx,
)
})
.icon_size(IconSize::Small)
.disabled(true),
)
.child(if self.all_staged() {
self.panel_button("unstage-all", "Unstage All").on_click(
cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(DiscardAll))),
)
} else {
self.panel_button("stage-all", "Stage All").on_click(
cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(StageAll))),
)
}),
.child(
Button::new("stage-all", "Stage All")
.label_size(LabelSize::Small)
.layer(ElevationIndex::ElevatedSurface)
.size(ButtonSize::Compact)
.style(ButtonStyle::Filled)
.disabled(true),
),
)
}
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
let git_state = self.git_state.clone();
let commit_message = git_state.read(cx).commit_message.clone();
let editor = self.editor.clone();
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
println!("{:?}", commit_message);
let focus_handle_1 = self.focus_handle(cx).clone();
let focus_handle_2 = self.focus_handle(cx).clone();
let commit_staged_button = self
.panel_button("commit-staged-changes", "Commit")
.tooltip(move |cx| {
let focus_handle = focus_handle_1.clone();
Tooltip::for_action_in(
"Commit all staged changes",
&CommitStagedChanges,
&focus_handle,
cx,
)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.commit_staged_changes(&CommitStagedChanges, cx)
}));
let commit_all_button = self
.panel_button("commit-all-changes", "Commit All")
.tooltip(move |cx| {
let focus_handle = focus_handle_2.clone();
Tooltip::for_action_in(
"Commit all changes, including unstaged changes",
&CommitAllChanges,
&focus_handle,
cx,
)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.commit_all_changes(&CommitAllChanges, cx)
}));
div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
v_flex()
.id("commit-editor-container")
.relative()
.h_full()
.py_2p5()
.px_3()
.bg(cx.theme().colors().editor_background)
.on_click(cx.listener(move |_, _: &ClickEvent, cx| cx.focus(&editor_focus_handle)))
.child(self.editor.clone())
.font_buffer(cx)
.text_ui_sm(cx)
.text_color(cx.theme().colors().text_muted)
.child("Add a message")
.gap_1()
.child(div().flex_grow())
.child(
h_flex()
.absolute()
.bottom_2p5()
.right_3()
.child(div().gap_1().flex_grow())
.child(if self.current_modifiers.alt {
commit_all_button
} else {
commit_staged_button
}),
),
)
}
fn render_empty_state(&self, cx: &ViewContext<Self>) -> impl IntoElement {
h_flex()
.h_full()
.flex_1()
.justify_center()
.items_center()
.child(
v_flex()
.gap_3()
.child("No changes to commit")
.text_ui_sm(cx)
.mx_auto()
.text_color(Color::Placeholder.color(cx)),
)
}
fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
if !Self::should_show_scrollbar(cx)
|| !(self.show_scrollbar || self.scrollbar_state.is_dragging())
{
return None;
}
Some(
div()
.occlude()
.id("project-panel-vertical-scroll")
.on_mouse_move(cx.listener(|_, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, cx| {
if !this.scrollbar_state.is_dragging()
&& !this.focus_handle.contains_focused(cx)
{
this.hide_scrollbar(cx);
cx.notify();
}
cx.stop_propagation();
}),
h_flex().child(div().gap_1().flex_grow()).child(
Button::new("commit", "Commit")
.label_size(LabelSize::Small)
.layer(ElevationIndex::ElevatedSurface)
.size(ButtonSize::Compact)
.style(ButtonStyle::Filled)
.disabled(true),
),
)
.on_scroll_wheel(cx.listener(|_, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_1()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(
// percentage as f32..end_offset as f32,
self.scrollbar_state.clone(),
)),
.cursor(CursorStyle::OperationNotAllowed)
.opacity(0.5),
)
}
fn render_entries(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let item_count = self
.visible_entries
.iter()
.map(|(_, worktree_entries, _)| worktree_entries.len())
.sum();
h_flex()
.size_full()
.overflow_hidden()
.child(
uniform_list(cx.view().clone(), "entries", item_count, {
|this, range, cx| {
let mut items = Vec::with_capacity(range.end - range.start);
this.for_each_visible_entry(range, cx, |id, details, cx| {
items.push(this.render_entry(id, details, cx));
});
items
}
})
.size_full()
.with_sizing_behavior(ListSizingBehavior::Infer)
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
// .with_width_from_item(self.max_width_item_index)
.track_scroll(self.scroll_handle.clone()),
)
.children(self.render_scrollbar(cx))
}
fn render_entry(
&self,
id: ProjectEntryId,
details: EntryDetails,
cx: &ViewContext<Self>,
) -> impl IntoElement {
let id = id.to_proto() as usize;
let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
let is_staged = ToggleState::Selected;
h_flex()
.id(id)
.h(px(28.))
.w_full()
.pl(px(12. + 12. * details.depth as f32))
.pr(px(4.))
.items_center()
.gap_2()
.font_buffer(cx)
.text_ui_sm(cx)
.when(!details.is_dir(), |this| {
this.child(Checkbox::new(checkbox_id, is_staged))
})
.when_some(details.status, |this, status| {
this.child(git_status_icon(status))
})
.child(h_flex().gap_1p5().child(details.display_name.clone()))
}
}
impl Render for GitPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let project = self.project.read(cx);
v_flex()
.id("git_panel")
.key_context(self.dispatch_context())
.track_focus(&self.focus_handle)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.when(!project.is_read_only(cx), |this| {
this.on_action(cx.listener(|this, &StageAll, cx| this.stage_all(&StageAll, cx)))
.on_action(
cx.listener(|this, &UnstageAll, cx| this.unstage_all(&UnstageAll, cx)),
)
.on_action(
cx.listener(|this, &DiscardAll, cx| this.discard_all(&DiscardAll, cx)),
)
.on_action(cx.listener(|this, &CommitStagedChanges, cx| {
this.commit_staged_changes(&CommitStagedChanges, cx)
}))
.on_action(cx.listener(|this, &CommitAllChanges, cx| {
this.commit_all_changes(&CommitAllChanges, cx)
}))
})
.on_hover(cx.listener(|this, hovered, cx| {
if *hovered {
this.show_scrollbar = true;
this.hide_scrollbar_task.take();
cx.notify();
} else if !this.focus_handle.contains_focused(cx) {
this.hide_scrollbar(cx);
}
}))
.size_full()
.overflow_hidden()
.key_context("GitPanel")
.font_buffer(cx)
.py_1()
.id("git_panel")
.track_focus(&self.focus_handle)
.size_full()
.overflow_hidden()
.bg(ElevationIndex::Surface.bg(cx))
.child(self.render_panel_header(cx))
.child(self.render_divider(cx))
.child(if !self.no_entries() {
self.render_entries(cx).into_any_element()
} else {
self.render_empty_state(cx).into_any_element()
})
.child(self.render_divider(cx))
.child(
h_flex()
.items_center()
.h(px(8.))
.child(Divider::horizontal_dashed().color(DividerColor::Border)),
)
.child(div().flex_1())
.child(
h_flex()
.items_center()
.h(px(8.))
.child(Divider::horizontal_dashed().color(DividerColor::Border)),
)
.child(self.render_commit_editor(cx))
}
}
@@ -834,8 +141,6 @@ impl FocusableView for GitPanel {
}
}
impl EventEmitter<Event> for GitPanel {}
impl EventEmitter<PanelEvent> for GitPanel {}
impl Panel for GitPanel {
@@ -843,35 +148,27 @@ impl Panel for GitPanel {
"GitPanel"
}
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
GitPanelSettings::get_global(cx).dock
fn position(&self, _cx: &gpui::WindowContext) -> DockPosition {
DockPosition::Left
}
fn position_is_valid(&self, position: DockPosition) -> bool {
matches!(position, DockPosition::Left | DockPosition::Right)
}
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
settings::update_settings_file::<GitPanelSettings>(
self.fs.clone(),
cx,
move |settings, _| settings.dock = Some(position),
);
}
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
self.width
.unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
fn size(&self, _cx: &gpui::WindowContext) -> Pixels {
self.width.unwrap_or(px(360.))
}
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
self.width = size;
self.serialize(cx);
cx.notify();
}
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button)
fn icon(&self, _cx: &gpui::WindowContext) -> Option<ui::IconName> {
Some(ui::IconName::GitBranch)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {

View File

@@ -1,89 +1 @@
use ::settings::Settings;
use git::repository::GitFileStatus;
use gpui::{actions, prelude::*, AppContext, Global, Hsla, Model};
use settings::GitPanelSettings;
use ui::{Color, Icon, IconName, IntoElement, SharedString};
pub mod git_panel;
mod settings;
actions!(
git_ui,
[
StageAll,
UnstageAll,
DiscardAll,
CommitStagedChanges,
CommitAllChanges,
ClearMessage
]
);
pub fn init(cx: &mut AppContext) {
GitPanelSettings::register(cx);
let git_state = cx.new_model(|_cx| GitState::new());
cx.set_global(GlobalGitState(git_state));
}
struct GlobalGitState(Model<GitState>);
impl Global for GlobalGitState {}
pub struct GitState {
commit_message: Option<SharedString>,
}
impl GitState {
pub fn new() -> Self {
GitState {
commit_message: None,
}
}
pub fn set_message(&mut self, message: Option<SharedString>) {
self.commit_message = message;
}
pub fn clear_message(&mut self) {
self.commit_message = None;
}
pub fn get_global(cx: &mut AppContext) -> Model<GitState> {
cx.global::<GlobalGitState>().0.clone()
}
}
// impl EventEmitter<Event> for GitState {}
// #[derive(Clone, Debug, PartialEq, Eq)]
// pub enum Event {}
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,
};
// 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))
}
GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
}
}

View File

@@ -1,41 +0,0 @@
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()
}
}

View File

@@ -48,17 +48,8 @@ impl CursorPosition {
) {
let editor = editor.downgrade();
self.update_position = cx.spawn(|cursor_position, mut cx| async move {
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;
}
if let Some(debounce) = debounce {
cx.background_executor().timer(debounce).await;
}
editor

View File

@@ -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 unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
return LRESULT(1);
}
let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsWindowStatePtr>;
if ptr.is_null() {

View File

@@ -4,8 +4,8 @@ use editor::{scroll::Autoscroll, Editor};
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
use fs::Fs;
use gpui::{
actions, div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement,
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
Render, Subscription, View, ViewContext, WeakView, WindowContext,
};
use language::{
language_settings::{
@@ -16,6 +16,7 @@ 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,
@@ -28,8 +29,6 @@ use workspace::{
use zed_actions::OpenBrowser;
use zeta::RateCompletionModal;
actions!(zeta, [RateCompletions]);
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
struct CopilotErrorToast;
@@ -205,22 +204,16 @@ impl Render for InlineCompletionButton {
}
div().child(
IconButton::new("zeta", IconName::ZedPredict)
.tooltip(|cx| {
Tooltip::with_meta(
"Zed Predict",
Some(&RateCompletions),
"Click to rate completions",
cx,
)
})
Button::new("zeta", "ζ")
.label_size(LabelSize::Small)
.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)),
)
}
}

View File

@@ -147,7 +147,7 @@ pub trait LanguageModel: Send + Sync {
let events = self.stream_completion(request, cx);
async move {
let mut events = events.await?.fuse();
let mut events = events.await?;
let mut message_id = None;
let mut first_item_text = None;

View File

@@ -1,10 +1,7 @@
use std::sync::Arc;
use feature_flags::ZedPro;
use gpui::{
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Task,
View, WeakView,
};
use gpui::{Action, AnyElement, AppContext, DismissEvent, SharedString, Task};
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
use picker::{Picker, PickerDelegate};
use proto::Plan;
@@ -15,101 +12,19 @@ const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &AppContext) + 'static>;
pub struct LanguageModelSelector {
picker: View<Picker<LanguageModelPickerDelegate>>,
}
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>,
pub struct LanguageModelSelector<T: PopoverTrigger> {
handle: Option<PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>>,
on_model_changed: OnModelChanged,
trigger: T,
handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
info_text: Option<SharedString>,
}
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))
}
pub struct LanguageModelPickerDelegate {
on_model_changed: OnModelChanged,
all_models: Vec<ModelInfo>,
filtered_models: Vec<ModelInfo>,
selected_index: usize,
}
#[derive(Clone)]
@@ -117,14 +32,34 @@ struct ModelInfo {
model: Arc<dyn LanguageModel>,
icon: IconName,
availability: LanguageModelAvailability,
is_selected: bool,
}
pub struct LanguageModelPickerDelegate {
language_model_selector: WeakView<LanguageModelSelector>,
on_model_changed: OnModelChanged,
all_models: Vec<ModelInfo>,
filtered_models: Vec<ModelInfo>,
selected_index: usize,
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
}
}
impl PickerDelegate for LanguageModelPickerDelegate {
@@ -207,15 +142,23 @@ 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>>) {
self.language_model_selector
.update(cx, |_this, cx| cx.emit(DismissEvent))
.ok();
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn render_header(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
let configured_models_count = LanguageModelRegistry::global(cx)
@@ -252,22 +195,11 @@ 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)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.start_slot(
div().pr_0p5().child(
Icon::new(model_info.icon)
@@ -303,7 +235,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
}),
),
)
.end_slot(div().when(is_selected, |this| {
.end_slot(div().when(model_info.is_selected, |this| {
this.child(
Icon::new(IconName::Check)
.color(Color::Accent)
@@ -364,3 +296,58 @@ 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))
}
}

View File

@@ -281,7 +281,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.start_slot::<Icon>(language_icon)
.child(HighlightedLabel::new(label, mat.positions.clone())),
)

View File

@@ -14,7 +14,7 @@ use lsp::{
};
use project::{search::SearchQuery, Project, WorktreeId};
use std::{borrow::Cow, sync::Arc};
use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState};
use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, Selection};
use workspace::{
item::{Item, ItemHandle},
searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
@@ -1251,9 +1251,9 @@ impl Render for LspLogToolbarItemView {
Checkbox::new(
"LspLogEnableRpcTrace",
if rpc_trace_enabled {
ToggleState::Selected
Selection::Selected
} else {
ToggleState::Unselected
Selection::Unselected
},
)
.on_click(cx.listener_for(
@@ -1261,7 +1261,7 @@ impl Render for LspLogToolbarItemView {
move |view, selection, cx| {
let enabled = matches!(
selection,
ToggleState::Selected
Selection::Selected
);
view.toggle_rpc_logging_for_server(
server_id, enabled, cx,

View File

@@ -1,5 +1,3 @@
(comment) @comment
[
(addition)
(new_file)
@@ -14,35 +12,4 @@
(location) @attribute
(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)
(command) @function

View File

@@ -315,10 +315,7 @@ impl ContextProvider for PythonContextProvider {
toolchains
.active_toolchain(worktree_id, "Python".into(), &mut cx)
.await
.map_or_else(
|| "python3".to_owned(),
|toolchain| format!("\"{}\"", toolchain.path),
)
.map_or_else(|| "python3".to_owned(), |toolchain| toolchain.path.into())
} else {
String::from("python3")
};
@@ -339,17 +336,14 @@ impl ContextProvider for PythonContextProvider {
TaskTemplate {
label: "execute selection".to_owned(),
command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![
"-c".to_owned(),
VariableName::SelectedText.template_value_with_whitespace(),
],
args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()],
..TaskTemplate::default()
},
// Execute an entire file
TaskTemplate {
label: format!("run '{}'", VariableName::File.template_value()),
command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![VariableName::File.template_value_with_whitespace()],
args: vec![VariableName::File.template_value()],
..TaskTemplate::default()
},
];
@@ -364,7 +358,7 @@ impl ContextProvider for PythonContextProvider {
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
VariableName::File.template_value_with_whitespace(),
VariableName::File.template_value(),
],
..TaskTemplate::default()
},
@@ -375,7 +369,7 @@ impl ContextProvider for PythonContextProvider {
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace()
"$ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
],
tags: vec![
"python-unittest-class".to_owned(),
@@ -394,7 +388,7 @@ impl ContextProvider for PythonContextProvider {
args: vec![
"-m".to_owned(),
"pytest".to_owned(),
VariableName::File.template_value_with_whitespace(),
VariableName::File.template_value(),
],
..TaskTemplate::default()
},
@@ -405,7 +399,7 @@ impl ContextProvider for PythonContextProvider {
args: vec![
"-m".to_owned(),
"pytest".to_owned(),
PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
"$ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
],
tags: vec![
"python-pytest-class".to_owned(),

View File

@@ -1,23 +1,18 @@
; Identifier naming conventions; these "soft conventions" should stay at the top of the file as they're often overridden
; CamelCase for classes
((identifier) @type.class
(#match? @type.class "^_*[A-Z][A-Za-z0-9_]*$"))
; ALL_CAPS for constants:
((identifier) @constant
(#match? @constant "^_*[A-Z][A-Z0-9_]*$"))
(attribute attribute: (identifier) @property)
(type (identifier) @type)
(generic_type (identifier) @type)
(comment) @comment
(string) @string
(escape_sequence) @string.escape
; Type alias
(type_alias_statement "type" @keyword)
; Identifier naming conventions
((identifier) @type.class
(#match? @type.class "^[A-Z]"))
((identifier) @constant
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
; TypeVar with constraints in type parameters
(type
(tuple (identifier) @type)
@@ -31,20 +26,15 @@
; Function calls
(decorator
"@" @punctuation.special
(identifier) @function.decorator)
(call
function: (attribute attribute: (identifier) @function.method.call))
(call
function: (identifier) @function.call)
(decorator
"@" @punctuation.special
[
(identifier) @function.decorator
(attribute attribute: (identifier) @function.decorator)
(call function: (identifier) @function.decorator.call)
(call (attribute attribute: (identifier) @function.decorator.call))
])
; Function and class definitions
(function_definition
@@ -57,9 +47,9 @@
(call
function: (identifier) @type.class.call
(#match? @type.class.call "^_*[A-Z][A-Za-z0-9_]*$"))
(#match? @type.class.call "^[A-Z][A-Z0-9_]*[a-z]"))
; Builtins
; Builtin functions
((call
function: (identifier) @function.builtin)
@@ -67,9 +57,6 @@
@function.builtin
"^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$"))
((identifier) @type.builtin
(#any-of? @type.builtin "int" "float" "complex" "bool" "list" "tuple" "range" "str" "bytes" "bytearray" "memoryview" "set" "frozenset" "dict"))
; Literals
[
@@ -92,6 +79,10 @@
(#match? @variable.special "^self|cls$")
]
(comment) @comment
(string) @string
(escape_sequence) @string.escape
[
"("
")"
@@ -123,10 +114,7 @@
. (expression_statement (string) @string.doc))
(module
[
(expression_statement (assignment))
(type_alias_statement)
]
(expression_statement (assignment))
. (expression_statement (string) @string.doc))
(class_definition
@@ -175,9 +163,6 @@
">>"
"|"
"~"
] @operator
[
"and"
"in"
"is"
@@ -185,7 +170,7 @@
"or"
"is not"
"not in"
] @keyword.operator
] @operator
[
"as"
@@ -200,7 +185,6 @@
"elif"
"else"
"except"
"except*"
"exec"
"finally"
"for"

View File

@@ -608,10 +608,6 @@ 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 {
@@ -648,7 +644,6 @@ impl LanguageServer {
will_rename: Some(true),
..Default::default()
}),
apply_edit: Some(true),
..Default::default()
}),
text_document: Some(TextDocumentClientCapabilities {
@@ -765,11 +760,9 @@ 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 {
@@ -783,7 +776,6 @@ impl LanguageServer {
}
}),
locale: None,
..Default::default()
}
}

View File

@@ -20,7 +20,7 @@ use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};
use ui::{
h_flex, relative, tooltip_container, v_flex, Checkbox, Clickable, Color, FluentBuilder,
IconButton, IconName, IconSize, InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview,
StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, ViewContext, VisibleOnHover,
Selection, StatefulInteractiveElement, StyledExt, StyledImage, ViewContext, VisibleOnHover,
VisualContext as _,
};
use workspace::Workspace;
@@ -180,9 +180,9 @@ fn render_markdown_list_item(
Checkbox::new(
"checkbox",
if *checked {
ToggleState::Selected
Selection::Selected
} else {
ToggleState::Unselected
Selection::Unselected
},
)
.when_some(
@@ -192,8 +192,8 @@ fn render_markdown_list_item(
let range = range.clone();
move |selection, cx| {
let checked = match selection {
ToggleState::Selected => true,
ToggleState::Unselected => false,
Selection::Selected => true,
Selection::Unselected => false,
_ => return,
};

View File

@@ -280,7 +280,7 @@ impl PickerDelegate for OutlineViewDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.child(
div()
.text_ui(cx)

View File

@@ -51,7 +51,7 @@ use workspace::{
ui::{
h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder,
HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, Label,
LabelCommon, ListItem, Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable,
LabelCommon, ListItem, Scrollbar, ScrollbarState, Selectable, StyledExt, StyledTypography,
Tooltip,
},
OpenInTerminal, WeakItemHandle, Workspace,
@@ -2045,7 +2045,7 @@ impl OutlinePanel {
ListItem::new(item_id)
.indent_level(depth)
.indent_step_size(px(settings.indent_size))
.toggle_state(is_active)
.selected(is_active)
.when_some(icon_element, |list_item, icon_element| {
list_item.child(h_flex().child(icon_element))
})

View File

@@ -58,7 +58,6 @@ impl Prettier {
"prettier.config.js",
"prettier.config.cjs",
".editorconfig",
".prettierignore",
];
pub async fn locate_prettier_installation(
@@ -135,101 +134,6 @@ 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,
@@ -297,7 +201,6 @@ impl Prettier {
&self,
buffer: &Model<Buffer>,
buffer_path: Option<PathBuf>,
ignore_dir: Option<PathBuf>,
cx: &mut AsyncAppContext,
) -> anyhow::Result<Diff> {
match self {
@@ -412,17 +315,11 @@ 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: {:?}, ignore_path: {:?}",
"Formatting file {:?} with prettier, plugins :{:?}, options: {:?}",
buffer.file().map(|f| f.full_path(cx)),
plugins,
prettier_options,
ignore_path,
);
anyhow::Ok(FormatParams {
@@ -432,7 +329,6 @@ impl Prettier {
plugins,
path: buffer_path,
prettier_options,
ignore_path,
},
})
})?
@@ -553,7 +449,6 @@ 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)]
@@ -945,150 +840,4 @@ 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"
);
}
}

View File

@@ -44,9 +44,7 @@ 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));
@@ -70,9 +68,7 @@ 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}`,
),
});
});
@@ -193,22 +189,6 @@ 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

View File

@@ -36,7 +36,6 @@ 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>,
}
@@ -66,13 +65,11 @@ 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() {
@@ -214,65 +211,6 @@ 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,
@@ -716,13 +654,6 @@ 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() {
@@ -740,7 +671,7 @@ pub(super) async fn format_with_prettier(
.flatten();
let format_result = prettier
.format(buffer, buffer_path, ignore_dir, cx)
.format(buffer, buffer_path, cx)
.await
.map(crate::lsp_store::FormatOperation::Prettier)
.with_context(|| format!("{} failed to format buffer", prettier_description));

View File

@@ -281,7 +281,6 @@ 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();
@@ -596,12 +595,6 @@ 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>,
@@ -3147,8 +3140,6 @@ 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;
@@ -3194,37 +3185,8 @@ 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| {
@@ -3360,11 +3322,12 @@ 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))
.selectable(false)
.selected(is_marked || is_active)
.when_some(canonical_path, |this, path| {
this.end_slot::<AnyElement>(
div()
@@ -3404,11 +3367,13 @@ impl ProjectPanel {
} else {
IconDecorationKind::Dot
},
default_color,
if is_marked || is_active {
item_colors.marked_active
} else {
item_colors.default
},
cx,
)
.group_name(Some(GROUP_NAME.into()))
.knockout_hover_color(bg_hover_color)
.color(decoration_color.color(cx))
.position(Point {
x: px(-2.),
@@ -3524,6 +3489,26 @@ 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>> {

View File

@@ -11,7 +11,7 @@ use std::{borrow::Cow, cmp::Reverse, sync::Arc};
use theme::ActiveTheme;
use util::ResultExt;
use workspace::{
ui::{v_flex, Color, Label, LabelCommon, LabelLike, ListItem, ListItemSpacing, Toggleable},
ui::{v_flex, Color, Label, LabelCommon, LabelLike, ListItem, ListItemSpacing, Selectable},
Workspace,
};
@@ -240,7 +240,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.child(
v_flex()
.child(

View File

@@ -395,7 +395,7 @@ impl PickerDelegate for RecentProjectsDelegate {
Some(
ListItem::new(ix)
.toggle_state(selected)
.selected(selected)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.child(

View File

@@ -653,7 +653,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new(("new-remote-project", ix))
.toggle_state(
.selected(
ssh_server.open_folder.focus_handle.contains_focused(cx),
)
.inset(true)
@@ -688,7 +688,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new(("server-options", ix))
.toggle_state(
.selected(
ssh_server.configure.focus_handle.contains_focused(cx),
)
.inset(true)
@@ -772,7 +772,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new((element_id_base, ix))
.toggle_state(navigation.focus_handle.contains_focused(cx))
.selected(navigation.focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(
@@ -984,7 +984,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new("add-nickname")
.toggle_state(entries[0].focus_handle.contains_focused(cx))
.selected(entries[0].focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
@@ -1043,7 +1043,7 @@ impl RemoteServerProjects {
})
.child(
ListItem::new("copy-server-address")
.toggle_state(entries[1].focus_handle.contains_focused(cx))
.selected(entries[1].focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Copy).color(Color::Muted))
@@ -1116,7 +1116,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new("remove-server")
.toggle_state(entries[2].focus_handle.contains_focused(cx))
.selected(entries[2].focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Trash).color(Color::Error))
@@ -1144,7 +1144,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new("go-back")
.toggle_state(entries[3].focus_handle.contains_focused(cx))
.selected(entries[3].focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(
@@ -1233,7 +1233,7 @@ impl RemoteServerProjects {
.anchor_scroll(state.add_new_server.scroll_anchor.clone())
.child(
ListItem::new("register-remove-server-button")
.toggle_state(state.add_new_server.focus_handle.contains_focused(cx))
.selected(state.add_new_server.focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Plus).color(Color::Muted))

View File

@@ -16,7 +16,6 @@ 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

View File

@@ -150,7 +150,7 @@ impl PickerDelegate for KernelPickerDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.child(
h_flex()
.w_full()

View File

@@ -3,11 +3,6 @@ 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 _;
@@ -16,8 +11,8 @@ use crate::Session;
use super::RunningKernel;
use anyhow::Result;
use jupyter_websocket_client::{
JupyterWebSocket, JupyterWebSocketReader, JupyterWebSocketWriter, KernelLaunchRequest,
KernelSpecsResponse, RemoteServer,
JupyterWebSocketReader, JupyterWebSocketWriter, KernelLaunchRequest, KernelSpecsResponse,
RemoteServer,
};
use std::{fmt::Debug, sync::Arc};
@@ -156,31 +151,7 @@ impl RemoteRunningKernel {
)
.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 (kernel_socket, _response) = remote_server.connect_to_kernel(&kernel_id).await?;
let (mut w, mut r): (JupyterWebSocketWriter, JupyterWebSocketReader) =
kernel_socket.split();

View File

@@ -209,7 +209,6 @@ impl Render for BufferSearchBar {
let input_base_styles = || {
h_flex()
.min_w_32()
.w(input_width)
.h_8()
.px_2()
@@ -272,7 +271,7 @@ impl Render for BufferSearchBar {
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.toggle_replace(&ToggleReplace, cx);
}))
.toggle_state(self.replace_enabled)
.selected(self.replace_enabled)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
@@ -300,7 +299,7 @@ impl Render for BufferSearchBar {
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.toggle_selection(&ToggleSelection, cx);
}))
.toggle_state(self.selection_search_enabled)
.selected(self.selection_search_enabled)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
@@ -530,11 +529,6 @@ 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);

View File

@@ -35,7 +35,7 @@ use std::{
use theme::ThemeSettings;
use ui::{
h_flex, prelude::*, utils::SearchInputWidth, v_flex, Icon, IconButton, IconButtonShape,
IconName, KeyBinding, Label, LabelCommon, LabelSize, Toggleable, Tooltip,
IconName, KeyBinding, Label, LabelCommon, LabelSize, Selectable, Tooltip,
};
use util::paths::PathMatcher;
use workspace::{
@@ -1595,7 +1595,6 @@ impl Render for ProjectSearchBar {
let input_base_styles = || {
h_flex()
.min_w_32()
.w(input_width)
.h_8()
.px_2()
@@ -1645,7 +1644,7 @@ impl Render for ProjectSearchBar {
.on_click(cx.listener(|this, _, cx| {
this.toggle_filters(cx);
}))
.toggle_state(
.selected(
self.active_project_search
.as_ref()
.map(|search| search.read(cx).filters_enabled)
@@ -1669,7 +1668,7 @@ impl Render for ProjectSearchBar {
.on_click(cx.listener(|this, _, cx| {
this.toggle_replace(&ToggleReplace, cx);
}))
.toggle_state(
.selected(
self.active_project_search
.as_ref()
.map(|search| search.read(cx).replace_enabled)
@@ -1878,7 +1877,7 @@ impl Render for ProjectSearchBar {
.child(
IconButton::new("project-search-opened-only", IconName::FileSearch)
.shape(IconButtonShape::Square)
.toggle_state(self.is_opened_only_enabled(cx))
.selected(self.is_opened_only_enabled(cx))
.tooltip(|cx| Tooltip::text("Only Search Open Files", cx))
.on_click(cx.listener(|this, _, cx| {
this.toggle_opened_only(cx);

View File

@@ -113,7 +113,7 @@ impl SearchOptions {
.on_click(action)
.style(ButtonStyle::Subtle)
.shape(IconButtonShape::Square)
.toggle_state(active)
.selected(active)
.tooltip({
let action = self.to_toggle_action();
let label = self.label();

View File

@@ -145,7 +145,7 @@ impl RenderOnce for ThemeModeControl {
ToggleButton::new("light", "Light")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.toggle_state(value == ThemeMode::Light)
.selected(value == ThemeMode::Light)
.on_click(|_, cx| Self::write(ThemeMode::Light, cx))
.first(),
)
@@ -153,7 +153,7 @@ impl RenderOnce for ThemeModeControl {
ToggleButton::new("system", "System")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.toggle_state(value == ThemeMode::System)
.selected(value == ThemeMode::System)
.on_click(|_, cx| Self::write(ThemeMode::System, cx))
.middle(),
)
@@ -161,7 +161,7 @@ impl RenderOnce for ThemeModeControl {
ToggleButton::new("dark", "Dark")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.toggle_state(value == ThemeMode::Dark)
.selected(value == ThemeMode::Dark)
.on_click(|_, cx| Self::write(ThemeMode::Dark, cx))
.last(),
)
@@ -375,8 +375,8 @@ impl RenderOnce for UiFontLigaturesControl {
|selection, cx| {
Self::write(
match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
},
cx,
);

View File

@@ -219,7 +219,7 @@ impl PickerDelegate for ScopeSelectorDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.child(HighlightedLabel::new(label, mat.positions.clone())),
)
}

View File

@@ -59,7 +59,7 @@ impl PickerDelegate for Delegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.child(Label::new(candidate)),
)
}

View File

@@ -407,7 +407,7 @@ impl PickerDelegate for TabSwitcherDelegate {
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.inset(true)
.toggle_state(selected)
.selected(selected)
.child(h_flex().w_full().child(label))
.start_slot::<Icon>(icon)
.map(|el| {

View File

@@ -135,10 +135,6 @@ impl VariableName {
pub fn template_value(&self) -> String {
format!("${self}")
}
/// Generates a `"$VARIABLE"`-like string, to be used instead of `Self::template_value` when expanded value could contain spaces or special characters.
pub fn template_value_with_whitespace(&self) -> String {
format!("\"${self}\"")
}
}
impl FromStr for VariableName {

View File

@@ -13,7 +13,7 @@ use task::{ResolvedTask, TaskContext, TaskTemplate};
use ui::{
div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement,
KeyBinding, LabelSize, ListItem, ListItemSpacing, RenderOnce, Toggleable, Tooltip,
KeyBinding, LabelSize, ListItem, ListItemSpacing, RenderOnce, Selectable, Tooltip,
WindowContext,
};
use util::ResultExt;
@@ -379,7 +379,7 @@ impl PickerDelegate for TasksModalDelegate {
};
item
})
.toggle_state(selected)
.selected(selected)
.child(highlighted_location.render(cx)),
)
}

View File

@@ -26,7 +26,7 @@ use terminal::{
Terminal,
};
use ui::{
prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable,
prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Selectable,
Tooltip,
};
use util::{ResultExt, TryFutureExt};
@@ -201,7 +201,7 @@ impl TerminalPanel {
let zoomed = pane.is_zoomed();
IconButton::new("toggle_zoom", IconName::Maximize)
.icon_size(IconSize::Small)
.toggle_state(zoomed)
.selected(zoomed)
.selected_icon(IconName::Minimize)
.on_click(cx.listener(|pane, _, cx| {
pane.toggle_zoom(&workspace::ToggleZoom, cx);

Some files were not shown because too many files have changed in this diff Show More