Detailed changes
@@ -24,11 +24,13 @@ jobs:
- name: Prettier Check on /docs
working-directory: ./docs
run: |
- pnpm dlx prettier . --check || {
+ pnpm dlx prettier@${PRETTIER_VERSION} . --check || {
echo "To fix, run from the root of the zed repo:"
- echo " cd docs && pnpm dlx prettier . --write && cd .."
+ echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .."
false
}
+ env:
+ PRETTIER_VERSION: 3.5.0
- name: Check for Typos with Typos-CLI
uses: crate-ci/typos@8e6a4285bcbde632c5d79900a7779746e8b7ea3f # v1.24.6
@@ -1,36 +1,35 @@
-/.direnv
-.envrc
-.idea
-**/target
+**/*.db
**/cargo-target
-/zed.xcworkspace
-.DS_Store
-/plugins/bin
-/script/node_modules
-/crates/theme/schemas/theme.json
-/crates/collab/seed.json
-/crates/zed/resources/flatpak/flatpak-cargo-sources.json
-/dev.zed.Zed*.json
-/assets/*licenses.*
+**/target
**/venv
-.build
*.wasm
-Packages
*.xcodeproj
-xcuserdata/
-DerivedData/
-.swiftpm/config/registries.json
-.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.DS_Store
+.blob_store
+.build
+.envrc
+.flatpak-builder
+.idea
.netrc
-.swiftpm
-**/*.db
.pytest_cache
+.swiftpm
+.swiftpm/config/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.venv
-.blob_store
.vscode
.wrangler
-.flatpak-builder
-.envrc
+/.direnv
+/assets/*licenses.*
+/crates/collab/seed.json
+/crates/theme/schemas/theme.json
+/crates/zed/resources/flatpak/flatpak-cargo-sources.json
+/dev.zed.Zed*.json
+/plugins/bin
+/script/node_modules
+/zed.xcworkspace
+DerivedData/
+Packages
+xcuserdata/
# Don't commit any secrets to the repo.
.env.secret.toml
@@ -2942,6 +2942,28 @@ dependencies = [
"gpui",
]
+[[package]]
+name = "component"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "gpui",
+ "linkme",
+ "once_cell",
+ "parking_lot",
+ "theme",
+]
+
+[[package]]
+name = "component_preview"
+version = "0.1.0"
+dependencies = [
+ "component",
+ "gpui",
+ "ui",
+ "workspace",
+]
+
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -5330,6 +5352,7 @@ dependencies = [
"editor",
"feature_flags",
"futures 0.3.31",
+ "fuzzy",
"git",
"gpui",
"language",
@@ -5349,6 +5372,7 @@ dependencies = [
"util",
"windows 0.58.0",
"workspace",
+ "zed_actions",
]
[[package]]
@@ -7184,7 +7208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -7278,6 +7302,26 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "linkme"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "566336154b9e58a4f055f6dd4cbab62c7dc0826ce3c0a04e63b2d2ecd784cdae"
+dependencies = [
+ "linkme-impl",
+]
+
+[[package]]
+name = "linkme-impl"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edbe595006d355eaf9ae11db92707d4338cd2384d16866131cc1afdbdd35d8d9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+]
+
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
@@ -8691,9 +8735,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.20.2"
+version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
name = "oo7"
@@ -9000,7 +9044,10 @@ dependencies = [
name = "panel"
version = "0.1.0"
dependencies = [
+ "editor",
"gpui",
+ "settings",
+ "theme",
"ui",
"workspace",
]
@@ -14318,8 +14365,10 @@ name = "ui"
version = "0.1.0"
dependencies = [
"chrono",
+ "component",
"gpui",
"itertools 0.14.0",
+ "linkme",
"menu",
"serde",
"settings",
@@ -14347,6 +14396,7 @@ name = "ui_macros"
version = "0.1.0"
dependencies = [
"convert_case 0.7.1",
+ "linkme",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -14616,22 +14666,6 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
-[[package]]
-name = "vcs_menu"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "fuzzy",
- "git",
- "gpui",
- "picker",
- "project",
- "ui",
- "util",
- "workspace",
- "zed_actions",
-]
-
[[package]]
name = "version-compare"
version = "0.2.0"
@@ -16134,6 +16168,7 @@ dependencies = [
"client",
"clock",
"collections",
+ "component",
"db",
"derive_more",
"env_logger 0.11.6",
@@ -16568,6 +16603,7 @@ dependencies = [
"collections",
"command_palette",
"command_palette_hooks",
+ "component_preview",
"copilot",
"db",
"diagnostics",
@@ -16657,7 +16693,6 @@ dependencies = [
"urlencoding",
"util",
"uuid",
- "vcs_menu",
"vim",
"vim_mode_setting",
"welcome",
@@ -16766,12 +16801,13 @@ dependencies = [
[[package]]
name = "zed_llm_client"
-version = "0.2.0"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ea4d8ead1e1158e5ebdd6735df25973781da70de5c8008e3a13595865ca4f31"
+checksum = "614669bead4741b2fc352ae1967318be16949cf46f59013e548c6dbfdfc01252"
dependencies = [
"serde",
"serde_json",
+ "uuid",
]
[[package]]
@@ -26,6 +26,8 @@ members = [
"crates/collections",
"crates/command_palette",
"crates/command_palette_hooks",
+ "crates/component",
+ "crates/component_preview",
"crates/context_server",
"crates/context_server_settings",
"crates/copilot",
@@ -147,7 +149,6 @@ members = [
"crates/ui_macros",
"crates/util",
"crates/util_macros",
- "crates/vcs_menu",
"crates/vim",
"crates/vim_mode_setting",
"crates/welcome",
@@ -227,6 +228,8 @@ collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
+component = { path = "crates/component" }
+component_preview = { path = "crates/component_preview" }
context_server = { path = "crates/context_server" }
context_server_settings = { path = "crates/context_server_settings" }
copilot = { path = "crates/copilot" }
@@ -346,7 +349,6 @@ ui_input = { path = "crates/ui_input" }
ui_macros = { path = "crates/ui_macros" }
util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
-vcs_menu = { path = "crates/vcs_menu" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
welcome = { path = "crates/welcome" }
@@ -428,6 +430,7 @@ jupyter-websocket-client = { version = "0.9.0" }
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
+linkme = "0.3.31"
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [
"dispatcher",
"services-dispatcher",
@@ -561,7 +564,7 @@ wasmtime = { version = "24", default-features = false, features = [
wasmtime-wasi = "24"
which = "6.0.0"
wit-component = "0.201"
-zed_llm_client = "0.2"
+zed_llm_client = "0.4"
zstd = "0.11"
metal = "0.31"
@@ -676,7 +679,6 @@ telemetry_events = { codegen-units = 1 }
theme_selector = { codegen-units = 1 }
time_format = { codegen-units = 1 }
ui_input = { codegen-units = 1 }
-vcs_menu = { codegen-units = 1 }
zed_actions = { codegen-units = 1 }
[profile.release]
@@ -150,8 +150,19 @@
"postcss": "css",
"ppt": "document",
"pptx": "document",
+ "prettier.config.cjs": "prettier",
+ "prettier.config.js": "prettier",
+ "prettier.config.mjs": "prettier",
"prettierignore": "prettier",
"prettierrc": "prettier",
+ "prettierrc.cjs": "prettier",
+ "prettierrc.js": "prettier",
+ "prettierrc.json": "prettier",
+ "prettierrc.json5": "prettier",
+ "prettierrc.mjs": "prettier",
+ "prettierrc.toml": "prettier",
+ "prettierrc.yaml": "prettier",
+ "prettierrc.yml": "prettier",
"prisma": "prisma",
"profile": "terminal",
"ps1": "terminal",
@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.5"/>
+<path d="M8 9V11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<circle cx="8" cy="9" r="1" fill="black"/>
+<rect x="3.75" y="5.75" width="8.5" height="7.5" rx="1.25" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
+</svg>
@@ -122,7 +122,8 @@
"ctrl-i": "editor::ShowSignatureHelp",
"alt-g b": "editor::ToggleGitBlame",
"menu": "editor::OpenContextMenu",
- "shift-f10": "editor::OpenContextMenu"
+ "shift-f10": "editor::OpenContextMenu",
+ "ctrl-shift-e": "editor::ToggleEditPrediction"
}
},
{
@@ -535,8 +536,7 @@
{
"bindings": {
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
- "ctrl-alt-i": "zed::DebugElements",
- "ctrl-:": "editor::ToggleInlayHints"
+ "ctrl-alt-i": "zed::DebugElements"
}
},
{
@@ -554,7 +554,8 @@
"ctrl-shift-e": "pane::RevealInProjectPanel",
"ctrl-f8": "editor::GoToHunk",
"ctrl-shift-f8": "editor::GoToPrevHunk",
- "ctrl-enter": "assistant::InlineAssist"
+ "ctrl-enter": "assistant::InlineAssist",
+ "ctrl-:": "editor::ToggleInlayHints"
}
},
{
@@ -39,8 +39,8 @@
"cmd-m": "zed::Minimize",
"fn-f": "zed::ToggleFullScreen",
"ctrl-cmd-f": "zed::ToggleFullScreen",
- "ctrl-shift-z": "zeta::RateCompletions",
- "ctrl-shift-i": "edit_prediction::ToggleMenu"
+ "ctrl-cmd-z": "zeta::RateCompletions",
+ "ctrl-cmd-i": "edit_prediction::ToggleMenu"
}
},
{
@@ -132,7 +132,8 @@
"cmd-alt-g b": "editor::ToggleGitBlame",
"cmd-i": "editor::ShowSignatureHelp",
"ctrl-f12": "editor::GoToDeclaration",
- "alt-ctrl-f12": "editor::GoToDeclarationSplit"
+ "alt-ctrl-f12": "editor::GoToDeclarationSplit",
+ "ctrl-cmd-e": "editor::ToggleEditPrediction"
}
},
{
@@ -619,8 +620,7 @@
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
// TODO: Move this to a dock open action
"cmd-shift-c": "collab_panel::ToggleFocus",
- "cmd-alt-i": "zed::DebugElements",
- "ctrl-:": "editor::ToggleInlayHints"
+ "cmd-alt-i": "zed::DebugElements"
}
},
{
@@ -633,7 +633,8 @@
"cmd-shift-e": "pane::RevealInProjectPanel",
"cmd-f8": "editor::GoToHunk",
"cmd-shift-f8": "editor::GoToPrevHunk",
- "ctrl-enter": "assistant::InlineAssist"
+ "ctrl-enter": "assistant::InlineAssist",
+ "ctrl-:": "editor::ToggleInlayHints"
}
},
{
@@ -737,6 +738,7 @@
"context": "GitPanel > Editor",
"use_key_equivalents": true,
"bindings": {
+ "enter": "editor::Newline",
"cmd-enter": "git::Commit",
"tab": "git_panel::FocusChanges",
"shift-tab": "git_panel::FocusChanges",
@@ -381,6 +381,12 @@
"ctrl-q": ["vim::PushLiteral", {}]
}
},
+ {
+ "context": "Editor && vim_mode == waiting && (vim_operator == ys || vim_operator == cs)",
+ "bindings": {
+ "escape": "vim::SwitchToNormalMode"
+ }
+ },
{
"context": "vim_mode == operator",
"bindings": {
@@ -81,7 +81,7 @@
"terminal.ansi.bright_green": "#4d6140ff",
"terminal.ansi.dim_green": "#d1e0bfff",
"terminal.ansi.yellow": "#dec184ff",
- "terminal.ansi.bright_yellow": "#786441ff",
+ "terminal.ansi.bright_yellow": "#e5c07bff",
"terminal.ansi.dim_yellow": "#f1dfc1ff",
"terminal.ansi.blue": "#74ade8ff",
"terminal.ansi.bright_blue": "#385378ff",
@@ -457,7 +457,7 @@
"terminal.ansi.bright_green": "#b2cfa9ff",
"terminal.ansi.dim_green": "#354d2eff",
"terminal.ansi.yellow": "#dec184ff",
- "terminal.ansi.bright_yellow": "#f1dfc1ff",
+ "terminal.ansi.bright_yellow": "#826221ff",
"terminal.ansi.dim_yellow": "#786441ff",
"terminal.ansi.blue": "#5c78e2ff",
"terminal.ansi.bright_blue": "#b5baf2ff",
@@ -250,10 +250,10 @@ impl AssistantPanel {
)
.child(
PopoverMenu::new("assistant-panel-popover-menu")
- .trigger(
+ .trigger_with_tooltip(
IconButton::new("menu", IconName::EllipsisVertical)
- .icon_size(IconSize::Small)
- .tooltip(Tooltip::text("Toggle Assistant Menu")),
+ .icon_size(IconSize::Small),
+ Tooltip::text("Toggle Assistant Menu"),
)
.menu(move |window, cx| {
let zoom_label = if _pane.read(cx).is_zoomed() {
@@ -1595,22 +1595,22 @@ impl Render for PromptEditor {
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .tooltip(move |window, 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",
- window,
- cx,
- )
- }),
+ .icon_color(Color::Muted),
+ move |window, 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",
+ window,
+ cx,
+ )
+ },
))
.map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
@@ -646,22 +646,22 @@ impl Render for PromptEditor {
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .tooltip(move |window, 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",
- window,
- cx,
- )
- }),
+ .icon_color(Color::Muted),
+ move |window, 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",
+ window,
+ cx,
+ )
+ },
))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
@@ -74,16 +74,16 @@ impl Render for AssistantModelSelector {
.color(Color::Muted)
.size(IconSize::XSmall),
),
+ ),
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Change Model",
+ &ToggleModelSelector,
+ &focus_handle,
+ window,
+ cx,
)
- .tooltip(move |window, cx| {
- Tooltip::for_action_in(
- "Change Model",
- &ToggleModelSelector,
- &focus_handle,
- window,
- cx,
- )
- }),
+ },
)
.with_handle(self.menu_handle.clone())
}
@@ -660,11 +660,11 @@ impl AssistantPanel {
.gap(DynamicSpacing::Base02.rems(cx))
.child(
PopoverMenu::new("assistant-toolbar-new-popover-menu")
- .trigger(
+ .trigger_with_tooltip(
IconButton::new("new", IconName::Plus)
.icon_size(IconSize::Small)
- .style(ButtonStyle::Subtle)
- .tooltip(Tooltip::text("Newβ¦")),
+ .style(ButtonStyle::Subtle),
+ Tooltip::text("Newβ¦"),
)
.anchor(Corner::TopRight)
.with_handle(self.new_item_context_menu_handle.clone())
@@ -677,11 +677,11 @@ impl AssistantPanel {
)
.child(
PopoverMenu::new("assistant-toolbar-history-popover-menu")
- .trigger(
+ .trigger_with_tooltip(
IconButton::new("open-history", IconName::HistoryRerun)
.icon_size(IconSize::Small)
- .style(ButtonStyle::Subtle)
- .tooltip(Tooltip::text("Historyβ¦")),
+ .style(ButtonStyle::Subtle),
+ Tooltip::text("Historyβ¦"),
)
.anchor(Corner::TopRight)
.with_handle(self.open_history_context_menu_handle.clone())
@@ -411,22 +411,22 @@ impl Render for ContextStrip {
Some(context_picker.clone())
})
- .trigger(
+ .trigger_with_tooltip(
IconButton::new("add-context", IconName::Plus)
.icon_size(IconSize::Small)
- .style(ui::ButtonStyle::Filled)
- .tooltip({
- let focus_handle = focus_handle.clone();
- move |window, cx| {
- Tooltip::for_action_in(
- "Add Context",
- &ToggleContextPicker,
- &focus_handle,
- window,
- cx,
- )
- }
- }),
+ .style(ui::ButtonStyle::Filled),
+ {
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Add Context",
+ &ToggleContextPicker,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
+ },
)
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
@@ -2359,8 +2359,8 @@ impl ContextEditor {
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .tooltip(Tooltip::text("Type / to insert via keyboard")),
+ .icon_position(IconPosition::Start),
+ Tooltip::text("Type / to insert via keyboard"),
)
}
@@ -3323,10 +3323,10 @@ impl Render for ContextEditorToolbarItem {
.color(Color::Muted)
.size(IconSize::XSmall),
),
- )
- .tooltip(move |window, cx| {
- Tooltip::for_action("Change Model", &ToggleModelSelector, window, cx)
- }),
+ ),
+ move |window, cx| {
+ Tooltip::for_action("Change Model", &ToggleModelSelector, window, cx)
+ },
)
.with_handle(self.language_model_selector_menu_handle.clone()),
)
@@ -1,17 +1,22 @@
use std::sync::Arc;
use assistant_slash_command::SlashCommandWorkingSet;
-use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakEntity};
+use gpui::{AnyElement, AnyView, DismissEvent, SharedString, Task, WeakEntity};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
use crate::context_editor::ContextEditor;
#[derive(IntoElement)]
-pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
+pub(super) struct SlashCommandSelector<T, TT>
+where
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
working_set: Arc<SlashCommandWorkingSet>,
active_context_editor: WeakEntity<ContextEditor>,
trigger: T,
+ tooltip: TT,
}
#[derive(Clone)]
@@ -48,16 +53,22 @@ pub(crate) struct SlashCommandDelegate {
selected_index: usize,
}
-impl<T: PopoverTrigger> SlashCommandSelector<T> {
+impl<T, TT> SlashCommandSelector<T, TT>
+where
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
pub(crate) fn new(
working_set: Arc<SlashCommandWorkingSet>,
active_context_editor: WeakEntity<ContextEditor>,
trigger: T,
+ tooltip: TT,
) -> Self {
SlashCommandSelector {
working_set,
active_context_editor,
trigger,
+ tooltip,
}
}
}
@@ -241,7 +252,11 @@ impl PickerDelegate for SlashCommandDelegate {
}
}
-impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
+impl<T, TT> RenderOnce for SlashCommandSelector<T, TT>
+where
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let all_models = self
.working_set
@@ -322,7 +337,7 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
.ok();
PopoverMenu::new("model-switcher")
.menu(move |_window, _cx| Some(picker_view.clone()))
- .trigger(self.trigger)
+ .trigger_with_tooltip(self.trigger, self.tooltip)
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
@@ -0,0 +1,23 @@
+[package]
+name = "component"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/component.rs"
+
+[dependencies]
+collections.workspace = true
+gpui.workspace = true
+linkme.workspace = true
+once_cell = "1.20.3"
+parking_lot.workspace = true
+theme.workspace = true
+
+[features]
+default = []
@@ -0,0 +1,305 @@
+use std::ops::{Deref, DerefMut};
+
+use collections::HashMap;
+use gpui::{div, prelude::*, AnyElement, App, IntoElement, RenderOnce, SharedString, Window};
+use linkme::distributed_slice;
+use once_cell::sync::Lazy;
+use parking_lot::RwLock;
+use theme::ActiveTheme;
+
+pub trait Component {
+ fn scope() -> Option<&'static str>;
+ fn name() -> &'static str {
+ std::any::type_name::<Self>()
+ }
+ fn description() -> Option<&'static str> {
+ None
+ }
+}
+
+pub trait ComponentPreview: Component {
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement;
+}
+
+#[distributed_slice]
+pub static __ALL_COMPONENTS: [fn()] = [..];
+
+#[distributed_slice]
+pub static __ALL_PREVIEWS: [fn()] = [..];
+
+pub static COMPONENT_DATA: Lazy<RwLock<ComponentRegistry>> =
+ Lazy::new(|| RwLock::new(ComponentRegistry::new()));
+
+pub struct ComponentRegistry {
+ components: Vec<(Option<&'static str>, &'static str, Option<&'static str>)>,
+ previews: HashMap<&'static str, fn(&mut Window, &App) -> AnyElement>,
+}
+
+impl ComponentRegistry {
+ fn new() -> Self {
+ ComponentRegistry {
+ components: Vec::new(),
+ previews: HashMap::default(),
+ }
+ }
+}
+
+pub fn init() {
+ let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
+ let preview_fns: Vec<_> = __ALL_PREVIEWS.iter().cloned().collect();
+
+ for f in component_fns {
+ f();
+ }
+ for f in preview_fns {
+ f();
+ }
+}
+
+pub fn register_component<T: Component>() {
+ let component_data = (T::scope(), T::name(), T::description());
+ COMPONENT_DATA.write().components.push(component_data);
+}
+
+pub fn register_preview<T: ComponentPreview>() {
+ let preview_data = (T::name(), T::preview as fn(&mut Window, &App) -> AnyElement);
+ COMPONENT_DATA
+ .write()
+ .previews
+ .insert(preview_data.0, preview_data.1);
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct ComponentId(pub &'static str);
+
+#[derive(Clone)]
+pub struct ComponentMetadata {
+ name: SharedString,
+ scope: Option<SharedString>,
+ description: Option<SharedString>,
+ preview: Option<fn(&mut Window, &App) -> AnyElement>,
+}
+
+impl ComponentMetadata {
+ pub fn name(&self) -> SharedString {
+ self.name.clone()
+ }
+
+ pub fn scope(&self) -> Option<SharedString> {
+ self.scope.clone()
+ }
+
+ pub fn description(&self) -> Option<SharedString> {
+ self.description.clone()
+ }
+
+ pub fn preview(&self) -> Option<fn(&mut Window, &App) -> AnyElement> {
+ self.preview
+ }
+}
+
+pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
+
+impl AllComponents {
+ pub fn new() -> Self {
+ AllComponents(HashMap::default())
+ }
+
+ /// Returns all components with previews
+ pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
+ self.0.values().filter(|c| c.preview.is_some()).collect()
+ }
+
+ /// Returns all components with previews sorted by name
+ pub fn all_previews_sorted(&self) -> Vec<ComponentMetadata> {
+ let mut previews: Vec<ComponentMetadata> =
+ self.all_previews().into_iter().cloned().collect();
+ previews.sort_by_key(|a| a.name());
+ previews
+ }
+
+ /// Returns all components
+ pub fn all(&self) -> Vec<&ComponentMetadata> {
+ self.0.values().collect()
+ }
+
+ /// Returns all components sorted by name
+ pub fn all_sorted(&self) -> Vec<ComponentMetadata> {
+ let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
+ components.sort_by_key(|a| a.name());
+ components
+ }
+}
+
+impl Deref for AllComponents {
+ type Target = HashMap<ComponentId, ComponentMetadata>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for AllComponents {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+pub fn components() -> AllComponents {
+ let data = COMPONENT_DATA.read();
+ let mut all_components = AllComponents::new();
+
+ for &(scope, name, description) in &data.components {
+ let scope = scope.map(Into::into);
+ let preview = data.previews.get(name).cloned();
+ all_components.insert(
+ ComponentId(name),
+ ComponentMetadata {
+ name: name.into(),
+ scope,
+ description: description.map(Into::into),
+ preview,
+ },
+ );
+ }
+
+ all_components
+}
+
+/// Which side of the preview to show labels on
+#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ExampleLabelSide {
+ /// Left side
+ Left,
+ /// Right side
+ Right,
+ #[default]
+ /// Top side
+ Top,
+ /// Bottom side
+ Bottom,
+}
+
+/// A single example of a component.
+#[derive(IntoElement)]
+pub struct ComponentExample {
+ variant_name: SharedString,
+ element: AnyElement,
+ label_side: ExampleLabelSide,
+ grow: bool,
+}
+
+impl RenderOnce for ComponentExample {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+ let base = div().flex();
+
+ let base = match self.label_side {
+ ExampleLabelSide::Right => base.flex_row(),
+ ExampleLabelSide::Left => base.flex_row_reverse(),
+ ExampleLabelSide::Bottom => base.flex_col(),
+ ExampleLabelSide::Top => base.flex_col_reverse(),
+ };
+
+ base.gap_1()
+ .text_xs()
+ .text_color(cx.theme().colors().text_muted)
+ .when(self.grow, |this| this.flex_1())
+ .child(self.element)
+ .child(self.variant_name)
+ .into_any_element()
+ }
+}
+
+impl ComponentExample {
+ /// Create a new example with the given variant name and example value.
+ pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
+ Self {
+ variant_name: variant_name.into(),
+ element,
+ label_side: ExampleLabelSide::default(),
+ grow: false,
+ }
+ }
+
+ /// Set the example to grow to fill the available horizontal space.
+ pub fn grow(mut self) -> Self {
+ self.grow = true;
+ self
+ }
+}
+
+/// A group of component examples.
+#[derive(IntoElement)]
+pub struct ComponentExampleGroup {
+ pub title: Option<SharedString>,
+ pub examples: Vec<ComponentExample>,
+ pub grow: bool,
+}
+
+impl RenderOnce for ComponentExampleGroup {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+ div()
+ .flex_col()
+ .text_sm()
+ .text_color(cx.theme().colors().text_muted)
+ .when(self.grow, |this| this.w_full().flex_1())
+ .when_some(self.title, |this, title| this.gap_4().child(title))
+ .child(
+ div()
+ .flex()
+ .items_start()
+ .w_full()
+ .gap_6()
+ .children(self.examples)
+ .into_any_element(),
+ )
+ .into_any_element()
+ }
+}
+
+impl ComponentExampleGroup {
+ /// Create a new group of examples with the given title.
+ pub fn new(examples: Vec<ComponentExample>) -> Self {
+ Self {
+ title: None,
+ examples,
+ grow: false,
+ }
+ }
+
+ /// Create a new group of examples with the given title.
+ pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
+ Self {
+ title: Some(title.into()),
+ examples,
+ grow: false,
+ }
+ }
+
+ /// Set the group to grow to fill the available horizontal space.
+ pub fn grow(mut self) -> Self {
+ self.grow = true;
+ self
+ }
+}
+
+/// Create a single example
+pub fn single_example(
+ variant_name: impl Into<SharedString>,
+ example: AnyElement,
+) -> ComponentExample {
+ ComponentExample::new(variant_name, example)
+}
+
+/// Create a group of examples without a title
+pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
+ ComponentExampleGroup::new(examples)
+}
+
+/// Create a group of examples with a title
+pub fn example_group_with_title(
+ title: impl Into<SharedString>,
+ examples: Vec<ComponentExample>,
+) -> ComponentExampleGroup {
+ ComponentExampleGroup::with_title(title, examples)
+}
@@ -1,5 +1,5 @@
[package]
-name = "vcs_menu"
+name = "component_preview"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
@@ -8,14 +8,14 @@ license = "GPL-3.0-or-later"
[lints]
workspace = true
+[lib]
+path = "src/component_preview.rs"
+
+[features]
+default = []
+
[dependencies]
-anyhow.workspace = true
-fuzzy.workspace = true
-git.workspace = true
+component.workspace = true
gpui.workspace = true
-picker.workspace = true
-project.workspace = true
ui.workspace = true
-util.workspace = true
workspace.workspace = true
-zed_actions.workspace = true
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,178 @@
+//! # Component Preview
+//!
+//! A view for exploring Zed components.
+
+use component::{components, ComponentMetadata};
+use gpui::{prelude::*, App, EventEmitter, FocusHandle, Focusable, Window};
+use ui::prelude::*;
+
+use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
+
+pub fn init(cx: &mut App) {
+ cx.observe_new(|workspace: &mut Workspace, _, _cx| {
+ workspace.register_action(
+ |workspace, _: &workspace::OpenComponentPreview, window, cx| {
+ let component_preview = cx.new(ComponentPreview::new);
+ workspace.add_item_to_active_pane(
+ Box::new(component_preview),
+ None,
+ true,
+ window,
+ cx,
+ )
+ },
+ );
+ })
+ .detach();
+}
+
+struct ComponentPreview {
+ focus_handle: FocusHandle,
+}
+
+impl ComponentPreview {
+ pub fn new(cx: &mut Context<Self>) -> Self {
+ Self {
+ focus_handle: cx.focus_handle(),
+ }
+ }
+
+ fn render_sidebar(&self, _window: &Window, _cx: &Context<Self>) -> impl IntoElement {
+ let components = components().all_sorted();
+ let sorted_components = components.clone();
+
+ v_flex().gap_px().p_1().children(
+ sorted_components
+ .into_iter()
+ .map(|component| self.render_sidebar_entry(&component, _cx)),
+ )
+ }
+
+ fn render_sidebar_entry(
+ &self,
+ component: &ComponentMetadata,
+ _cx: &Context<Self>,
+ ) -> impl IntoElement {
+ h_flex()
+ .w_40()
+ .px_1p5()
+ .py_1()
+ .child(component.name().clone())
+ }
+
+ fn render_preview(
+ &self,
+ component: &ComponentMetadata,
+ window: &mut Window,
+ cx: &Context<Self>,
+ ) -> impl IntoElement {
+ let name = component.name();
+ let scope = component.scope();
+
+ let description = component.description();
+
+ v_group()
+ .w_full()
+ .gap_4()
+ .p_8()
+ .rounded_md()
+ .child(
+ v_flex()
+ .gap_1()
+ .child(
+ h_flex()
+ .gap_1()
+ .text_xl()
+ .child(div().child(name))
+ .when_some(scope, |this, scope| {
+ this.child(div().opacity(0.5).child(format!("({})", scope)))
+ }),
+ )
+ .when_some(description, |this, description| {
+ this.child(
+ div()
+ .text_ui_sm(cx)
+ .text_color(cx.theme().colors().text_muted)
+ .max_w(px(600.0))
+ .child(description),
+ )
+ }),
+ )
+ .when_some(component.preview(), |this, preview| {
+ this.child(preview(window, cx))
+ })
+ .into_any_element()
+ }
+
+ fn render_previews(&self, window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
+ v_flex()
+ .id("component-previews")
+ .size_full()
+ .overflow_y_scroll()
+ .p_4()
+ .gap_2()
+ .children(
+ components()
+ .all_previews_sorted()
+ .iter()
+ .map(|component| self.render_preview(component, window, cx)),
+ )
+ }
+}
+
+impl Render for ComponentPreview {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
+ h_flex()
+ .id("component-preview")
+ .key_context("ComponentPreview")
+ .items_start()
+ .overflow_hidden()
+ .size_full()
+ .max_h_full()
+ .track_focus(&self.focus_handle)
+ .px_2()
+ .bg(cx.theme().colors().editor_background)
+ .child(self.render_sidebar(window, cx))
+ .child(self.render_previews(window, cx))
+ }
+}
+
+impl EventEmitter<ItemEvent> for ComponentPreview {}
+
+impl Focusable for ComponentPreview {
+ fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl Item for ComponentPreview {
+ type Event = ItemEvent;
+
+ fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
+ Some("Component Preview".into())
+ }
+
+ fn telemetry_event_text(&self) -> Option<&'static str> {
+ None
+ }
+
+ fn show_toolbar(&self) -> bool {
+ false
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: Option<WorkspaceId>,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<gpui::Entity<Self>>
+ where
+ Self: Sized,
+ {
+ Some(cx.new(Self::new))
+ }
+
+ fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+ f(*event)
+ }
+}
@@ -242,6 +242,7 @@ impl EditPredictionProvider for CopilotCompletionProvider {
} else {
let position = cursor_position.bias_right(buffer);
Some(InlineCompletion {
+ id: None,
edits: vec![(position..position, completion_text.into())],
edit_preview: None,
})
@@ -490,6 +490,7 @@ enum InlineCompletion {
struct InlineCompletionState {
inlay_ids: Vec<InlayId>,
completion: InlineCompletion,
+ completion_id: Option<SharedString>,
invalidation_range: Range<Anchor>,
}
@@ -4893,7 +4894,11 @@ impl Editor {
return;
};
- self.report_inline_completion_event(true, cx);
+ self.report_inline_completion_event(
+ active_inline_completion.completion_id.clone(),
+ true,
+ cx,
+ );
match &active_inline_completion.completion {
InlineCompletion::Move { target, .. } => {
@@ -4942,7 +4947,11 @@ impl Editor {
return;
}
- self.report_inline_completion_event(true, cx);
+ self.report_inline_completion_event(
+ active_inline_completion.completion_id.clone(),
+ true,
+ cx,
+ );
match &active_inline_completion.completion {
InlineCompletion::Move { target, .. } => {
@@ -5000,7 +5009,12 @@ impl Editor {
cx: &mut Context<Self>,
) -> bool {
if should_report_inline_completion_event {
- self.report_inline_completion_event(false, cx);
+ let completion_id = self
+ .active_inline_completion
+ .as_ref()
+ .and_then(|active_completion| active_completion.completion_id.clone());
+
+ self.report_inline_completion_event(completion_id, false, cx);
}
if let Some(provider) = self.edit_prediction_provider() {
@@ -5010,7 +5024,7 @@ impl Editor {
self.take_active_inline_completion(cx)
}
- fn report_inline_completion_event(&self, accepted: bool, cx: &App) {
+ fn report_inline_completion_event(&self, id: Option<SharedString>, accepted: bool, cx: &App) {
let Some(provider) = self.edit_prediction_provider() else {
return;
};
@@ -5035,6 +5049,7 @@ impl Editor {
telemetry::event!(
event_type,
provider = provider.name(),
+ prediction_id = id,
suggestion_accepted = accepted,
file_extension = extension,
);
@@ -5250,6 +5265,7 @@ impl Editor {
self.active_inline_completion = Some(InlineCompletionState {
inlay_ids,
completion,
+ completion_id: inline_completion.id,
invalidation_range,
});
@@ -10283,26 +10299,14 @@ impl Editor {
if entry.diagnostic.is_primary
&& entry.diagnostic.severity <= DiagnosticSeverity::WARNING
&& entry.range.start != entry.range.end
+ // if we match with the active diagnostic, skip it
+ && Some(entry.diagnostic.group_id)
+ != self.active_diagnostics.as_ref().map(|d| d.group_id)
{
- let entry_group = entry.diagnostic.group_id;
- let in_next_group = self.active_diagnostics.as_ref().map_or(
- true,
- |active| match direction {
- Direction::Prev => {
- entry_group != active.group_id
- && (active.group_id == 0 || entry_group < active.group_id)
- }
- Direction::Next => {
- entry_group != active.group_id
- && (entry_group == 0 || entry_group > active.group_id)
- }
- },
- );
- if in_next_group {
- return Some((entry.range, entry.diagnostic.group_id));
- }
+ Some((entry.range, entry.diagnostic.group_id))
+ } else {
+ None
}
- None
});
if let Some((primary_range, group_id)) = group {
@@ -10653,176 +10653,6 @@ async fn go_to_prev_overlapping_diagnostic(
"});
}
-#[gpui::test]
-async fn cycle_through_same_place_diagnostics(
- executor: BackgroundExecutor,
- cx: &mut gpui::TestAppContext,
-) {
- init_test(cx, |_| {});
-
- let mut cx = EditorTestContext::new(cx).await;
- let lsp_store =
- cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
-
- cx.set_state(indoc! {"
- Λfn func(abc def: i32) -> u32 {
- }
- "});
-
- cx.update(|_, cx| {
- lsp_store.update(cx, |lsp_store, cx| {
- lsp_store
- .update_diagnostics(
- LanguageServerId(0),
- lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
- version: None,
- diagnostics: vec![
- lsp::Diagnostic {
- range: lsp::Range::new(
- lsp::Position::new(0, 11),
- lsp::Position::new(0, 12),
- ),
- severity: Some(lsp::DiagnosticSeverity::ERROR),
- ..Default::default()
- },
- lsp::Diagnostic {
- range: lsp::Range::new(
- lsp::Position::new(0, 12),
- lsp::Position::new(0, 15),
- ),
- severity: Some(lsp::DiagnosticSeverity::ERROR),
- ..Default::default()
- },
- lsp::Diagnostic {
- range: lsp::Range::new(
- lsp::Position::new(0, 12),
- lsp::Position::new(0, 15),
- ),
- severity: Some(lsp::DiagnosticSeverity::ERROR),
- ..Default::default()
- },
- lsp::Diagnostic {
- range: lsp::Range::new(
- lsp::Position::new(0, 25),
- lsp::Position::new(0, 28),
- ),
- severity: Some(lsp::DiagnosticSeverity::ERROR),
- ..Default::default()
- },
- ],
- },
- &[],
- cx,
- )
- .unwrap()
- });
- });
- executor.run_until_parked();
-
- //// Backward
-
- // Fourth diagnostic
- cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
- });
- cx.assert_editor_state(indoc! {"
- fn func(abc def: i32) -> Λu32 {
- }
- "});
-
- // Third diagnostic
- cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
- });
- cx.assert_editor_state(indoc! {"
- fn func(abc Λdef: i32) -> u32 {
- }
- "});
-
- // Second diagnostic, same place
- cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
- });
- cx.assert_editor_state(indoc! {"
- fn func(abc Λdef: i32) -> u32 {
- }
- "});
-
- // First diagnostic
- cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
- });
- cx.assert_editor_state(indoc! {"
- fn func(abcΛ def: i32) -> u32 {
- }
- "});
-
- // Wrapped over, fourth diagnostic
- cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
- });
- cx.assert_editor_state(indoc! {"
- fn func(abc def: i32) -> Λu32 {
- }
- "});
-
- cx.update_editor(|editor, window, cx| {
- editor.move_to_beginning(&MoveToBeginning, window, cx);
- });
- cx.assert_editor_state(indoc! {"
- Λfn func(abc def: i32) -> u32 {
- }
- "});
-
- //// Forward
-
- // First diagnostic
- cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
- });
- cx.assert_editor_state(indoc! {"
- fn func(abcΛ def: i32) -> u32 {
- }
- "});
-
- // Second diagnostic
- cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
- });
- cx.assert_editor_state(indoc! {"
- fn func(abc Λdef: i32) -> u32 {
- }
- "});
-
- // Third diagnostic, same place
- cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
- });
- cx.assert_editor_state(indoc! {"
- fn func(abc Λdef: i32) -> u32 {
- }
- "});
-
- // Fourth diagnostic
- cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
- });
- cx.assert_editor_state(indoc! {"
- fn func(abc def: i32) -> Λu32 {
- }
- "});
-
- // Wrapped around, first diagnostic
- cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
- });
- cx.assert_editor_state(indoc! {"
- fn func(abcΛ def: i32) -> u32 {
- }
- "});
-}
-
#[gpui::test]
async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -5800,6 +5800,9 @@ fn inline_completion_accept_indicator(
.child(accept_keystroke.key.clone());
let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
+ let accent_color = cx.theme().colors().text_accent;
+ let editor_bg_color = cx.theme().colors().editor_background;
+ let bg_color = editor_bg_color.blend(accent_color.opacity(0.2));
Some(
h_flex()
@@ -5807,7 +5810,7 @@ fn inline_completion_accept_indicator(
.pl_1()
.pr(padding_right)
.gap_1()
- .bg(cx.theme().colors().text_accent.opacity(0.15))
+ .bg(bg_color)
.border_1()
.border_color(cx.theme().colors().text_accent.opacity(0.8))
.rounded_md()
@@ -763,7 +763,7 @@ impl Editor {
this.child({
let focus = editor.focus_handle(cx);
PopoverMenu::new("hunk-controls-dropdown")
- .trigger(
+ .trigger_with_tooltip(
IconButton::new(
"toggle_editor_selections_icon",
IconName::EllipsisVertical,
@@ -774,19 +774,8 @@ impl Editor {
.toggle_state(
hunk_controls_menu_handle
.is_deployed(),
- )
- .when(
- !hunk_controls_menu_handle
- .is_deployed(),
- |this| {
- this.tooltip(|_, cx| {
- Tooltip::simple(
- "Hunk Controls",
- cx,
- )
- })
- },
),
+ Tooltip::simple("Hunk Controls", cx),
)
.anchor(Corner::TopRight)
.with_handle(hunk_controls_menu_handle)
@@ -333,6 +333,7 @@ fn propose_edits<T: ToOffset>(
cx.update(|_, cx| {
provider.update(cx, |provider, _| {
provider.set_inline_completion(Some(inline_completion::InlineCompletion {
+ id: None,
edits: edits.collect(),
edit_preview: None,
}))
@@ -20,6 +20,7 @@ diff.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
+fuzzy.workspace = true
git.workspace = true
gpui.workspace = true
language.workspace = true
@@ -38,6 +39,7 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
+zed_actions.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
@@ -1,27 +1,49 @@
use anyhow::{anyhow, Context as _, Result};
use fuzzy::{StringMatch, StringMatchCandidate};
+
use git::repository::Branch;
use gpui::{
- rems, AnyElement, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
- Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
- Subscription, Task, WeakEntity, Window,
+ rems, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+ InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
+ Task, WeakEntity, Window,
};
use picker::{Picker, PickerDelegate};
use project::ProjectPath;
-use std::{ops::Not, sync::Arc};
+use std::sync::Arc;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::notifications::DetachAndPromptErr;
use workspace::{ModalView, Workspace};
-use zed_actions::branches::OpenRecent;
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, _| {
- workspace.register_action(BranchList::open);
+ workspace.register_action(open);
})
.detach();
}
+pub fn open(
+ _: &mut Workspace,
+ _: &zed_actions::git::Branch,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+) {
+ let this = cx.entity().clone();
+ cx.spawn_in(window, |_, mut cx| async move {
+ // Modal branch picker has a longer trailoff than a popover one.
+ let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?;
+
+ this.update_in(&mut cx, |workspace, window, cx| {
+ workspace.toggle_modal(window, cx, |window, cx| {
+ BranchList::new(delegate, 34., window, cx)
+ })
+ })?;
+
+ Ok(())
+ })
+ .detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None)
+}
+
pub struct BranchList {
pub picker: Entity<Picker<BranchListDelegate>>,
rem_width: f32,
@@ -29,29 +51,7 @@ pub struct BranchList {
}
impl BranchList {
- pub fn open(
- _: &mut Workspace,
- _: &OpenRecent,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) {
- let this = cx.entity().clone();
- cx.spawn_in(window, |_, mut cx| async move {
- // Modal branch picker has a longer trailoff than a popover one.
- let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?;
-
- this.update_in(&mut cx, |workspace, window, cx| {
- workspace.toggle_modal(window, cx, |window, cx| {
- BranchList::new(delegate, 34., window, cx)
- })
- })?;
-
- Ok(())
- })
- .detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None)
- }
-
- fn new(
+ pub fn new(
delegate: BranchListDelegate,
rem_width: f32,
window: &mut Window,
@@ -91,6 +91,7 @@ impl Render for BranchList {
#[derive(Debug, Clone)]
enum BranchEntry {
Branch(StringMatch),
+ History(String),
NewBranch { name: String },
}
@@ -98,6 +99,7 @@ impl BranchEntry {
fn name(&self) -> &str {
match self {
Self::Branch(branch) => &branch.string,
+ Self::History(branch) => &branch,
Self::NewBranch { name } => &name,
}
}
@@ -114,7 +116,7 @@ pub struct BranchListDelegate {
}
impl BranchListDelegate {
- async fn new(
+ pub async fn new(
workspace: Entity<Workspace>,
branch_name_trailoff_after: usize,
cx: &AsyncApp,
@@ -141,7 +143,7 @@ impl BranchListDelegate {
})
}
- fn branch_count(&self) -> usize {
+ pub fn branch_count(&self) -> usize {
self.matches
.iter()
.filter(|item| matches!(item, BranchEntry::Branch(_)))
@@ -207,16 +209,10 @@ impl PickerDelegate for BranchListDelegate {
let Some(candidates) = candidates.log_err() else {
return;
};
- let matches = if query.is_empty() {
+ let matches: Vec<BranchEntry> = if query.is_empty() {
candidates
.into_iter()
- .enumerate()
- .map(|(index, candidate)| StringMatch {
- candidate_id: index,
- string: candidate.string,
- positions: Vec::new(),
- score: 0.0,
- })
+ .map(|candidate| BranchEntry::History(candidate.string))
.collect()
} else {
fuzzy::match_strings(
@@ -228,11 +224,15 @@ impl PickerDelegate for BranchListDelegate {
cx.background_executor().clone(),
)
.await
+ .iter()
+ .cloned()
+ .map(BranchEntry::Branch)
+ .collect()
};
picker
.update(&mut cx, |picker, _| {
let delegate = &mut picker.delegate;
- delegate.matches = matches.into_iter().map(BranchEntry::Branch).collect();
+ delegate.matches = matches;
if delegate.matches.is_empty() {
if !query.is_empty() {
delegate.matches.push(BranchEntry::NewBranch {
@@ -268,6 +268,7 @@ impl PickerDelegate for BranchListDelegate {
let project = workspace.read(cx).project().read(cx);
let branch_to_checkout = match branch {
BranchEntry::Branch(branch) => branch.string,
+ BranchEntry::History(string) => string,
BranchEntry::NewBranch { name: branch_name } => branch_name,
};
let worktree = project
@@ -311,7 +312,14 @@ impl PickerDelegate for BranchListDelegate {
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
- .map(|parent| match hit {
+ .when(matches!(hit, BranchEntry::History(_)), |el| {
+ el.end_slot(
+ Icon::new(IconName::HistoryRerun)
+ .color(Color::Muted)
+ .size(IconSize::Small),
+ )
+ })
+ .map(|el| match hit {
BranchEntry::Branch(branch) => {
let highlights: Vec<_> = branch
.positions
@@ -320,40 +328,13 @@ impl PickerDelegate for BranchListDelegate {
.copied()
.collect();
- parent.child(HighlightedLabel::new(shortened_branch_name, highlights))
+ el.child(HighlightedLabel::new(shortened_branch_name, highlights))
}
+ BranchEntry::History(_) => el.child(Label::new(shortened_branch_name)),
BranchEntry::NewBranch { name } => {
- parent.child(Label::new(format!("Create branch '{name}'")))
+ el.child(Label::new(format!("Create branch '{name}'")))
}
}),
)
}
-
- fn render_header(
- &self,
- _window: &mut Window,
- _: &mut Context<Picker<Self>>,
- ) -> Option<AnyElement> {
- let label = if self.last_query.is_empty() {
- Label::new("Recent Branches")
- .size(LabelSize::Small)
- .mt_1()
- .ml_3()
- .into_any_element()
- } else {
- let match_label = self.matches.is_empty().not().then(|| {
- let suffix = if self.branch_count() == 1 { "" } else { "es" };
- Label::new(format!("{} match{}", self.branch_count(), suffix))
- .color(Color::Muted)
- .size(LabelSize::Small)
- });
- h_flex()
- .px_3()
- .justify_between()
- .child(Label::new("Branches").size(LabelSize::Small))
- .children(match_label)
- .into_any_element()
- };
- Some(v_flex().mt_1().child(label).into_any_element())
- }
}
@@ -6,33 +6,32 @@ use crate::{
};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
-use editor::actions::MoveToEnd;
-use editor::scroll::ScrollbarAutoHide;
-use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
-use git::repository::RepoPath;
-use git::status::FileStatus;
-use git::{Commit, ToggleStaged};
+use editor::{
+ actions::MoveToEnd, scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode,
+ EditorSettings, MultiBuffer, ShowScrollbar,
+};
+use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
use gpui::*;
use language::{Buffer, File};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
use multi_buffer::ExcerptInfo;
-use panel::PanelHeader;
-use project::git::{GitEvent, Repository};
-use project::{Fs, Project, ProjectPath};
+use panel::{panel_editor_container, panel_editor_style, panel_filled_button, PanelHeader};
+use project::{
+ git::{GitEvent, Repository},
+ Fs, Project, ProjectPath,
+};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
-use theme::ThemeSettings;
use ui::{
- prelude::*, ButtonLike, Checkbox, Divider, DividerColor, ElevationIndex, IndentGuideColors,
- ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
+ prelude::*, ButtonLike, Checkbox, CheckboxWithLabel, Divider, DividerColor, ElevationIndex,
+ IndentGuideColors, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
};
use util::{maybe, ResultExt, TryFutureExt};
-use workspace::notifications::{DetachAndPromptErr, NotificationId};
-use workspace::Toast;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
- Workspace,
+ notifications::{DetachAndPromptErr, NotificationId},
+ Toast, Workspace,
};
actions!(
@@ -147,33 +146,33 @@ struct PendingOperation {
}
pub struct GitPanel {
+ active_repository: Option<Entity<Repository>>,
+ commit_editor: Entity<Editor>,
+ conflicted_count: usize,
+ conflicted_staged_count: usize,
current_modifiers: Modifiers,
+ enable_auto_coauthors: bool,
+ entries: Vec<GitListEntry>,
+ entries_by_path: collections::HashMap<RepoPath, usize>,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
hide_scrollbar_task: Option<Task<()>>,
+ new_count: usize,
+ new_staged_count: usize,
+ pending: Vec<PendingOperation>,
+ pending_commit: Option<Task<()>>,
pending_serialization: Task<Option<()>>,
- workspace: WeakEntity<Workspace>,
project: Entity<Project>,
- active_repository: Option<Entity<Repository>>,
+ repository_selector: Entity<RepositorySelector>,
scroll_handle: UniformListScrollHandle,
scrollbar_state: ScrollbarState,
selected_entry: Option<usize>,
show_scrollbar: bool,
+ tracked_count: usize,
+ tracked_staged_count: usize,
update_visible_entries_task: Task<()>,
- repository_selector: Entity<RepositorySelector>,
- commit_editor: Entity<Editor>,
- entries: Vec<GitListEntry>,
- entries_by_path: collections::HashMap<RepoPath, usize>,
width: Option<Pixels>,
- pending: Vec<PendingOperation>,
- pending_commit: Option<Task<()>>,
-
- conflicted_staged_count: usize,
- conflicted_count: usize,
- tracked_staged_count: usize,
- tracked_count: usize,
- new_staged_count: usize,
- new_count: usize,
+ workspace: WeakEntity<Workspace>,
}
fn commit_message_editor(
@@ -181,23 +180,10 @@ fn commit_message_editor(
window: &mut Window,
cx: &mut Context<'_, Editor>,
) -> Editor {
- let theme = ThemeSettings::get_global(cx);
-
- let mut text_style = window.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 commit_editor = if let Some(commit_message_buffer) = commit_message_buffer {
let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
Editor::new(
- EditorMode::AutoHeight { max_lines: 10 },
+ EditorMode::AutoHeight { max_lines: 6 },
buffer,
None,
false,
@@ -205,13 +191,12 @@ fn commit_message_editor(
cx,
)
} else {
- Editor::auto_height(10, window, cx)
+ Editor::auto_height(6, window, cx)
};
commit_editor.set_use_autoclose(false);
commit_editor.set_show_gutter(false, cx);
commit_editor.set_show_wrap_guides(false, cx);
commit_editor.set_show_indent_guides(false, cx);
- commit_editor.set_text_style_refinement(refinement);
commit_editor.set_placeholder_text("Enter commit message", cx);
commit_editor
}
@@ -260,37 +245,40 @@ impl GitPanel {
)
.detach();
+ let scrollbar_state =
+ ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity());
+
let repository_selector =
cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
let mut git_panel = Self {
- focus_handle: cx.focus_handle(),
- pending_serialization: Task::ready(None),
+ active_repository,
+ commit_editor,
+ conflicted_count: 0,
+ conflicted_staged_count: 0,
+ current_modifiers: window.modifiers(),
+ enable_auto_coauthors: true,
entries: Vec::new(),
entries_by_path: HashMap::default(),
+ focus_handle: cx.focus_handle(),
+ fs,
+ hide_scrollbar_task: None,
+ new_count: 0,
+ new_staged_count: 0,
pending: Vec::new(),
- current_modifiers: window.modifiers(),
- width: Some(px(360.)),
- scrollbar_state: ScrollbarState::new(scroll_handle.clone())
- .parent_entity(&cx.entity()),
+ pending_commit: None,
+ pending_serialization: Task::ready(None),
+ project,
repository_selector,
+ scroll_handle,
+ scrollbar_state,
selected_entry: None,
show_scrollbar: false,
- hide_scrollbar_task: None,
+ tracked_count: 0,
+ tracked_staged_count: 0,
update_visible_entries_task: Task::ready(()),
- pending_commit: None,
- active_repository,
- scroll_handle,
- fs,
- commit_editor,
- project,
+ width: Some(px(360.)),
workspace,
- conflicted_count: 0,
- conflicted_staged_count: 0,
- tracked_staged_count: 0,
- tracked_count: 0,
- new_staged_count: 0,
- new_count: 0,
};
git_panel.schedule_update(false, window, cx);
git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
@@ -990,6 +978,26 @@ impl GitPanel {
cx.notify();
}
+ fn toggle_auto_coauthors(&mut self, cx: &mut Context<Self>) {
+ self.enable_auto_coauthors = !self.enable_auto_coauthors;
+ cx.notify();
+ }
+
+ fn header_state(&self, header_type: Section) -> ToggleState {
+ let (staged_count, count) = match header_type {
+ Section::New => (self.new_staged_count, self.new_count),
+ Section::Tracked => (self.tracked_staged_count, self.tracked_count),
+ Section::Conflict => (self.conflicted_staged_count, self.conflicted_count),
+ };
+ if staged_count == 0 {
+ ToggleState::Unselected
+ } else if count == staged_count {
+ ToggleState::Selected
+ } else {
+ ToggleState::Indeterminate
+ }
+ }
+
fn update_counts(&mut self, repo: &Repository) {
self.conflicted_count = 0;
self.conflicted_staged_count = 0;
@@ -1043,21 +1051,6 @@ impl GitPanel {
self.conflicted_count > 0 && self.conflicted_count != self.conflicted_staged_count
}
- fn header_state(&self, header_type: Section) -> ToggleState {
- let (staged_count, count) = match header_type {
- Section::New => (self.new_staged_count, self.new_count),
- Section::Tracked => (self.tracked_staged_count, self.tracked_count),
- Section::Conflict => (self.conflicted_staged_count, self.conflicted_count),
- };
- if staged_count == 0 {
- ToggleState::Unselected
- } else if count == staged_count {
- ToggleState::Selected
- } else {
- ToggleState::Indeterminate
- }
- }
-
fn show_err_toast(&self, e: anyhow::Error, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else {
return;
@@ -1110,33 +1103,43 @@ impl GitPanel {
.git_state()
.read(cx)
.all_repositories();
- let entry_count = self
+
+ let branch = self
.active_repository
.as_ref()
- .map_or(0, |repo| repo.read(cx).entry_count());
+ .and_then(|repository| repository.read(cx).branch())
+ .unwrap_or_else(|| "(no current branch)".into());
+
+ let has_repo_above = all_repositories.iter().any(|repo| {
+ repo.read(cx)
+ .repository_entry
+ .work_directory
+ .is_above_project()
+ });
- let changes_string = match entry_count {
- 0 => "No changes".to_string(),
- 1 => "1 change".to_string(),
- n => format!("{} changes", n),
- };
+ let icon_button = Button::new("branch-selector", branch)
+ .color(Color::Muted)
+ .style(ButtonStyle::Subtle)
+ .icon(IconName::GitBranch)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .size(ButtonSize::Compact)
+ .icon_position(IconPosition::Start)
+ .tooltip(Tooltip::for_action_title(
+ "Switch Branch",
+ &zed_actions::git::Branch,
+ ))
+ .on_click(cx.listener(|_, _, window, cx| {
+ window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
+ }))
+ .style(ButtonStyle::Transparent);
self.panel_header_container(window, cx)
- .child(h_flex().gap_2().child(if all_repositories.len() <= 1 {
- div()
- .id("changes-label")
- .text_buffer(cx)
- .text_ui_sm(cx)
- .child(
- Label::new(changes_string)
- .single_line()
- .size(LabelSize::Small),
- )
- .into_any_element()
- } else {
- self.render_repository_selector(cx).into_any_element()
- }))
+ .child(h_flex().pl_1().child(icon_button))
.child(div().flex_grow())
+ .when(all_repositories.len() > 1 || has_repo_above, |el| {
+ el.child(self.render_repository_selector(cx))
+ })
}
pub fn render_repository_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -1146,45 +1149,30 @@ impl GitPanel {
.map(|repo| repo.read(cx).display_name(self.project.read(cx), cx))
.unwrap_or_default();
- let entry_count = self.entries.len();
-
RepositorySelectorPopoverMenu::new(
self.repository_selector.clone(),
ButtonLike::new("active-repository")
.style(ButtonStyle::Subtle)
- .child(
- h_flex().w_full().gap_0p5().child(
- div()
- .overflow_x_hidden()
- .flex_grow()
- .whitespace_nowrap()
- .child(
- h_flex()
- .gap_1()
- .child(
- Label::new(repository_display_name).size(LabelSize::Small),
- )
- .when(entry_count > 0, |flex| {
- flex.child(
- Label::new(format!("({})", entry_count))
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- })
- .into_any_element(),
- ),
- ),
- ),
+ .child(Label::new(repository_display_name).size(LabelSize::Small)),
+ Tooltip::text("Select a repository"),
)
}
- pub fn render_commit_editor(&self, cx: &Context<Self>) -> impl IntoElement {
+ pub fn render_commit_editor(
+ &self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
let editor = self.commit_editor.clone();
let can_commit = (self.has_staged_changes() || self.has_tracked_changes())
&& self.pending_commit.is_none()
&& !editor.read(cx).is_empty(cx)
&& !self.has_unstaged_conflicts()
&& self.has_write_access(cx);
+ // let can_commit_all =
+ // !self.commit_pending && self.can_commit_all && !editor.read(cx).is_empty(cx);
+ let panel_editor_style = panel_editor_style(true, window, cx);
+
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
let focus_handle_1 = self.focus_handle(cx).clone();
@@ -1199,8 +1187,7 @@ impl GitPanel {
"Commit All"
};
- let commit_button = self
- .panel_button("commit-changes", title)
+ let commit_button = panel_filled_button(title)
.tooltip(move |window, cx| {
let focus_handle = focus_handle_1.clone();
Tooltip::for_action_in(tooltip, &Commit, &focus_handle, window, cx)
@@ -1210,28 +1197,50 @@ impl GitPanel {
cx.listener(move |this, _: &ClickEvent, window, cx| this.commit_changes(window, 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, window, _cx| {
- window.focus(&editor_focus_handle);
- }))
- .child(self.commit_editor.clone())
- .child(
- h_flex()
- .absolute()
- .bottom_2p5()
- .right_3()
- .gap_1p5()
- .child(div().gap_1().flex_grow())
- .child(commit_button),
- ),
- )
+ let enable_coauthors = CheckboxWithLabel::new(
+ "enable-coauthors",
+ Label::new("Add Co-authors")
+ .color(Color::Disabled)
+ .size(LabelSize::XSmall),
+ self.enable_auto_coauthors.into(),
+ cx.listener(move |this, _, _, cx| this.toggle_auto_coauthors(cx)),
+ );
+
+ let footer_size = px(32.);
+ let gap = px(16.0);
+
+ let max_height = window.line_height() * 6. + gap + footer_size;
+
+ panel_editor_container(window, cx)
+ .id("commit-editor-container")
+ .relative()
+ .h(max_height)
+ .w_full()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .bg(cx.theme().colors().editor_background)
+ .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
+ window.focus(&editor_focus_handle);
+ }))
+ .child(EditorElement::new(&self.commit_editor, panel_editor_style))
+ .child(
+ h_flex()
+ .absolute()
+ .bottom_0()
+ .left_2()
+ .h(footer_size)
+ .flex_none()
+ .child(enable_coauthors),
+ )
+ .child(
+ h_flex()
+ .absolute()
+ .bottom_0()
+ .right_2()
+ .h(footer_size)
+ .flex_none()
+ .child(commit_button),
+ )
}
fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -1243,7 +1252,11 @@ impl GitPanel {
.child(
v_flex()
.gap_3()
- .child("No changes to commit")
+ .child(if self.active_repository.is_some() {
+ "No changes to commit"
+ } else {
+ "No Git repositories"
+ })
.text_ui_sm(cx)
.mx_auto()
.text_color(Color::Placeholder.color(cx)),
@@ -1357,6 +1370,7 @@ impl GitPanel {
v_flex()
.size_full()
+ .flex_grow()
.overflow_hidden()
.child(
uniform_list(cx.entity().clone(), "entries", entry_count, {
@@ -1505,7 +1519,7 @@ impl GitPanel {
.spacing(ListItemSpacing::Sparse)
.start_slot(start_slot)
.toggle_state(selected)
- .focused(selected && self.focus_handle.is_focused(window))
+ .focused(selected && self.focus_handle(cx).is_focused(window))
.disabled(!has_write_access)
.on_click({
cx.listener(move |this, _, _, cx| {
@@ -1608,7 +1622,7 @@ impl GitPanel {
.spacing(ListItemSpacing::Sparse)
.start_slot(start_slot)
.toggle_state(selected)
- .focused(selected && self.focus_handle.is_focused(window))
+ .focused(selected && self.focus_handle(cx).is_focused(window))
.disabled(!has_write_access)
.on_click({
cx.listener(move |this, _, window, cx| {
@@ -1714,7 +1728,7 @@ impl Render for GitPanel {
} else {
self.render_empty_state(cx).into_any_element()
})
- .child(self.render_commit_editor(cx))
+ .child(self.render_commit_editor(window, cx))
}
}
@@ -5,14 +5,18 @@ use gpui::App;
use project_diff::ProjectDiff;
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
+pub mod branch_picker;
pub mod git_panel;
mod git_panel_settings;
pub mod project_diff;
+// mod quick_commit;
pub mod repository_selector;
pub fn init(cx: &mut App) {
GitPanelSettings::register(cx);
+ branch_picker::init(cx);
cx.observe_new(ProjectDiff::register).detach();
+ // quick_commit::init(cx);
}
// TODO: Add updated status colors to theme
@@ -0,0 +1,307 @@
+#![allow(unused, dead_code)]
+
+use crate::repository_selector::RepositorySelector;
+use anyhow::Result;
+use git::{CommitAllChanges, CommitChanges};
+use language::Buffer;
+use panel::{panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button};
+use ui::{prelude::*, Tooltip};
+
+use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
+use gpui::*;
+use project::git::Repository;
+use project::{Fs, Project};
+use std::sync::Arc;
+use workspace::{ModalView, Workspace};
+
+actions!(
+ git,
+ [QuickCommitWithMessage, QuickCommitStaged, QuickCommitAll]
+);
+
+pub fn init(cx: &mut App) {
+ cx.observe_new(|workspace: &mut Workspace, window, cx| {
+ let Some(window) = window else {
+ return;
+ };
+ QuickCommitModal::register(workspace, window, cx)
+ })
+ .detach();
+}
+
+fn commit_message_editor(
+ commit_message_buffer: Option<Entity<Buffer>>,
+ window: &mut Window,
+ cx: &mut Context<'_, Editor>,
+) -> Editor {
+ let mut commit_editor = if let Some(commit_message_buffer) = commit_message_buffer {
+ let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
+ Editor::new(
+ EditorMode::AutoHeight { max_lines: 10 },
+ buffer,
+ None,
+ false,
+ window,
+ cx,
+ )
+ } else {
+ Editor::auto_height(10, window, cx)
+ };
+ commit_editor.set_use_autoclose(false);
+ commit_editor.set_show_gutter(false, cx);
+ commit_editor.set_show_wrap_guides(false, cx);
+ commit_editor.set_show_indent_guides(false, cx);
+ commit_editor.set_placeholder_text("Enter commit message", cx);
+ commit_editor
+}
+
+pub struct QuickCommitModal {
+ focus_handle: FocusHandle,
+ fs: Arc<dyn Fs>,
+ project: Entity<Project>,
+ active_repository: Option<Entity<Repository>>,
+ repository_selector: Entity<RepositorySelector>,
+ commit_editor: Entity<Editor>,
+ width: Option<Pixels>,
+ commit_task: Task<Result<()>>,
+ commit_pending: bool,
+ can_commit: bool,
+ can_commit_all: bool,
+ enable_auto_coauthors: bool,
+}
+
+impl Focusable for QuickCommitModal {
+ fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl EventEmitter<DismissEvent> for QuickCommitModal {}
+impl ModalView for QuickCommitModal {}
+
+impl QuickCommitModal {
+ pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
+ workspace.register_action(|workspace, _: &QuickCommitWithMessage, window, cx| {
+ let project = workspace.project().clone();
+ let fs = workspace.app_state().fs.clone();
+
+ workspace.toggle_modal(window, cx, move |window, cx| {
+ QuickCommitModal::new(project, fs, window, None, cx)
+ });
+ });
+ }
+
+ pub fn new(
+ project: Entity<Project>,
+ fs: Arc<dyn Fs>,
+ window: &mut Window,
+ commit_message_buffer: Option<Entity<Buffer>>,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let git_state = project.read(cx).git_state().clone();
+ let active_repository = project.read(cx).active_repository(cx);
+
+ let focus_handle = cx.focus_handle();
+
+ let commit_editor = cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx));
+ commit_editor.update(cx, |editor, cx| {
+ editor.clear(window, cx);
+ });
+
+ let repository_selector = cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
+
+ Self {
+ focus_handle,
+ fs,
+ project,
+ active_repository,
+ repository_selector,
+ commit_editor,
+ width: None,
+ commit_task: Task::ready(Ok(())),
+ commit_pending: false,
+ can_commit: false,
+ can_commit_all: false,
+ enable_auto_coauthors: true,
+ }
+ }
+
+ pub fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let all_repositories = self
+ .project
+ .read(cx)
+ .git_state()
+ .read(cx)
+ .all_repositories();
+ let entry_count = self
+ .active_repository
+ .as_ref()
+ .map_or(0, |repo| repo.read(cx).entry_count());
+
+ let changes_string = match entry_count {
+ 0 => "No changes".to_string(),
+ 1 => "1 change".to_string(),
+ n => format!("{} changes", n),
+ };
+
+ div().absolute().top_0().right_0().child(
+ panel_icon_button("open_change_list", IconName::PanelRight)
+ .disabled(true)
+ .tooltip(Tooltip::text("Changes list coming soon!")),
+ )
+ }
+
+ pub fn render_commit_editor(
+ &self,
+ name_and_email: Option<(SharedString, SharedString)>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
+ let editor = self.commit_editor.clone();
+ let can_commit = !self.commit_pending && self.can_commit && !editor.read(cx).is_empty(cx);
+ let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
+
+ let focus_handle_1 = self.focus_handle(cx).clone();
+ let focus_handle_2 = self.focus_handle(cx).clone();
+
+ let panel_editor_style = panel_editor_style(true, window, cx);
+
+ let commit_staged_button = panel_filled_button("Commit")
+ .tooltip(move |window, cx| {
+ let focus_handle = focus_handle_1.clone();
+ Tooltip::for_action_in(
+ "Commit all staged changes",
+ &CommitChanges,
+ &focus_handle,
+ window,
+ cx,
+ )
+ })
+ .when(!can_commit, |this| {
+ this.disabled(true).style(ButtonStyle::Transparent)
+ });
+ // .on_click({
+ // let name_and_email = name_and_email.clone();
+ // cx.listener(move |this, _: &ClickEvent, window, cx| {
+ // this.commit_changes(&CommitChanges, name_and_email.clone(), window, cx)
+ // })
+ // });
+
+ let commit_all_button = panel_filled_button("Commit All")
+ .tooltip(move |window, cx| {
+ let focus_handle = focus_handle_2.clone();
+ Tooltip::for_action_in(
+ "Commit all changes, including unstaged changes",
+ &CommitAllChanges,
+ &focus_handle,
+ window,
+ cx,
+ )
+ })
+ .when(!can_commit, |this| {
+ this.disabled(true).style(ButtonStyle::Transparent)
+ });
+ // .on_click({
+ // let name_and_email = name_and_email.clone();
+ // cx.listener(move |this, _: &ClickEvent, window, cx| {
+ // this.commit_tracked_changes(
+ // &CommitAllChanges,
+ // name_and_email.clone(),
+ // window,
+ // cx,
+ // )
+ // })
+ // });
+
+ let co_author_button = panel_icon_button("add-co-author", IconName::UserGroup)
+ .icon_color(if self.enable_auto_coauthors {
+ Color::Muted
+ } else {
+ Color::Accent
+ })
+ .icon_size(IconSize::Small)
+ .toggle_state(self.enable_auto_coauthors)
+ // .on_click({
+ // cx.listener(move |this, _: &ClickEvent, _, cx| {
+ // this.toggle_auto_coauthors(cx);
+ // })
+ // })
+ .tooltip(move |window, cx| {
+ Tooltip::with_meta(
+ "Toggle automatic co-authors",
+ None,
+ "Automatically adds current collaborators",
+ window,
+ cx,
+ )
+ });
+
+ panel_editor_container(window, cx)
+ .id("commit-editor-container")
+ .relative()
+ .w_full()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .h(px(140.))
+ .bg(cx.theme().colors().editor_background)
+ .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
+ window.focus(&editor_focus_handle);
+ }))
+ .child(EditorElement::new(&self.commit_editor, panel_editor_style))
+ .child(div().flex_1())
+ .child(
+ h_flex()
+ .items_center()
+ .h_8()
+ .justify_between()
+ .gap_1()
+ .child(co_author_button)
+ .child(commit_all_button)
+ .child(commit_staged_button),
+ )
+ }
+
+ pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ h_flex()
+ .w_full()
+ .justify_between()
+ .child(h_flex().child("cmd+esc clear message"))
+ .child(
+ h_flex()
+ .child(panel_filled_button("Commit"))
+ .child(panel_filled_button("Commit All")),
+ )
+ }
+
+ fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+ cx.emit(DismissEvent);
+ }
+}
+
+impl Render for QuickCommitModal {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
+ v_flex()
+ .id("quick-commit-modal")
+ .key_context("QuickCommit")
+ .on_action(cx.listener(Self::dismiss))
+ .relative()
+ .bg(cx.theme().colors().elevated_surface_background)
+ .rounded(px(16.))
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .py_2()
+ .px_4()
+ .w(self.width.unwrap_or(px(640.)))
+ .h(px(450.))
+ .flex_1()
+ .overflow_hidden()
+ .child(self.render_header(window, cx))
+ .child(
+ v_flex()
+ .flex_1()
+ // TODO: pass name_and_email
+ .child(self.render_commit_editor(None, window, cx)),
+ )
+ .child(self.render_footer(window, cx))
+ }
+}
@@ -1,6 +1,6 @@
use gpui::{
- AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
- Task, WeakEntity,
+ AnyElement, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+ Subscription, Task, WeakEntity,
};
use picker::{Picker, PickerDelegate};
use project::{
@@ -34,6 +34,7 @@ impl RepositorySelector {
let picker = cx.new(|cx| {
Picker::nonsearchable_uniform_list(delegate, window, cx)
.max_height(Some(rems(20.).into()))
+ .width(rems(15.))
});
let _subscriptions =
@@ -78,20 +79,27 @@ impl Render for RepositorySelector {
}
#[derive(IntoElement)]
-pub struct RepositorySelectorPopoverMenu<T>
+pub struct RepositorySelectorPopoverMenu<T, TT>
where
- T: PopoverTrigger,
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
{
repository_selector: Entity<RepositorySelector>,
trigger: T,
+ tooltip: TT,
handle: Option<PopoverMenuHandle<RepositorySelector>>,
}
-impl<T: PopoverTrigger> RepositorySelectorPopoverMenu<T> {
- pub fn new(repository_selector: Entity<RepositorySelector>, trigger: T) -> Self {
+impl<T, TT> RepositorySelectorPopoverMenu<T, TT>
+where
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
+ pub fn new(repository_selector: Entity<RepositorySelector>, trigger: T, tooltip: TT) -> Self {
Self {
repository_selector,
trigger,
+ tooltip,
handle: None,
}
}
@@ -102,13 +110,17 @@ impl<T: PopoverTrigger> RepositorySelectorPopoverMenu<T> {
}
}
-impl<T: PopoverTrigger> RenderOnce for RepositorySelectorPopoverMenu<T> {
+impl<T, TT> RenderOnce for RepositorySelectorPopoverMenu<T, TT>
+where
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let repository_selector = self.repository_selector.clone();
PopoverMenu::new("repository-switcher")
.menu(move |_window, _cx| Some(repository_selector.clone()))
- .trigger(self.trigger)
+ .trigger_with_tooltip(self.trigger, self.tooltip)
.attach(gpui::Corner::BottomLeft)
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
}
@@ -0,0 +1,479 @@
+use std::{
+ ops::Range,
+ rc::Rc,
+ time::{Duration, Instant},
+};
+
+use gpui::{
+ canvas, div, point, prelude::*, px, rgb, size, uniform_list, App, Application, Bounds, Context,
+ MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, SharedString,
+ UniformListScrollHandle, Window, WindowBounds, WindowOptions,
+};
+
+const TOTAL_ITEMS: usize = 10000;
+const SCROLLBAR_THUMB_WIDTH: Pixels = px(8.);
+const SCROLLBAR_THUMB_HEIGHT: Pixels = px(100.);
+
+pub struct Quote {
+ name: SharedString,
+ symbol: SharedString,
+ last_done: f64,
+ prev_close: f64,
+ open: f64,
+ high: f64,
+ low: f64,
+ timestamp: Instant,
+ volume: i64,
+ turnover: f64,
+ ttm: f64,
+ market_cap: f64,
+ float_cap: f64,
+ shares: f64,
+ pb: f64,
+ pe: f64,
+ eps: f64,
+ dividend: f64,
+ dividend_yield: f64,
+ dividend_per_share: f64,
+ dividend_date: SharedString,
+ dividend_payment: f64,
+}
+
+impl Quote {
+ pub fn random() -> Self {
+ use rand::Rng;
+ let mut rng = rand::thread_rng();
+ // simulate a base price in a realistic range
+ let prev_close = rng.gen_range(100.0..200.0);
+ let change = rng.gen_range(-5.0..5.0);
+ let last_done = prev_close + change;
+ let open = prev_close + rng.gen_range(-3.0..3.0);
+ let high = (prev_close + rng.gen_range::<f64, _>(0.0..10.0)).max(open);
+ let low = (prev_close - rng.gen_range::<f64, _>(0.0..10.0)).min(open);
+ // Randomize the timestamp in the past 24 hours
+ let timestamp = Instant::now() - Duration::from_secs(rng.gen_range(0..86400));
+ let volume = rng.gen_range(1_000_000..100_000_000);
+ let turnover = last_done * volume as f64;
+ let symbol = {
+ let mut ticker = String::new();
+ if rng.gen_bool(0.5) {
+ ticker.push_str(&format!(
+ "{:03}.{}",
+ rng.gen_range(100..1000),
+ rng.gen_range(0..10)
+ ));
+ } else {
+ ticker.push_str(&format!(
+ "{}{}",
+ rng.gen_range('A'..='Z'),
+ rng.gen_range('A'..='Z')
+ ));
+ }
+ ticker.push_str(&format!(".{}", rng.gen_range('A'..='Z')));
+ ticker
+ };
+ let name = format!(
+ "{} {} - #{}",
+ symbol,
+ rng.gen_range(1..100),
+ rng.gen_range(10000..100000)
+ );
+ let ttm = rng.gen_range(0.0..10.0);
+ let market_cap = rng.gen_range(1_000_000.0..10_000_000.0);
+ let float_cap = market_cap + rng.gen_range(1_000.0..10_000.0);
+ let shares = rng.gen_range(100.0..1000.0);
+ let pb = market_cap / shares;
+ let pe = market_cap / shares;
+ let eps = market_cap / shares;
+ let dividend = rng.gen_range(0.0..10.0);
+ let dividend_yield = rng.gen_range(0.0..10.0);
+ let dividend_per_share = rng.gen_range(0.0..10.0);
+ let dividend_date = SharedString::new(format!(
+ "{}-{}-{}",
+ rng.gen_range(2000..2023),
+ rng.gen_range(1..12),
+ rng.gen_range(1..28)
+ ));
+ let dividend_payment = rng.gen_range(0.0..10.0);
+
+ Self {
+ name: name.into(),
+ symbol: symbol.into(),
+ last_done,
+ prev_close,
+ open,
+ high,
+ low,
+ timestamp,
+ volume,
+ turnover,
+ pb,
+ pe,
+ eps,
+ ttm,
+ market_cap,
+ float_cap,
+ shares,
+ dividend,
+ dividend_yield,
+ dividend_per_share,
+ dividend_date,
+ dividend_payment,
+ }
+ }
+
+ fn change(&self) -> f64 {
+ (self.last_done - self.prev_close) / self.prev_close * 100.0
+ }
+
+ fn change_color(&self) -> gpui::Hsla {
+ if self.change() > 0.0 {
+ gpui::green()
+ } else {
+ gpui::red()
+ }
+ }
+
+ fn turnover_ratio(&self) -> f64 {
+ self.volume as f64 / self.turnover * 100.0
+ }
+}
+
+#[derive(IntoElement)]
+struct TableRow {
+ ix: usize,
+ quote: Rc<Quote>,
+}
+impl TableRow {
+ fn new(ix: usize, quote: Rc<Quote>) -> Self {
+ Self { ix, quote }
+ }
+
+ fn render_cell(&self, key: &str, width: Pixels, color: gpui::Hsla) -> impl IntoElement {
+ div()
+ .whitespace_nowrap()
+ .truncate()
+ .w(width)
+ .px_1()
+ .child(match key {
+ "id" => div().child(format!("{}", self.ix)),
+ "symbol" => div().child(self.quote.symbol.clone()),
+ "name" => div().child(self.quote.name.clone()),
+ "last_done" => div()
+ .text_color(color)
+ .child(format!("{:.3}", self.quote.last_done)),
+ "prev_close" => div()
+ .text_color(color)
+ .child(format!("{:.3}", self.quote.prev_close)),
+ "change" => div()
+ .text_color(color)
+ .child(format!("{:.2}%", self.quote.change())),
+ "timestamp" => div()
+ .text_color(color)
+ .child(format!("{:?}", self.quote.timestamp.elapsed().as_secs())),
+ "open" => div()
+ .text_color(color)
+ .child(format!("{:.2}", self.quote.open)),
+ "low" => div()
+ .text_color(color)
+ .child(format!("{:.2}", self.quote.low)),
+ "high" => div()
+ .text_color(color)
+ .child(format!("{:.2}", self.quote.high)),
+ "ttm" => div()
+ .text_color(color)
+ .child(format!("{:.2}", self.quote.ttm)),
+ "eps" => div()
+ .text_color(color)
+ .child(format!("{:.2}", self.quote.eps)),
+ "market_cap" => {
+ div().child(format!("{:.2} M", self.quote.market_cap / 1_000_000.0))
+ }
+ "float_cap" => div().child(format!("{:.2} M", self.quote.float_cap / 1_000_000.0)),
+ "turnover" => div().child(format!("{:.2} M", self.quote.turnover / 1_000_000.0)),
+ "volume" => div().child(format!("{:.2} M", self.quote.volume as f64 / 1_000_000.0)),
+ "turnover_ratio" => div().child(format!("{:.2}%", self.quote.turnover_ratio())),
+ "pe" => div().child(format!("{:.2}", self.quote.pe)),
+ "pb" => div().child(format!("{:.2}", self.quote.pb)),
+ "shares" => div().child(format!("{:.2}", self.quote.shares)),
+ "dividend" => div().child(format!("{:.2}", self.quote.dividend)),
+ "yield" => div().child(format!("{:.2}%", self.quote.dividend_yield)),
+ "dividend_per_share" => {
+ div().child(format!("{:.2}", self.quote.dividend_per_share))
+ }
+ "dividend_date" => div().child(format!("{}", self.quote.dividend_date)),
+ "dividend_payment" => div().child(format!("{:.2}", self.quote.dividend_payment)),
+ _ => div().child("--"),
+ })
+ }
+}
+
+const FIELDS: [(&str, f32); 24] = [
+ ("id", 64.),
+ ("symbol", 64.),
+ ("name", 180.),
+ ("last_done", 80.),
+ ("prev_close", 80.),
+ ("open", 80.),
+ ("low", 80.),
+ ("high", 80.),
+ ("ttm", 50.),
+ ("market_cap", 96.),
+ ("float_cap", 96.),
+ ("turnover", 120.),
+ ("volume", 100.),
+ ("turnover_ratio", 96.),
+ ("pe", 64.),
+ ("pb", 64.),
+ ("eps", 64.),
+ ("shares", 96.),
+ ("dividend", 64.),
+ ("yield", 64.),
+ ("dividend_per_share", 64.),
+ ("dividend_date", 96.),
+ ("dividend_payment", 64.),
+ ("timestamp", 120.),
+];
+
+impl RenderOnce for TableRow {
+ fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+ let color = self.quote.change_color();
+ div()
+ .flex()
+ .flex_row()
+ .border_b_1()
+ .border_color(rgb(0xE0E0E0))
+ .bg(if self.ix % 2 == 0 {
+ rgb(0xFFFFFF)
+ } else {
+ rgb(0xFAFAFA)
+ })
+ .py_0p5()
+ .px_2()
+ .children(FIELDS.map(|(key, width)| self.render_cell(key, px(width), color)))
+ }
+}
+
+struct DataTable {
+ /// Use `Rc` to share the same quote data across multiple items, avoid cloning.
+ quotes: Vec<Rc<Quote>>,
+ visible_range: Range<usize>,
+ scroll_handle: UniformListScrollHandle,
+ /// The position in thumb bounds when dragging start mouse down.
+ drag_position: Option<Point<Pixels>>,
+}
+
+impl DataTable {
+ fn new() -> Self {
+ Self {
+ quotes: Vec::new(),
+ visible_range: 0..0,
+ scroll_handle: UniformListScrollHandle::new(),
+ drag_position: None,
+ }
+ }
+
+ fn generate(&mut self) {
+ self.quotes = (0..TOTAL_ITEMS).map(|_| Rc::new(Quote::random())).collect();
+ }
+
+ fn table_bounds(&self) -> Bounds<Pixels> {
+ self.scroll_handle.0.borrow().base_handle.bounds()
+ }
+
+ fn scroll_top(&self) -> Pixels {
+ self.scroll_handle.0.borrow().base_handle.offset().y
+ }
+
+ fn scroll_height(&self) -> Pixels {
+ self.scroll_handle
+ .0
+ .borrow()
+ .last_item_size
+ .unwrap_or_default()
+ .contents
+ .height
+ }
+
+ fn render_scrollbar(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let scroll_height = self.scroll_height();
+ let table_bounds = self.table_bounds();
+ let table_height = table_bounds.size.height;
+ if table_height == px(0.) {
+ return div().id("scrollbar");
+ }
+
+ let percentage = -self.scroll_top() / scroll_height;
+ let offset_top = (table_height * percentage).clamp(
+ px(4.),
+ (table_height - SCROLLBAR_THUMB_HEIGHT - px(4.)).max(px(4.)),
+ );
+ let entity = cx.entity();
+ let scroll_handle = self.scroll_handle.0.borrow().base_handle.clone();
+
+ div()
+ .id("scrollbar")
+ .absolute()
+ .top(offset_top)
+ .right_1()
+ .h(SCROLLBAR_THUMB_HEIGHT)
+ .w(SCROLLBAR_THUMB_WIDTH)
+ .bg(rgb(0xC0C0C0))
+ .hover(|this| this.bg(rgb(0xA0A0A0)))
+ .rounded_lg()
+ .child(
+ canvas(
+ |_, _, _| (),
+ move |thumb_bounds, _, window, _| {
+ window.on_mouse_event({
+ let entity = entity.clone();
+ move |ev: &MouseDownEvent, _, _, cx| {
+ if !thumb_bounds.contains(&ev.position) {
+ return;
+ }
+
+ entity.update(cx, |this, _| {
+ this.drag_position = Some(
+ ev.position - thumb_bounds.origin - table_bounds.origin,
+ );
+ })
+ }
+ });
+ window.on_mouse_event({
+ let entity = entity.clone();
+ move |_: &MouseUpEvent, _, _, cx| {
+ entity.update(cx, |this, _| {
+ this.drag_position = None;
+ })
+ }
+ });
+
+ window.on_mouse_event(move |ev: &MouseMoveEvent, _, _, cx| {
+ if !ev.dragging() {
+ return;
+ }
+
+ let Some(drag_pos) = entity.read(cx).drag_position else {
+ return;
+ };
+
+ let inside_offset = drag_pos.y;
+ let percentage = ((ev.position.y - table_bounds.origin.y
+ + inside_offset)
+ / (table_bounds.size.height))
+ .clamp(0., 1.);
+
+ let offset_y = ((scroll_height - table_bounds.size.height)
+ * percentage)
+ .clamp(px(0.), scroll_height - SCROLLBAR_THUMB_HEIGHT);
+ scroll_handle.set_offset(point(px(0.), -offset_y));
+ cx.notify(entity.entity_id());
+ })
+ },
+ )
+ .size_full(),
+ )
+ }
+}
+
+impl Render for DataTable {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let entity = cx.entity();
+
+ div()
+ .font_family(".SystemUIFont")
+ .bg(gpui::white())
+ .text_sm()
+ .size_full()
+ .p_4()
+ .gap_2()
+ .flex()
+ .flex_col()
+ .child(format!(
+ "Total {} items, visible range: {:?}",
+ self.quotes.len(),
+ self.visible_range
+ ))
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .flex_1()
+ .overflow_hidden()
+ .border_1()
+ .border_color(rgb(0xE0E0E0))
+ .rounded_md()
+ .child(
+ div()
+ .flex()
+ .flex_row()
+ .w_full()
+ .overflow_hidden()
+ .border_b_1()
+ .border_color(rgb(0xE0E0E0))
+ .text_color(rgb(0x555555))
+ .bg(rgb(0xF0F0F0))
+ .py_1()
+ .px_2()
+ .text_xs()
+ .children(FIELDS.map(|(key, width)| {
+ div()
+ .whitespace_nowrap()
+ .flex_shrink_0()
+ .truncate()
+ .px_1()
+ .w(px(width))
+ .child(key.replace("_", " ").to_uppercase())
+ })),
+ )
+ .child(
+ div()
+ .relative()
+ .size_full()
+ .child(
+ uniform_list(entity, "items", self.quotes.len(), {
+ move |this, range, _, _| {
+ this.visible_range = range.clone();
+ let mut items = Vec::with_capacity(range.end - range.start);
+ for i in range {
+ if let Some(quote) = this.quotes.get(i) {
+ items.push(TableRow::new(i, quote.clone()));
+ }
+ }
+ items
+ }
+ })
+ .size_full()
+ .track_scroll(self.scroll_handle.clone()),
+ )
+ .child(self.render_scrollbar(window, cx)),
+ ),
+ )
+ }
+}
+
+fn main() {
+ Application::new().run(|cx: &mut App| {
+ cx.open_window(
+ WindowOptions {
+ focus: true,
+ window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
+ None,
+ size(px(1280.0), px(1000.0)),
+ cx,
+ ))),
+ ..Default::default()
+ },
+ |_, cx| {
+ cx.new(|_| {
+ let mut table = DataTable::new();
+ table.generate();
+ table
+ })
+ },
+ )
+ .unwrap();
+
+ cx.activate(true);
+ });
+}
@@ -116,6 +116,7 @@ impl<T: ?Sized> ArenaBox<T> {
}
}
+ #[track_caller]
fn validate(&self) {
assert!(
self.valid.get(),
@@ -1,4 +1,4 @@
-use gpui::{App, Context, Entity};
+use gpui::{App, Context, Entity, SharedString};
use language::Buffer;
use project::Project;
use std::ops::Range;
@@ -15,6 +15,8 @@ pub enum Direction {
#[derive(Clone)]
pub struct InlineCompletion {
+ /// The ID of the completion, if it has one.
+ pub id: Option<SharedString>,
pub edits: Vec<(Range<language::Anchor>, String)>,
pub edit_preview: Option<language::EditPreview>,
}
@@ -22,7 +24,7 @@ pub struct InlineCompletion {
pub enum DataCollectionState {
/// The provider doesn't support data collection.
Unsupported,
- /// Data collection is enabled
+ /// Data collection is enabled.
Enabled,
/// Data collection is disabled or unanswered.
Disabled,
@@ -1,7 +1,11 @@
use anyhow::Result;
use client::UserStore;
use copilot::{Copilot, Status};
-use editor::{actions::ShowEditPrediction, scroll::Autoscroll, Editor};
+use editor::{
+ actions::{ShowEditPrediction, ToggleEditPrediction},
+ scroll::Autoscroll,
+ Editor,
+};
use feature_flags::{
FeatureFlagAppExt, PredictEditsFeatureFlag, PredictEditsRateCompletionsFeatureFlag,
};
@@ -44,6 +48,7 @@ struct CopilotErrorToast;
pub struct InlineCompletionButton {
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
+ editor_show_predictions: bool,
editor_focus_handle: Option<FocusHandle>,
language: Option<Arc<Language>>,
file: Option<Arc<dyn File>>,
@@ -137,9 +142,12 @@ impl Render for InlineCompletionButton {
})
})
.anchor(Corner::BottomRight)
- .trigger(IconButton::new("copilot-icon", icon).tooltip(|window, cx| {
- Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx)
- }))
+ .trigger_with_tooltip(
+ IconButton::new("copilot-icon", icon),
+ |window, cx| {
+ Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx)
+ },
+ )
.with_handle(self.popover_menu_handle.clone()),
)
}
@@ -206,7 +214,8 @@ impl Render for InlineCompletionButton {
_ => None,
})
.anchor(Corner::BottomRight)
- .trigger(IconButton::new("supermaven-icon", icon).tooltip(
+ .trigger_with_tooltip(
+ IconButton::new("supermaven-icon", icon),
move |window, cx| {
if has_menu {
Tooltip::for_action(
@@ -219,7 +228,7 @@ impl Render for InlineCompletionButton {
Tooltip::text(tooltip_text.clone())(window, cx)
}
},
- ))
+ )
.with_handle(self.popover_menu_handle.clone()),
);
}
@@ -275,15 +284,29 @@ impl Render for InlineCompletionButton {
);
}
+ let show_editor_predictions = self.editor_show_predictions;
+
let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
.shape(IconButtonShape::Square)
+ .when(enabled && !show_editor_predictions, |this| {
+ this.indicator(Indicator::dot().color(Color::Muted))
+ .indicator_border_color(Some(cx.theme().colors().status_bar_background))
+ })
.when(!self.popover_menu_handle.is_deployed(), |element| {
- if enabled {
- element.tooltip(|window, cx| {
- Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
- })
- } else {
- element.tooltip(|window, cx| {
+ element.tooltip(move |window, cx| {
+ if enabled {
+ if show_editor_predictions {
+ Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
+ } else {
+ Tooltip::with_meta(
+ "Edit Prediction",
+ Some(&ToggleMenu),
+ "Hidden For This File",
+ window,
+ cx,
+ )
+ }
+ } else {
Tooltip::with_meta(
"Edit Prediction",
Some(&ToggleMenu),
@@ -291,8 +314,8 @@ impl Render for InlineCompletionButton {
window,
cx,
)
- })
- }
+ }
+ })
});
let this = cx.entity().clone();
@@ -347,6 +370,7 @@ impl InlineCompletionButton {
Self {
editor_subscription: None,
editor_enabled: None,
+ editor_show_predictions: true,
editor_focus_handle: None,
language: None,
file: None,
@@ -384,6 +408,21 @@ impl InlineCompletionButton {
menu = menu.header("Show Edit Predictions For");
+ if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
+ menu = menu.toggleable_entry(
+ "This File",
+ self.editor_show_predictions,
+ IconPosition::Start,
+ Some(Box::new(ToggleEditPrediction)),
+ {
+ let editor_focus_handle = editor_focus_handle.clone();
+ move |window, cx| {
+ editor_focus_handle.dispatch_action(&ToggleEditPrediction, window, cx);
+ }
+ },
+ );
+ }
+
if let Some(language) = self.language.clone() {
let fs = fs.clone();
let language_enabled =
@@ -393,7 +432,7 @@ impl InlineCompletionButton {
menu = menu.toggleable_entry(
language.name(),
language_enabled,
- IconPosition::End,
+ IconPosition::Start,
None,
move |_, cx| {
toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx)
@@ -406,7 +445,7 @@ impl InlineCompletionButton {
menu = menu.toggleable_entry(
"All Files",
globally_enabled,
- IconPosition::End,
+ IconPosition::Start,
None,
move |_, cx| toggle_inline_completions_globally(fs.clone(), cx),
);
@@ -422,7 +461,7 @@ impl InlineCompletionButton {
// TODO: We want to add something later that communicates whether
// the current project is open-source.
ContextMenuEntry::new("Share Training Data")
- .toggleable(IconPosition::End, data_collection.is_enabled())
+ .toggleable(IconPosition::Start, data_collection.is_enabled())
.documentation_aside(|_| {
Label::new(indoc!{"
Help us improve our open model by sharing data from open source repositories. \
@@ -450,6 +489,8 @@ impl InlineCompletionButton {
menu = menu.item(
ContextMenuEntry::new("Configure Excluded Files")
+ .icon(IconName::LockOutlined)
+ .icon_color(Color::Muted)
.documentation_aside(|_| {
Label::new(indoc!{"
Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element()
@@ -486,7 +527,6 @@ impl InlineCompletionButton {
Some(Box::new(ShowEditPrediction)),
{
let editor_focus_handle = editor_focus_handle.clone();
-
move |window, cx| {
editor_focus_handle.dispatch_action(&ShowEditPrediction, window, cx);
}
@@ -571,6 +611,7 @@ impl InlineCompletionButton {
.unwrap_or(true),
)
};
+ self.editor_show_predictions = editor.should_show_inline_completions(cx);
self.edit_prediction_provider = editor.edit_prediction_provider();
self.language = language.cloned();
self.file = file;
@@ -2,7 +2,7 @@ use std::sync::Arc;
use feature_flags::ZedPro;
use gpui::{
- Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+ Action, AnyElement, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
Subscription, Task, WeakEntity,
};
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
@@ -115,20 +115,31 @@ impl Render for LanguageModelSelector {
}
#[derive(IntoElement)]
-pub struct LanguageModelSelectorPopoverMenu<T>
+pub struct LanguageModelSelectorPopoverMenu<T, TT>
where
- T: PopoverTrigger,
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
{
language_model_selector: Entity<LanguageModelSelector>,
trigger: T,
+ tooltip: TT,
handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
}
-impl<T: PopoverTrigger> LanguageModelSelectorPopoverMenu<T> {
- pub fn new(language_model_selector: Entity<LanguageModelSelector>, trigger: T) -> Self {
+impl<T, TT> LanguageModelSelectorPopoverMenu<T, TT>
+where
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
+ pub fn new(
+ language_model_selector: Entity<LanguageModelSelector>,
+ trigger: T,
+ tooltip: TT,
+ ) -> Self {
Self {
language_model_selector,
trigger,
+ tooltip,
handle: None,
}
}
@@ -139,13 +150,17 @@ impl<T: PopoverTrigger> LanguageModelSelectorPopoverMenu<T> {
}
}
-impl<T: PopoverTrigger> RenderOnce for LanguageModelSelectorPopoverMenu<T> {
+impl<T, TT> RenderOnce for LanguageModelSelectorPopoverMenu<T, TT>
+where
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let language_model_selector = self.language_model_selector.clone();
PopoverMenu::new("model-switcher")
.menu(move |_window, _cx| Some(language_model_selector.clone()))
- .trigger(self.trigger)
+ .trigger_with_tooltip(self.trigger, self.tooltip)
.anchor(gpui::Corner::BottomRight)
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
.offset(gpui::Point {
@@ -293,7 +293,7 @@ impl SyntaxTreeView {
impl Render for SyntaxTreeView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let mut rendered = div().flex_1();
+ let mut rendered = div().flex_1().bg(cx.theme().colors().editor_background);
if let Some(layer) = self
.editor
@@ -7320,6 +7320,7 @@ impl ToOffset for Point {
}
impl ToOffset for usize {
+ #[track_caller]
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
assert!(*self <= snapshot.len(), "offset is out of range");
*self
@@ -2,10 +2,14 @@ mod outline_panel_settings;
use std::{
cmp,
+ collections::BTreeMap,
hash::Hash,
ops::Range,
path::{Path, PathBuf, MAIN_SEPARATOR_STR},
- sync::{atomic::AtomicBool, Arc, OnceLock},
+ sync::{
+ atomic::{self, AtomicBool},
+ Arc, OnceLock,
+ },
time::Duration,
u32,
};
@@ -103,6 +107,7 @@ pub struct OutlinePanel {
active_item: Option<ActiveItem>,
_subscriptions: Vec<Subscription>,
updating_fs_entries: bool,
+ updating_cached_entries: bool,
new_entries_for_fs_update: HashSet<ExcerptId>,
fs_entries_update_task: Task<()>,
cached_entries_update_task: Task<()>,
@@ -777,7 +782,10 @@ impl OutlinePanel {
excerpt.invalidate_outlines();
}
}
- outline_panel.update_non_fs_items(window, cx);
+ let update_cached_items = outline_panel.update_non_fs_items(window, cx);
+ if update_cached_items {
+ outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
+ }
} else if &outline_panel_settings != new_settings {
outline_panel_settings = *new_settings;
cx.notify();
@@ -814,6 +822,7 @@ impl OutlinePanel {
active_item: None,
pending_serialization: Task::ready(None),
updating_fs_entries: false,
+ updating_cached_entries: false,
new_entries_for_fs_update: HashSet::default(),
preserve_selection_on_buffer_fold_toggles: HashSet::default(),
fs_entries_update_task: Task::ready(()),
@@ -922,7 +931,7 @@ impl OutlinePanel {
cx.propagate()
} else if let Some(selected_entry) = self.selected_entry().cloned() {
self.toggle_expanded(&selected_entry, window, cx);
- self.scroll_editor_to_entry(&selected_entry, true, false, window, cx);
+ self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
}
}
@@ -977,7 +986,7 @@ impl OutlinePanel {
&mut self,
entry: &PanelEntry,
prefer_selection_change: bool,
- change_focus: bool,
+ prefer_focus_change: bool,
window: &mut Window,
cx: &mut Context<OutlinePanel>,
) {
@@ -987,9 +996,13 @@ impl OutlinePanel {
let active_multi_buffer = active_editor.read(cx).buffer().clone();
let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
let mut change_selection = prefer_selection_change;
+ let mut change_focus = prefer_focus_change;
let mut scroll_to_buffer = None;
let scroll_target = match entry {
- PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => None,
+ PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => {
+ change_focus = false;
+ None
+ }
PanelEntry::Fs(FsEntry::ExternalFile(file)) => {
change_selection = false;
scroll_to_buffer = Some(file.buffer_id);
@@ -1033,6 +1046,7 @@ impl OutlinePanel {
}),
PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
change_selection = false;
+ change_focus = false;
multi_buffer_snapshot.anchor_in_excerpt(excerpt.id, excerpt.range.context.start)
}
PanelEntry::Search(search_entry) => Some(search_entry.match_range.start),
@@ -2613,7 +2627,7 @@ impl OutlinePanel {
.spawn(async move {
let mut processed_external_buffers = HashSet::default();
let mut new_worktree_entries =
- HashMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
+ BTreeMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
let mut worktree_excerpts = HashMap::<
WorktreeId,
HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
@@ -2896,8 +2910,8 @@ impl OutlinePanel {
outline_panel.fs_entries = new_fs_entries;
outline_panel.fs_entries_depth = new_depth_map;
outline_panel.fs_children_count = new_children_count;
- outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
outline_panel.update_non_fs_items(window, cx);
+ outline_panel.update_cached_entries(debounce, window, cx);
cx.notify();
})
@@ -2922,7 +2936,11 @@ impl OutlinePanel {
window: &mut Window,
cx: &mut Context<Self>| {
if matches!(e, SearchEvent::MatchesInvalidated) {
- outline_panel.update_search_matches(window, cx);
+ let update_cached_items = outline_panel.update_search_matches(window, cx);
+ if update_cached_items {
+ outline_panel.selected_entry.invalidate();
+ outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
+ }
};
outline_panel.autoscroll(cx);
},
@@ -3188,10 +3206,12 @@ impl OutlinePanel {
}
let syntax_theme = cx.theme().syntax().clone();
+ let first_update = Arc::new(AtomicBool::new(true));
for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
for (excerpt_id, excerpt_range) in excerpt_ranges {
let syntax_theme = syntax_theme.clone();
let buffer_snapshot = buffer_snapshot.clone();
+ let first_update = first_update.clone();
self.outline_fetch_tasks.insert(
(buffer_id, excerpt_id),
cx.spawn_in(window, |outline_panel, mut cx| async move {
@@ -3215,13 +3235,16 @@ impl OutlinePanel {
.or_default()
.get_mut(&excerpt_id)
{
+ let debounce = if first_update
+ .fetch_and(false, atomic::Ordering::AcqRel)
+ {
+ None
+ } else {
+ Some(UPDATE_DEBOUNCE)
+ };
excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
+ outline_panel.update_cached_entries(debounce, window, cx);
}
- outline_panel.update_cached_entries(
- Some(UPDATE_DEBOUNCE),
- window,
- cx,
- );
})
.ok();
}),
@@ -3376,6 +3399,7 @@ impl OutlinePanel {
let is_singleton = self.is_singleton_active(cx);
let query = self.query(cx);
+ self.updating_cached_entries = true;
self.cached_entries_update_task = cx.spawn_in(window, |outline_panel, mut cx| async move {
if let Some(debounce) = debounce {
cx.background_executor().timer(debounce).await;
@@ -3410,6 +3434,7 @@ impl OutlinePanel {
}
outline_panel.autoscroll(cx);
+ outline_panel.updating_cached_entries = false;
cx.notify();
})
.ok();
@@ -3468,7 +3493,8 @@ impl OutlinePanel {
.copied()
.unwrap_or(0);
while let Some(parent) = parent_dirs.last() {
- if directory_entry.entry.path.starts_with(&parent.path) {
+ if !is_root && directory_entry.entry.path.starts_with(&parent.path)
+ {
break;
}
parent_dirs.pop();
@@ -3915,19 +3941,27 @@ impl OutlinePanel {
!self.collapsed_entries.contains(&entry_to_check)
}
- fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) {
+ fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) -> bool {
if !self.active {
- return;
+ return false;
}
- self.update_search_matches(window, cx);
+ let mut update_cached_items = false;
+ update_cached_items |= self.update_search_matches(window, cx);
self.fetch_outdated_outlines(window, cx);
- self.autoscroll(cx);
+ if update_cached_items {
+ self.selected_entry.invalidate();
+ }
+ update_cached_items
}
- fn update_search_matches(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) {
+ fn update_search_matches(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context<OutlinePanel>,
+ ) -> bool {
if !self.active {
- return;
+ return false;
}
let project_search = self
@@ -4010,10 +4044,7 @@ impl OutlinePanel {
cx,
));
}
- if update_cached_entries {
- self.selected_entry.invalidate();
- self.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
- }
+ update_cached_entries
}
#[allow(clippy::too_many_arguments)]
@@ -4426,41 +4457,42 @@ impl OutlinePanel {
cx: &mut Context<Self>,
) -> Div {
let contents = if self.cached_entries.is_empty() {
- let header = if self.updating_fs_entries {
- "Loading outlines"
+ let header = if self.updating_fs_entries || self.updating_cached_entries {
+ None
} else if query.is_some() {
- "No matches for query"
+ Some("No matches for query")
} else {
- "No outlines available"
+ Some("No outlines available")
};
v_flex()
.flex_1()
.justify_center()
.size_full()
- .child(h_flex().justify_center().child(Label::new(header)))
- .when_some(query.clone(), |panel, query| {
- panel.child(h_flex().justify_center().child(Label::new(query)))
+ .when_some(header, |panel, header| {
+ panel
+ .child(h_flex().justify_center().child(Label::new(header)))
+ .when_some(query.clone(), |panel, query| {
+ panel.child(h_flex().justify_center().child(Label::new(query)))
+ })
+ .child(
+ h_flex()
+ .pt(DynamicSpacing::Base04.rems(cx))
+ .justify_center()
+ .child({
+ let keystroke =
+ match self.position(window, cx) {
+ DockPosition::Left => window
+ .keystroke_text_for(&workspace::ToggleLeftDock),
+ DockPosition::Bottom => window
+ .keystroke_text_for(&workspace::ToggleBottomDock),
+ DockPosition::Right => window
+ .keystroke_text_for(&workspace::ToggleRightDock),
+ };
+ Label::new(format!("Toggle this panel with {keystroke}"))
+ }),
+ )
})
- .child(
- h_flex()
- .pt(DynamicSpacing::Base04.rems(cx))
- .justify_center()
- .child({
- let keystroke = match self.position(window, cx) {
- DockPosition::Left => {
- window.keystroke_text_for(&workspace::ToggleLeftDock)
- }
- DockPosition::Bottom => {
- window.keystroke_text_for(&workspace::ToggleBottomDock)
- }
- DockPosition::Right => {
- window.keystroke_text_for(&workspace::ToggleRightDock)
- }
- };
- Label::new(format!("Toggle this panel with {keystroke}"))
- }),
- )
} else {
let list_contents = {
let items_len = self.cached_entries.len();
@@ -4995,11 +5027,17 @@ fn subscribe_for_editor_events(
}
EditorEvent::ExcerptsExpanded { ids } => {
outline_panel.invalidate_outlines(ids);
- outline_panel.update_non_fs_items(window, cx);
+ let update_cached_items = outline_panel.update_non_fs_items(window, cx);
+ if update_cached_items {
+ outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
+ }
}
EditorEvent::ExcerptsEdited { ids } => {
outline_panel.invalidate_outlines(ids);
- outline_panel.update_non_fs_items(window, cx);
+ let update_cached_items = outline_panel.update_non_fs_items(window, cx);
+ if update_cached_items {
+ outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
+ }
}
EditorEvent::BufferFoldToggled { ids, .. } => {
outline_panel.invalidate_outlines(ids);
@@ -5073,7 +5111,10 @@ fn subscribe_for_editor_events(
excerpt.invalidate_outlines();
}
}
- outline_panel.update_non_fs_items(window, cx);
+ let update_cached_items = outline_panel.update_non_fs_items(window, cx);
+ if update_cached_items {
+ outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
+ }
}
_ => {}
}
@@ -5117,6 +5158,7 @@ mod tests {
use project::FakeFs;
use search::project_search::{self, perform_project_search};
use serde_json::json;
+ use workspace::OpenVisible;
use super::*;
@@ -5173,7 +5215,7 @@ mod tests {
});
});
- let all_matches = r#"/
+ let all_matches = r#"/rust-analyzer/
crates/
ide/src/
inlay_hints/
@@ -5208,9 +5250,11 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
select_first_in_all_matches(
"search: match config.param_names_for_lifetime_elision_hints {"
@@ -5222,9 +5266,11 @@ mod tests {
outline_panel.select_parent(&SelectParent, window, cx);
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
select_first_in_all_matches("fn_lifetime_fn.rs")
);
@@ -5238,12 +5284,14 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
format!(
- r#"/
+ r#"/rust-analyzer/
crates/
ide/src/
inlay_hints/
@@ -5273,9 +5321,11 @@ mod tests {
outline_panel.select_parent(&SelectParent, window, cx);
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
select_first_in_all_matches("inlay_hints/")
);
@@ -5285,9 +5335,11 @@ mod tests {
outline_panel.select_parent(&SelectParent, window, cx);
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
select_first_in_all_matches("ide/src/")
);
@@ -5302,12 +5354,14 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
format!(
- r#"/
+ r#"/rust-analyzer/
crates/
ide/src/{SELECTED_MARKER}
rust-analyzer/src/
@@ -5328,9 +5382,11 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
select_first_in_all_matches("ide/src/")
);
@@ -5387,7 +5443,7 @@ mod tests {
);
});
});
- let all_matches = r#"/
+ let all_matches = r#"/rust-analyzer/
crates/
ide/src/
inlay_hints/
@@ -5414,9 +5470,11 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
None,
+ cx,
),
all_matches,
);
@@ -5435,12 +5493,15 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
None,
+ cx,
),
all_matches
.lines()
+ .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
.filter(|item| item.contains(filter_text))
.collect::<Vec<_>>()
.join("\n"),
@@ -5458,9 +5519,11 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
None,
+ cx,
),
all_matches,
);
@@ -5517,7 +5580,7 @@ mod tests {
);
});
});
- let all_matches = r#"/
+ let all_matches = r#"/rust-analyzer/
crates/
ide/src/
inlay_hints/
@@ -5559,9 +5622,11 @@ mod tests {
outline_panel.update_in(cx, |outline_panel, window, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry(),
+ cx,
),
select_first_in_all_matches(initial_outline_selection)
);
@@ -5580,9 +5645,11 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry(),
+ cx,
),
select_first_in_all_matches(navigated_outline_selection)
);
@@ -5616,9 +5683,11 @@ mod tests {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry(),
+ cx,
),
select_first_in_all_matches(next_navigated_outline_selection)
);
@@ -5651,9 +5720,11 @@ mod tests {
);
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry(),
+ cx,
),
"fn_lifetime_fn.rs <==== selected"
);
@@ -5665,6 +5736,176 @@ mod tests {
});
}
+ #[gpui::test]
+ async fn test_multiple_workrees(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "one": {
+ "a.txt": "aaa aaa"
+ },
+ "two": {
+ "b.txt": "a aaa"
+ }
+
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await;
+ let workspace = add_outline_panel(&project, cx).await;
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let outline_panel = outline_panel(&workspace, cx);
+ outline_panel.update_in(cx, |outline_panel, window, cx| {
+ outline_panel.set_active(true, window, cx)
+ });
+
+ let items = workspace
+ .update(cx, |workspace, window, cx| {
+ workspace.open_paths(
+ vec![PathBuf::from("/root/two")],
+ OpenVisible::OnlyDirectories,
+ None,
+ window,
+ cx,
+ )
+ })
+ .unwrap()
+ .await;
+ assert_eq!(items.len(), 1, "Were opening another worktree directory");
+ assert!(
+ items[0].is_none(),
+ "Directory should be opened successfully"
+ );
+
+ workspace
+ .update(cx, |workspace, window, cx| {
+ ProjectSearchView::deploy_search(
+ workspace,
+ &workspace::DeploySearch::default(),
+ window,
+ cx,
+ )
+ })
+ .unwrap();
+ let search_view = workspace
+ .update(cx, |workspace, _, cx| {
+ workspace
+ .active_pane()
+ .read(cx)
+ .items()
+ .find_map(|item| item.downcast::<ProjectSearchView>())
+ .expect("Project search view expected to appear after new search event trigger")
+ })
+ .unwrap();
+
+ let query = "aaa";
+ perform_project_search(&search_view, query, cx);
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .results_editor()
+ .update(cx, |results_editor, cx| {
+ assert_eq!(
+ results_editor.display_text(cx).match_indices(query).count(),
+ 3
+ );
+ });
+ });
+
+ cx.executor()
+ .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+ cx.run_until_parked();
+ outline_panel.update(cx, |outline_panel, cx| {
+ assert_eq!(
+ display_entries(
+ &project,
+ &snapshot(&outline_panel, cx),
+ &outline_panel.cached_entries,
+ outline_panel.selected_entry(),
+ cx,
+ ),
+ r#"/root/one/
+ a.txt
+ search: aaa aaa <==== selected
+ search: aaa aaa
+/root/two/
+ b.txt
+ search: a aaa"#
+ );
+ });
+
+ outline_panel.update_in(cx, |outline_panel, window, cx| {
+ outline_panel.select_prev(&SelectPrev, window, cx);
+ outline_panel.open(&Open, window, cx);
+ });
+ cx.executor()
+ .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+ cx.run_until_parked();
+ outline_panel.update(cx, |outline_panel, cx| {
+ assert_eq!(
+ display_entries(
+ &project,
+ &snapshot(&outline_panel, cx),
+ &outline_panel.cached_entries,
+ outline_panel.selected_entry(),
+ cx,
+ ),
+ r#"/root/one/
+ a.txt <==== selected
+/root/two/
+ b.txt
+ search: a aaa"#
+ );
+ });
+
+ outline_panel.update_in(cx, |outline_panel, window, cx| {
+ outline_panel.select_next(&SelectNext, window, cx);
+ outline_panel.open(&Open, window, cx);
+ });
+ cx.executor()
+ .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+ cx.run_until_parked();
+ outline_panel.update(cx, |outline_panel, cx| {
+ assert_eq!(
+ display_entries(
+ &project,
+ &snapshot(&outline_panel, cx),
+ &outline_panel.cached_entries,
+ outline_panel.selected_entry(),
+ cx,
+ ),
+ r#"/root/one/
+ a.txt
+/root/two/ <==== selected"#
+ );
+ });
+
+ outline_panel.update_in(cx, |outline_panel, window, cx| {
+ outline_panel.open(&Open, window, cx);
+ });
+ cx.executor()
+ .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+ cx.run_until_parked();
+ outline_panel.update(cx, |outline_panel, cx| {
+ assert_eq!(
+ display_entries(
+ &project,
+ &snapshot(&outline_panel, cx),
+ &outline_panel.cached_entries,
+ outline_panel.selected_entry(),
+ cx,
+ ),
+ r#"/root/one/
+ a.txt
+/root/two/ <==== selected
+ b.txt
+ search: a aaa"#
+ );
+ });
+ }
+
#[gpui::test]
async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
init_test(cx);
@@ -5730,9 +5971,11 @@ struct OutlineEntryExcerpt {
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
indoc!(
"
@@ -5755,9 +5998,11 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
indoc!(
"
@@ -5780,9 +6025,11 @@ outline: struct OutlineEntryExcerpt <==== selected
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
indoc!(
"
@@ -5805,9 +6052,11 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
indoc!(
"
@@ -5830,9 +6079,11 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
indoc!(
"
@@ -5855,9 +6106,11 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
indoc!(
"
@@ -5880,9 +6133,11 @@ outline: struct OutlineEntryExcerpt <==== selected
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
indoc!(
"
@@ -5905,9 +6160,11 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
indoc!(
"
@@ -5930,9 +6187,11 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
indoc!(
"
@@ -5955,9 +6214,11 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
indoc!(
"
@@ -5980,9 +6241,11 @@ outline: struct OutlineEntryExcerpt <==== selected
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
indoc!(
"
@@ -6084,11 +6347,13 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
- r#"/
+ r#"/frontend-project/
public/lottie/
syntax-tree.json
search: { "something": "static" } <==== selected
@@ -6119,11 +6384,13 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
- r#"/
+ r#"/frontend-project/
public/lottie/
syntax-tree.json
search: { "something": "static" }
@@ -6145,11 +6412,13 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
- r#"/
+ r#"/frontend-project/
public/lottie/
syntax-tree.json
search: { "something": "static" }
@@ -6175,11 +6444,13 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
- r#"/
+ r#"/frontend-project/
public/lottie/
syntax-tree.json
search: { "something": "static" }
@@ -6204,11 +6475,13 @@ outline: struct OutlineEntryExcerpt
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
+ &project,
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
- outline_panel.selected_entry()
+ outline_panel.selected_entry(),
+ cx,
),
- r#"/
+ r#"/frontend-project/
public/lottie/
syntax-tree.json
search: { "something": "static" }
@@ -6255,9 +6528,11 @@ outline: struct OutlineEntryExcerpt
}
fn display_entries(
+ project: &Entity<Project>,
multi_buffer_snapshot: &MultiBufferSnapshot,
cached_entries: &[CachedEntry],
selected_entry: Option<&PanelEntry>,
+ cx: &mut App,
) -> String {
let mut display_string = String::new();
for entry in cached_entries {
@@ -6272,15 +6547,33 @@ outline: struct OutlineEntryExcerpt
FsEntry::ExternalFile(_) => {
panic!("Did not cover external files with tests")
}
- FsEntry::Directory(directory) => format!(
- "{}/",
- directory
- .entry
- .path
- .file_name()
- .map(|name| name.to_string_lossy().to_string())
- .unwrap_or_default()
- ),
+ FsEntry::Directory(directory) => {
+ match project
+ .read(cx)
+ .worktree_for_id(directory.worktree_id, cx)
+ .and_then(|worktree| {
+ if worktree.read(cx).root_entry() == Some(&directory.entry.entry) {
+ Some(worktree.read(cx).abs_path())
+ } else {
+ None
+ }
+ }) {
+ Some(root_path) => format!(
+ "{}/{}",
+ root_path.display(),
+ directory.entry.path.display(),
+ ),
+ None => format!(
+ "{}/",
+ directory
+ .entry
+ .path
+ .file_name()
+ .unwrap_or_default()
+ .to_string_lossy()
+ ),
+ }
+ }
FsEntry::File(file) => file
.entry
.path
@@ -12,6 +12,9 @@ workspace = true
path = "src/panel.rs"
[dependencies]
+editor.workspace = true
gpui.workspace = true
+settings.workspace = true
+theme.workspace = true
ui.workspace = true
workspace.workspace = true
@@ -1,5 +1,8 @@
//! # panel
-use gpui::actions;
+use editor::{Editor, EditorElement, EditorStyle};
+use gpui::{actions, Entity, TextStyle};
+use settings::Settings;
+use theme::ThemeSettings;
use ui::{prelude::*, Tab};
actions!(panel, [NextPanelTab, PreviousPanelTab]);
@@ -46,7 +49,8 @@ pub fn panel_button(label: impl Into<SharedString>) -> ui::Button {
let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into());
ui::Button::new(id, label)
.label_size(ui::LabelSize::Small)
- .layer(ui::ElevationIndex::Surface)
+ // TODO: Change this once we use on_surface_bg in button_like
+ .layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::Compact)
}
@@ -57,10 +61,65 @@ pub fn panel_filled_button(label: impl Into<SharedString>) -> ui::Button {
pub fn panel_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
let id = ElementId::Name(id.into());
ui::IconButton::new(id, icon)
- .layer(ui::ElevationIndex::Surface)
+ // TODO: Change this once we use on_surface_bg in button_like
+ .layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::Compact)
}
pub fn panel_filled_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
panel_icon_button(id, icon).style(ui::ButtonStyle::Filled)
}
+
+pub fn panel_editor_container(_window: &mut Window, cx: &mut App) -> Div {
+ v_flex()
+ .size_full()
+ .gap(px(8.))
+ .p_2()
+ .bg(cx.theme().colors().editor_background)
+}
+
+pub fn panel_editor_style(monospace: bool, window: &mut Window, cx: &mut App) -> EditorStyle {
+ let settings = ThemeSettings::get_global(cx);
+
+ let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
+
+ let (font_family, font_features, font_weight, line_height) = if monospace {
+ (
+ settings.buffer_font.family.clone(),
+ settings.buffer_font.features.clone(),
+ settings.buffer_font.weight,
+ font_size * settings.buffer_line_height.value(),
+ )
+ } else {
+ (
+ settings.ui_font.family.clone(),
+ settings.ui_font.features.clone(),
+ settings.ui_font.weight,
+ window.line_height(),
+ )
+ };
+
+ EditorStyle {
+ background: cx.theme().colors().editor_background,
+ local_player: cx.theme().players().local(),
+ text: TextStyle {
+ color: cx.theme().colors().text,
+ font_family,
+ font_features,
+ font_size: TextSize::Small.rems(cx).into(),
+ font_weight,
+ line_height: line_height.into(),
+ ..Default::default()
+ },
+ ..Default::default()
+ }
+}
+
+pub fn panel_editor_element(
+ editor: &Entity<Editor>,
+ monospace: bool,
+ window: &mut Window,
+ cx: &mut App,
+) -> EditorElement {
+ EditorElement::new(editor, panel_editor_style(monospace, window, cx))
+}
@@ -145,12 +145,24 @@ pub fn settings_file() -> &'static PathBuf {
SETTINGS_FILE.get_or_init(|| config_dir().join("settings.json"))
}
+/// Returns the path to the `settings_backup.json` file.
+pub fn settings_backup_file() -> &'static PathBuf {
+ static SETTINGS_FILE: OnceLock<PathBuf> = OnceLock::new();
+ SETTINGS_FILE.get_or_init(|| config_dir().join("settings_backup.json"))
+}
+
/// Returns the path to the `keymap.json` file.
pub fn keymap_file() -> &'static PathBuf {
static KEYMAP_FILE: OnceLock<PathBuf> = OnceLock::new();
KEYMAP_FILE.get_or_init(|| config_dir().join("keymap.json"))
}
+/// Returns the path to the `keymap_backup.json` file.
+pub fn keymap_backup_file() -> &'static PathBuf {
+ static KEYMAP_FILE: OnceLock<PathBuf> = OnceLock::new();
+ KEYMAP_FILE.get_or_init(|| config_dir().join("keymap_backup.json"))
+}
+
/// Returns the path to the `tasks.json` file.
pub fn tasks_file() -> &'static PathBuf {
static TASKS_FILE: OnceLock<PathBuf> = OnceLock::new();
@@ -15,7 +15,7 @@ use gpui::{
use language::{Buffer, LanguageRegistry};
use rpc::{proto, AnyProtoClient};
use settings::WorktreeId;
-use std::path::Path;
+use std::path::{Path, PathBuf};
use std::sync::Arc;
use text::BufferId;
use util::{maybe, ResultExt};
@@ -299,19 +299,25 @@ impl Repository {
(self.worktree_id, self.repository_entry.work_directory_id())
}
+ pub fn branch(&self) -> Option<Arc<str>> {
+ self.repository_entry.branch()
+ }
+
pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
maybe!({
- let path = self.repo_path_to_project_path(&"".into())?;
- Some(
- project
- .absolute_path(&path, cx)?
- .file_name()?
- .to_string_lossy()
- .to_string()
- .into(),
- )
+ let project_path = self.repo_path_to_project_path(&"".into())?;
+ let worktree_name = project
+ .worktree_for_id(project_path.worktree_id, cx)?
+ .read(cx)
+ .root_name();
+
+ let mut path = PathBuf::new();
+ path = path.join(worktree_name);
+ path = path.join(project_path.path);
+ Some(path.to_string_lossy().to_string())
})
- .unwrap_or("".into())
+ .unwrap_or_else(|| self.repository_entry.work_directory.display_name())
+ .into()
}
pub fn activate(&self, cx: &mut Context<Self>) {
@@ -1535,6 +1535,10 @@ impl Project {
})
}
+ /// Renames the project entry with given `entry_id`.
+ ///
+ /// `new_path` is a relative path to worktree root.
+ /// If root entry is renamed then its new root name is used instead.
pub fn rename_entry(
&mut self,
entry_id: ProjectEntryId,
@@ -1551,12 +1555,18 @@ impl Project {
};
let worktree_id = worktree.read(cx).id();
+ let is_root_entry = self.entry_is_worktree_root(entry_id, cx);
let lsp_store = self.lsp_store().downgrade();
cx.spawn(|_, mut cx| async move {
let (old_abs_path, new_abs_path) = {
let root_path = worktree.update(&mut cx, |this, _| this.abs_path())?;
- (root_path.join(&old_path), root_path.join(&new_path))
+ let new_abs_path = if is_root_entry {
+ root_path.parent().unwrap().join(&new_path)
+ } else {
+ root_path.join(&new_path)
+ };
+ (root_path.join(&old_path), new_abs_path)
};
LspStore::will_rename_entry(
lsp_store.clone(),
@@ -733,7 +733,9 @@ impl ProjectPanel {
.action("Copy Path", Box::new(CopyPath))
.action("Copy Relative Path", Box::new(CopyRelativePath))
.separator()
- .action("Rename", Box::new(Rename))
+ .when(!is_root || !cfg!(target_os = "windows"), |menu| {
+ menu.action("Rename", Box::new(Rename))
+ })
.when(!is_root & !is_remote, |menu| {
menu.action("Trash", Box::new(Trash { skip_prompt: false }))
})
@@ -1348,6 +1350,10 @@ impl ProjectPanel {
if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
let sub_entry_id = self.unflatten_entry_id(entry_id);
if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) {
+ #[cfg(target_os = "windows")]
+ if Some(entry) == worktree.read(cx).root_entry() {
+ return;
+ }
self.edit_state = Some(EditState {
worktree_id,
entry_id: sub_entry_id,
@@ -7280,6 +7286,84 @@ mod tests {
);
}
+ #[gpui::test]
+ #[cfg_attr(target_os = "windows", ignore)]
+ async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
+ init_test_with_editor(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ "dir1": {
+ "file1.txt": "content 1",
+ },
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
+ let workspace =
+ cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+
+ toggle_expand_dir(&panel, "root1/dir1", cx);
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..20, cx),
+ &["v root1", " v dir1 <== selected", " file1.txt",],
+ "Initial state with worktrees"
+ );
+
+ select_path(&panel, "root1", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..20, cx),
+ &["v root1 <== selected", " v dir1", " file1.txt",],
+ );
+
+ // Rename root1 to new_root1
+ panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..20, cx),
+ &[
+ "v [EDITOR: 'root1'] <== selected",
+ " v dir1",
+ " file1.txt",
+ ],
+ );
+
+ let confirm = panel.update_in(cx, |panel, window, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
+ panel.confirm_edit(window, cx).unwrap()
+ });
+ confirm.await.unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..20, cx),
+ &[
+ "v new_root1 <== selected",
+ " v dir1",
+ " file1.txt",
+ ],
+ "Should update worktree name"
+ );
+
+ // Ensure internal paths have been updated
+ select_path(&panel, "new_root1/dir1/file1.txt", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..20, cx),
+ &[
+ "v new_root1",
+ " v dir1",
+ " file1.txt <== selected",
+ ],
+ "Files in renamed worktree are selectable"
+ );
+ }
+
#[gpui::test]
async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
@@ -2,6 +2,7 @@ use crate::kernels::KernelSpecification;
use crate::repl_store::ReplStore;
use crate::KERNEL_DOCS_URL;
+use gpui::AnyView;
use gpui::DismissEvent;
use gpui::FontWeight;
@@ -19,10 +20,15 @@ use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
type OnSelect = Box<dyn Fn(KernelSpecification, &mut Window, &mut App)>;
#[derive(IntoElement)]
-pub struct KernelSelector<T: PopoverTrigger> {
+pub struct KernelSelector<T, TT>
+where
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
handle: Option<PopoverMenuHandle<Picker<KernelPickerDelegate>>>,
on_select: OnSelect,
trigger: T,
+ tooltip: TT,
info_text: Option<SharedString>,
worktree_id: WorktreeId,
}
@@ -44,12 +50,17 @@ fn truncate_path(path: &SharedString, max_length: usize) -> SharedString {
}
}
-impl<T: PopoverTrigger> KernelSelector<T> {
- pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T) -> Self {
+impl<T, TT> KernelSelector<T, TT>
+where
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
+ pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T, tooltip: TT) -> Self {
KernelSelector {
on_select,
handle: None,
trigger,
+ tooltip,
info_text: None,
worktree_id,
}
@@ -235,7 +246,11 @@ impl PickerDelegate for KernelPickerDelegate {
}
}
-impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
+impl<T, TT> RenderOnce for KernelSelector<T, TT>
+where
+ T: PopoverTrigger + ButtonCommon,
+ TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let store = ReplStore::global(cx).read(cx);
@@ -262,7 +277,7 @@ impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
PopoverMenu::new("kernel-switcher")
.menu(move |_window, _cx| Some(picker_view.clone()))
- .trigger(self.trigger)
+ .trigger_with_tooltip(self.trigger, self.tooltip)
.attach(gpui::Corner::BottomLeft)
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
}
@@ -588,24 +588,24 @@ impl KeymapFile {
let Some(new_text) = migrate_keymap(&old_text) else {
return Ok(());
};
- let initial_path = paths::keymap_file().as_path();
- if fs.is_file(initial_path).await {
- let backup_path = paths::home_dir().join(".zed_keymap_backup");
- fs.atomic_write(backup_path, old_text)
+ let keymap_path = paths::keymap_file().as_path();
+ if fs.is_file(keymap_path).await {
+ fs.atomic_write(paths::keymap_backup_file().to_path_buf(), old_text)
.await
.with_context(|| {
"Failed to create settings backup in home directory".to_string()
})?;
- let resolved_path = fs.canonicalize(initial_path).await.with_context(|| {
- format!("Failed to canonicalize keymap path {:?}", initial_path)
- })?;
+ let resolved_path = fs
+ .canonicalize(keymap_path)
+ .await
+ .with_context(|| format!("Failed to canonicalize keymap path {:?}", keymap_path))?;
fs.atomic_write(resolved_path.clone(), new_text)
.await
.with_context(|| format!("Failed to write keymap to file {:?}", resolved_path))?;
} else {
- fs.atomic_write(initial_path.to_path_buf(), new_text)
+ fs.atomic_write(keymap_path.to_path_buf(), new_text)
.await
- .with_context(|| format!("Failed to write keymap to file {:?}", initial_path))?;
+ .with_context(|| format!("Failed to write keymap to file {:?}", keymap_path))?;
}
Ok(())
@@ -415,11 +415,11 @@ impl SettingsStore {
let new_text = cx.read_global(|store: &SettingsStore, cx| {
store.new_text_for_update::<T>(old_text, |content| update(content, cx))
})?;
- let initial_path = paths::settings_file().as_path();
- if fs.is_file(initial_path).await {
+ let settings_path = paths::settings_file().as_path();
+ if fs.is_file(settings_path).await {
let resolved_path =
- fs.canonicalize(initial_path).await.with_context(|| {
- format!("Failed to canonicalize settings path {:?}", initial_path)
+ fs.canonicalize(settings_path).await.with_context(|| {
+ format!("Failed to canonicalize settings path {:?}", settings_path)
})?;
fs.atomic_write(resolved_path.clone(), new_text)
@@ -428,10 +428,10 @@ impl SettingsStore {
format!("Failed to write settings to file {:?}", resolved_path)
})?;
} else {
- fs.atomic_write(initial_path.to_path_buf(), new_text)
+ fs.atomic_write(settings_path.to_path_buf(), new_text)
.await
.with_context(|| {
- format!("Failed to write settings to file {:?}", initial_path)
+ format!("Failed to write settings to file {:?}", settings_path)
})?;
}
@@ -1011,17 +1011,16 @@ impl SettingsStore {
let Some(new_text) = migrate_settings(&old_text) else {
return anyhow::Ok(());
};
- let initial_path = paths::settings_file().as_path();
- if fs.is_file(initial_path).await {
- let backup_path = paths::home_dir().join(".zed_settings_backup");
- fs.atomic_write(backup_path, old_text)
+ let settings_path = paths::settings_file().as_path();
+ if fs.is_file(settings_path).await {
+ fs.atomic_write(paths::settings_backup_file().to_path_buf(), old_text)
.await
.with_context(|| {
"Failed to create settings backup in home directory".to_string()
})?;
let resolved_path =
- fs.canonicalize(initial_path).await.with_context(|| {
- format!("Failed to canonicalize settings path {:?}", initial_path)
+ fs.canonicalize(settings_path).await.with_context(|| {
+ format!("Failed to canonicalize settings path {:?}", settings_path)
})?;
fs.atomic_write(resolved_path.clone(), new_text)
.await
@@ -1029,10 +1028,10 @@ impl SettingsStore {
format!("Failed to write settings to file {:?}", resolved_path)
})?;
} else {
- fs.atomic_write(initial_path.to_path_buf(), new_text)
+ fs.atomic_write(settings_path.to_path_buf(), new_text)
.await
.with_context(|| {
- format!("Failed to write settings to file {:?}", initial_path)
+ format!("Failed to write settings to file {:?}", settings_path)
})?;
}
anyhow::Ok(())
@@ -36,7 +36,6 @@ pub enum ComponentStory {
TabBar,
Text,
ToggleButton,
- ToolStrip,
ViewportUnits,
WithRemSize,
Vector,
@@ -73,7 +72,6 @@ impl ComponentStory {
Self::TabBar => cx.new(|_| ui::TabBarStory).into(),
Self::Text => TextStory::model(cx).into(),
Self::ToggleButton => cx.new(|_| ui::ToggleButtonStory).into(),
- Self::ToolStrip => cx.new(|_| ui::ToolStripStory).into(),
Self::ViewportUnits => cx.new(|_| crate::stories::ViewportUnitsStory).into(),
Self::WithRemSize => cx.new(|_| crate::stories::WithRemSizeStory).into(),
Self::Vector => cx.new(|_| ui::VectorStory).into(),
@@ -92,6 +92,7 @@ fn completion_from_diff(
}
InlineCompletion {
+ id: None,
edits,
edit_preview: None,
}
@@ -139,10 +139,9 @@ impl TerminalPanel {
.gap(DynamicSpacing::Base02.rems(cx))
.child(
PopoverMenu::new("terminal-tab-bar-popover-menu")
- .trigger(
- IconButton::new("plus", IconName::Plus)
- .icon_size(IconSize::Small)
- .tooltip(Tooltip::text("Newβ¦")),
+ .trigger_with_tooltip(
+ IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
+ Tooltip::text("Newβ¦"),
)
.anchor(Corner::TopRight)
.with_handle(pane.new_item_context_menu_handle.clone())
@@ -169,10 +168,10 @@ impl TerminalPanel {
.children(assistant_tab_bar_button.clone())
.child(
PopoverMenu::new("terminal-pane-tab-bar-split")
- .trigger(
+ .trigger_with_tooltip(
IconButton::new("terminal-pane-split", IconName::Split)
- .icon_size(IconSize::Small)
- .tooltip(Tooltip::text("Split Pane")),
+ .icon_size(IconSize::Small),
+ Tooltip::text("Split Pane"),
)
.anchor(Corner::TopRight)
.with_handle(pane.split_item_context_menu_handle.clone())
@@ -133,16 +133,14 @@ impl ApplicationMenu {
.menu(move |window, cx| {
Self::build_menu_from_items(entry.clone(), window, cx).into()
})
- .trigger(
+ .trigger_with_tooltip(
IconButton::new(
SharedString::from(format!("{}-menu-trigger", menu_name)),
ui::IconName::Menu,
)
.style(ButtonStyle::Subtle)
- .icon_size(IconSize::Small)
- .when(!handle.is_deployed(), |this| {
- this.tooltip(Tooltip::text("Open Application Menu"))
- }),
+ .icon_size(IconSize::Small),
+ Tooltip::text("Open Application Menu"),
)
.with_handle(handle),
)
@@ -530,7 +530,7 @@ impl TitleBar {
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Recent Branches",
- Some(&zed_actions::branches::OpenRecent),
+ Some(&zed_actions::git::Branch),
"Local branches only",
window,
cx,
@@ -538,7 +538,7 @@ impl TitleBar {
})
.on_click(move |_, window, cx| {
let _ = workspace.update(cx, |_this, cx| {
- window.dispatch_action(zed_actions::branches::OpenRecent.boxed_clone(), cx);
+ window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
});
}),
)
@@ -673,6 +673,10 @@ impl TitleBar {
"Themesβ¦",
zed_actions::theme_selector::Toggle::default().boxed_clone(),
)
+ .action(
+ "Icon Themesβ¦",
+ zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
+ )
.action("Extensions", zed_actions::Extensions.boxed_clone())
.separator()
.link(
@@ -686,7 +690,7 @@ impl TitleBar {
})
.into()
})
- .trigger(
+ .trigger_with_tooltip(
ButtonLike::new("user-menu")
.child(
h_flex()
@@ -702,8 +706,8 @@ impl TitleBar {
.color(Color::Muted),
),
)
- .style(ButtonStyle::Subtle)
- .tooltip(Tooltip::text("Toggle User Menu")),
+ .style(ButtonStyle::Subtle),
+ Tooltip::text("Toggle User Menu"),
)
.anchor(gpui::Corner::TopRight)
} else {
@@ -716,6 +720,10 @@ impl TitleBar {
"Themesβ¦",
zed_actions::theme_selector::Toggle::default().boxed_clone(),
)
+ .action(
+ "Icon Themesβ¦",
+ zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
+ )
.action("Extensions", zed_actions::Extensions.boxed_clone())
.separator()
.link(
@@ -728,10 +736,9 @@ impl TitleBar {
})
.into()
})
- .trigger(
- IconButton::new("user-menu", IconName::ChevronDown)
- .icon_size(IconSize::Small)
- .tooltip(Tooltip::text("Toggle User Menu")),
+ .trigger_with_tooltip(
+ IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small),
+ Tooltip::text("Toggle User Menu"),
)
}
}
@@ -14,8 +14,10 @@ path = "src/ui.rs"
[dependencies]
chrono.workspace = true
+component.workspace = true
gpui.workspace = true
itertools = { workspace = true, optional = true }
+linkme.workspace = true
menu.workspace = true
serde.workspace = true
settings.workspace = true
@@ -31,3 +33,7 @@ windows.workspace = true
[features]
default = []
stories = ["dep:itertools", "dep:story"]
+
+# cargo-machete doesn't understand that linkme is used in the component macro
+[package.metadata.cargo-machete]
+ignored = ["linkme"]
@@ -29,7 +29,6 @@ mod tab;
mod tab_bar;
mod table;
mod toggle;
-mod tool_strip;
mod tooltip;
#[cfg(feature = "stories")]
@@ -66,7 +65,6 @@ pub use tab::*;
pub use tab_bar::*;
pub use table::*;
pub use toggle::*;
-pub use tool_strip::*;
pub use tooltip::*;
#[cfg(feature = "stories")]
@@ -1,4 +1,4 @@
-use crate::prelude::*;
+use crate::{prelude::*, Indicator};
use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
@@ -14,7 +14,7 @@ use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
/// .grayscale(true)
/// .border_color(gpui::red());
/// ```
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
pub struct Avatar {
image: Img,
size: Option<AbsoluteLength>,
@@ -96,3 +96,60 @@ impl RenderOnce for Avatar {
.children(self.indicator.map(|indicator| div().child(indicator)))
}
}
+
+impl ComponentPreview for Avatar {
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4";
+
+ v_flex()
+ .gap_6()
+ .children(vec![
+ example_group_with_title(
+ "Sizes",
+ vec![
+ single_example(
+ "Default",
+ Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
+ .into_any_element(),
+ ),
+ single_example(
+ "Small",
+ Avatar::new(example_avatar).size(px(24.)).into_any_element(),
+ ),
+ single_example(
+ "Large",
+ Avatar::new(example_avatar).size(px(48.)).into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "Styles",
+ vec![
+ single_example("Default", Avatar::new(example_avatar).into_any_element()),
+ single_example(
+ "Grayscale",
+ Avatar::new(example_avatar)
+ .grayscale(true)
+ .into_any_element(),
+ ),
+ single_example(
+ "With Border",
+ Avatar::new(example_avatar)
+ .border_color(gpui::red())
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "With Indicator",
+ vec![single_example(
+ "Dot",
+ Avatar::new(example_avatar)
+ .indicator(Indicator::dot().color(Color::Success))
+ .into_any_element(),
+ )],
+ ),
+ ])
+ .into_any_element()
+ }
+}
@@ -1,5 +1,7 @@
#![allow(missing_docs)]
-use gpui::{AnyView, DefiniteLength};
+use component::{example_group_with_title, single_example, ComponentPreview};
+use gpui::{AnyElement, AnyView, DefiniteLength};
+use ui_macros::IntoComponent;
use crate::{
prelude::*, Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding,
@@ -78,7 +80,7 @@ use super::button_icon::ButtonIcon;
/// });
/// ```
///
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
pub struct Button {
base: ButtonLike,
label: SharedString,
@@ -93,7 +95,7 @@ pub struct Button {
selected_icon: Option<IconName>,
selected_icon_color: Option<Color>,
key_binding: Option<KeyBinding>,
- keybinding_position: KeybindingPosition,
+ key_binding_position: KeybindingPosition,
alpha: Option<f32>,
}
@@ -119,7 +121,7 @@ impl Button {
selected_icon: None,
selected_icon_color: None,
key_binding: None,
- keybinding_position: KeybindingPosition::default(),
+ key_binding_position: KeybindingPosition::default(),
alpha: None,
}
}
@@ -195,7 +197,7 @@ impl Button {
/// This method allows you to specify where the keybinding should be displayed
/// in relation to the button's label.
pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self {
- self.keybinding_position = position;
+ self.key_binding_position = position;
self
}
@@ -425,7 +427,7 @@ impl RenderOnce for Button {
.child(
h_flex()
.when(
- self.keybinding_position == KeybindingPosition::Start,
+ self.key_binding_position == KeybindingPosition::Start,
|this| this.flex_row_reverse(),
)
.gap(DynamicSpacing::Base06.rems(cx))
@@ -455,101 +457,124 @@ impl RenderOnce for Button {
}
impl ComponentPreview for Button {
- fn description() -> impl Into<Option<&'static str>> {
- "A button allows users to take actions, and make choices, with a single tap."
- }
-
- fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
- vec![
- example_group_with_title(
- "Styles",
- vec![
- single_example("Default", Button::new("default", "Default")),
- single_example(
- "Filled",
- Button::new("filled", "Filled").style(ButtonStyle::Filled),
- ),
- single_example(
- "Subtle",
- Button::new("outline", "Subtle").style(ButtonStyle::Subtle),
- ),
- single_example(
- "Transparent",
- Button::new("transparent", "Transparent").style(ButtonStyle::Transparent),
- ),
- ],
- ),
- example_group_with_title(
- "Tinted",
- vec![
- single_example(
- "Accent",
- Button::new("tinted_accent", "Accent")
- .style(ButtonStyle::Tinted(TintColor::Accent)),
- ),
- single_example(
- "Error",
- Button::new("tinted_negative", "Error")
- .style(ButtonStyle::Tinted(TintColor::Error)),
- ),
- single_example(
- "Warning",
- Button::new("tinted_warning", "Warning")
- .style(ButtonStyle::Tinted(TintColor::Warning)),
- ),
- single_example(
- "Success",
- Button::new("tinted_positive", "Success")
- .style(ButtonStyle::Tinted(TintColor::Success)),
- ),
- ],
- ),
- example_group_with_title(
- "States",
- vec![
- single_example("Default", Button::new("default_state", "Default")),
- single_example(
- "Disabled",
- Button::new("disabled", "Disabled").disabled(true),
- ),
- single_example(
- "Selected",
- Button::new("selected", "Selected").toggle_state(true),
- ),
- ],
- ),
- example_group_with_title(
- "With Icons",
- vec![
- single_example(
- "Icon Start",
- Button::new("icon_start", "Icon Start")
- .icon(IconName::Check)
- .icon_position(IconPosition::Start),
- ),
- single_example(
- "Icon End",
- Button::new("icon_end", "Icon End")
- .icon(IconName::Check)
- .icon_position(IconPosition::End),
- ),
- single_example(
- "Icon Color",
- Button::new("icon_color", "Icon Color")
- .icon(IconName::Check)
- .icon_color(Color::Accent),
- ),
- single_example(
- "Tinted Icons",
- Button::new("tinted_icons", "Error")
- .style(ButtonStyle::Tinted(TintColor::Error))
- .color(Color::Error)
- .icon_color(Color::Error)
- .icon(IconName::Trash)
- .icon_position(IconPosition::Start),
- ),
- ],
- ),
- ]
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ v_flex()
+ .gap_6()
+ .children(vec![
+ example_group_with_title(
+ "Styles",
+ vec![
+ single_example(
+ "Default",
+ Button::new("default", "Default").into_any_element(),
+ ),
+ single_example(
+ "Filled",
+ Button::new("filled", "Filled")
+ .style(ButtonStyle::Filled)
+ .into_any_element(),
+ ),
+ single_example(
+ "Subtle",
+ Button::new("outline", "Subtle")
+ .style(ButtonStyle::Subtle)
+ .into_any_element(),
+ ),
+ single_example(
+ "Transparent",
+ Button::new("transparent", "Transparent")
+ .style(ButtonStyle::Transparent)
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "Tinted",
+ vec![
+ single_example(
+ "Accent",
+ Button::new("tinted_accent", "Accent")
+ .style(ButtonStyle::Tinted(TintColor::Accent))
+ .into_any_element(),
+ ),
+ single_example(
+ "Error",
+ Button::new("tinted_negative", "Error")
+ .style(ButtonStyle::Tinted(TintColor::Error))
+ .into_any_element(),
+ ),
+ single_example(
+ "Warning",
+ Button::new("tinted_warning", "Warning")
+ .style(ButtonStyle::Tinted(TintColor::Warning))
+ .into_any_element(),
+ ),
+ single_example(
+ "Success",
+ Button::new("tinted_positive", "Success")
+ .style(ButtonStyle::Tinted(TintColor::Success))
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "States",
+ vec![
+ single_example(
+ "Default",
+ Button::new("default_state", "Default").into_any_element(),
+ ),
+ single_example(
+ "Disabled",
+ Button::new("disabled", "Disabled")
+ .disabled(true)
+ .into_any_element(),
+ ),
+ single_example(
+ "Selected",
+ Button::new("selected", "Selected")
+ .toggle_state(true)
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "With Icons",
+ vec![
+ single_example(
+ "Icon Start",
+ Button::new("icon_start", "Icon Start")
+ .icon(IconName::Check)
+ .icon_position(IconPosition::Start)
+ .into_any_element(),
+ ),
+ single_example(
+ "Icon End",
+ Button::new("icon_end", "Icon End")
+ .icon(IconName::Check)
+ .icon_position(IconPosition::End)
+ .into_any_element(),
+ ),
+ single_example(
+ "Icon Color",
+ Button::new("icon_color", "Icon Color")
+ .icon(IconName::Check)
+ .icon_color(Color::Accent)
+ .into_any_element(),
+ ),
+ single_example(
+ "Tinted Icons",
+ Button::new("tinted_icons", "Error")
+ .style(ButtonStyle::Tinted(TintColor::Error))
+ .color(Color::Error)
+ .icon_color(Color::Error)
+ .icon(IconName::Trash)
+ .icon_position(IconPosition::Start)
+ .into_any_element(),
+ ),
+ ],
+ ),
+ ])
+ .into_any_element()
}
}
@@ -506,7 +506,9 @@ impl RenderOnce for ButtonLike {
.group("")
.flex_none()
.h(self.height.unwrap_or(self.size.rems().into()))
- .when_some(self.width, |this, width| this.w(width).justify_center())
+ .when_some(self.width, |this, width| {
+ this.w(width).justify_center().text_center()
+ })
.when_some(self.rounding, |this, rounding| match rounding {
ButtonLikeRounding::All => this.rounded_md(),
ButtonLikeRounding::Left => this.rounded_l_md(),
@@ -22,6 +22,7 @@ pub struct IconButton {
icon_size: IconSize,
icon_color: Color,
selected_icon: Option<IconName>,
+ selected_icon_color: Option<Color>,
indicator: Option<Indicator>,
indicator_border_color: Option<Hsla>,
alpha: Option<f32>,
@@ -36,6 +37,7 @@ impl IconButton {
icon_size: IconSize::default(),
icon_color: Color::Default,
selected_icon: None,
+ selected_icon_color: None,
indicator: None,
indicator_border_color: None,
alpha: None,
@@ -69,6 +71,12 @@ impl IconButton {
self
}
+ /// Sets the icon color used when the button is in a selected state.
+ pub fn selected_icon_color(mut self, color: impl Into<Option<Color>>) -> Self {
+ self.selected_icon_color = color.into();
+ self
+ }
+
pub fn indicator(mut self, indicator: Indicator) -> Self {
self.indicator = Some(indicator);
self
@@ -181,6 +189,7 @@ impl RenderOnce for IconButton {
.disabled(is_disabled)
.toggle_state(is_selected)
.selected_icon(self.selected_icon)
+ .selected_icon_color(self.selected_icon_color)
.when_some(selected_style, |this, style| this.selected_style(style))
.when_some(self.indicator, |this, indicator| {
this.indicator(indicator)
@@ -1,4 +1,5 @@
use crate::prelude::*;
+use component::{example_group, single_example, ComponentPreview};
use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled};
use smallvec::SmallVec;
@@ -22,7 +23,8 @@ pub fn h_group() -> ContentGroup {
}
/// A flexible container component that can hold other elements.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "layout")]
pub struct ContentGroup {
base: Div,
border: bool,
@@ -87,16 +89,8 @@ impl RenderOnce for ContentGroup {
}
impl ComponentPreview for ContentGroup {
- fn description() -> impl Into<Option<&'static str>> {
- "A flexible container component that can hold other elements. It can be customized with or without a border and background fill."
- }
-
- fn example_label_side() -> ExampleLabelSide {
- ExampleLabelSide::Bottom
- }
-
- fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
- vec![example_group(vec![
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ example_group(vec![
single_example(
"Default",
ContentGroup::new()
@@ -104,7 +98,8 @@ impl ComponentPreview for ContentGroup {
.items_center()
.justify_center()
.h_48()
- .child(Label::new("Default ContentBox")),
+ .child(Label::new("Default ContentBox"))
+ .into_any_element(),
)
.grow(),
single_example(
@@ -115,7 +110,8 @@ impl ComponentPreview for ContentGroup {
.justify_center()
.h_48()
.borderless()
- .child(Label::new("Borderless ContentBox")),
+ .child(Label::new("Borderless ContentBox"))
+ .into_any_element(),
)
.grow(),
single_example(
@@ -126,10 +122,11 @@ impl ComponentPreview for ContentGroup {
.justify_center()
.h_48()
.unfilled()
- .child(Label::new("Unfilled ContentBox")),
+ .child(Label::new("Unfilled ContentBox"))
+ .into_any_element(),
)
.grow(),
])
- .grow()]
+ .into_any_element()
}
}
@@ -672,14 +672,16 @@ impl Render for ContextMenu {
*toggle,
|list_item, (position, toggled)| {
let contents = if toggled {
- v_flex().flex_none().child(
+ div().flex_none().child(
Icon::new(IconName::Check)
- .color(Color::Accent),
+ .color(Color::Accent)
+ .size(*icon_size)
)
} else {
- v_flex().flex_none().size(
- IconSize::default().rems(),
- )
+ div().flex_none().child(
+ Icon::new(IconName::Check)
+ .size(*icon_size)
+ ).invisible()
};
match position {
IconPosition::Start => {
@@ -1,4 +1,4 @@
-use crate::{prelude::*, Avatar};
+use crate::prelude::*;
use gpui::{AnyElement, StyleRefinement};
use smallvec::SmallVec;
@@ -60,60 +60,60 @@ impl RenderOnce for Facepile {
}
}
-impl ComponentPreview for Facepile {
- fn description() -> impl Into<Option<&'static str>> {
- "A facepile is a collection of faces stacked horizontallyβ\
- always with the leftmost face on top and descending in z-index.\
- \n\nFacepiles are used to display a group of people or things,\
- such as a list of participants in a collaboration session."
- }
- fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
- let few_faces: [&'static str; 3] = [
- "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
- "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
- "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
- ];
+// impl ComponentPreview for Facepile {
+// fn description() -> impl Into<Option<&'static str>> {
+// "A facepile is a collection of faces stacked horizontallyβ\
+// always with the leftmost face on top and descending in z-index.\
+// \n\nFacepiles are used to display a group of people or things,\
+// such as a list of participants in a collaboration session."
+// }
+// fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
+// let few_faces: [&'static str; 3] = [
+// "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
+// "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
+// "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
+// ];
- let many_faces: [&'static str; 6] = [
- "https://avatars.githubusercontent.com/u/326587?s=60&v=4",
- "https://avatars.githubusercontent.com/u/2280405?s=60&v=4",
- "https://avatars.githubusercontent.com/u/1789?s=60&v=4",
- "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
- "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
- "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
- ];
+// let many_faces: [&'static str; 6] = [
+// "https://avatars.githubusercontent.com/u/326587?s=60&v=4",
+// "https://avatars.githubusercontent.com/u/2280405?s=60&v=4",
+// "https://avatars.githubusercontent.com/u/1789?s=60&v=4",
+// "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
+// "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
+// "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
+// ];
- vec![example_group_with_title(
- "Examples",
- vec![
- single_example(
- "Few Faces",
- Facepile::new(
- few_faces
- .iter()
- .map(|&url| Avatar::new(url).into_any_element())
- .collect(),
- ),
- ),
- single_example(
- "Many Faces",
- Facepile::new(
- many_faces
- .iter()
- .map(|&url| Avatar::new(url).into_any_element())
- .collect(),
- ),
- ),
- single_example(
- "Custom Size",
- Facepile::new(
- few_faces
- .iter()
- .map(|&url| Avatar::new(url).size(px(24.)).into_any_element())
- .collect(),
- ),
- ),
- ],
- )]
- }
-}
+// vec![example_group_with_title(
+// "Examples",
+// vec![
+// single_example(
+// "Few Faces",
+// Facepile::new(
+// few_faces
+// .iter()
+// .map(|&url| Avatar::new(url).into_any_element())
+// .collect(),
+// ),
+// ),
+// single_example(
+// "Many Faces",
+// Facepile::new(
+// many_faces
+// .iter()
+// .map(|&url| Avatar::new(url).into_any_element())
+// .collect(),
+// ),
+// ),
+// single_example(
+// "Custom Size",
+// Facepile::new(
+// few_faces
+// .iter()
+// .map(|&url| Avatar::new(url).size(px(24.)).into_any_element())
+// .collect(),
+// ),
+// ),
+// ],
+// )]
+// }
+// }
@@ -7,17 +7,13 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
pub use decorated_icon::*;
-use gpui::{img, svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
+use gpui::{img, svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation};
pub use icon_decoration::*;
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString, IntoStaticStr};
use ui_macros::DerivePathStr;
-use crate::{
- prelude::*,
- traits::component_preview::{ComponentExample, ComponentPreview},
- Indicator,
-};
+use crate::{prelude::*, Indicator};
#[derive(IntoElement)]
pub enum AnyIcon {
@@ -234,6 +230,7 @@ pub enum IconName {
Link,
ListTree,
ListX,
+ LockOutlined,
MagnifyingGlass,
MailOpen,
Maximize,
@@ -363,7 +360,7 @@ impl IconSource {
}
}
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
pub struct Icon {
source: IconSource,
color: Color,
@@ -493,24 +490,41 @@ impl RenderOnce for IconWithIndicator {
}
impl ComponentPreview for Icon {
- fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Icon>> {
- let arrow_icons = vec![
- IconName::ArrowDown,
- IconName::ArrowLeft,
- IconName::ArrowRight,
- IconName::ArrowUp,
- IconName::ArrowCircle,
- ];
-
- vec![example_group_with_title(
- "Arrow Icons",
- arrow_icons
- .into_iter()
- .map(|icon| {
- let name = format!("{:?}", icon).to_string();
- ComponentExample::new(name, Icon::new(icon))
- })
- .collect(),
- )]
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ v_flex()
+ .gap_6()
+ .children(vec![
+ example_group_with_title(
+ "Sizes",
+ vec![
+ single_example("Default", Icon::new(IconName::Star).into_any_element()),
+ single_example(
+ "Small",
+ Icon::new(IconName::Star)
+ .size(IconSize::Small)
+ .into_any_element(),
+ ),
+ single_example(
+ "Large",
+ Icon::new(IconName::Star)
+ .size(IconSize::XLarge)
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "Colors",
+ vec![
+ single_example("Default", Icon::new(IconName::Bell).into_any_element()),
+ single_example(
+ "Custom Color",
+ Icon::new(IconName::Bell)
+ .color(Color::Error)
+ .into_any_element(),
+ ),
+ ],
+ ),
+ ])
+ .into_any_element()
}
}
@@ -1,10 +1,8 @@
-use gpui::{IntoElement, Point};
+use gpui::{AnyElement, IntoElement, Point};
-use crate::{
- prelude::*, traits::component_preview::ComponentPreview, IconDecoration, IconDecorationKind,
-};
+use crate::{prelude::*, IconDecoration, IconDecorationKind};
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
pub struct DecoratedIcon {
icon: Icon,
decoration: Option<IconDecoration>,
@@ -27,12 +25,7 @@ impl RenderOnce for DecoratedIcon {
}
impl ComponentPreview for DecoratedIcon {
- fn examples(_: &mut Window, cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
- let icon_1 = Icon::new(IconName::FileDoc);
- let icon_2 = Icon::new(IconName::FileDoc);
- let icon_3 = Icon::new(IconName::FileDoc);
- let icon_4 = Icon::new(IconName::FileDoc);
-
+ fn preview(_window: &mut Window, cx: &App) -> AnyElement {
let decoration_x = IconDecoration::new(
IconDecorationKind::X,
cx.theme().colors().surface_background,
@@ -66,22 +59,32 @@ impl ComponentPreview for DecoratedIcon {
y: px(-2.),
});
- let examples = vec![
- single_example("no_decoration", DecoratedIcon::new(icon_1, None)),
- single_example(
- "with_decoration",
- DecoratedIcon::new(icon_2, Some(decoration_x)),
- ),
- single_example(
- "with_decoration",
- DecoratedIcon::new(icon_3, Some(decoration_triangle)),
- ),
- single_example(
- "with_decoration",
- DecoratedIcon::new(icon_4, Some(decoration_dot)),
- ),
- ];
-
- vec![example_group(examples)]
+ v_flex()
+ .gap_6()
+ .children(vec![example_group_with_title(
+ "Decorations",
+ vec![
+ single_example(
+ "No Decoration",
+ DecoratedIcon::new(Icon::new(IconName::FileDoc), None).into_any_element(),
+ ),
+ single_example(
+ "X Decoration",
+ DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_x))
+ .into_any_element(),
+ ),
+ single_example(
+ "Triangle Decoration",
+ DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_triangle))
+ .into_any_element(),
+ ),
+ single_example(
+ "Dot Decoration",
+ DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_dot))
+ .into_any_element(),
+ ),
+ ],
+ )])
+ .into_any_element()
}
}
@@ -1,8 +1,8 @@
use gpui::{svg, Hsla, IntoElement, Point};
-use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
+use strum::{EnumIter, EnumString, IntoStaticStr};
use ui_macros::DerivePathStr;
-use crate::{prelude::*, traits::component_preview::ComponentPreview};
+use crate::prelude::*;
const ICON_DECORATION_SIZE: Pixels = px(11.);
@@ -149,21 +149,3 @@ impl RenderOnce for IconDecoration {
.child(background)
}
}
-
-impl ComponentPreview for IconDecoration {
- fn examples(_: &mut Window, cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
- let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
-
- let examples = all_kinds
- .iter()
- .map(|kind| {
- single_example(
- format!("{kind:?}"),
- IconDecoration::new(*kind, cx.theme().colors().surface_background, cx),
- )
- })
- .collect();
-
- vec![example_group(examples)]
- }
-}
@@ -83,34 +83,3 @@ impl RenderOnce for Indicator {
}
}
}
-
-impl ComponentPreview for Indicator {
- fn description() -> impl Into<Option<&'static str>> {
- "An indicator visually represents a status or state."
- }
-
- fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
- vec![
- example_group_with_title(
- "Types",
- vec![
- single_example("Dot", Indicator::dot().color(Color::Info)),
- single_example("Bar", Indicator::bar().color(Color::Player(2))),
- single_example(
- "Icon",
- Indicator::icon(Icon::new(IconName::Check).color(Color::Success)),
- ),
- ],
- ),
- example_group_with_title(
- "Examples",
- vec![
- single_example("Info", Indicator::dot().color(Color::Info)),
- single_example("Success", Indicator::dot().color(Color::Success)),
- single_example("Warning", Indicator::dot().color(Color::Warning)),
- single_example("Error", Indicator::dot().color(Color::Error)),
- ],
- ),
- ]
- }
-}
@@ -1,6 +1,6 @@
use crate::{h_flex, prelude::*};
use crate::{ElevationIndex, KeyBinding};
-use gpui::{point, App, BoxShadow, IntoElement, Window};
+use gpui::{point, AnyElement, App, BoxShadow, IntoElement, Window};
use smallvec::smallvec;
/// Represents a hint for a keybinding, optionally with a prefix and suffix.
@@ -17,7 +17,7 @@ use smallvec::smallvec;
/// .prefix("Save:")
/// .size(Pixels::from(14.0));
/// ```
-#[derive(Debug, IntoElement, Clone)]
+#[derive(Debug, IntoElement, IntoComponent)]
pub struct KeybindingHint {
prefix: Option<SharedString>,
suffix: Option<SharedString>,
@@ -206,102 +206,99 @@ impl RenderOnce for KeybindingHint {
}
impl ComponentPreview for KeybindingHint {
- fn description() -> impl Into<Option<&'static str>> {
- "Used to display hint text for keyboard shortcuts. Can have a prefix and suffix."
- }
-
- fn examples(window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
- let home_fallback = gpui::KeyBinding::new("home", menu::SelectFirst, None);
- let home = KeyBinding::for_action(&menu::SelectFirst, window)
- .unwrap_or(KeyBinding::new(home_fallback));
-
- let end_fallback = gpui::KeyBinding::new("end", menu::SelectLast, None);
- let end = KeyBinding::for_action(&menu::SelectLast, window)
- .unwrap_or(KeyBinding::new(end_fallback));
-
+ fn preview(window: &mut Window, _cx: &App) -> AnyElement {
let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
let enter = KeyBinding::for_action(&menu::Confirm, window)
.unwrap_or(KeyBinding::new(enter_fallback));
- let escape_fallback = gpui::KeyBinding::new("escape", menu::Cancel, None);
- let escape = KeyBinding::for_action(&menu::Cancel, window)
- .unwrap_or(KeyBinding::new(escape_fallback));
-
- vec![
- example_group_with_title(
- "Basic",
- vec![
- single_example(
- "With Prefix",
- KeybindingHint::with_prefix("Go to Start:", home.clone()),
- ),
- single_example(
- "With Suffix",
- KeybindingHint::with_suffix(end.clone(), "Go to End"),
- ),
- single_example(
- "With Prefix and Suffix",
- KeybindingHint::new(enter.clone())
- .prefix("Confirm:")
- .suffix("Execute selected action"),
- ),
- ],
- ),
- example_group_with_title(
- "Sizes",
- vec![
- single_example(
- "Small",
- KeybindingHint::new(home.clone())
- .size(Pixels::from(12.0))
- .prefix("Small:"),
- ),
- single_example(
- "Medium",
- KeybindingHint::new(end.clone())
- .size(Pixels::from(16.0))
- .suffix("Medium"),
- ),
- single_example(
- "Large",
- KeybindingHint::new(enter.clone())
- .size(Pixels::from(20.0))
- .prefix("Large:")
- .suffix("Size"),
- ),
- ],
- ),
- example_group_with_title(
- "Elevations",
- vec![
- single_example(
- "Surface",
- KeybindingHint::new(home.clone())
- .elevation(ElevationIndex::Surface)
- .prefix("Surface:"),
- ),
- single_example(
- "Elevated Surface",
- KeybindingHint::new(end.clone())
- .elevation(ElevationIndex::ElevatedSurface)
- .suffix("Elevated"),
- ),
- single_example(
- "Editor Surface",
- KeybindingHint::new(enter.clone())
- .elevation(ElevationIndex::EditorSurface)
- .prefix("Editor:")
- .suffix("Surface"),
- ),
- single_example(
- "Modal Surface",
- KeybindingHint::new(escape.clone())
- .elevation(ElevationIndex::ModalSurface)
- .prefix("Modal:")
- .suffix("Escape"),
- ),
- ],
- ),
- ]
+ v_flex()
+ .gap_6()
+ .children(vec![
+ example_group_with_title(
+ "Basic",
+ vec![
+ single_example(
+ "With Prefix",
+ KeybindingHint::with_prefix("Go to Start:", enter.clone())
+ .into_any_element(),
+ ),
+ single_example(
+ "With Suffix",
+ KeybindingHint::with_suffix(enter.clone(), "Go to End")
+ .into_any_element(),
+ ),
+ single_example(
+ "With Prefix and Suffix",
+ KeybindingHint::new(enter.clone())
+ .prefix("Confirm:")
+ .suffix("Execute selected action")
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "Sizes",
+ vec![
+ single_example(
+ "Small",
+ KeybindingHint::new(enter.clone())
+ .size(Pixels::from(12.0))
+ .prefix("Small:")
+ .into_any_element(),
+ ),
+ single_example(
+ "Medium",
+ KeybindingHint::new(enter.clone())
+ .size(Pixels::from(16.0))
+ .suffix("Medium")
+ .into_any_element(),
+ ),
+ single_example(
+ "Large",
+ KeybindingHint::new(enter.clone())
+ .size(Pixels::from(20.0))
+ .prefix("Large:")
+ .suffix("Size")
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "Elevations",
+ vec![
+ single_example(
+ "Surface",
+ KeybindingHint::new(enter.clone())
+ .elevation(ElevationIndex::Surface)
+ .prefix("Surface:")
+ .into_any_element(),
+ ),
+ single_example(
+ "Elevated Surface",
+ KeybindingHint::new(enter.clone())
+ .elevation(ElevationIndex::ElevatedSurface)
+ .suffix("Elevated")
+ .into_any_element(),
+ ),
+ single_example(
+ "Editor Surface",
+ KeybindingHint::new(enter.clone())
+ .elevation(ElevationIndex::EditorSurface)
+ .prefix("Editor:")
+ .suffix("Surface")
+ .into_any_element(),
+ ),
+ single_example(
+ "Modal Surface",
+ KeybindingHint::new(enter.clone())
+ .elevation(ElevationIndex::ModalSurface)
+ .prefix("Modal:")
+ .suffix("Enter")
+ .into_any_element(),
+ ),
+ ],
+ ),
+ ])
+ .into_any_element()
}
}
@@ -75,6 +75,11 @@ impl LabelCommon for HighlightedLabel {
self.base = self.base.single_line();
self
}
+
+ fn buffer_font(mut self, cx: &App) -> Self {
+ self.base = self.base.buffer_font(cx);
+ self
+ }
}
pub fn highlight_ranges(
@@ -1,6 +1,6 @@
#![allow(missing_docs)]
-use gpui::{App, StyleRefinement, Window};
+use gpui::{AnyElement, App, StyleRefinement, Window};
use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
@@ -32,7 +32,7 @@ use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
///
/// let my_label = Label::new("Deleted").strikethrough(true);
/// ```
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
pub struct Label {
base: LabelLike,
label: SharedString,
@@ -172,6 +172,11 @@ impl LabelCommon for Label {
self.base = self.base.single_line();
self
}
+
+ fn buffer_font(mut self, cx: &App) -> Self {
+ self.base = self.base.buffer_font(cx);
+ self
+ }
}
impl RenderOnce for Label {
@@ -179,3 +184,53 @@ impl RenderOnce for Label {
self.base.child(self.label)
}
}
+
+impl ComponentPreview for Label {
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ v_flex()
+ .gap_6()
+ .children(vec![
+ example_group_with_title(
+ "Sizes",
+ vec![
+ single_example("Default", Label::new("Default Label").into_any_element()),
+ single_example("Small", Label::new("Small Label").size(LabelSize::Small).into_any_element()),
+ single_example("Large", Label::new("Large Label").size(LabelSize::Large).into_any_element()),
+ ],
+ ),
+ example_group_with_title(
+ "Colors",
+ vec![
+ single_example("Default", Label::new("Default Color").into_any_element()),
+ single_example("Accent", Label::new("Accent Color").color(Color::Accent).into_any_element()),
+ single_example("Error", Label::new("Error Color").color(Color::Error).into_any_element()),
+ ],
+ ),
+ example_group_with_title(
+ "Styles",
+ vec![
+ single_example("Default", Label::new("Default Style").into_any_element()),
+ single_example("Bold", Label::new("Bold Style").weight(gpui::FontWeight::BOLD).into_any_element()),
+ single_example("Italic", Label::new("Italic Style").italic(true).into_any_element()),
+ single_example("Strikethrough", Label::new("Strikethrough Style").strikethrough(true).into_any_element()),
+ single_example("Underline", Label::new("Underline Style").underline(true).into_any_element()),
+ ],
+ ),
+ example_group_with_title(
+ "Line Height Styles",
+ vec![
+ single_example("Default", Label::new("Default Line Height").into_any_element()),
+ single_example("UI Label", Label::new("UI Label Line Height").line_height_style(LineHeightStyle::UiLabel).into_any_element()),
+ ],
+ ),
+ example_group_with_title(
+ "Special Cases",
+ vec![
+ single_example("Single Line", Label::new("Single\nLine\nText").single_line().into_any_element()),
+ single_example("Text Ellipsis", Label::new("This is a very long text that should be truncated with an ellipsis").text_ellipsis().into_any_element()),
+ ],
+ ),
+ ])
+ .into_any_element()
+ }
+}
@@ -55,6 +55,9 @@ pub trait LabelCommon {
/// Sets the label to render as a single line.
fn single_line(self) -> Self;
+
+ /// Sets the font to the buffer's
+ fn buffer_font(self, cx: &App) -> Self;
}
#[derive(IntoElement)]
@@ -159,6 +162,13 @@ impl LabelCommon for LabelLike {
self.single_line = true;
self
}
+
+ fn buffer_font(mut self, cx: &App) -> Self {
+ self.base = self
+ .base
+ .font(theme::ThemeSettings::get_global(cx).buffer_font.clone());
+ self
+ }
}
impl ParentElement for LabelLike {
@@ -3,8 +3,8 @@
use std::{cell::RefCell, rc::Rc};
use gpui::{
- anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnyElement, App, Bounds,
- Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable as _,
+ anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnyElement, AnyView, App,
+ Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable as _,
GlobalElementId, HitboxId, InteractiveElement, IntoElement, LayoutId, Length, ManagedView,
MouseDownEvent, ParentElement, Pixels, Point, Style, Window,
};
@@ -178,6 +178,28 @@ impl<M: ManagedView> PopoverMenu<M> {
self
}
+ pub fn trigger_with_tooltip<T: PopoverTrigger + ButtonCommon>(
+ mut self,
+ t: T,
+ tooltip_builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
+ ) -> Self {
+ let on_open = self.on_open.clone();
+ self.child_builder = Some(Box::new(move |menu, builder| {
+ let open = menu.borrow().is_some();
+ t.toggle_state(open)
+ .when_some(builder, |el, builder| {
+ el.on_click(move |_, window, cx| {
+ show_menu(&builder, &menu, on_open.clone(), window, cx)
+ })
+ .when(!open, |t| {
+ t.tooltip(move |window, cx| tooltip_builder(window, cx))
+ })
+ })
+ .into_any_element()
+ }));
+ self
+ }
+
/// anchor defines which corner of the menu to anchor to the attachment point
/// (by default the cursor position, but see attach)
pub fn anchor(mut self, anchor: Corner) -> Self {
@@ -4,9 +4,6 @@ use std::sync::Arc;
use crate::prelude::*;
-/// A [`Checkbox`] that has a [`Label`].
-///
-/// [`Checkbox`]: crate::components::Checkbox
#[derive(IntoElement)]
pub struct RadioWithLabel {
id: ElementId,
@@ -15,7 +15,6 @@ mod list_item;
mod tab;
mod tab_bar;
mod toggle_button;
-mod tool_strip;
pub use avatar::*;
pub use button::*;
@@ -31,4 +30,3 @@ pub use list_item::*;
pub use tab::*;
pub use tab_bar::*;
pub use toggle_button::*;
-pub use tool_strip::*;
@@ -1,33 +0,0 @@
-use gpui::Render;
-use story::{Story, StoryItem, StorySection};
-
-use crate::{prelude::*, ToolStrip, Tooltip};
-
-pub struct ToolStripStory;
-
-impl Render for ToolStripStory {
- fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
- Story::container()
- .child(Story::title_for::<ToolStrip>())
- .child(
- StorySection::new().child(StoryItem::new(
- "Vertical Tool Strip",
- h_flex().child(
- ToolStrip::vertical("tool_strip_example")
- .tool(
- IconButton::new("example_tool", IconName::AudioOn)
- .tooltip(Tooltip::text("Example tool")),
- )
- .tool(
- IconButton::new("example_tool_2", IconName::MicMute)
- .tooltip(Tooltip::text("Example tool 2")),
- )
- .tool(
- IconButton::new("example_tool_3", IconName::Screen)
- .tooltip(Tooltip::text("Example tool 3")),
- ),
- ),
- )),
- )
- }
-}
@@ -27,7 +27,7 @@ pub enum TabCloseSide {
End,
}
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
pub struct Tab {
div: Stateful<Div>,
selected: bool,
@@ -171,3 +171,48 @@ impl RenderOnce for Tab {
)
}
}
+
+impl ComponentPreview for Tab {
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ v_flex()
+ .gap_6()
+ .children(vec![example_group_with_title(
+ "Variations",
+ vec![
+ single_example(
+ "Default",
+ Tab::new("default").child("Default Tab").into_any_element(),
+ ),
+ single_example(
+ "Selected",
+ Tab::new("selected")
+ .toggle_state(true)
+ .child("Selected Tab")
+ .into_any_element(),
+ ),
+ single_example(
+ "First",
+ Tab::new("first")
+ .position(TabPosition::First)
+ .child("First Tab")
+ .into_any_element(),
+ ),
+ single_example(
+ "Middle",
+ Tab::new("middle")
+ .position(TabPosition::Middle(Ordering::Equal))
+ .child("Middle Tab")
+ .into_any_element(),
+ ),
+ single_example(
+ "Last",
+ Tab::new("last")
+ .position(TabPosition::Last)
+ .child("Last Tab")
+ .into_any_element(),
+ ),
+ ],
+ )])
+ .into_any_element()
+ }
+}
@@ -2,7 +2,7 @@ use crate::{prelude::*, Indicator};
use gpui::{div, AnyElement, FontWeight, IntoElement, Length};
/// A table component
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
pub struct Table {
column_headers: Vec<SharedString>,
rows: Vec<Vec<TableCell>>,
@@ -152,88 +152,110 @@ where
}
impl ComponentPreview for Table {
- fn description() -> impl Into<Option<&'static str>> {
- "Used for showing tabular data. Tables may show both text and elements in their cells."
- }
-
- fn example_label_side() -> ExampleLabelSide {
- ExampleLabelSide::Top
- }
-
- fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
- vec![
- example_group(vec![
- single_example(
- "Simple Table",
- Table::new(vec!["Name", "Age", "City"])
- .width(px(400.))
- .row(vec!["Alice", "28", "New York"])
- .row(vec!["Bob", "32", "San Francisco"])
- .row(vec!["Charlie", "25", "London"]),
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ v_flex()
+ .gap_6()
+ .children(vec![
+ example_group_with_title(
+ "Basic Tables",
+ vec![
+ single_example(
+ "Simple Table",
+ Table::new(vec!["Name", "Age", "City"])
+ .width(px(400.))
+ .row(vec!["Alice", "28", "New York"])
+ .row(vec!["Bob", "32", "San Francisco"])
+ .row(vec!["Charlie", "25", "London"])
+ .into_any_element(),
+ ),
+ single_example(
+ "Two Column Table",
+ Table::new(vec!["Category", "Value"])
+ .width(px(300.))
+ .row(vec!["Revenue", "$100,000"])
+ .row(vec!["Expenses", "$75,000"])
+ .row(vec!["Profit", "$25,000"])
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "Styled Tables",
+ vec![
+ single_example(
+ "Default",
+ Table::new(vec!["Product", "Price", "Stock"])
+ .width(px(400.))
+ .row(vec!["Laptop", "$999", "In Stock"])
+ .row(vec!["Phone", "$599", "Low Stock"])
+ .row(vec!["Tablet", "$399", "Out of Stock"])
+ .into_any_element(),
+ ),
+ single_example(
+ "Striped",
+ Table::new(vec!["Product", "Price", "Stock"])
+ .width(px(400.))
+ .striped()
+ .row(vec!["Laptop", "$999", "In Stock"])
+ .row(vec!["Phone", "$599", "Low Stock"])
+ .row(vec!["Tablet", "$399", "Out of Stock"])
+ .row(vec!["Headphones", "$199", "In Stock"])
+ .into_any_element(),
+ ),
+ ],
),
- single_example(
- "Two Column Table",
- Table::new(vec!["Category", "Value"])
- .width(px(300.))
- .row(vec!["Revenue", "$100,000"])
- .row(vec!["Expenses", "$75,000"])
- .row(vec!["Profit", "$25,000"]),
+ example_group_with_title(
+ "Mixed Content Table",
+ vec![single_example(
+ "Table with Elements",
+ Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
+ .width(px(840.))
+ .row(vec![
+ element_cell(
+ Indicator::dot().color(Color::Success).into_any_element(),
+ ),
+ string_cell("Project A"),
+ string_cell("High"),
+ string_cell("2023-12-31"),
+ element_cell(
+ Button::new("view_a", "View")
+ .style(ButtonStyle::Filled)
+ .full_width()
+ .into_any_element(),
+ ),
+ ])
+ .row(vec![
+ element_cell(
+ Indicator::dot().color(Color::Warning).into_any_element(),
+ ),
+ string_cell("Project B"),
+ string_cell("Medium"),
+ string_cell("2024-03-15"),
+ element_cell(
+ Button::new("view_b", "View")
+ .style(ButtonStyle::Filled)
+ .full_width()
+ .into_any_element(),
+ ),
+ ])
+ .row(vec![
+ element_cell(
+ Indicator::dot().color(Color::Error).into_any_element(),
+ ),
+ string_cell("Project C"),
+ string_cell("Low"),
+ string_cell("2024-06-30"),
+ element_cell(
+ Button::new("view_c", "View")
+ .style(ButtonStyle::Filled)
+ .full_width()
+ .into_any_element(),
+ ),
+ ])
+ .into_any_element(),
+ )],
),
- ]),
- example_group(vec![single_example(
- "Striped Table",
- Table::new(vec!["Product", "Price", "Stock"])
- .width(px(600.))
- .striped()
- .row(vec!["Laptop", "$999", "In Stock"])
- .row(vec!["Phone", "$599", "Low Stock"])
- .row(vec!["Tablet", "$399", "Out of Stock"])
- .row(vec!["Headphones", "$199", "In Stock"]),
- )]),
- example_group_with_title(
- "Mixed Content Table",
- vec![single_example(
- "Table with Elements",
- Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
- .width(px(840.))
- .row(vec![
- element_cell(Indicator::dot().color(Color::Success).into_any_element()),
- string_cell("Project A"),
- string_cell("High"),
- string_cell("2023-12-31"),
- element_cell(
- Button::new("view_a", "View")
- .style(ButtonStyle::Filled)
- .full_width()
- .into_any_element(),
- ),
- ])
- .row(vec![
- element_cell(Indicator::dot().color(Color::Warning).into_any_element()),
- string_cell("Project B"),
- string_cell("Medium"),
- string_cell("2024-03-15"),
- element_cell(
- Button::new("view_b", "View")
- .style(ButtonStyle::Filled)
- .full_width()
- .into_any_element(),
- ),
- ])
- .row(vec![
- element_cell(Indicator::dot().color(Color::Error).into_any_element()),
- string_cell("Project C"),
- string_cell("Low"),
- string_cell("2024-06-30"),
- element_cell(
- Button::new("view_c", "View")
- .style(ButtonStyle::Filled)
- .full_width()
- .into_any_element(),
- ),
- ]),
- )],
- ),
- ]
+ ])
+ .into_any_element()
}
}
@@ -1,5 +1,6 @@
use gpui::{
- div, hsla, prelude::*, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled, Window,
+ div, hsla, prelude::*, AnyElement, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled,
+ Window,
};
use std::sync::Arc;
@@ -38,7 +39,8 @@ pub enum ToggleStyle {
/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
/// Each checkbox works independently from other checkboxes in the list,
/// therefore checking an additional box does not affect any other selections.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
pub struct Checkbox {
id: ElementId,
toggle_state: ToggleState,
@@ -237,7 +239,8 @@ impl RenderOnce for Checkbox {
}
/// A [`Checkbox`] that has a [`Label`].
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
pub struct CheckboxWithLabel {
id: ElementId,
label: Label,
@@ -314,7 +317,8 @@ impl RenderOnce for CheckboxWithLabel {
/// # Switch
///
/// Switches are used to represent opposite states, such as enabled or disabled.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
pub struct Switch {
id: ElementId,
toggle_state: ToggleState,
@@ -429,6 +433,7 @@ impl RenderOnce for Switch {
h_flex()
.id(self.id)
.gap(DynamicSpacing::Base06.rems(cx))
+ .cursor_pointer()
.child(switch)
.when_some(
self.on_click.filter(|_| !self.disabled),
@@ -445,286 +450,249 @@ impl RenderOnce for Switch {
}
}
+/// A [`Switch`] that has a [`Label`].
+#[derive(IntoElement)]
+// #[component(scope = "input")]
+pub struct SwitchWithLabel {
+ id: ElementId,
+ label: Label,
+ toggle_state: ToggleState,
+ on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
+ disabled: bool,
+}
+
+impl SwitchWithLabel {
+ /// Creates a switch with an attached label.
+ pub fn new(
+ id: impl Into<ElementId>,
+ label: Label,
+ toggle_state: impl Into<ToggleState>,
+ on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
+ ) -> Self {
+ Self {
+ id: id.into(),
+ label,
+ toggle_state: toggle_state.into(),
+ on_click: Arc::new(on_click),
+ disabled: false,
+ }
+ }
+
+ /// Sets the disabled state of the [`SwitchWithLabel`].
+ pub fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+}
+
+impl RenderOnce for SwitchWithLabel {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+ h_flex()
+ .id(SharedString::from(format!("{}-container", self.id)))
+ .gap(DynamicSpacing::Base08.rems(cx))
+ .child(
+ Switch::new(self.id.clone(), self.toggle_state)
+ .disabled(self.disabled)
+ .on_click({
+ let on_click = self.on_click.clone();
+ move |checked, window, cx| {
+ (on_click)(checked, window, cx);
+ }
+ }),
+ )
+ .child(
+ div()
+ .id(SharedString::from(format!("{}-label", self.id)))
+ .child(self.label),
+ )
+ }
+}
+
impl ComponentPreview for Checkbox {
- fn description() -> impl Into<Option<&'static str>> {
- "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ v_flex()
+ .gap_6()
+ .children(vec![
+ example_group_with_title(
+ "States",
+ vec![
+ single_example(
+ "Unselected",
+ Checkbox::new("checkbox_unselected", ToggleState::Unselected)
+ .into_any_element(),
+ ),
+ single_example(
+ "Indeterminate",
+ Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate)
+ .into_any_element(),
+ ),
+ single_example(
+ "Selected",
+ Checkbox::new("checkbox_selected", ToggleState::Selected)
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "Styles",
+ vec![
+ single_example(
+ "Default",
+ Checkbox::new("checkbox_default", ToggleState::Selected)
+ .into_any_element(),
+ ),
+ single_example(
+ "Filled",
+ Checkbox::new("checkbox_filled", ToggleState::Selected)
+ .fill()
+ .into_any_element(),
+ ),
+ single_example(
+ "ElevationBased",
+ Checkbox::new("checkbox_elevation", ToggleState::Selected)
+ .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface))
+ .into_any_element(),
+ ),
+ single_example(
+ "Custom Color",
+ Checkbox::new("checkbox_custom", ToggleState::Selected)
+ .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7)))
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "Disabled",
+ vec![
+ single_example(
+ "Unselected",
+ Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
+ .disabled(true)
+ .into_any_element(),
+ ),
+ single_example(
+ "Selected",
+ Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
+ .disabled(true)
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "With Label",
+ vec![single_example(
+ "Default",
+ Checkbox::new("checkbox_with_label", ToggleState::Selected)
+ .label("Always save on quit")
+ .into_any_element(),
+ )],
+ ),
+ ])
+ .into_any_element()
}
+}
- fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
- vec![
- example_group_with_title(
- "Default",
- vec![
- single_example(
- "Unselected",
- Checkbox::new("checkbox_unselected", ToggleState::Unselected),
- ),
- single_example(
- "Indeterminate",
- Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate),
- ),
- single_example(
- "Selected",
- Checkbox::new("checkbox_selected", ToggleState::Selected),
- ),
- ],
- ),
- example_group_with_title(
- "Default (Filled)",
- vec![
- single_example(
- "Unselected",
- Checkbox::new("checkbox_unselected", ToggleState::Unselected).fill(),
- ),
- single_example(
- "Indeterminate",
- Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate).fill(),
- ),
- single_example(
- "Selected",
- Checkbox::new("checkbox_selected", ToggleState::Selected).fill(),
- ),
- ],
- ),
- example_group_with_title(
- "ElevationBased",
- vec![
- single_example(
- "Unselected",
- Checkbox::new("checkbox_unfilled_unselected", ToggleState::Unselected)
- .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
- ),
- single_example(
- "Indeterminate",
- Checkbox::new(
- "checkbox_unfilled_indeterminate",
- ToggleState::Indeterminate,
- )
- .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
- ),
- single_example(
- "Selected",
- Checkbox::new("checkbox_unfilled_selected", ToggleState::Selected)
- .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
- ),
- ],
- ),
- example_group_with_title(
- "ElevationBased (Filled)",
- vec![
- single_example(
- "Unselected",
- Checkbox::new("checkbox_filled_unselected", ToggleState::Unselected)
- .fill()
- .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
- ),
- single_example(
- "Indeterminate",
- Checkbox::new("checkbox_filled_indeterminate", ToggleState::Indeterminate)
- .fill()
- .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
- ),
- single_example(
- "Selected",
- Checkbox::new("checkbox_filled_selected", ToggleState::Selected)
- .fill()
- .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
- ),
- ],
- ),
- example_group_with_title(
- "Custom Color",
- vec![
- single_example(
- "Unselected",
- Checkbox::new("checkbox_custom_unselected", ToggleState::Unselected)
- .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
- ),
- single_example(
- "Indeterminate",
- Checkbox::new("checkbox_custom_indeterminate", ToggleState::Indeterminate)
- .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
- ),
- single_example(
- "Selected",
- Checkbox::new("checkbox_custom_selected", ToggleState::Selected)
- .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
- ),
- ],
- ),
- example_group_with_title(
- "Custom Color (Filled)",
- vec![
- single_example(
- "Unselected",
- Checkbox::new("checkbox_custom_filled_unselected", ToggleState::Unselected)
- .fill()
- .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
- ),
- single_example(
- "Indeterminate",
- Checkbox::new(
- "checkbox_custom_filled_indeterminate",
- ToggleState::Indeterminate,
- )
- .fill()
- .style(ToggleStyle::Custom(hsla(
- 142.0 / 360.,
- 0.68,
- 0.45,
- 0.7,
- ))),
- ),
- single_example(
- "Selected",
- Checkbox::new("checkbox_custom_filled_selected", ToggleState::Selected)
- .fill()
- .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
- ),
- ],
- ),
- example_group_with_title(
- "Disabled",
- vec![
- single_example(
- "Unselected",
- Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
- .disabled(true),
- ),
- single_example(
- "Indeterminate",
- Checkbox::new(
- "checkbox_disabled_indeterminate",
- ToggleState::Indeterminate,
- )
- .disabled(true),
- ),
- single_example(
- "Selected",
- Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
- .disabled(true),
- ),
- ],
- ),
- example_group_with_title(
- "Disabled (Filled)",
+impl ComponentPreview for Switch {
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ v_flex()
+ .gap_6()
+ .children(vec![
+ example_group_with_title(
+ "States",
+ vec![
+ single_example(
+ "Off",
+ Switch::new("switch_off", ToggleState::Unselected)
+ .on_click(|_, _, _cx| {})
+ .into_any_element(),
+ ),
+ single_example(
+ "On",
+ Switch::new("switch_on", ToggleState::Selected)
+ .on_click(|_, _, _cx| {})
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "Disabled",
+ vec![
+ single_example(
+ "Off",
+ Switch::new("switch_disabled_off", ToggleState::Unselected)
+ .disabled(true)
+ .into_any_element(),
+ ),
+ single_example(
+ "On",
+ Switch::new("switch_disabled_on", ToggleState::Selected)
+ .disabled(true)
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "With Label",
+ vec![
+ single_example(
+ "Label",
+ Switch::new("switch_with_label", ToggleState::Selected)
+ .label("Always save on quit")
+ .into_any_element(),
+ ),
+ // TODO: Where did theme_preview_keybinding go?
+ // single_example(
+ // "Keybinding",
+ // Switch::new("switch_with_keybinding", ToggleState::Selected)
+ // .key_binding(theme_preview_keybinding("cmd-shift-e"))
+ // .into_any_element(),
+ // ),
+ ],
+ ),
+ ])
+ .into_any_element()
+ }
+}
+
+impl ComponentPreview for CheckboxWithLabel {
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ v_flex()
+ .gap_6()
+ .children(vec![example_group_with_title(
+ "States",
vec![
single_example(
"Unselected",
- Checkbox::new(
- "checkbox_disabled_filled_unselected",
+ CheckboxWithLabel::new(
+ "checkbox_with_label_unselected",
+ Label::new("Always save on quit"),
ToggleState::Unselected,
+ |_, _, _| {},
)
- .fill()
- .disabled(true),
+ .into_any_element(),
),
single_example(
"Indeterminate",
- Checkbox::new(
- "checkbox_disabled_filled_indeterminate",
+ CheckboxWithLabel::new(
+ "checkbox_with_label_indeterminate",
+ Label::new("Always save on quit"),
ToggleState::Indeterminate,
+ |_, _, _| {},
)
- .fill()
- .disabled(true),
+ .into_any_element(),
),
single_example(
"Selected",
- Checkbox::new("checkbox_disabled_filled_selected", ToggleState::Selected)
- .fill()
- .disabled(true),
- ),
- ],
- ),
- ]
- }
-}
-
-impl ComponentPreview for Switch {
- fn description() -> impl Into<Option<&'static str>> {
- "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting."
- }
-
- fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
- vec![
- example_group_with_title(
- "Default",
- vec![
- single_example(
- "Off",
- Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _, _cx| {}),
- ),
- single_example(
- "On",
- Switch::new("switch_on", ToggleState::Selected).on_click(|_, _, _cx| {}),
- ),
- ],
- ),
- example_group_with_title(
- "Disabled",
- vec![
- single_example(
- "Off",
- Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true),
- ),
- single_example(
- "On",
- Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true),
- ),
- ],
- ),
- example_group_with_title(
- "Label Permutations",
- vec![
- single_example(
- "Label",
- Switch::new("switch_with_label", ToggleState::Selected)
- .label("Always save on quit"),
- ),
- single_example(
- "Keybinding",
- Switch::new("switch_with_label", ToggleState::Selected)
- .key_binding(theme_preview_keybinding("cmd-shift-e")),
+ CheckboxWithLabel::new(
+ "checkbox_with_label_selected",
+ Label::new("Always save on quit"),
+ ToggleState::Selected,
+ |_, _, _| {},
+ )
+ .into_any_element(),
),
],
- ),
- ]
- }
-}
-
-impl ComponentPreview for CheckboxWithLabel {
- fn description() -> impl Into<Option<&'static str>> {
- "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
- }
-
- fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
- vec![example_group(vec![
- single_example(
- "Unselected",
- CheckboxWithLabel::new(
- "checkbox_with_label_unselected",
- Label::new("Always save on quit"),
- ToggleState::Unselected,
- |_, _, _| {},
- ),
- ),
- single_example(
- "Indeterminate",
- CheckboxWithLabel::new(
- "checkbox_with_label_indeterminate",
- Label::new("Always save on quit"),
- ToggleState::Indeterminate,
- |_, _, _| {},
- ),
- ),
- single_example(
- "Selected",
- CheckboxWithLabel::new(
- "checkbox_with_label_selected",
- Label::new("Always save on quit"),
- ToggleState::Selected,
- |_, _, _| {},
- ),
- ),
- ])]
+ )])
+ .into_any_element()
}
}
@@ -1,58 +0,0 @@
-#![allow(missing_docs)]
-
-use gpui::Axis;
-
-use crate::prelude::*;
-
-#[derive(IntoElement)]
-pub struct ToolStrip {
- id: ElementId,
- tools: Vec<IconButton>,
- axis: Axis,
-}
-
-impl ToolStrip {
- fn new(id: ElementId, axis: Axis) -> Self {
- Self {
- id,
- tools: vec![],
- axis,
- }
- }
-
- pub fn vertical(id: impl Into<ElementId>) -> Self {
- Self::new(id.into(), Axis::Vertical)
- }
-
- pub fn tools(mut self, tools: Vec<IconButton>) -> Self {
- self.tools = tools;
- self
- }
-
- pub fn tool(mut self, tool: IconButton) -> Self {
- self.tools.push(tool);
- self
- }
-}
-
-impl RenderOnce for ToolStrip {
- fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
- let group = format!("tool_strip_{}", self.id.clone());
-
- div()
- .id(self.id.clone())
- .group(group)
- .map(|element| match self.axis {
- Axis::Vertical => element.v_flex(),
- Axis::Horizontal => element.h_flex(),
- })
- .flex_none()
- .gap(DynamicSpacing::Base04.rems(cx))
- .p(DynamicSpacing::Base02.rems(cx))
- .border_1()
- .border_color(cx.theme().colors().border)
- .rounded(rems_from_px(6.0))
- .bg(cx.theme().colors().elevated_surface_background)
- .children(self.tools)
- }
-}
@@ -1,12 +1,13 @@
#![allow(missing_docs)]
-use gpui::{Action, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
+use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
use settings::Settings;
use theme::ThemeSettings;
use crate::prelude::*;
use crate::{h_flex, v_flex, Color, KeyBinding, Label, LabelSize, StyledExt};
+#[derive(IntoComponent)]
pub struct Tooltip {
title: SharedString,
meta: Option<SharedString>,
@@ -35,6 +36,22 @@ impl Tooltip {
}
}
+ pub fn for_action_title(
+ title: impl Into<SharedString>,
+ action: &dyn Action,
+ ) -> impl Fn(&mut Window, &mut App) -> AnyView {
+ let title = title.into();
+ let action = action.boxed_clone();
+ move |window, cx| {
+ cx.new(|_| Self {
+ title: title.clone(),
+ meta: None,
+ key_binding: KeyBinding::for_action(action.as_ref(), window),
+ })
+ .into()
+ }
+ }
+
pub fn for_action(
title: impl Into<SharedString>,
action: &dyn Action,
@@ -188,3 +205,15 @@ impl Render for LinkPreview {
})
}
}
+
+impl ComponentPreview for Tooltip {
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ example_group(vec![single_example(
+ "Text only",
+ Button::new("delete-example", "Delete")
+ .tooltip(Tooltip::text("This is a tooltip!"))
+ .into_any_element(),
+ )])
+ .into_any_element()
+ }
+}
@@ -6,9 +6,11 @@ pub use gpui::{
InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, Window,
};
+pub use component::{example_group, example_group_with_title, single_example, ComponentPreview};
+pub use ui_macros::IntoComponent;
+
pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle, StyledTypography, TextSize};
pub use crate::traits::clickable::*;
-pub use crate::traits::component_preview::*;
pub use crate::traits::disableable::*;
pub use crate::traits::fixed::*;
pub use crate::traits::styled_ext::*;
@@ -86,3 +86,9 @@ impl Color {
}
}
}
+
+impl From<Hsla> for Color {
+ fn from(color: Hsla) -> Self {
+ Color::Custom(color)
+ }
+}
@@ -1,5 +1,7 @@
+use crate::prelude::*;
use gpui::{
- div, rems, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, Window,
+ div, rems, AnyElement, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled,
+ Window,
};
use settings::Settings;
use theme::{ActiveTheme, ThemeSettings};
@@ -188,7 +190,7 @@ impl HeadlineSize {
/// A headline element, used to emphasize some text and
/// create a visual hierarchy.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
pub struct Headline {
size: HeadlineSize,
text: SharedString,
@@ -230,3 +232,44 @@ impl Headline {
self
}
}
+
+impl ComponentPreview for Headline {
+ fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+ v_flex()
+ .gap_6()
+ .children(vec![example_group_with_title(
+ "Headline Sizes",
+ vec![
+ single_example(
+ "XLarge",
+ Headline::new("XLarge Headline")
+ .size(HeadlineSize::XLarge)
+ .into_any_element(),
+ ),
+ single_example(
+ "Large",
+ Headline::new("Large Headline")
+ .size(HeadlineSize::Large)
+ .into_any_element(),
+ ),
+ single_example(
+ "Medium (Default)",
+ Headline::new("Medium Headline").into_any_element(),
+ ),
+ single_example(
+ "Small",
+ Headline::new("Small Headline")
+ .size(HeadlineSize::Small)
+ .into_any_element(),
+ ),
+ single_example(
+ "XSmall",
+ Headline::new("XSmall Headline")
+ .size(HeadlineSize::XSmall)
+ .into_any_element(),
+ ),
+ ],
+ )])
+ .into_any_element()
+ }
+}
@@ -1,5 +1,4 @@
pub mod clickable;
-pub mod component_preview;
pub mod disableable;
pub mod fixed;
pub mod styled_ext;
@@ -1,205 +0,0 @@
-#![allow(missing_docs)]
-use crate::{prelude::*, KeyBinding};
-use gpui::{AnyElement, SharedString};
-
-/// Which side of the preview to show labels on
-#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ExampleLabelSide {
- /// Left side
- Left,
- /// Right side
- Right,
- #[default]
- /// Top side
- Top,
- /// Bottom side
- Bottom,
-}
-
-/// Implement this trait to enable rich UI previews with metadata in the Theme Preview tool.
-pub trait ComponentPreview: IntoElement {
- fn title() -> &'static str {
- std::any::type_name::<Self>()
- }
-
- fn description() -> impl Into<Option<&'static str>> {
- None
- }
-
- fn example_label_side() -> ExampleLabelSide {
- ExampleLabelSide::default()
- }
-
- fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>>;
-
- fn custom_example(_window: &mut Window, _cx: &mut App) -> impl Into<Option<AnyElement>> {
- None::<AnyElement>
- }
-
- fn component_previews(window: &mut Window, cx: &mut App) -> Vec<AnyElement> {
- Self::examples(window, cx)
- .into_iter()
- .map(|example| Self::render_example_group(example))
- .collect()
- }
-
- fn render_component_previews(window: &mut Window, cx: &mut App) -> AnyElement {
- let title = Self::title();
- let (source, title) = title
- .rsplit_once("::")
- .map_or((None, title), |(s, t)| (Some(s), t));
- let description = Self::description().into();
-
- v_flex()
- .w_full()
- .gap_6()
- .p_4()
- .border_1()
- .border_color(cx.theme().colors().border)
- .rounded_md()
- .child(
- v_flex()
- .gap_1()
- .child(
- h_flex()
- .gap_1()
- .child(Headline::new(title).size(HeadlineSize::Small))
- .when_some(source, |this, source| {
- this.child(Label::new(format!("({})", source)).color(Color::Muted))
- }),
- )
- .when_some(description, |this, description| {
- this.child(
- div()
- .text_ui_sm(cx)
- .text_color(cx.theme().colors().text_muted)
- .max_w(px(600.0))
- .child(description),
- )
- }),
- )
- .when_some(
- Self::custom_example(window, cx).into(),
- |this, custom_example| this.child(custom_example),
- )
- .children(Self::component_previews(window, cx))
- .into_any_element()
- }
-
- fn render_example_group(group: ComponentExampleGroup<Self>) -> AnyElement {
- v_flex()
- .gap_6()
- .when(group.grow, |this| this.w_full().flex_1())
- .when_some(group.title, |this, title| {
- this.child(Label::new(title).size(LabelSize::Small))
- })
- .child(
- h_flex()
- .w_full()
- .gap_6()
- .children(group.examples.into_iter().map(Self::render_example))
- .into_any_element(),
- )
- .into_any_element()
- }
-
- fn render_example(example: ComponentExample<Self>) -> AnyElement {
- let base = div().flex();
-
- let base = match Self::example_label_side() {
- ExampleLabelSide::Right => base.flex_row(),
- ExampleLabelSide::Left => base.flex_row_reverse(),
- ExampleLabelSide::Bottom => base.flex_col(),
- ExampleLabelSide::Top => base.flex_col_reverse(),
- };
-
- base.gap_1()
- .when(example.grow, |this| this.flex_1())
- .child(example.element)
- .child(
- Label::new(example.variant_name)
- .size(LabelSize::XSmall)
- .color(Color::Muted),
- )
- .into_any_element()
- }
-}
-
-/// A single example of a component.
-pub struct ComponentExample<T> {
- variant_name: SharedString,
- element: T,
- grow: bool,
-}
-
-impl<T> ComponentExample<T> {
- /// Create a new example with the given variant name and example value.
- pub fn new(variant_name: impl Into<SharedString>, example: T) -> Self {
- Self {
- variant_name: variant_name.into(),
- element: example,
- grow: false,
- }
- }
-
- /// Set the example to grow to fill the available horizontal space.
- pub fn grow(mut self) -> Self {
- self.grow = true;
- self
- }
-}
-
-/// A group of component examples.
-pub struct ComponentExampleGroup<T> {
- pub title: Option<SharedString>,
- pub examples: Vec<ComponentExample<T>>,
- pub grow: bool,
-}
-
-impl<T> ComponentExampleGroup<T> {
- /// Create a new group of examples with the given title.
- pub fn new(examples: Vec<ComponentExample<T>>) -> Self {
- Self {
- title: None,
- examples,
- grow: false,
- }
- }
-
- /// Create a new group of examples with the given title.
- pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample<T>>) -> Self {
- Self {
- title: Some(title.into()),
- examples,
- grow: false,
- }
- }
-
- /// Set the group to grow to fill the available horizontal space.
- pub fn grow(mut self) -> Self {
- self.grow = true;
- self
- }
-}
-
-/// Create a single example
-pub fn single_example<T>(variant_name: impl Into<SharedString>, example: T) -> ComponentExample<T> {
- ComponentExample::new(variant_name, example)
-}
-
-/// Create a group of examples without a title
-pub fn example_group<T>(examples: Vec<ComponentExample<T>>) -> ComponentExampleGroup<T> {
- ComponentExampleGroup::new(examples)
-}
-
-/// Create a group of examples with a title
-pub fn example_group_with_title<T>(
- title: impl Into<SharedString>,
- examples: Vec<ComponentExample<T>>,
-) -> ComponentExampleGroup<T> {
- ComponentExampleGroup::with_title(title, examples)
-}
-
-pub fn theme_preview_keybinding(keystrokes: &str) -> KeyBinding {
- KeyBinding::new(gpui::KeyBinding::new(keystrokes, gpui::NoAction {}, None))
-}
@@ -13,7 +13,8 @@ path = "src/ui_macros.rs"
proc-macro = true
[dependencies]
+convert_case.workspace = true
+linkme.workspace = true
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true
-convert_case.workspace = true
@@ -0,0 +1,97 @@
+use convert_case::{Case, Casing};
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput, Lit, Meta, MetaList, MetaNameValue, NestedMeta};
+
+pub fn derive_into_component(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ let mut scope_val = None;
+ let mut description_val = None;
+
+ for attr in &input.attrs {
+ if attr.path.is_ident("component") {
+ if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() {
+ for item in nested {
+ if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
+ path,
+ lit: Lit::Str(s),
+ ..
+ })) = item
+ {
+ let ident = path.get_ident().map(|i| i.to_string()).unwrap_or_default();
+ if ident == "scope" {
+ scope_val = Some(s.value());
+ } else if ident == "description" {
+ description_val = Some(s.value());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ let name = &input.ident;
+
+ let scope_impl = if let Some(s) = scope_val {
+ quote! {
+ fn scope() -> Option<&'static str> {
+ Some(#s)
+ }
+ }
+ } else {
+ quote! {
+ fn scope() -> Option<&'static str> {
+ None
+ }
+ }
+ };
+
+ let description_impl = if let Some(desc) = description_val {
+ quote! {
+ fn description() -> Option<&'static str> {
+ Some(#desc)
+ }
+ }
+ } else {
+ quote! {}
+ };
+
+ let register_component_name = syn::Ident::new(
+ &format!(
+ "__register_component_{}",
+ Casing::to_case(&name.to_string(), Case::Snake)
+ ),
+ name.span(),
+ );
+ let register_preview_name = syn::Ident::new(
+ &format!(
+ "__register_preview_{}",
+ Casing::to_case(&name.to_string(), Case::Snake)
+ ),
+ name.span(),
+ );
+
+ let expanded = quote! {
+ impl component::Component for #name {
+ #scope_impl
+
+ fn name() -> &'static str {
+ stringify!(#name)
+ }
+
+ #description_impl
+ }
+
+ #[linkme::distributed_slice(component::__ALL_COMPONENTS)]
+ fn #register_component_name() {
+ component::register_component::<#name>();
+ }
+
+ #[linkme::distributed_slice(component::__ALL_PREVIEWS)]
+ fn #register_preview_name() {
+ component::register_preview::<#name>();
+ }
+ };
+
+ expanded.into()
+}
@@ -1,3 +1,4 @@
+mod derive_component;
mod derive_path_str;
mod dynamic_spacing;
@@ -58,3 +59,27 @@ pub fn path_str(_args: TokenStream, input: TokenStream) -> TokenStream {
pub fn derive_dynamic_spacing(input: TokenStream) -> TokenStream {
dynamic_spacing::derive_spacing(input)
}
+
+/// Derives the `Component` trait for a struct.
+///
+/// This macro generates implementations for the `Component` trait and associated
+/// registration functions for the component system.
+///
+/// # Attributes
+///
+/// - `#[component(scope = "...")]`: Required. Specifies the scope of the component.
+/// - `#[component(description = "...")]`: Optional. Provides a description for the component.
+///
+/// # Example
+///
+/// ```
+/// use ui_macros::Component;
+///
+/// #[derive(Component)]
+/// #[component(scope = "toggle", description = "A element that can be toggled on and off")]
+/// struct Checkbox;
+/// ```
+#[proc_macro_derive(IntoComponent, attributes(component))]
+pub fn derive_component(input: TokenStream) -> TokenStream {
+ derive_component::derive_into_component(input)
+}
@@ -105,7 +105,7 @@ impl<T: AsRef<Path>> PathExt for T {
/// leverages Rust's type system to ensure that all paths entering Zed are always "sanitized" by removing the `\\\\?\\` prefix.
/// On non-Windows operating systems, this struct is effectively a no-op.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct SanitizedPath(Arc<Path>);
+pub struct SanitizedPath(pub Arc<Path>);
impl SanitizedPath {
pub fn starts_with(&self, prefix: &SanitizedPath) -> bool {
@@ -407,6 +407,9 @@ impl Object {
if let Some(range) = self.range(map, selection.clone(), around) {
selection.start = range.start;
selection.end = range.end;
+ if !around && self.is_multiline() {
+ preserve_indented_newline(map, selection);
+ }
true
} else {
false
@@ -414,6 +417,49 @@ impl Object {
}
}
+/// Returns a range without the final newline char.
+///
+/// If the selection spans multiple lines and is preceded by an opening brace (`{`),
+/// this function will trim the selection to exclude the final newline
+/// in order to preserve a properly indented line.
+fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
+ let (start_point, end_point) = (selection.start.to_point(map), selection.end.to_point(map));
+
+ if start_point.row == end_point.row {
+ return;
+ }
+
+ let start_offset = selection.start.to_offset(map, Bias::Left);
+ let mut pos = start_offset;
+
+ while pos > 0 {
+ pos -= 1;
+ let current_char = map.buffer_chars_at(pos).next().map(|(ch, _)| ch);
+
+ match current_char {
+ Some(ch) if !ch.is_whitespace() => break,
+ Some('\n') if pos > 0 => {
+ let prev_char = map.buffer_chars_at(pos - 1).next().map(|(ch, _)| ch);
+ if prev_char == Some('{') {
+ let end_pos = selection.end.to_offset(map, Bias::Left);
+ for (ch, offset) in map.reverse_buffer_chars_at(end_pos) {
+ match ch {
+ '\n' => {
+ selection.end = offset.to_display_point(map);
+ break;
+ }
+ ch if !ch.is_whitespace() => break,
+ _ => continue,
+ }
+ }
+ }
+ break;
+ }
+ _ => continue,
+ }
+ }
+}
+
/// Returns a range that surrounds the word `relative_to` is in.
///
/// If `relative_to` is at the start of a word, return the word.
@@ -1333,20 +1379,38 @@ fn surrounding_markers(
}
if !around && search_across_lines {
+ // Handle trailing newline after opening
if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
if ch == '\n' {
- opening.end = range.end
+ opening.end = range.end;
+
+ // After newline, skip leading whitespace
+ let mut chars = movement::chars_after(map, opening.end).peekable();
+ while let Some((ch, range)) = chars.peek() {
+ if !ch.is_whitespace() {
+ break;
+ }
+ opening.end = range.end;
+ chars.next();
+ }
}
}
+ // Handle leading whitespace before closing
+ let mut last_newline_end = None;
for (ch, range) in movement::chars_before(map, closing.start) {
if !ch.is_whitespace() {
break;
}
- if ch != '\n' {
- closing.start = range.start
+ if ch == '\n' {
+ last_newline_end = Some(range.end);
+ break;
}
}
+ // Adjust closing.start to exclude whitespace after a newline, if present
+ if let Some(end) = last_newline_end {
+ closing.start = end;
+ }
}
let result = if around {
@@ -1681,60 +1745,46 @@ mod test {
#[gpui::test]
async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx).await;
+ let mut cx = VimTestContext::new(cx, true).await;
- cx.set_shared_state(indoc! {
- "func empty(a string) bool {
- if a == \"\" {
- return true
- }
- Λreturn false
- }"
- })
- .await;
- cx.simulate_shared_keystrokes("v i {").await;
- cx.shared_state().await.assert_eq(indoc! {"
- func empty(a string) bool {
- Β« if a == \"\" {
- return true
- }
- return false
- ΛΒ»}"});
- cx.set_shared_state(indoc! {
- "func empty(a string) bool {
- if a == \"\" {
- Λreturn true
- }
- return false
- }"
- })
- .await;
- cx.simulate_shared_keystrokes("v i {").await;
- cx.shared_state().await.assert_eq(indoc! {"
- func empty(a string) bool {
- if a == \"\" {
- Β« return true
- ΛΒ» }
- return false
- }"});
+ cx.set_state(
+ indoc! {
+ "func empty(a string) bool {
+ if a == \"\" {
+ return true
+ }
+ Λreturn false
+ }"
+ },
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v i {");
- cx.set_shared_state(indoc! {
- "func empty(a string) bool {
- if a == \"\" Λ{
- return true
- }
- return false
- }"
- })
- .await;
- cx.simulate_shared_keystrokes("v i {").await;
- cx.shared_state().await.assert_eq(indoc! {"
- func empty(a string) bool {
- if a == \"\" {
- Β« return true
- ΛΒ» }
- return false
- }"});
+ cx.set_state(
+ indoc! {
+ "func empty(a string) bool {
+ if a == \"\" {
+ Λreturn true
+ }
+ return false
+ }"
+ },
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v i {");
+
+ cx.set_state(
+ indoc! {
+ "func empty(a string) bool {
+ if a == \"\" Λ{
+ return true
+ }
+ return false
+ }"
+ },
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v i {");
}
#[gpui::test]
@@ -2254,6 +2304,20 @@ mod test {
}
}
+ #[gpui::test]
+ async fn test_anybrackets_trailing_space(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state("(trailingΛ whitespace )")
+ .await;
+ cx.simulate_shared_keystrokes("v i b").await;
+ cx.shared_state().await.assert_matches();
+ cx.simulate_shared_keystrokes("escape y i b").await;
+ cx.shared_clipboard()
+ .await
+ .assert_eq("trailing whitespace ");
+ }
+
#[gpui::test]
async fn test_tags(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new_html(cx).await;
@@ -0,0 +1,11 @@
+{"Put":{"state":"(trailingΛ whitespace )"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"b"}
+{"Get":{"state":"(Β«trailing whitespace ΛΒ»)","mode":"Visual"}}
+{"Key":"escape"}
+{"Key":"y"}
+{"Key":"i"}
+{"Key":"b"}
+{"Get":{"state":"(Λtrailing whitespace )","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"trailing whitespace "}}
@@ -34,6 +34,7 @@ call.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
+component.workspace = true
db.workspace = true
derive_more.workspace = true
fs.workspace = true
@@ -448,6 +448,14 @@ pub mod simple_message_notification {
self
}
+ pub fn primary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
+ where
+ F: 'static + Fn(&mut Window, &mut Context<Self>),
+ {
+ self.primary_on_click = Some(on_click);
+ self
+ }
+
pub fn secondary_message<S>(mut self, message: S) -> Self
where
S: Into<SharedString>,
@@ -474,6 +482,14 @@ pub mod simple_message_notification {
self
}
+ pub fn secondary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
+ where
+ F: 'static + Fn(&mut Window, &mut Context<Self>),
+ {
+ self.secondary_on_click = Some(on_click);
+ self
+ }
+
pub fn more_info_message<S>(mut self, message: S) -> Self
where
S: Into<SharedString>,
@@ -441,10 +441,9 @@ impl Pane {
.gap(DynamicSpacing::Base04.rems(cx))
.child(
PopoverMenu::new("pane-tab-bar-popover-menu")
- .trigger(
- IconButton::new("plus", IconName::Plus)
- .icon_size(IconSize::Small)
- .tooltip(Tooltip::text("New...")),
+ .trigger_with_tooltip(
+ IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
+ Tooltip::text("New..."),
)
.anchor(Corner::TopRight)
.with_handle(pane.new_item_context_menu_handle.clone())
@@ -474,10 +473,10 @@ impl Pane {
)
.child(
PopoverMenu::new("pane-tab-bar-split")
- .trigger(
+ .trigger_with_tooltip(
IconButton::new("split", IconName::Split)
- .icon_size(IconSize::Small)
- .tooltip(Tooltip::text("Split Pane")),
+ .icon_size(IconSize::Small),
+ Tooltip::text("Split Pane"),
)
.anchor(Corner::TopRight)
.with_handle(pane.split_item_context_menu_handle.clone())
@@ -27,7 +27,6 @@ pub fn init(cx: &mut App) {
enum ThemePreviewPage {
Overview,
Typography,
- Components,
}
impl ThemePreviewPage {
@@ -35,7 +34,6 @@ impl ThemePreviewPage {
match self {
Self::Overview => "Overview",
Self::Typography => "Typography",
- Self::Components => "Components",
}
}
}
@@ -64,9 +62,6 @@ impl ThemePreview {
ThemePreviewPage::Typography => {
self.render_typography_page(window, cx).into_any_element()
}
- ThemePreviewPage::Components => {
- self.render_components_page(window, cx).into_any_element()
- }
}
}
}
@@ -392,28 +387,6 @@ impl ThemePreview {
)
}
- fn render_components_page(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
- let layer = ElevationIndex::Surface;
-
- v_flex()
- .id("theme-preview-components")
- .overflow_scroll()
- .size_full()
- .gap_2()
- .child(Button::render_component_previews(window, cx))
- .child(Checkbox::render_component_previews(window, cx))
- .child(CheckboxWithLabel::render_component_previews(window, cx))
- .child(ContentGroup::render_component_previews(window, cx))
- .child(DecoratedIcon::render_component_previews(window, cx))
- .child(Facepile::render_component_previews(window, cx))
- .child(Icon::render_component_previews(window, cx))
- .child(IconDecoration::render_component_previews(window, cx))
- .child(KeybindingHint::render_component_previews(window, cx))
- .child(Indicator::render_component_previews(window, cx))
- .child(Switch::render_component_previews(window, cx))
- .child(Table::render_component_previews(window, cx))
- }
-
fn render_page_nav(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.id("theme-preview-nav")
@@ -148,6 +148,7 @@ actions!(
Open,
OpenFiles,
OpenInTerminal,
+ OpenComponentPreview,
ReloadActiveItem,
SaveAs,
SaveWithoutFormat,
@@ -378,6 +379,7 @@ fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, c
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
init_settings(cx);
+ component::init();
theme_preview::init(cx);
cx.on_action(Workspace::close_global);
@@ -4440,10 +4442,12 @@ impl Workspace {
if let Some(focus_on) = focus_on {
focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
} else {
- self.panes
- .last()
- .unwrap()
- .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
+ if self.active_pane() == pane {
+ self.panes
+ .last()
+ .unwrap()
+ .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
+ }
}
if self.last_active_center_pane == Some(pane.downgrade()) {
self.last_active_center_pane = None;
@@ -14,11 +14,12 @@ workspace = true
[features]
test-support = [
+ "gpui/test-support",
+ "http_client/test-support",
"language/test-support",
"settings/test-support",
"text/test-support",
- "gpui/test-support",
- "http_client/test-support",
+ "util/test-support",
]
[dependencies]
@@ -59,3 +60,4 @@ pretty_assertions.workspace = true
rand.workspace = true
rpc = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }
@@ -213,12 +213,6 @@ impl Deref for RepositoryEntry {
}
}
-impl AsRef<Path> for RepositoryEntry {
- fn as_ref(&self) -> &Path {
- &self.path
- }
-}
-
impl RepositoryEntry {
pub fn branch(&self) -> Option<Arc<str>> {
self.branch.clone()
@@ -326,33 +320,53 @@ impl RepositoryEntry {
/// But if a sub-folder of a git repository is opened, this corresponds to the
/// project root and the .git folder is located in a parent directory.
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
-pub struct WorkDirectory {
- path: Arc<Path>,
-
- /// If location_in_repo is set, it means the .git folder is external
- /// and in a parent folder of the project root.
- /// In that case, the work_directory field will point to the
- /// project-root and location_in_repo contains the location of the
- /// project-root in the repository.
- ///
- /// Example:
- ///
- /// my_root_folder/ <-- repository root
- /// .git
- /// my_sub_folder_1/
- /// project_root/ <-- Project root, Zed opened here
- /// ...
- ///
- /// For this setup, the attributes will have the following values:
- ///
- /// work_directory: pointing to "" entry
- /// location_in_repo: Some("my_sub_folder_1/project_root")
- pub(crate) location_in_repo: Option<Arc<Path>>,
+pub enum WorkDirectory {
+ InProject {
+ relative_path: Arc<Path>,
+ },
+ AboveProject {
+ absolute_path: Arc<Path>,
+ location_in_repo: Arc<Path>,
+ },
}
impl WorkDirectory {
- pub fn path_key(&self) -> PathKey {
- PathKey(self.path.clone())
+ #[cfg(test)]
+ fn in_project(path: &str) -> Self {
+ let path = Path::new(path);
+ Self::InProject {
+ relative_path: path.into(),
+ }
+ }
+
+ #[cfg(test)]
+ fn canonicalize(&self) -> Self {
+ match self {
+ WorkDirectory::InProject { relative_path } => WorkDirectory::InProject {
+ relative_path: relative_path.clone(),
+ },
+ WorkDirectory::AboveProject {
+ absolute_path,
+ location_in_repo,
+ } => WorkDirectory::AboveProject {
+ absolute_path: absolute_path.canonicalize().unwrap().into(),
+ location_in_repo: location_in_repo.clone(),
+ },
+ }
+ }
+
+ pub fn is_above_project(&self) -> bool {
+ match self {
+ WorkDirectory::InProject { .. } => false,
+ WorkDirectory::AboveProject { .. } => true,
+ }
+ }
+
+ fn path_key(&self) -> PathKey {
+ match self {
+ WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()),
+ WorkDirectory::AboveProject { .. } => PathKey(Path::new("").into()),
+ }
}
/// Returns true if the given path is a child of the work directory.
@@ -360,9 +374,14 @@ impl WorkDirectory {
/// Note that the path may not be a member of this repository, if there
/// is a repository in a directory between these two paths
/// external .git folder in a parent folder of the project root.
+ #[track_caller]
pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool {
let path = path.as_ref();
- path.starts_with(&self.path)
+ debug_assert!(path.is_relative());
+ match self {
+ WorkDirectory::InProject { relative_path } => path.starts_with(relative_path),
+ WorkDirectory::AboveProject { .. } => true,
+ }
}
/// relativize returns the given project path relative to the root folder of the
@@ -371,53 +390,71 @@ impl WorkDirectory {
/// of the project root folder, then the returned RepoPath is relative to the root
/// of the repository and not a valid path inside the project.
pub fn relativize(&self, path: &Path) -> Result<RepoPath> {
- let repo_path = if let Some(location_in_repo) = &self.location_in_repo {
- // Avoid joining a `/` to location_in_repo in the case of a single-file worktree.
- if path == Path::new("") {
- RepoPath(location_in_repo.clone())
- } else {
- location_in_repo.join(path).into()
+ // path is assumed to be relative to worktree root.
+ debug_assert!(path.is_relative());
+ match self {
+ WorkDirectory::InProject { relative_path } => Ok(path
+ .strip_prefix(relative_path)
+ .map_err(|_| {
+ anyhow!(
+ "could not relativize {:?} against {:?}",
+ path,
+ relative_path
+ )
+ })?
+ .into()),
+ WorkDirectory::AboveProject {
+ location_in_repo, ..
+ } => {
+ // Avoid joining a `/` to location_in_repo in the case of a single-file worktree.
+ if path == Path::new("") {
+ Ok(RepoPath(location_in_repo.clone()))
+ } else {
+ Ok(location_in_repo.join(path).into())
+ }
}
- } else {
- path.strip_prefix(&self.path)
- .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, self.path))?
- .into()
- };
- Ok(repo_path)
+ }
}
/// This is the opposite operation to `relativize` above
pub fn unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
- if let Some(location) = &self.location_in_repo {
- // If we fail to strip the prefix, that means this status entry is
- // external to this worktree, and we definitely won't have an entry_id
- path.strip_prefix(location).ok().map(Into::into)
- } else {
- Some(self.path.join(path).into())
+ match self {
+ WorkDirectory::InProject { relative_path } => Some(relative_path.join(path).into()),
+ WorkDirectory::AboveProject {
+ location_in_repo, ..
+ } => {
+ // If we fail to strip the prefix, that means this status entry is
+ // external to this worktree, and we definitely won't have an entry_id
+ path.strip_prefix(location_in_repo).ok().map(Into::into)
+ }
}
}
-}
-impl Default for WorkDirectory {
- fn default() -> Self {
- Self {
- path: Arc::from(Path::new("")),
- location_in_repo: None,
+ pub fn display_name(&self) -> String {
+ match self {
+ WorkDirectory::InProject { relative_path } => relative_path.display().to_string(),
+ WorkDirectory::AboveProject {
+ absolute_path,
+ location_in_repo,
+ } => {
+ let num_of_dots = location_in_repo.components().count();
+
+ "../".repeat(num_of_dots)
+ + &absolute_path
+ .file_name()
+ .map(|s| s.to_string_lossy())
+ .unwrap_or_default()
+ + "/"
+ }
}
}
}
-impl Deref for WorkDirectory {
- type Target = Path;
-
- fn deref(&self) -> &Self::Target {
- self.as_ref()
- }
-}
-
-impl AsRef<Path> for WorkDirectory {
- fn as_ref(&self) -> &Path {
- self.path.as_ref()
+impl Default for WorkDirectory {
+ fn default() -> Self {
+ Self::InProject {
+ relative_path: Arc::from(Path::new("")),
+ }
}
}
@@ -487,7 +524,7 @@ impl sum_tree::Item for LocalRepositoryEntry {
fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
PathSummary {
- max_path: self.work_directory.path.clone(),
+ max_path: self.work_directory.path_key().0,
item_summary: Unit,
}
}
@@ -497,7 +534,7 @@ impl KeyedItem for LocalRepositoryEntry {
type Key = PathKey;
fn key(&self) -> Self::Key {
- PathKey(self.work_directory.path.clone())
+ self.work_directory.path_key()
}
}
@@ -1395,16 +1432,7 @@ impl LocalWorktree {
drop(barrier);
}
ScanState::RootUpdated { new_path } => {
- if let Some(new_path) = new_path {
- this.snapshot.git_repositories = Default::default();
- this.snapshot.ignores_by_parent_abs_path = Default::default();
- let root_name = new_path
- .as_path()
- .file_name()
- .map_or(String::new(), |f| f.to_string_lossy().to_string());
- this.snapshot.update_abs_path(new_path, root_name);
- }
- this.restart_background_scanners(cx);
+ this.update_abs_path_and_refresh(new_path, cx);
}
}
cx.notify();
@@ -1844,6 +1872,10 @@ impl LocalWorktree {
}))
}
+ /// Rename an entry.
+ ///
+ /// `new_path` is the new relative path to the worktree root.
+ /// If the root entry is renamed then `new_path` is the new root name instead.
fn rename_entry(
&self,
entry_id: ProjectEntryId,
@@ -1856,8 +1888,18 @@ impl LocalWorktree {
};
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
- let Ok(abs_new_path) = self.absolutize(&new_path) else {
- return Task::ready(Err(anyhow!("absolutizing path {new_path:?}")));
+
+ let is_root_entry = self.root_entry().is_some_and(|e| e.id == entry_id);
+ let abs_new_path = if is_root_entry {
+ let Some(root_parent_path) = self.abs_path().parent() else {
+ return Task::ready(Err(anyhow!("no parent for path {:?}", self.abs_path)));
+ };
+ root_parent_path.join(&new_path)
+ } else {
+ let Ok(absolutize_path) = self.absolutize(&new_path) else {
+ return Task::ready(Err(anyhow!("absolutizing path {new_path:?}")));
+ };
+ absolutize_path
};
let abs_path = abs_new_path.clone();
let fs = self.fs.clone();
@@ -1891,9 +1933,19 @@ impl LocalWorktree {
rename.await?;
Ok(this
.update(&mut cx, |this, cx| {
- this.as_local_mut()
- .unwrap()
- .refresh_entry(new_path.clone(), Some(old_path), cx)
+ let local = this.as_local_mut().unwrap();
+ if is_root_entry {
+ // We eagerly update `abs_path` and refresh this worktree.
+ // Otherwise, the FS watcher would do it on the `RootUpdated` event,
+ // but with a noticeable delay, so we handle it proactively.
+ local.update_abs_path_and_refresh(
+ Some(SanitizedPath::from(abs_path.clone())),
+ cx,
+ );
+ Task::ready(Ok(this.root_entry().cloned()))
+ } else {
+ local.refresh_entry(new_path.clone(), Some(old_path), cx)
+ }
})?
.await?
.map(CreatedEntry::Included)
@@ -2158,6 +2210,23 @@ impl LocalWorktree {
self.share_private_files = true;
self.restart_background_scanners(cx);
}
+
+ fn update_abs_path_and_refresh(
+ &mut self,
+ new_path: Option<SanitizedPath>,
+ cx: &Context<Worktree>,
+ ) {
+ if let Some(new_path) = new_path {
+ self.snapshot.git_repositories = Default::default();
+ self.snapshot.ignores_by_parent_abs_path = Default::default();
+ let root_name = new_path
+ .as_path()
+ .file_name()
+ .map_or(String::new(), |f| f.to_string_lossy().to_string());
+ self.snapshot.update_abs_path(new_path, root_name);
+ }
+ self.restart_background_scanners(cx);
+ }
}
impl RemoteWorktree {
@@ -2574,12 +2643,11 @@ impl Snapshot {
self.repositories.insert_or_replace(
RepositoryEntry {
work_directory_id,
- work_directory: WorkDirectory {
- path: work_dir_entry.path.clone(),
- // When syncing repository entries from a peer, we don't need
- // the location_in_repo field, since git operations don't happen locally
- // anyway.
- location_in_repo: None,
+ // When syncing repository entries from a peer, we don't need
+ // the location_in_repo field, since git operations don't happen locally
+ // anyway.
+ work_directory: WorkDirectory::InProject {
+ relative_path: work_dir_entry.path.clone(),
},
branch: repository.branch.map(Into::into),
statuses_by_path: statuses,
@@ -2690,23 +2758,13 @@ impl Snapshot {
&self.repositories
}
- pub fn repositories_with_abs_paths(
- &self,
- ) -> impl '_ + Iterator<Item = (&RepositoryEntry, PathBuf)> {
- let base = self.abs_path();
- self.repositories.iter().map(|repo| {
- let path = repo.work_directory.location_in_repo.as_deref();
- let path = path.unwrap_or(repo.work_directory.as_ref());
- (repo, base.join(path))
- })
- }
-
/// Get the repository whose work directory corresponds to the given path.
pub(crate) fn repository(&self, work_directory: PathKey) -> Option<RepositoryEntry> {
self.repositories.get(&work_directory, &()).cloned()
}
/// Get the repository whose work directory contains the given path.
+ #[track_caller]
pub fn repository_for_path(&self, path: &Path) -> Option<&RepositoryEntry> {
self.repositories
.iter()
@@ -2716,6 +2774,7 @@ impl Snapshot {
/// Given an ordered iterator of entries, returns an iterator of those entries,
/// along with their containing git repository.
+ #[track_caller]
pub fn entries_with_repositories<'a>(
&'a self,
entries: impl 'a + Iterator<Item = &'a Entry>,
@@ -2817,6 +2876,7 @@ impl Snapshot {
pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
let path = path.as_ref();
+ debug_assert!(path.is_relative());
self.traverse_from_path(true, true, true, path)
.entry()
.and_then(|entry| {
@@ -3080,7 +3140,7 @@ impl LocalSnapshot {
let work_dir_paths = self
.repositories
.iter()
- .map(|repo| repo.work_directory.path.clone())
+ .map(|repo| repo.work_directory.path_key())
.collect::<HashSet<_>>();
assert_eq!(dotgit_paths.len(), work_dir_paths.len());
assert_eq!(self.repositories.iter().count(), work_dir_paths.len());
@@ -3288,7 +3348,7 @@ impl BackgroundScannerState {
.git_repositories
.retain(|id, _| removed_ids.binary_search(id).is_err());
self.snapshot.repositories.retain(&(), |repository| {
- !repository.work_directory.starts_with(path)
+ !repository.work_directory.path_key().0.starts_with(path)
});
#[cfg(test)]
@@ -3326,20 +3386,26 @@ impl BackgroundScannerState {
}
};
- self.insert_git_repository_for_path(work_dir_path, dot_git_path, None, fs, watcher)
+ self.insert_git_repository_for_path(
+ WorkDirectory::InProject {
+ relative_path: work_dir_path,
+ },
+ dot_git_path,
+ fs,
+ watcher,
+ )
}
fn insert_git_repository_for_path(
&mut self,
- work_dir_path: Arc<Path>,
+ work_directory: WorkDirectory,
dot_git_path: Arc<Path>,
- location_in_repo: Option<Arc<Path>>,
fs: &dyn Fs,
watcher: &dyn Watcher,
) -> Option<LocalRepositoryEntry> {
let work_dir_id = self
.snapshot
- .entry_for_path(work_dir_path.clone())
+ .entry_for_path(work_directory.path_key().0)
.map(|entry| entry.id)?;
if self.snapshot.git_repositories.get(&work_dir_id).is_some() {
@@ -3373,10 +3439,6 @@ impl BackgroundScannerState {
};
log::trace!("constructed libgit2 repo in {:?}", t0.elapsed());
- let work_directory = WorkDirectory {
- path: work_dir_path.clone(),
- location_in_repo,
- };
if let Some(git_hosting_provider_registry) = self.git_hosting_provider_registry.clone() {
git_hosting_providers::register_additional_providers(
@@ -3839,7 +3901,7 @@ impl sum_tree::Item for RepositoryEntry {
fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
PathSummary {
- max_path: self.work_directory.path.clone(),
+ max_path: self.work_directory.path_key().0,
item_summary: Unit,
}
}
@@ -3849,7 +3911,7 @@ impl sum_tree::KeyedItem for RepositoryEntry {
type Key = PathKey;
fn key(&self) -> Self::Key {
- PathKey(self.work_directory.path.clone())
+ self.work_directory.path_key()
}
}
@@ -4088,7 +4150,7 @@ impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId {
}
}
-#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
+#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct PathKey(Arc<Path>);
impl Default for PathKey {
@@ -4167,15 +4229,15 @@ impl BackgroundScanner {
// We associate the external git repo with our root folder and
// also mark where in the git repo the root folder is located.
self.state.lock().insert_git_repository_for_path(
- Path::new("").into(),
- ancestor_dot_git.into(),
- Some(
- root_abs_path
+ WorkDirectory::AboveProject {
+ absolute_path: ancestor.into(),
+ location_in_repo: root_abs_path
.as_path()
.strip_prefix(ancestor)
.unwrap()
.into(),
- ),
+ },
+ ancestor_dot_git.into(),
self.fs.as_ref(),
self.watcher.as_ref(),
);
@@ -4401,6 +4463,14 @@ impl BackgroundScanner {
return false;
};
+ if abs_path.0.file_name() == Some(*GITIGNORE) {
+ for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&relative_path)) {
+ if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.dot_git_dir_abs_path.as_ref()) {
+ dot_git_abs_paths.push(repo.dot_git_dir_abs_path.to_path_buf());
+ }
+ }
+ }
+
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
snapshot
.entry_for_path(parent)
@@ -4984,7 +5054,7 @@ impl BackgroundScanner {
snapshot
.snapshot
.repositories
- .remove(&PathKey(repository.work_directory.path.clone()), &());
+ .remove(&repository.work_directory.path_key(), &());
return Some(());
}
}
@@ -5169,8 +5239,12 @@ impl BackgroundScanner {
let local_repository = match existing_repository_entry {
None => {
+ let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path())
+ else {
+ return;
+ };
match state.insert_git_repository(
- dot_git_dir.into(),
+ relative.into(),
self.fs.as_ref(),
self.watcher.as_ref(),
) {
@@ -5274,7 +5348,7 @@ impl BackgroundScanner {
fn update_git_statuses(&self, job: UpdateGitStatusesJob) {
log::trace!(
"updating git statuses for repo {:?}",
- job.local_repository.work_directory.path
+ job.local_repository.work_directory.display_name()
);
let t0 = Instant::now();
@@ -5288,7 +5362,7 @@ impl BackgroundScanner {
};
log::trace!(
"computed git statuses for repo {:?} in {:?}",
- job.local_repository.work_directory.path,
+ job.local_repository.work_directory.display_name(),
t0.elapsed()
);
@@ -5299,8 +5373,8 @@ impl BackgroundScanner {
let Some(mut repository) =
snapshot.repository(job.local_repository.work_directory.path_key())
else {
- log::error!("Got an UpdateGitStatusesJob for a repository that isn't in the snapshot");
- debug_assert!(false);
+ // happens when a folder is deleted
+ log::debug!("Got an UpdateGitStatusesJob for a repository that isn't in the snapshot");
return;
};
@@ -5352,7 +5426,7 @@ impl BackgroundScanner {
log::trace!(
"applied git status updates for repo {:?} in {:?}",
- job.local_repository.work_directory.path,
+ job.local_repository.work_directory.display_name(),
t0.elapsed(),
);
}
@@ -1,6 +1,6 @@
use crate::{
- worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot, Worktree,
- WorktreeModelHandle,
+ worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot,
+ WorkDirectory, Worktree, WorktreeModelHandle,
};
use anyhow::Result;
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
@@ -2200,7 +2200,10 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
cx.read(|cx| {
let tree = tree.read(cx);
let repo = tree.repositories().iter().next().unwrap();
- assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
+ assert_eq!(
+ repo.work_directory,
+ WorkDirectory::in_project("projects/project1")
+ );
assert_eq!(
tree.status_for_file(Path::new("projects/project1/a")),
Some(StatusCode::Modified.worktree()),
@@ -2221,7 +2224,10 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
cx.read(|cx| {
let tree = tree.read(cx);
let repo = tree.repositories().iter().next().unwrap();
- assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
+ assert_eq!(
+ repo.work_directory,
+ WorkDirectory::in_project("projects/project2")
+ );
assert_eq!(
tree.status_for_file(Path::new("projects/project2/a")),
Some(StatusCode::Modified.worktree()),
@@ -2275,12 +2281,15 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
- assert_eq!(repo.path.as_ref(), Path::new("dir1"));
+ assert_eq!(repo.work_directory, WorkDirectory::in_project("dir1"));
let repo = tree
.repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
.unwrap();
- assert_eq!(repo.path.as_ref(), Path::new("dir1/deps/dep1"));
+ assert_eq!(
+ repo.work_directory,
+ WorkDirectory::in_project("dir1/deps/dep1")
+ );
let entries = tree.files(false, 0);
@@ -2289,7 +2298,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
.map(|(entry, repo)| {
(
entry.path.as_ref(),
- repo.map(|repo| repo.path.to_path_buf()),
+ repo.map(|repo| repo.work_directory.clone()),
)
})
.collect::<Vec<_>>();
@@ -2300,9 +2309,12 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
(Path::new("c.txt"), None),
(
Path::new("dir1/deps/dep1/src/a.txt"),
- Some(Path::new("dir1/deps/dep1").into())
+ Some(WorkDirectory::in_project("dir1/deps/dep1"))
+ ),
+ (
+ Path::new("dir1/src/b.txt"),
+ Some(WorkDirectory::in_project("dir1"))
),
- (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
]
);
});
@@ -2408,8 +2420,10 @@ async fn test_file_status(cx: &mut TestAppContext) {
let snapshot = tree.snapshot();
assert_eq!(snapshot.repositories().iter().count(), 1);
let repo_entry = snapshot.repositories().iter().next().unwrap();
- assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
- assert!(repo_entry.location_in_repo.is_none());
+ assert_eq!(
+ repo_entry.work_directory,
+ WorkDirectory::in_project("project")
+ );
assert_eq!(
snapshot.status_for_file(project_path.join(B_TXT)),
@@ -2760,15 +2774,14 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
let snapshot = tree.snapshot();
assert_eq!(snapshot.repositories().iter().count(), 1);
let repo = snapshot.repositories().iter().next().unwrap();
- // Path is blank because the working directory of
- // the git repository is located at the root of the project
- assert_eq!(repo.path.as_ref(), Path::new(""));
-
- // This is the missing path between the root of the project (sub-folder-2) and its
- // location relative to the root of the repository.
assert_eq!(
- repo.location_in_repo,
- Some(Arc::from(Path::new("sub-folder-1/sub-folder-2")))
+ repo.work_directory.canonicalize(),
+ WorkDirectory::AboveProject {
+ absolute_path: Arc::from(root.path().join("my-repo").canonicalize().unwrap()),
+ location_in_repo: Arc::from(Path::new(util::separator!(
+ "sub-folder-1/sub-folder-2"
+ )))
+ }
);
assert_eq!(snapshot.status_for_file("c.txt"), None);
@@ -39,6 +39,7 @@ collab_ui.workspace = true
collections.workspace = true
command_palette.workspace = true
command_palette_hooks.workspace = true
+component_preview.workspace = true
copilot.workspace = true
db.workspace = true
diagnostics.workspace = true
@@ -54,8 +55,8 @@ file_icons.workspace = true
fs.workspace = true
futures.workspace = true
git.workspace = true
-git_ui.workspace = true
git_hosting_providers.workspace = true
+git_ui.workspace = true
go_to_line.workspace = true
gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] }
gpui_tokio.workspace = true
@@ -126,7 +127,6 @@ url.workspace = true
urlencoding = "2.1.2"
util.workspace = true
uuid.workspace = true
-vcs_menu.workspace = true
vim.workspace = true
vim_mode_setting.workspace = true
welcome.workspace = true
@@ -490,6 +490,7 @@ fn main() {
project_panel::init(Assets, cx);
git_ui::git_panel::init(cx);
outline_panel::init(Assets, cx);
+ component_preview::init(cx);
tasks_ui::init(cx);
snippets_ui::init(cx);
channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
@@ -505,7 +506,6 @@ fn main() {
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
collab_ui::init(&app_state, cx);
git_ui::init(cx);
- vcs_menu::init(cx);
feedback::init(cx);
markdown_preview::init(cx);
welcome::init(cx);
@@ -1217,25 +1217,29 @@ fn show_keymap_migration_notification_if_needed(
if !KeymapFile::should_migrate_keymap(keymap_file) {
return false;
}
- show_app_notification(notification_id, cx, move |cx| {
- cx.new(move |_cx| {
- let message = "A newer version of Zed has simplified several keymaps. Your existing keymaps may be deprecated. You can migrate them by clicking below. A backup will be created in your home directory.";
- let button_text = "Backup and Migrate Keymap";
- MessageNotification::new_from_builder(move |_, _| {
- gpui::div().text_xs().child(message).into_any()
- })
- .primary_message(button_text)
- .primary_on_click(move |_, cx| {
- let fs = <dyn Fs>::global(cx);
- cx.spawn(move |weak_notification, mut cx| async move {
- KeymapFile::migrate_keymap(fs).await.ok();
- weak_notification.update(&mut cx, |_, cx| {
+ let message = MarkdownString(format!(
+ "Keymap migration needed, as the format for some actions has changed. \
+ You can migrate your keymap by clicking below. A backup will be created at {}.",
+ MarkdownString::inline_code(&paths::keymap_backup_file().to_string_lossy())
+ ));
+ show_markdown_app_notification(
+ notification_id,
+ message,
+ "Backup and Migrate Keymap".into(),
+ move |_, cx| {
+ let fs = <dyn Fs>::global(cx);
+ cx.spawn(move |weak_notification, mut cx| async move {
+ KeymapFile::migrate_keymap(fs).await.ok();
+ weak_notification
+ .update(&mut cx, |_, cx| {
cx.emit(DismissEvent);
- }).ok();
- }).detach();
+ })
+ .ok();
})
- })
- });
+ .detach();
+ },
+ cx,
+ );
return true;
}
@@ -1247,33 +1251,55 @@ fn show_settings_migration_notification_if_needed(
if !SettingsStore::should_migrate_settings(&settings) {
return;
}
- show_app_notification(notification_id, cx, move |cx| {
- cx.new(move |_cx| {
- let message = "A newer version of Zed has updated some settings. Your existing settings may be deprecated. You can migrate them by clicking below. A backup will be created in your home directory.";
- let button_text = "Backup and Migrate Settings";
- MessageNotification::new_from_builder(move |_, _| {
- gpui::div().text_xs().child(message).into_any()
- })
- .primary_message(button_text)
- .primary_on_click(move |_, cx| {
- let fs = <dyn Fs>::global(cx);
- cx.update_global(|store: &mut SettingsStore, _| store.migrate_settings(fs));
- cx.emit(DismissEvent);
- })
- })
- });
+ let message = MarkdownString(format!(
+ "Settings migration needed, as the format for some settings has changed. \
+ You can migrate your settings by clicking below. A backup will be created at {}.",
+ MarkdownString::inline_code(&paths::settings_backup_file().to_string_lossy())
+ ));
+ show_markdown_app_notification(
+ notification_id,
+ message,
+ "Backup and Migrate Settings".into(),
+ move |_, cx| {
+ let fs = <dyn Fs>::global(cx);
+ cx.update_global(|store: &mut SettingsStore, _| store.migrate_settings(fs));
+ cx.emit(DismissEvent);
+ },
+ cx,
+ );
}
fn show_keymap_file_load_error(
notification_id: NotificationId,
- markdown_error_message: MarkdownString,
+ error_message: MarkdownString,
cx: &mut App,
) {
+ show_markdown_app_notification(
+ notification_id.clone(),
+ error_message,
+ "Open Keymap File".into(),
+ |window, cx| {
+ window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
+ cx.emit(DismissEvent);
+ },
+ cx,
+ )
+}
+
+fn show_markdown_app_notification<F>(
+ notification_id: NotificationId,
+ message: MarkdownString,
+ primary_button_message: SharedString,
+ primary_button_on_click: F,
+ cx: &mut App,
+) where
+ F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
+{
let parsed_markdown = cx.background_executor().spawn(async move {
let file_location_directory = None;
let language_registry = None;
markdown_preview::markdown_parser::parse_markdown(
- &markdown_error_message.0,
+ &message.0,
file_location_directory,
language_registry,
)
@@ -1282,10 +1308,14 @@ fn show_keymap_file_load_error(
cx.spawn(move |cx| async move {
let parsed_markdown = Arc::new(parsed_markdown.await);
+ let primary_button_message = primary_button_message.clone();
+ let primary_button_on_click = Arc::new(primary_button_on_click);
cx.update(|cx| {
show_app_notification(notification_id, cx, move |cx| {
let workspace_handle = cx.entity().downgrade();
let parsed_markdown = parsed_markdown.clone();
+ let primary_button_message = primary_button_message.clone();
+ let primary_button_on_click = primary_button_on_click.clone();
cx.new(move |_cx| {
MessageNotification::new_from_builder(move |window, cx| {
gpui::div()
@@ -1298,11 +1328,8 @@ fn show_keymap_file_load_error(
))
.into_any()
})
- .primary_message("Open Keymap File")
- .primary_on_click(|window, cx| {
- window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
- cx.emit(DismissEvent);
- })
+ .primary_message(primary_button_message)
+ .primary_on_click_arc(primary_button_on_click)
})
})
})
@@ -168,15 +168,13 @@ impl Render for QuickActionBar {
let focus = editor.focus_handle(cx);
PopoverMenu::new("editor-selections-dropdown")
- .trigger(
+ .trigger_with_tooltip(
IconButton::new("toggle_editor_selections_icon", IconName::CursorIBeam)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
- .toggle_state(self.toggle_selections_handle.is_deployed())
- .when(!self.toggle_selections_handle.is_deployed(), |this| {
- this.tooltip(Tooltip::text("Selection Controls"))
- }),
+ .toggle_state(self.toggle_selections_handle.is_deployed()),
+ Tooltip::text("Selection Controls"),
)
.with_handle(self.toggle_selections_handle.clone())
.anchor(Corner::TopRight)
@@ -213,38 +211,84 @@ impl Render for QuickActionBar {
})
});
+ let editor_focus_handle = editor.focus_handle(cx);
let editor = editor.downgrade();
let editor_settings_dropdown = {
let vim_mode_enabled = VimModeSetting::get_global(cx).0;
PopoverMenu::new("editor-settings")
- .trigger(
+ .trigger_with_tooltip(
IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
- .toggle_state(self.toggle_settings_handle.is_deployed())
- .when(!self.toggle_settings_handle.is_deployed(), |this| {
- this.tooltip(Tooltip::text("Editor Controls"))
- }),
+ .toggle_state(self.toggle_settings_handle.is_deployed()),
+ Tooltip::text("Editor Controls"),
)
.anchor(Corner::TopRight)
.with_handle(self.toggle_settings_handle.clone())
.menu(move |window, cx| {
- let menu = ContextMenu::build(window, cx, |mut menu, _, _| {
- if supports_inlay_hints {
+ let menu = ContextMenu::build(window, cx, {
+ let focus_handle = editor_focus_handle.clone();
+ |mut menu, _, _| {
+ menu = menu.context(focus_handle);
+
+ if supports_inlay_hints {
+ menu = menu.toggleable_entry(
+ "Inlay Hints",
+ inlay_hints_enabled,
+ IconPosition::Start,
+ Some(editor::actions::ToggleInlayHints.boxed_clone()),
+ {
+ let editor = editor.clone();
+ move |window, cx| {
+ editor
+ .update(cx, |editor, cx| {
+ editor.toggle_inlay_hints(
+ &editor::actions::ToggleInlayHints,
+ window,
+ cx,
+ );
+ })
+ .ok();
+ }
+ },
+ );
+ }
+
+ menu = menu.toggleable_entry(
+ "Selection Menu",
+ selection_menu_enabled,
+ IconPosition::Start,
+ Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
+ {
+ let editor = editor.clone();
+ move |window, cx| {
+ editor
+ .update(cx, |editor, cx| {
+ editor.toggle_selection_menu(
+ &editor::actions::ToggleSelectionMenu,
+ window,
+ cx,
+ )
+ })
+ .ok();
+ }
+ },
+ );
+
menu = menu.toggleable_entry(
- "Inlay Hints",
- inlay_hints_enabled,
+ "Auto Signature Help",
+ auto_signature_help_enabled,
IconPosition::Start,
- Some(editor::actions::ToggleInlayHints.boxed_clone()),
+ Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
{
let editor = editor.clone();
move |window, cx| {
editor
.update(cx, |editor, cx| {
- editor.toggle_inlay_hints(
- &editor::actions::ToggleInlayHints,
+ editor.toggle_auto_signature_help_menu(
+ &editor::actions::ToggleAutoSignatureHelp,
window,
cx,
);
@@ -253,138 +297,96 @@ impl Render for QuickActionBar {
}
},
);
- }
- menu = menu.toggleable_entry(
- "Selection Menu",
- selection_menu_enabled,
- IconPosition::Start,
- Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
- {
- let editor = editor.clone();
- move |window, cx| {
- editor
- .update(cx, |editor, cx| {
- editor.toggle_selection_menu(
- &editor::actions::ToggleSelectionMenu,
- window,
- cx,
- )
- })
- .ok();
- }
- },
- );
-
- menu = menu.toggleable_entry(
- "Auto Signature Help",
- auto_signature_help_enabled,
- IconPosition::Start,
- Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
- {
- let editor = editor.clone();
- move |window, cx| {
- editor
- .update(cx, |editor, cx| {
- editor.toggle_auto_signature_help_menu(
- &editor::actions::ToggleAutoSignatureHelp,
- window,
- cx,
- );
- })
- .ok();
- }
- },
- );
-
- let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions")
- .toggleable(IconPosition::Start, inline_completion_enabled && show_inline_completions)
- .disabled(!inline_completion_enabled)
- .action(Some(
- editor::actions::ToggleEditPrediction.boxed_clone(),
- )).handler({
- let editor = editor.clone();
- move |window, cx| {
- editor
- .update(cx, |editor, cx| {
- editor.toggle_inline_completions(
- &editor::actions::ToggleEditPrediction,
- window,
- cx,
- );
- })
- .ok();
- }
- });
- if !inline_completion_enabled {
- inline_completion_entry = inline_completion_entry.documentation_aside(|_| {
- Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element()
- });
- }
+ let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions")
+ .toggleable(IconPosition::Start, inline_completion_enabled && show_inline_completions)
+ .disabled(!inline_completion_enabled)
+ .action(Some(
+ editor::actions::ToggleEditPrediction.boxed_clone(),
+ )).handler({
+ let editor = editor.clone();
+ move |window, cx| {
+ editor
+ .update(cx, |editor, cx| {
+ editor.toggle_inline_completions(
+ &editor::actions::ToggleEditPrediction,
+ window,
+ cx,
+ );
+ })
+ .ok();
+ }
+ });
+ if !inline_completion_enabled {
+ inline_completion_entry = inline_completion_entry.documentation_aside(|_| {
+ Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element()
+ });
+ }
+
+ menu = menu.item(inline_completion_entry);
+
+ menu = menu.separator();
+
+ menu = menu.toggleable_entry(
+ "Inline Git Blame",
+ git_blame_inline_enabled,
+ IconPosition::Start,
+ Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
+ {
+ let editor = editor.clone();
+ move |window, cx| {
+ editor
+ .update(cx, |editor, cx| {
+ editor.toggle_git_blame_inline(
+ &editor::actions::ToggleGitBlameInline,
+ window,
+ cx,
+ )
+ })
+ .ok();
+ }
+ },
+ );
- menu = menu.item(inline_completion_entry);
-
- menu = menu.separator();
-
- menu = menu.toggleable_entry(
- "Inline Git Blame",
- git_blame_inline_enabled,
- IconPosition::Start,
- Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
- {
- let editor = editor.clone();
- move |window, cx| {
- editor
- .update(cx, |editor, cx| {
- editor.toggle_git_blame_inline(
- &editor::actions::ToggleGitBlameInline,
- window,
- cx,
- )
- })
- .ok();
- }
- },
- );
-
- menu = menu.toggleable_entry(
- "Column Git Blame",
- show_git_blame_gutter,
- IconPosition::Start,
- Some(editor::actions::ToggleGitBlame.boxed_clone()),
- {
- let editor = editor.clone();
- move |window, cx| {
- editor
- .update(cx, |editor, cx| {
- editor.toggle_git_blame(
- &editor::actions::ToggleGitBlame,
- window,
- cx,
- )
- })
- .ok();
- }
- },
- );
-
- menu = menu.separator();
-
- menu = menu.toggleable_entry(
- "Vim Mode",
- vim_mode_enabled,
- IconPosition::Start,
- None,
- {
- move |window, cx| {
- let new_value = !vim_mode_enabled;
- VimModeSetting::override_global(VimModeSetting(new_value), cx);
- window.refresh();
- }
- },
- );
-
- menu
+ menu = menu.toggleable_entry(
+ "Column Git Blame",
+ show_git_blame_gutter,
+ IconPosition::Start,
+ Some(editor::actions::ToggleGitBlame.boxed_clone()),
+ {
+ let editor = editor.clone();
+ move |window, cx| {
+ editor
+ .update(cx, |editor, cx| {
+ editor.toggle_git_blame(
+ &editor::actions::ToggleGitBlame,
+ window,
+ cx,
+ )
+ })
+ .ok();
+ }
+ },
+ );
+
+ menu = menu.separator();
+
+ menu = menu.toggleable_entry(
+ "Vim Mode",
+ vim_mode_enabled,
+ IconPosition::Start,
+ None,
+ {
+ move |window, cx| {
+ let new_value = !vim_mode_enabled;
+ VimModeSetting::override_global(VimModeSetting(new_value), cx);
+ window.refresh();
+ }
+ },
+ );
+
+ menu
+ }
});
Some(menu)
})
@@ -209,16 +209,16 @@ impl QuickActionBar {
})
.into()
})
- .trigger(
+ .trigger_with_tooltip(
ButtonLike::new_rounded_right(element_id("dropdown"))
.child(
Icon::new(IconName::ChevronDownSmall)
.size(IconSize::XSmall)
.color(Color::Muted),
)
- .tooltip(Tooltip::text("REPL Menu"))
.width(rems(1.).into())
.disabled(menu_state.popover_disabled),
+ Tooltip::text("REPL Menu"),
);
let button = ButtonLike::new_rounded_left("toggle_repl_icon")
@@ -343,8 +343,8 @@ impl QuickActionBar {
.color(Color::Muted)
.size(IconSize::XSmall),
),
- )
- .tooltip(Tooltip::text("Select Kernel")),
+ ),
+ Tooltip::text("Select Kernel"),
)
.with_handle(menu_handle.clone())
.into_any_element()
@@ -47,10 +47,10 @@ actions!(
]
);
-pub mod branches {
- use gpui::actions;
+pub mod git {
+ use gpui::action_with_deprecated_aliases;
- actions!(branches, [OpenRecent]);
+ action_with_deprecated_aliases!(git, Branch, ["branches::OpenRecent"]);
}
pub mod command_palette {
@@ -0,0 +1,238 @@
+use crate::{
+ tokens_for_bytes, CURSOR_MARKER, EDITABLE_REGION_END_MARKER, EDITABLE_REGION_START_MARKER,
+ START_OF_FILE_MARKER,
+};
+use language::{BufferSnapshot, Point};
+use std::{fmt::Write, ops::Range};
+
+#[derive(Debug)]
+pub struct InputExcerpt {
+ pub editable_range: Range<Point>,
+ pub prompt: String,
+ pub speculated_output: String,
+}
+
+pub fn excerpt_for_cursor_position(
+ position: Point,
+ path: &str,
+ snapshot: &BufferSnapshot,
+ editable_region_token_limit: usize,
+ context_token_limit: usize,
+) -> InputExcerpt {
+ let mut scope_range = position..position;
+ let mut remaining_edit_tokens = editable_region_token_limit;
+
+ while let Some(parent) = snapshot.syntax_ancestor(scope_range.clone()) {
+ let parent_tokens = tokens_for_bytes(parent.byte_range().len());
+ let parent_point_range = Point::new(
+ parent.start_position().row as u32,
+ parent.start_position().column as u32,
+ )
+ ..Point::new(
+ parent.end_position().row as u32,
+ parent.end_position().column as u32,
+ );
+ if parent_point_range == scope_range {
+ break;
+ } else if parent_tokens <= editable_region_token_limit {
+ scope_range = parent_point_range;
+ remaining_edit_tokens = editable_region_token_limit - parent_tokens;
+ } else {
+ break;
+ }
+ }
+
+ let editable_range = expand_range(snapshot, scope_range, remaining_edit_tokens);
+ let context_range = expand_range(snapshot, editable_range.clone(), context_token_limit);
+
+ let mut prompt = String::new();
+ let mut speculated_output = String::new();
+
+ writeln!(&mut prompt, "```{path}").unwrap();
+ if context_range.start == Point::zero() {
+ writeln!(&mut prompt, "{START_OF_FILE_MARKER}").unwrap();
+ }
+
+ for chunk in snapshot.chunks(context_range.start..editable_range.start, false) {
+ prompt.push_str(chunk.text);
+ }
+
+ push_editable_range(position, snapshot, editable_range.clone(), &mut prompt);
+ push_editable_range(
+ position,
+ snapshot,
+ editable_range.clone(),
+ &mut speculated_output,
+ );
+
+ for chunk in snapshot.chunks(editable_range.end..context_range.end, false) {
+ prompt.push_str(chunk.text);
+ }
+ write!(prompt, "\n```").unwrap();
+
+ InputExcerpt {
+ editable_range,
+ prompt,
+ speculated_output,
+ }
+}
+
+fn push_editable_range(
+ cursor_position: Point,
+ snapshot: &BufferSnapshot,
+ editable_range: Range<Point>,
+ prompt: &mut String,
+) {
+ writeln!(prompt, "{EDITABLE_REGION_START_MARKER}").unwrap();
+ for chunk in snapshot.chunks(editable_range.start..cursor_position, false) {
+ prompt.push_str(chunk.text);
+ }
+ prompt.push_str(CURSOR_MARKER);
+ for chunk in snapshot.chunks(cursor_position..editable_range.end, false) {
+ prompt.push_str(chunk.text);
+ }
+ write!(prompt, "\n{EDITABLE_REGION_END_MARKER}").unwrap();
+}
+
+fn expand_range(
+ snapshot: &BufferSnapshot,
+ range: Range<Point>,
+ mut remaining_tokens: usize,
+) -> Range<Point> {
+ let mut expanded_range = range.clone();
+ expanded_range.start.column = 0;
+ expanded_range.end.column = snapshot.line_len(expanded_range.end.row);
+ loop {
+ let mut expanded = false;
+
+ if remaining_tokens > 0 && expanded_range.start.row > 0 {
+ expanded_range.start.row -= 1;
+ let line_tokens =
+ tokens_for_bytes(snapshot.line_len(expanded_range.start.row) as usize);
+ remaining_tokens = remaining_tokens.saturating_sub(line_tokens);
+ expanded = true;
+ }
+
+ if remaining_tokens > 0 && expanded_range.end.row < snapshot.max_point().row {
+ expanded_range.end.row += 1;
+ expanded_range.end.column = snapshot.line_len(expanded_range.end.row);
+ let line_tokens = tokens_for_bytes(expanded_range.end.column as usize);
+ remaining_tokens = remaining_tokens.saturating_sub(line_tokens);
+ expanded = true;
+ }
+
+ if !expanded {
+ break;
+ }
+ }
+ expanded_range
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::{App, AppContext};
+ use indoc::indoc;
+ use language::{Buffer, Language, LanguageConfig, LanguageMatcher};
+ use std::sync::Arc;
+
+ #[gpui::test]
+ fn test_excerpt_for_cursor_position(cx: &mut App) {
+ let text = indoc! {r#"
+ fn foo() {
+ let x = 42;
+ println!("Hello, world!");
+ }
+
+ fn bar() {
+ let x = 42;
+ let mut sum = 0;
+ for i in 0..x {
+ sum += i;
+ }
+ println!("Sum: {}", sum);
+ return sum;
+ }
+
+ fn generate_random_numbers() -> Vec<i32> {
+ let mut rng = rand::thread_rng();
+ let mut numbers = Vec::new();
+ for _ in 0..5 {
+ numbers.push(rng.gen_range(1..101));
+ }
+ numbers
+ }
+ "#};
+ let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
+ let snapshot = buffer.read(cx).snapshot();
+
+ // Ensure we try to fit the largest possible syntax scope, resorting to line-based expansion
+ // when a larger scope doesn't fit the editable region.
+ let excerpt = excerpt_for_cursor_position(Point::new(12, 5), "main.rs", &snapshot, 50, 32);
+ assert_eq!(
+ excerpt.prompt,
+ indoc! {r#"
+ ```main.rs
+ let x = 42;
+ println!("Hello, world!");
+ <|editable_region_start|>
+ }
+
+ fn bar() {
+ let x = 42;
+ let mut sum = 0;
+ for i in 0..x {
+ sum += i;
+ }
+ println!("Sum: {}", sum);
+ r<|user_cursor_is_here|>eturn sum;
+ }
+
+ fn generate_random_numbers() -> Vec<i32> {
+ <|editable_region_end|>
+ let mut rng = rand::thread_rng();
+ let mut numbers = Vec::new();
+ ```"#}
+ );
+
+ // The `bar` function won't fit within the editable region, so we resort to line-based expansion.
+ let excerpt = excerpt_for_cursor_position(Point::new(12, 5), "main.rs", &snapshot, 40, 32);
+ assert_eq!(
+ excerpt.prompt,
+ indoc! {r#"
+ ```main.rs
+ fn bar() {
+ let x = 42;
+ let mut sum = 0;
+ <|editable_region_start|>
+ for i in 0..x {
+ sum += i;
+ }
+ println!("Sum: {}", sum);
+ r<|user_cursor_is_here|>eturn sum;
+ }
+
+ fn generate_random_numbers() -> Vec<i32> {
+ let mut rng = rand::thread_rng();
+ <|editable_region_end|>
+ let mut numbers = Vec::new();
+ for _ in 0..5 {
+ numbers.push(rng.gen_range(1..101));
+ ```"#}
+ );
+ }
+
+ fn rust_lang() -> Language {
+ Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::LANGUAGE.into()),
+ )
+ }
+}
@@ -1,5 +1,6 @@
mod completion_diff_element;
mod init;
+mod input_excerpt;
mod license_detection;
mod onboarding_banner;
mod onboarding_modal;
@@ -25,9 +26,8 @@ use gpui::{
actions, App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, Subscription, Task,
};
use http_client::{HttpClient, Method};
-use language::{
- Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, Point, ToOffset, ToPoint,
-};
+use input_excerpt::excerpt_for_cursor_position;
+use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToOffset, ToPoint};
use language_models::LlmApiToken;
use postage::watch;
use project::Project;
@@ -57,38 +57,13 @@ const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>";
const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1);
const ZED_PREDICT_DATA_COLLECTION_CHOICE: &str = "zed_predict_data_collection_choice";
-// TODO(mgsloan): more systematic way to choose or tune these fairly arbitrary constants?
-
-/// Typical number of string bytes per token for the purposes of limiting model input. This is
-/// intentionally low to err on the side of underestimating limits.
-const BYTES_PER_TOKEN_GUESS: usize = 3;
-
-/// Output token limit, used to inform the size of the input. A copy of this constant is also in
-/// `crates/collab/src/llm.rs`.
-const MAX_OUTPUT_TOKENS: usize = 2048;
-
-/// Total bytes limit for editable region of buffer excerpt.
-///
-/// The number of output tokens is relevant to the size of the input excerpt because the model is
-/// tasked with outputting a modified excerpt. `2/3` is chosen so that there are some output tokens
-/// remaining for the model to specify insertions.
-const BUFFER_EXCERPT_BYTE_LIMIT: usize = (MAX_OUTPUT_TOKENS * 2 / 3) * BYTES_PER_TOKEN_GUESS;
+const MAX_CONTEXT_TOKENS: usize = 100;
+const MAX_REWRITE_TOKENS: usize = 300;
+const MAX_EVENT_TOKENS: usize = 400;
-/// Total line limit for editable region of buffer excerpt.
-const BUFFER_EXCERPT_LINE_LIMIT: u32 = 64;
-
-/// Note that this is not the limit for the overall prompt, just for the inputs to the template
-/// instantiated in `crates/collab/src/llm.rs`.
-const TOTAL_BYTE_LIMIT: usize = BUFFER_EXCERPT_BYTE_LIMIT * 2;
-
-/// Maximum number of events to include in the prompt.
+/// Maximum number of events to track.
const MAX_EVENT_COUNT: usize = 16;
-/// Maximum number of string bytes in a single event. Arbitrarily choosing this to be 4x the size of
-/// equally splitting up the the remaining bytes after the largest possible buffer excerpt.
-const PER_EVENT_BYTE_LIMIT: usize =
- (TOTAL_BYTE_LIMIT - BUFFER_EXCERPT_BYTE_LIMIT) / MAX_EVENT_COUNT * 4;
-
actions!(edit_prediction, [ClearHistory]);
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
@@ -106,12 +81,6 @@ impl std::fmt::Display for InlineCompletionId {
}
}
-impl InlineCompletionId {
- fn new() -> Self {
- Self(Uuid::new_v4())
- }
-}
-
#[derive(Clone)]
struct ZetaGlobal(Entity<Zeta>);
@@ -214,7 +183,8 @@ pub struct Zeta {
data_collection_choice: Entity<DataCollectionChoice>,
llm_token: LlmApiToken,
_llm_token_subscription: Subscription,
- tos_accepted: bool, // Terms of service accepted
+ /// Whether the terms of service have been accepted.
+ tos_accepted: bool,
_user_store_subscription: Subscription,
license_detection_watchers: HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
}
@@ -418,7 +388,8 @@ impl Zeta {
struct BackgroundValues {
input_events: String,
input_excerpt: String,
- excerpt_range: Range<usize>,
+ speculated_output: String,
+ editable_range: Range<usize>,
input_outline: String,
}
@@ -429,32 +400,21 @@ impl Zeta {
let path = path.clone();
async move {
let path = path.to_string_lossy();
- let (excerpt_range, excerpt_len_guess) = excerpt_range_for_position(
+ let input_excerpt = excerpt_for_cursor_position(
cursor_point,
- BUFFER_EXCERPT_BYTE_LIMIT,
- BUFFER_EXCERPT_LINE_LIMIT,
- &path,
- &snapshot,
- )?;
- let input_excerpt = prompt_for_excerpt(
- cursor_offset,
- &excerpt_range,
- excerpt_len_guess,
&path,
&snapshot,
+ MAX_REWRITE_TOKENS,
+ MAX_CONTEXT_TOKENS,
);
-
- let bytes_remaining = TOTAL_BYTE_LIMIT.saturating_sub(input_excerpt.len());
- let input_events = prompt_for_events(events.iter(), bytes_remaining);
-
- // Note that input_outline is not currently used in prompt generation and so
- // is not counted towards TOTAL_BYTE_LIMIT.
+ let input_events = prompt_for_events(&events, MAX_EVENT_TOKENS);
let input_outline = prompt_for_outline(&snapshot);
anyhow::Ok(BackgroundValues {
input_events,
- input_excerpt,
- excerpt_range,
+ input_excerpt: input_excerpt.prompt,
+ speculated_output: input_excerpt.speculated_output,
+ editable_range: input_excerpt.editable_range.to_offset(&snapshot),
input_outline,
})
}
@@ -462,7 +422,7 @@ impl Zeta {
.await?;
log::debug!(
- "Events:\n{}\nExcerpt:\n{}",
+ "Events:\n{}\nExcerpt:\n{:?}",
values.input_events,
values.input_excerpt
);
@@ -470,6 +430,7 @@ impl Zeta {
let body = PredictEditsBody {
input_events: values.input_events.clone(),
input_excerpt: values.input_excerpt.clone(),
+ speculated_output: Some(values.speculated_output),
outline: Some(values.input_outline.clone()),
can_collect_data,
diagnostic_groups: diagnostic_groups.and_then(|diagnostic_groups| {
@@ -485,14 +446,13 @@ impl Zeta {
let response = perform_predict_edits(client, llm_token, is_staff, body).await?;
- let output_excerpt = response.output_excerpt;
- log::debug!("completion response: {}", output_excerpt);
+ log::debug!("completion response: {}", &response.output_excerpt);
Self::process_completion_response(
- output_excerpt,
+ response,
buffer,
&snapshot,
- values.excerpt_range,
+ values.editable_range,
cursor_offset,
path,
values.input_outline,
@@ -508,6 +468,8 @@ impl Zeta {
// Generates several example completions of various states to fill the Zeta completion modal
#[cfg(any(test, feature = "test-support"))]
pub fn fill_with_fake_completions(&mut self, cx: &mut Context<Self>) -> Task<()> {
+ use language::Point;
+
let test_buffer_text = indoc::indoc! {r#"a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
@@ -526,6 +488,7 @@ impl Zeta {
&buffer,
position,
PredictEditsResponse {
+ request_id: Uuid::parse_str("e7861db5-0cea-4761-b1c5-ad083ac53a80").unwrap(),
output_excerpt: format!("{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
[here's an edit]
@@ -542,6 +505,7 @@ and then another
&buffer,
position,
PredictEditsResponse {
+ request_id: Uuid::parse_str("077c556a-2c49-44e2-bbc6-dafc09032a5e").unwrap(),
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
@@ -558,6 +522,7 @@ and then another
&buffer,
position,
PredictEditsResponse {
+ request_id: Uuid::parse_str("df8c7b23-3d1d-4f99-a306-1f6264a41277").unwrap(),
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
@@ -575,6 +540,7 @@ and then another
&buffer,
position,
PredictEditsResponse {
+ request_id: Uuid::parse_str("c743958d-e4d8-44a8-aa5b-eb1e305c5f5c").unwrap(),
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
@@ -592,6 +558,7 @@ and then another
&buffer,
position,
PredictEditsResponse {
+ request_id: Uuid::parse_str("ff5cd7ab-ad06-4808-986e-d3391e7b8355").unwrap(),
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
@@ -608,6 +575,7 @@ and then another
&buffer,
position,
PredictEditsResponse {
+ request_id: Uuid::parse_str("83cafa55-cdba-4b27-8474-1865ea06be94").unwrap(),
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
@@ -623,6 +591,7 @@ and then another
&buffer,
position,
PredictEditsResponse {
+ request_id: Uuid::parse_str("d5bd3afd-8723-47c7-bd77-15a3a926867b").unwrap(),
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
@@ -697,7 +666,7 @@ and then another
loop {
let request_builder = http_client::Request::builder().method(Method::POST).uri(
http_client
- .build_zed_llm_url("/predict_edits", &[])?
+ .build_zed_llm_url("/predict_edits/v2", &[])?
.as_ref(),
);
let request = request_builder
@@ -734,10 +703,10 @@ and then another
#[allow(clippy::too_many_arguments)]
fn process_completion_response(
- output_excerpt: String,
+ prediction_response: PredictEditsResponse,
buffer: Entity<Buffer>,
snapshot: &BufferSnapshot,
- excerpt_range: Range<usize>,
+ editable_range: Range<usize>,
cursor_offset: usize,
path: Arc<Path>,
input_outline: String,
@@ -747,6 +716,8 @@ and then another
cx: &AsyncApp,
) -> Task<Result<Option<InlineCompletion>>> {
let snapshot = snapshot.clone();
+ let request_id = prediction_response.request_id;
+ let output_excerpt = prediction_response.output_excerpt;
cx.spawn(|cx| async move {
let output_excerpt: Arc<str> = output_excerpt.into();
@@ -754,9 +725,9 @@ and then another
.background_executor()
.spawn({
let output_excerpt = output_excerpt.clone();
- let excerpt_range = excerpt_range.clone();
+ let editable_range = editable_range.clone();
let snapshot = snapshot.clone();
- async move { Self::parse_edits(output_excerpt, excerpt_range, &snapshot) }
+ async move { Self::parse_edits(output_excerpt, editable_range, &snapshot) }
})
.await?
.into();
@@ -777,9 +748,9 @@ and then another
let edit_preview = edit_preview.await;
Ok(Some(InlineCompletion {
- id: InlineCompletionId::new(),
+ id: InlineCompletionId(request_id),
path,
- excerpt_range,
+ excerpt_range: editable_range,
cursor_offset,
edits,
edit_preview,
@@ -796,7 +767,7 @@ and then another
fn parse_edits(
output_excerpt: Arc<str>,
- excerpt_range: Range<usize>,
+ editable_range: Range<usize>,
snapshot: &BufferSnapshot,
) -> Result<Vec<(Range<Anchor>, String)>> {
let content = output_excerpt.replace(CURSOR_MARKER, "");
@@ -840,13 +811,13 @@ and then another
let new_text = &content[..codefence_end];
let old_text = snapshot
- .text_for_range(excerpt_range.clone())
+ .text_for_range(editable_range.clone())
.collect::<String>();
Ok(Self::compute_edits(
old_text,
new_text,
- excerpt_range.start,
+ editable_range.start,
&snapshot,
))
}
@@ -1080,9 +1051,7 @@ fn prompt_for_outline(snapshot: &BufferSnapshot) -> String {
.unwrap();
if let Some(outline) = snapshot.outline(None) {
- let guess_size = outline.items.len() * 15;
- input_outline.reserve(guess_size);
- for item in outline.items.iter() {
+ for item in &outline.items {
let spacing = " ".repeat(item.depth);
writeln!(input_outline, "{}{}", spacing, item.text).unwrap();
}
@@ -1093,181 +1062,20 @@ fn prompt_for_outline(snapshot: &BufferSnapshot) -> String {
input_outline
}
-fn prompt_for_excerpt(
- offset: usize,
- excerpt_range: &Range<usize>,
- mut len_guess: usize,
- path: &str,
- snapshot: &BufferSnapshot,
-) -> String {
- let point_range = excerpt_range.to_point(snapshot);
-
- // Include one line of extra context before and after editable range, if those lines are non-empty.
- let extra_context_before_range =
- if point_range.start.row > 0 && !snapshot.is_line_blank(point_range.start.row - 1) {
- let range =
- (Point::new(point_range.start.row - 1, 0)..point_range.start).to_offset(snapshot);
- len_guess += range.end - range.start;
- Some(range)
- } else {
- None
- };
- let extra_context_after_range = if point_range.end.row < snapshot.max_point().row
- && !snapshot.is_line_blank(point_range.end.row + 1)
- {
- let range = (point_range.end
- ..Point::new(
- point_range.end.row + 1,
- snapshot.line_len(point_range.end.row + 1),
- ))
- .to_offset(snapshot);
- len_guess += range.end - range.start;
- Some(range)
- } else {
- None
- };
-
- let mut prompt_excerpt = String::with_capacity(len_guess);
- writeln!(prompt_excerpt, "```{}", path).unwrap();
-
- if excerpt_range.start == 0 {
- writeln!(prompt_excerpt, "{START_OF_FILE_MARKER}").unwrap();
- }
-
- if let Some(extra_context_before_range) = extra_context_before_range {
- for chunk in snapshot.text_for_range(extra_context_before_range) {
- prompt_excerpt.push_str(chunk);
- }
- }
- writeln!(prompt_excerpt, "{EDITABLE_REGION_START_MARKER}").unwrap();
- for chunk in snapshot.text_for_range(excerpt_range.start..offset) {
- prompt_excerpt.push_str(chunk);
- }
- prompt_excerpt.push_str(CURSOR_MARKER);
- for chunk in snapshot.text_for_range(offset..excerpt_range.end) {
- prompt_excerpt.push_str(chunk);
- }
- write!(prompt_excerpt, "\n{EDITABLE_REGION_END_MARKER}").unwrap();
-
- if let Some(extra_context_after_range) = extra_context_after_range {
- for chunk in snapshot.text_for_range(extra_context_after_range) {
- prompt_excerpt.push_str(chunk);
- }
- }
-
- write!(prompt_excerpt, "\n```").unwrap();
- debug_assert!(
- prompt_excerpt.len() <= len_guess,
- "Excerpt length {} exceeds estimated length {}",
- prompt_excerpt.len(),
- len_guess
- );
- prompt_excerpt
-}
-
-fn excerpt_range_for_position(
- cursor_point: Point,
- byte_limit: usize,
- line_limit: u32,
- path: &str,
- snapshot: &BufferSnapshot,
-) -> Result<(Range<usize>, usize)> {
- let cursor_row = cursor_point.row;
- let last_buffer_row = snapshot.max_point().row;
-
- // This is an overestimate because it includes parts of prompt_for_excerpt which are
- // conditionally skipped.
- let mut len_guess = 0;
- len_guess += "```".len() + path.len() + 1;
- len_guess += START_OF_FILE_MARKER.len() + 1;
- len_guess += EDITABLE_REGION_START_MARKER.len() + 1;
- len_guess += CURSOR_MARKER.len();
- len_guess += EDITABLE_REGION_END_MARKER.len() + 1;
- len_guess += "```".len() + 1;
-
- len_guess += usize::try_from(snapshot.line_len(cursor_row) + 1).unwrap();
-
- if len_guess > byte_limit {
- return Err(anyhow!("Current line too long to send to model."));
- }
-
- let mut excerpt_start_row = cursor_row;
- let mut excerpt_end_row = cursor_row;
- let mut no_more_before = cursor_row == 0;
- let mut no_more_after = cursor_row >= last_buffer_row;
- let mut row_delta = 1;
- loop {
- if !no_more_before {
- let row = cursor_point.row - row_delta;
- let line_len: usize = usize::try_from(snapshot.line_len(row) + 1).unwrap();
- let mut new_len_guess = len_guess + line_len;
- if row == 0 {
- new_len_guess += START_OF_FILE_MARKER.len() + 1;
- }
- if new_len_guess <= byte_limit {
- len_guess = new_len_guess;
- excerpt_start_row = row;
- if row == 0 {
- no_more_before = true;
- }
- } else {
- no_more_before = true;
- }
- }
- if excerpt_end_row - excerpt_start_row >= line_limit {
- break;
- }
- if !no_more_after {
- let row = cursor_point.row + row_delta;
- let line_len: usize = usize::try_from(snapshot.line_len(row) + 1).unwrap();
- let new_len_guess = len_guess + line_len;
- if new_len_guess <= byte_limit {
- len_guess = new_len_guess;
- excerpt_end_row = row;
- if row >= last_buffer_row {
- no_more_after = true;
- }
- } else {
- no_more_after = true;
- }
- }
- if excerpt_end_row - excerpt_start_row >= line_limit {
- break;
- }
- if no_more_before && no_more_after {
+fn prompt_for_events(events: &VecDeque<Event>, mut remaining_tokens: usize) -> String {
+ let mut result = String::new();
+ for event in events.iter().rev() {
+ let event_string = event.to_prompt();
+ let event_tokens = tokens_for_bytes(event_string.len());
+ if event_tokens > remaining_tokens {
break;
}
- row_delta += 1;
- }
-
- let excerpt_start = Point::new(excerpt_start_row, 0);
- let excerpt_end = Point::new(excerpt_end_row, snapshot.line_len(excerpt_end_row));
- Ok((
- excerpt_start.to_offset(snapshot)..excerpt_end.to_offset(snapshot),
- len_guess,
- ))
-}
-fn prompt_for_events<'a>(
- events: impl Iterator<Item = &'a Event>,
- mut bytes_remaining: usize,
-) -> String {
- let mut result = String::new();
- for event in events {
if !result.is_empty() {
- result.push('\n');
- result.push('\n');
- }
- let event_string = event.to_prompt();
- let len = event_string.len();
- if len > PER_EVENT_BYTE_LIMIT {
- continue;
+ result.insert_str(0, "\n\n");
}
- if len > bytes_remaining {
- break;
- }
- bytes_remaining -= len;
- result.push_str(&event_string);
+ result.insert_str(0, &event_string);
+ remaining_tokens -= event_tokens;
}
result
}
@@ -1744,12 +1552,20 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider
}
Some(inline_completion::InlineCompletion {
+ id: Some(completion.id.to_string().into()),
edits: edits[edit_start_ix..edit_end_ix].to_vec(),
edit_preview: Some(completion.edit_preview.clone()),
})
}
}
+fn tokens_for_bytes(bytes: usize) -> usize {
+ /// Typical number of string bytes per token for the purposes of limiting model input. This is
+ /// intentionally low to err on the side of underestimating limits.
+ const BYTES_PER_TOKEN_GUESS: usize = 3;
+ bytes / BYTES_PER_TOKEN_GUESS
+}
+
#[cfg(test)]
mod tests {
use client::test::FakeServer;
@@ -1757,6 +1573,7 @@ mod tests {
use gpui::TestAppContext;
use http_client::FakeHttpClient;
use indoc::indoc;
+ use language::Point;
use language_models::RefreshLlmTokenListener;
use rpc::proto;
use settings::SettingsStore;
@@ -1784,7 +1601,7 @@ mod tests {
edit_preview,
path: Path::new("").into(),
snapshot: cx.read(|cx| buffer.read(cx).snapshot()),
- id: InlineCompletionId::new(),
+ id: InlineCompletionId(Uuid::new_v4()),
excerpt_range: 0..0,
cursor_offset: 0,
input_outline: "".into(),
@@ -1903,6 +1720,8 @@ mod tests {
.status(200)
.body(
serde_json::to_string(&PredictEditsResponse {
+ request_id: Uuid::parse_str("7e86480f-3536-4d2c-9334-8213e3445d45")
+ .unwrap(),
output_excerpt: completion_response.to_string(),
})
.unwrap()
@@ -30,7 +30,7 @@ Here is an example of the structure of an icon theme:
"collapsed": "./icons/folder.svg",
"expanded": "./icons/folder-open.svg"
},
- "chevon_icons": {
+ "chevron_icons": {
"collapsed": "./icons/chevron-right.svg",
"expanded": "./icons/chevron-down.svg"
},
@@ -7,7 +7,7 @@ Elm support is available through the [Elm extension](https://github.com/zed-exte
## Setup
-Zed support for Elm requires installation of `elm`, `elm-format`, `elm-review` and `elm`.
+Zed support for Elm requires installation of `elm`, `elm-format`, and `elm-review`.
1. [Install Elm](https://guide.elm-lang.org/install/elm.html) (or run `brew install elm` on macOS).
2. Install `elm-review` to support code linting:
@@ -79,6 +79,12 @@ h6 code {
display: none !important;
}
+h2 {
+ padding-bottom: 1rem;
+ border-bottom: 1px solid;
+ border-color: var(--border-light);
+}
+
h2,
h3 {
margin-block-start: 1.5em;
@@ -13,8 +13,9 @@
--menu-bar-height: 64px;
--font: "IA Writer Quattro S", sans-serif;
--title-font: "Lora", "Helvetica Neue", Helvetica, Arial, sans-serif;
- --mono-font: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
- Liberation Mono, Courier New, monospace;
+ --mono-font:
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono,
+ Courier New, monospace;
--code-font-size: 0.875em
/* please adjust the ace font size accordingly in editor.js */;
@@ -97,7 +98,7 @@
--title-color: hsl(220, 92%, 80%);
--border: hsl(220, 13%, 20%);
- --border-light: hsl(220, 13%, 90%);
+ --border-light: hsl(220, 13%, 15%);
--border-hover: hsl(220, 13%, 40%);
--media-bg: hsl(220, 13%, 8%);
@@ -1,3 +1,3 @@
-target
*.wasm
grammars
+target
@@ -1 +1,4 @@
("{" @open "}" @close)
+("(" @open ")" @close)
+("[" @open "]" @close)
+("\"" @open "\"" @close)