Compare commits
8 Commits
git-panel-
...
zeta-synta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a8965ccc7 | ||
|
|
3364cd9889 | ||
|
|
b6ac26b1e2 | ||
|
|
9ab17f5b32 | ||
|
|
d5f5a615e8 | ||
|
|
f4f618c9cb | ||
|
|
71b4d98367 | ||
|
|
77328e8300 |
4
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -26,8 +26,8 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, add 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@@ -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
35
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 `👋`.
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,5 +23,4 @@ pub struct Context {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
FetchedUrl,
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
)))
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -251,7 +251,6 @@ gpui::actions!(
|
||||
DisplayCursorNames,
|
||||
DuplicateLineDown,
|
||||
DuplicateLineUp,
|
||||
DuplicateSelection,
|
||||
ExpandAllHunkDiffs,
|
||||
ExpandMacroRecursively,
|
||||
FindAllReferences,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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())),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -15,4 +15,3 @@ doctest = false
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
util.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -395,7 +395,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.child(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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())),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user