Compare commits

..

5 Commits

Author SHA1 Message Date
Anthony
0bcb4bd400 Calculate scroll position based on middle item 2025-10-07 16:58:03 -04:00
Anthony
98f26b2b43 Some code clean up 2025-10-07 16:37:02 -04:00
Anthony
9b3bb0b4aa Calculate selected navbar entry on each frame based on scrollbar pos
This fixes a problem where moving the scrollbar manually wouldn't select
the correct sub entry in the navbar or update the navbar at all
2025-10-07 16:35:15 -04:00
Anthony
5b85cdcf3c Add a scroll to top of item function to scroll handle
This enables settings UI to scroll to the top of a selected header
instead of just when the header is visible
2025-10-07 16:20:40 -04:00
Anthony
557199052e Improve nav bar navigation 2025-10-07 16:00:50 -04:00
77 changed files with 4022 additions and 4827 deletions

12
Cargo.lock generated
View File

@@ -8779,6 +8779,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"settings",
"shlex",
"smol",
"task",
"text",
@@ -10535,15 +10536,20 @@ dependencies = [
name = "onboarding"
version = "0.1.0"
dependencies = [
"ai_onboarding",
"anyhow",
"client",
"component",
"db",
"documented",
"editor",
"fs",
"fuzzy",
"git",
"gpui",
"itertools 0.14.0",
"language",
"language_model",
"menu",
"notifications",
"picker",
@@ -12007,6 +12013,7 @@ dependencies = [
"dap_adapters",
"extension",
"fancy-regex 0.14.0",
"feature_flags",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -14356,15 +14363,14 @@ dependencies = [
"anyhow",
"assets",
"client",
"command_palette_hooks",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"fuzzy",
"gpui",
"heck 0.5.0",
"language",
"log",
"menu",
"node_runtime",
"paths",
@@ -19977,7 +19983,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.208.2"
version = "0.208.0"
dependencies = [
"acp_tools",
"activity_indicator",

View File

@@ -476,6 +476,7 @@ bitflags = "2.6.0"
blade-graphics = { version = "0.7.0" }
blade-macros = { version = "0.3.0" }
blade-util = { version = "0.3.0" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.21"

View File

@@ -1229,6 +1229,9 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
@@ -1246,8 +1249,6 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-w": "workspace::CloseWindow",
"escape": "workspace::CloseWindow",
"ctrl-m": "settings_editor::Minimize",
"ctrl-f": "search::FocusSearch",
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
@@ -1264,17 +1265,5 @@
"ctrl-pageup": "settings_editor::FocusPreviousFile",
"ctrl-pagedown": "settings_editor::FocusNextFile"
}
},
{
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",
"pagedown": "settings_editor::FocusNextRootNavEntry",
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
}
]

View File

@@ -1334,7 +1334,10 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "onboarding::Finish",
"cmd-1": "onboarding::ActivateBasicsPage",
"cmd-2": "onboarding::ActivateEditingPage",
"cmd-3": "onboarding::ActivateAISetupPage",
"cmd-escape": "onboarding::Finish",
"alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
@@ -1351,8 +1354,6 @@
"use_key_equivalents": true,
"bindings": {
"cmd-w": "workspace::CloseWindow",
"escape": "workspace::CloseWindow",
"cmd-m": "settings_editor::Minimize",
"cmd-f": "search::FocusSearch",
"cmd-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
@@ -1369,17 +1370,5 @@
"cmd-{": "settings_editor::FocusPreviousFile",
"cmd-}": "settings_editor::FocusNextFile"
}
},
{
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",
"pagedown": "settings_editor::FocusNextRootNavEntry",
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
}
]

View File

@@ -1257,6 +1257,9 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn",
"shift-alt-a": "onboarding::OpenAccount"
@@ -1267,8 +1270,6 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-w": "workspace::CloseWindow",
"escape": "workspace::CloseWindow",
"ctrl-m": "settings_editor::Minimize",
"ctrl-f": "search::FocusSearch",
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
@@ -1285,17 +1286,5 @@
"ctrl-pageup": "settings_editor::FocusPreviousFile",
"ctrl-pagedown": "settings_editor::FocusNextFile"
}
},
{
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",
"pagedown": "settings_editor::FocusNextRootNavEntry",
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
}
]

View File

@@ -76,7 +76,7 @@
"ui_font_size": 16,
// The default font size for agent responses in the agent panel. Falls back to the UI font size if unset.
"agent_ui_font_size": null,
// The default font size for user messages in the agent panel.
// The default font size for user messages in the agent panel. Falls back to the buffer font size if unset.
"agent_buffer_font_size": 12,
// How much to fade out unused code.
"unnecessary_code_fade": 0.3,
@@ -1233,8 +1233,8 @@
"git_gutter": "tracked_files",
/// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
///
/// Default: 0
"gutter_debounce": 0,
/// Default: null
"gutter_debounce": null,
// Control whether the git blame information is shown inline,
// in the currently focused line.
"inline_blame": {
@@ -1401,8 +1401,8 @@
// 4. A box drawn around the following character
// "hollow"
//
// Default: "block"
"cursor_shape": "block",
// Default: not set, defaults to "block"
"cursor_shape": null,
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
@@ -1424,8 +1424,8 @@
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Whether to keep the text selection after copying it to the clipboard.
"keep_selection_on_copy": true,
// Whether to keep the text selection after copying it to the clipboard
"keep_selection_on_copy": false,
// Whether to show the terminal button in the status bar
"button": true,
// Any key-value pairs added to this list will be added to the terminal's

View File

@@ -1418,6 +1418,7 @@ mod tests {
}
#[gpui::test]
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_save_load_thread(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
@@ -1497,8 +1498,7 @@ mod tests {
model.send_last_completion_stream_text_chunk("Lorem.");
model.end_last_completion_stream();
cx.run_until_parked();
summary_model
.send_last_completion_stream_text_chunk(&format!("Explaining {}", path!("/a/b.md")));
summary_model.send_last_completion_stream_text_chunk("Explaining /a/b.md");
summary_model.end_last_completion_stream();
send.await.unwrap();
@@ -1538,7 +1538,7 @@ mod tests {
history_entries(&history_store, cx),
vec![(
HistoryEntryId::AcpThread(session_id.clone()),
format!("Explaining {}", path!("/a/b.md"))
"Explaining /a/b.md".into()
)]
);
let acp_thread = agent

View File

@@ -1046,33 +1046,32 @@ impl AcpThreadView {
};
let connection = thread.read(cx).connection().clone();
let can_login = !connection.auth_methods().is_empty() || self.login.is_some();
// Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
let logout_supported = text == "/logout"
&& self
.available_commands
.borrow()
.iter()
.any(|command| command.name == "logout");
if can_login && !logout_supported {
let this = cx.weak_entity();
let agent = self.agent.clone();
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired {
description: None,
provider_id: None,
},
agent,
connection,
window,
cx,
);
});
cx.notify();
let auth_methods = connection.auth_methods();
let has_supported_auth = auth_methods.iter().any(|method| {
let id = method.id.0.as_ref();
id == "claude-login" || id == "spawn-gemini-cli"
});
let can_login = has_supported_auth || auth_methods.is_empty() || self.login.is_some();
if !can_login {
return;
}
};
let this = cx.weak_entity();
let agent = self.agent.clone();
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired {
description: None,
provider_id: None,
},
agent,
connection,
window,
cx,
);
});
cx.notify();
return;
}
self.send_impl(self.message_editor.clone(), window, cx)
@@ -2728,7 +2727,7 @@ impl AcpThreadView {
let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
let command_failed = command_finished
&& output.is_some_and(|o| o.exit_status.is_some_and(|status| !status.success()));
&& output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
let time_elapsed = if let Some(output) = output {
output.ended_at.duration_since(started_at)

View File

@@ -15,6 +15,7 @@ use context_server::ContextServerId;
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest;
use extension_host::ExtensionStore;
use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
use fs::Fs;
use gpui::{
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
@@ -408,7 +409,7 @@ impl AgentConfiguration {
SwitchField::new(
"always-allow-tool-actions-switch",
Some("Allow running commands without asking for confirmation"),
"Allow running commands without asking for confirmation",
Some(
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
),
@@ -428,7 +429,7 @@ impl AgentConfiguration {
SwitchField::new(
"single-file-review",
Some("Enable single-file agent reviews"),
"Enable single-file agent reviews",
Some("Agent edits are also displayed in single-file editors for review.".into()),
single_file_review,
move |state, _window, cx| {
@@ -449,7 +450,7 @@ impl AgentConfiguration {
SwitchField::new(
"sound-notification",
Some("Play sound when finished generating"),
"Play sound when finished generating",
Some(
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
),
@@ -469,7 +470,7 @@ impl AgentConfiguration {
SwitchField::new(
"modifier-send",
Some("Use modifier to submit a message"),
"Use modifier to submit a message",
Some(
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
),
@@ -1084,11 +1085,14 @@ impl AgentConfiguration {
"Claude Code",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiOpenAi,
"Codex",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
this
.child(self.render_agent_server(
IconName::AiOpenAi,
"Codex",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
})
.child(self.render_agent_server(
IconName::AiGemini,
"Gemini CLI",

View File

@@ -75,6 +75,7 @@ use zed_actions::{
assistant::{OpenRulesLibrary, ToggleFocus},
};
use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
const AGENT_PANEL_KEY: &str = "agent_panel";
#[derive(Serialize, Deserialize, Debug)]
@@ -1938,32 +1939,34 @@ impl AgentPanel {
}
}),
)
.item(
ContextMenuEntry::new("New Codex Thread")
.icon(IconName::AiOpenAi)
.disabled(is_via_collab)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
AgentType::Codex,
window,
cx,
);
});
}
});
.when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
this.item(
ContextMenuEntry::new("New Codex Thread")
.icon(IconName::AiOpenAi)
.disabled(is_via_collab)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
AgentType::Codex,
window,
cx,
);
});
}
});
}
}
}
}),
)
}),
)
})
.item(
ContextMenuEntry::new("New Gemini CLI Thread")
.icon(IconName::AiGemini)

View File

@@ -267,7 +267,7 @@ impl Settings for EditorSettings {
delay: drag_and_drop_selection.delay.unwrap(),
},
lsp_document_colors: editor.lsp_document_colors.unwrap(),
minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap().0,
minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap(),
}
}

View File

@@ -262,77 +262,6 @@ impl EditorLspTestContext {
Self::new(language, capabilities, cx).await
}
pub async fn new_tsx(
capabilities: lsp::ServerCapabilities,
cx: &mut gpui::TestAppContext,
) -> EditorLspTestContext {
let mut word_characters: HashSet<char> = Default::default();
word_characters.insert('$');
word_characters.insert('#');
let language = Language::new(
LanguageConfig {
name: "TSX".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["tsx".to_string()],
..Default::default()
},
brackets: language::BracketPairConfig {
pairs: vec![language::BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
surround: true,
newline: true,
}],
disabled_scopes_by_bracket_ix: Default::default(),
},
word_characters,
..Default::default()
},
Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
)
.with_queries(LanguageQueries {
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("<" @open "/>" @close)
("</" @open ">" @close)
("\"" @open "\"" @close)
("'" @open "'" @close)
("`" @open "`" @close)
((jsx_element (jsx_opening_element) @open (jsx_closing_element) @close) (#set! newline.only))"#})),
indents: Some(Cow::from(indoc! {r#"
[
(call_expression)
(assignment_expression)
(member_expression)
(lexical_declaration)
(variable_declaration)
(assignment_expression)
(if_statement)
(for_statement)
] @indent
(_ "[" "]" @end) @indent
(_ "<" ">" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent
(jsx_opening_element ">" @end) @indent
(jsx_element
(jsx_opening_element) @start
(jsx_closing_element)? @end) @indent
"#})),
..Default::default()
})
.expect("Could not parse queries");
Self::new(language, capabilities, cx).await
}
pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self {
let language = Language::new(
LanguageConfig {

View File

@@ -17,3 +17,9 @@ pub struct PanicFeatureFlag;
impl FeatureFlag for PanicFeatureFlag {
const NAME: &'static str = "panic";
}
pub struct CodexAcpFeatureFlag;
impl FeatureFlag for CodexAcpFeatureFlag {
const NAME: &'static str = "codex-acp";
}

View File

@@ -755,7 +755,7 @@ impl PickerDelegate for OpenPathDelegate {
.with_default_highlights(
&window.text_style(),
vec![(
delta..delta + label_len,
delta..label_len,
HighlightStyle::color(Color::Conflict.color(cx)),
)],
)
@@ -765,7 +765,7 @@ impl PickerDelegate for OpenPathDelegate {
.with_default_highlights(
&window.text_style(),
vec![(
delta..delta + label_len,
delta..label_len,
HighlightStyle::color(Color::Created.color(cx)),
)],
)

View File

@@ -180,8 +180,7 @@ impl StyledText {
"Can't use `with_default_highlights` and `with_highlights`"
);
let runs = Self::compute_runs(&self.text, default_style, highlights);
self.runs = Some(runs);
self
self.with_runs(runs)
}
/// Set the styling attributes for the given text, as well as
@@ -194,7 +193,15 @@ impl StyledText {
self.runs.is_none(),
"Can't use `with_highlights` and `with_default_highlights`"
);
self.delayed_highlights = Some(highlights.into_iter().collect::<Vec<_>>());
self.delayed_highlights = Some(
highlights
.into_iter()
.inspect(|(run, _)| {
debug_assert!(self.text.is_char_boundary(run.start));
debug_assert!(self.text.is_char_boundary(run.end));
})
.collect::<Vec<_>>(),
);
self
}
@@ -207,8 +214,10 @@ impl StyledText {
let mut ix = 0;
for (range, highlight) in highlights {
if ix < range.start {
debug_assert!(text.is_char_boundary(range.start));
runs.push(default_style.clone().to_run(range.start - ix));
}
debug_assert!(text.is_char_boundary(range.end));
runs.push(
default_style
.clone()
@@ -225,6 +234,11 @@ impl StyledText {
/// Set the text runs for this piece of text.
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
let mut text = &**self.text;
for run in &runs {
text = text.get(run.len..).expect("invalid text run");
}
assert!(text.is_empty(), "invalid text run");
self.runs = Some(runs);
self
}

View File

@@ -1213,11 +1213,6 @@ impl WindowBounds {
WindowBounds::Fullscreen(bounds) => *bounds,
}
}
/// Creates a new window bounds that centers the window on the screen.
pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
WindowBounds::Windowed(Bounds::centered(None, size, cx))
}
}
impl Default for WindowOptions {

View File

@@ -1449,11 +1449,9 @@ fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool {
#[inline]
pub(crate) fn current_modifiers() -> Modifiers {
let altgr = is_virtual_key_pressed(VK_RMENU) && is_virtual_key_pressed(VK_LCONTROL);
Modifiers {
control: is_virtual_key_pressed(VK_CONTROL) && !altgr,
alt: is_virtual_key_pressed(VK_MENU) && !altgr,
control: is_virtual_key_pressed(VK_CONTROL),
alt: is_virtual_key_pressed(VK_MENU),
shift: is_virtual_key_pressed(VK_SHIFT),
platform: is_virtual_key_pressed(VK_LWIN) || is_virtual_key_pressed(VK_RWIN),
function: false,

View File

@@ -225,19 +225,15 @@ impl LineWrapper {
fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
let mut truncate_at = result.len() - ellipsis.len();
let mut run_end = None;
for (run_index, run) in runs.iter_mut().enumerate() {
if run.len <= truncate_at {
truncate_at -= run.len;
} else {
run.len = truncate_at + ellipsis.len();
run_end = Some(run_index + 1);
runs.truncate(run_index + 1);
break;
}
}
if let Some(run_end) = run_end {
runs.truncate(run_end);
}
}
/// A fragment of a line that can be wrapped.

View File

@@ -58,7 +58,7 @@ mod prompts;
use crate::util::atomic_incr_if_not_zero;
pub use prompts::*;
pub(crate) const DEFAULT_WINDOW_SIZE: Size<Pixels> = size(px(1536.), px(864.));
pub(crate) const DEFAULT_WINDOW_SIZE: Size<Pixels> = size(px(1024.), px(700.));
/// Represents the two different phases when dispatching events.
#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
@@ -4633,14 +4633,6 @@ pub struct WindowHandle<V> {
state_type: PhantomData<V>,
}
impl<V> Debug for WindowHandle<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WindowHandle")
.field("any_handle", &self.any_handle.id.as_u64())
.finish()
}
}
impl<V: 'static + Render> WindowHandle<V> {
/// Creates a new handle from a window ID.
/// This does not check if the root type of the window is `V`.

View File

@@ -99,10 +99,6 @@ impl LanguageModelImage {
.and_then(image::DynamicImage::from_decoder),
ImageFormat::Gif => image::codecs::gif::GifDecoder::new(image_bytes)
.and_then(image::DynamicImage::from_decoder),
ImageFormat::Bmp => image::codecs::bmp::BmpDecoder::new(image_bytes)
.and_then(image::DynamicImage::from_decoder),
ImageFormat::Tiff => image::codecs::tiff::TiffDecoder::new(image_bytes)
.and_then(image::DynamicImage::from_decoder),
_ => return None,
}
.log_err()?;

View File

@@ -91,6 +91,7 @@ tree-sitter-typescript = { workspace = true, optional = true }
tree-sitter-yaml = { workspace = true, optional = true }
util.workspace = true
workspace-hack.workspace = true
shlex.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true

View File

@@ -1180,7 +1180,15 @@ impl ToolchainLister for PythonToolchainProvider {
}
Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
if let Some(prefix) = &toolchain.prefix {
let activate_keyword = shell.activate_keyword();
let activate_keyword = match shell {
ShellKind::Cmd => ".",
ShellKind::Nushell => "overlay use",
ShellKind::PowerShell => ".",
ShellKind::Fish => "source",
ShellKind::Csh => "source",
ShellKind::Tcsh => "source",
ShellKind::Posix | ShellKind::Rc => "source",
};
let activate_script_name = match shell {
ShellKind::Posix | ShellKind::Rc => "activate",
ShellKind::Csh => "activate.csh",
@@ -1192,7 +1200,8 @@ impl ToolchainLister for PythonToolchainProvider {
};
let path = prefix.join(BINARY_DIR).join(activate_script_name);
if let Some(quoted) = shell.try_quote(&path.to_string_lossy())
if let Ok(quoted) =
shlex::try_quote(&path.to_string_lossy()).map(Cow::into_owned)
&& fs.is_file(&path).await
{
activation_script.push(format!("{activate_keyword} {quoted}"));

View File

@@ -1,6 +1,3 @@
((line_comment) @injection.content
(#set! injection.language "comment"))
(macro_invocation
macro: (identifier) @_macro_name
(#not-any-of? @_macro_name "view" "html")

View File

@@ -880,10 +880,6 @@ impl<'a> MarkdownParser<'a> {
contents: paragraph,
}));
}
} else if local_name!("blockquote") == name.local {
if let Some(blockquote) = self.extract_html_blockquote(node, source_range) {
elements.push(ParsedMarkdownElement::BlockQuote(blockquote));
}
} else if local_name!("table") == name.local {
if let Some(table) = self.extract_html_table(node, source_range) {
elements.push(ParsedMarkdownElement::Table(table));
@@ -1006,24 +1002,6 @@ impl<'a> MarkdownParser<'a> {
Some(image)
}
fn extract_html_blockquote(
&self,
node: &Rc<markup5ever_rcdom::Node>,
source_range: Range<usize>,
) -> Option<ParsedMarkdownBlockQuote> {
let mut children = Vec::new();
self.consume_children(source_range.clone(), node, &mut children);
if children.is_empty() {
None
} else {
Some(ParsedMarkdownBlockQuote {
children,
source_range,
})
}
}
fn extract_html_table(
&self,
node: &Rc<markup5ever_rcdom::Node>,
@@ -1432,61 +1410,6 @@ mod tests {
);
}
#[gpui::test]
async fn test_html_block_quote() {
let parsed = parse(
"<blockquote>
<p>some description</p>
</blockquote>",
)
.await;
assert_eq!(
ParsedMarkdown {
children: vec![block_quote(
vec![ParsedMarkdownElement::Paragraph(text(
"some description",
0..76
))],
0..76,
)]
},
parsed
);
}
#[gpui::test]
async fn test_html_nested_block_quote() {
let parsed = parse(
"<blockquote>
<p>some description</p>
<blockquote>
<p>second description</p>
</blockquote>
</blockquote>",
)
.await;
assert_eq!(
ParsedMarkdown {
children: vec![block_quote(
vec![
ParsedMarkdownElement::Paragraph(text("some description", 0..173)),
block_quote(
vec![ParsedMarkdownElement::Paragraph(text(
"second description",
0..173
))],
0..173,
)
],
0..173,
)]
},
parsed
);
}
#[gpui::test]
async fn test_html_table() {
let parsed = parse(

View File

@@ -65,13 +65,7 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt
}
}
/// Runs the provided migrations on the given text.
/// Will automatically return `Ok(None)` if there's no content to migrate.
fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result<Option<String>> {
if text.is_empty() {
return Ok(None);
}
let mut current_text = text.to_string();
let mut result: Option<String> = None;
for migration in migrations.iter() {
@@ -377,11 +371,6 @@ mod tests {
assert_migrated_correctly(migrated, output);
}
#[test]
fn test_empty_content() {
assert_migrate_settings("", None)
}
#[test]
fn test_replace_array_with_single_string() {
assert_migrate_keymap(

View File

@@ -15,15 +15,20 @@ path = "src/onboarding.rs"
default = []
[dependencies]
ai_onboarding.workspace = true
anyhow.workspace = true
client.workspace = true
component.workspace = true
db.workspace = true
documented.workspace = true
editor.workspace = true
fs.workspace = true
fuzzy.workspace = true
git.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
menu.workspace = true
notifications.workspace = true
picker.workspace = true

View File

@@ -0,0 +1,427 @@
use std::sync::Arc;
use ai_onboarding::AiUpsellCard;
use client::{Client, UserStore, zed_urls};
use fs::Fs;
use gpui::{
Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity,
Window, prelude::*,
};
use itertools;
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use project::DisableAiSettings;
use settings::{Settings, update_settings_file};
use ui::{
Badge, ButtonLike, Divider, KeyBinding, Modal, ModalFooter, ModalHeader, Section, SwitchField,
ToggleState, prelude::*, tooltip_container,
};
use util::ResultExt;
use workspace::{ModalView, Workspace};
use zed_actions::agent::OpenSettings;
const FEATURED_PROVIDERS: [&str; 4] = ["anthropic", "google", "openai", "ollama"];
fn render_llm_provider_section(
tab_index: &mut isize,
workspace: WeakEntity<Workspace>,
disabled: bool,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
v_flex()
.gap_4()
.child(
v_flex()
.child(Label::new("Or use other LLM providers").size(LabelSize::Large))
.child(
Label::new("Bring your API keys to use the available providers with Zed's UI for free.")
.color(Color::Muted),
),
)
.child(render_llm_provider_card(tab_index, workspace, disabled, window, cx))
}
fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> impl IntoElement {
let (title, description) = if disabled {
(
"AI is disabled across Zed",
"Re-enable it any time in Settings.",
)
} else {
(
"Privacy is the default for Zed",
"Any use or storage of your data is with your explicit, single-use, opt-in consent.",
)
};
v_flex()
.relative()
.pt_2()
.pb_2p5()
.pl_3()
.pr_2()
.border_1()
.border_dashed()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(cx.theme().colors().surface_background.opacity(0.3))
.rounded_lg()
.overflow_hidden()
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new(title))
.child(
h_flex()
.gap_1()
.child(
Badge::new("Privacy")
.icon(IconName::ShieldCheck)
.tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()),
)
.child(
Button::new("learn_more", "Learn More")
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::ai_privacy_and_security(cx))
})
.tab_index({
*tab_index += 1;
*tab_index - 1
}),
),
),
)
.child(
Label::new(description)
.size(LabelSize::Small)
.color(Color::Muted),
)
}
fn render_llm_provider_card(
tab_index: &mut isize,
workspace: WeakEntity<Workspace>,
disabled: bool,
_: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let registry = LanguageModelRegistry::read_global(cx);
v_flex()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().surface_background.opacity(0.5))
.rounded_lg()
.overflow_hidden()
.children(itertools::intersperse_with(
FEATURED_PROVIDERS
.into_iter()
.flat_map(|provider_name| {
registry.provider(&LanguageModelProviderId::new(provider_name))
})
.enumerate()
.map(|(index, provider)| {
let group_name = SharedString::new(format!("onboarding-hover-group-{}", index));
let is_authenticated = provider.is_authenticated(cx);
ButtonLike::new(("onboarding-ai-setup-buttons", index))
.size(ButtonSize::Large)
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.child(
h_flex()
.group(&group_name)
.px_0p5()
.w_full()
.gap_2()
.justify_between()
.child(
h_flex()
.gap_1()
.child(
Icon::new(provider.icon())
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(Label::new(provider.name().0)),
)
.child(
h_flex()
.gap_1()
.when(!is_authenticated, |el| {
el.visible_on_hover(group_name.clone())
.child(
Icon::new(IconName::Settings)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new("Configure")
.color(Color::Muted)
.size(LabelSize::Small),
)
})
.when(is_authenticated && !disabled, |el| {
el.child(
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::XSmall),
)
.child(
Label::new("Configured")
.color(Color::Muted)
.size(LabelSize::Small),
)
}),
),
)
.on_click({
let workspace = workspace.clone();
move |_, window, cx| {
workspace
.update(cx, |workspace, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
telemetry::event!(
"Welcome AI Modal Opened",
provider = provider.name().0,
);
let modal = AiConfigurationModal::new(
provider.clone(),
window,
cx,
);
window.focus(&modal.focus_handle(cx));
modal
});
})
.log_err();
}
})
.into_any_element()
}),
|| Divider::horizontal().into_any_element(),
))
.child(Divider::horizontal())
.child(
Button::new("agent_settings", "Add Many Others")
.size(ButtonSize::Large)
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_color(Color::Muted)
.icon_size(IconSize::XSmall)
.on_click(|_event, window, cx| {
window.dispatch_action(OpenSettings.boxed_clone(), cx)
})
.tab_index({
*tab_index += 1;
*tab_index - 1
}),
)
}
pub(crate) fn render_ai_setup_page(
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
client: Arc<Client>,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let mut tab_index = 0;
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
v_flex()
.gap_2()
.child(
SwitchField::new(
"enable_ai",
"Enable AI features",
None,
if is_ai_disabled {
ToggleState::Unselected
} else {
ToggleState::Selected
},
|&toggle_state, _, cx| {
let enabled = match toggle_state {
ToggleState::Indeterminate => {
return;
}
ToggleState::Unselected => true,
ToggleState::Selected => false,
};
telemetry::event!(
"Welcome AI Enabled",
toggle = if enabled { "on" } else { "off" },
);
let fs = <dyn Fs>::global(cx);
update_settings_file(fs, cx, move |settings, _| {
settings.disable_ai = Some(enabled.into());
});
},
)
.tab_index({
tab_index += 1;
tab_index - 1
}),
)
.child(render_privacy_card(&mut tab_index, is_ai_disabled, cx))
.child(
v_flex()
.mt_2()
.gap_6()
.child(
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx)
.tab_index(Some({
tab_index += 1;
tab_index - 1
})),
)
.child(render_llm_provider_section(
&mut tab_index,
workspace,
is_ai_disabled,
window,
cx,
))
.when(is_ai_disabled, |this| {
this.child(
div()
.id("backdrop")
.size_full()
.absolute()
.inset_0()
.bg(cx.theme().colors().editor_background)
.opacity(0.8)
.block_mouse_except_scroll(),
)
}),
)
}
struct AiConfigurationModal {
focus_handle: FocusHandle,
selected_provider: Arc<dyn LanguageModelProvider>,
configuration_view: AnyView,
}
impl AiConfigurationModal {
fn new(
selected_provider: Arc<dyn LanguageModelProvider>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let configuration_view = selected_provider.configuration_view(
language_model::ConfigurationViewTargetAgent::ZedAgent,
window,
cx,
);
Self {
focus_handle,
configuration_view,
selected_provider,
}
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl ModalView for AiConfigurationModal {}
impl EventEmitter<DismissEvent> for AiConfigurationModal {}
impl Focusable for AiConfigurationModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for AiConfigurationModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("OnboardingAiConfigurationModal")
.w(rems(34.))
.elevation_3(cx)
.track_focus(&self.focus_handle)
.on_action(
cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
)
.child(
Modal::new("onboarding-ai-setup-modal", None)
.header(
ModalHeader::new()
.icon(
Icon::new(self.selected_provider.icon())
.color(Color::Muted)
.size(IconSize::Small),
)
.headline(self.selected_provider.name().0),
)
.section(Section::new().child(self.configuration_view.clone()))
.footer(
ModalFooter::new().end_slot(
Button::new("ai-onb-modal-Done", "Done")
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&self.focus_handle.clone(),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(cx.listener(|this, _event, _window, cx| {
this.cancel(&menu::Cancel, cx)
})),
),
),
)
}
}
pub struct AiPrivacyTooltip {}
impl AiPrivacyTooltip {
pub fn new() -> Self {
Self {}
}
}
impl Render for AiPrivacyTooltip {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
const DESCRIPTION: &str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. ";
tooltip_container(cx, move |this, _| {
this.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::ShieldCheck)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("Privacy First")),
)
.child(
div().max_w_64().child(
Label::new(DESCRIPTION)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
})
}
}

View File

@@ -2,23 +2,19 @@ use std::sync::Arc;
use client::TelemetrySettings;
use fs::Fs;
use gpui::{Action, App, IntoElement};
use gpui::{App, IntoElement};
use settings::{BaseKeymap, Settings, update_settings_file};
use theme::{
Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection,
ThemeSettings,
};
use ui::{
ButtonLike, ParentElement as _, StatefulInteractiveElement, SwitchField, TintColor,
ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*,
rems_from_px,
ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup,
ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px,
};
use vim_mode_setting::VimModeSetting;
use crate::{
ImportCursorSettings, ImportVsCodeSettings, SettingsImportState,
theme_preview::{ThemePreviewStyle, ThemePreviewTile},
};
use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile};
const LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
@@ -82,7 +78,6 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
)
}),
)
.size(ToggleButtonGroupSize::Medium)
.tab_index(tab_index)
.selected_index(theme_mode as usize)
.style(ui::ToggleButtonGroupStyle::Outlined)
@@ -233,87 +228,91 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
.gap_4()
.border_t_1()
.border_color(cx.theme().colors().border_variant.opacity(0.5))
.child(
SwitchField::new(
"onboarding-telemetry-metrics",
None::<&str>,
Some("Help improve Zed by sending anonymous usage data".into()),
if TelemetrySettings::get_global(cx).metrics {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
{
let fs = fs.clone();
move |selection, _, cx| {
let enabled = match selection {
ToggleState::Selected => true,
ToggleState::Unselected => false,
ToggleState::Indeterminate => {
return;
}
};
.child(Label::new("Telemetry").size(LabelSize::Large))
.child(SwitchField::new(
"onboarding-telemetry-metrics",
"Help Improve Zed",
Some("Anonymous usage data helps us build the right features and improve your experience.".into()),
if TelemetrySettings::get_global(cx).metrics {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
{
let fs = fs.clone();
move |selection, _, cx| {
let enabled = match selection {
ToggleState::Selected => true,
ToggleState::Unselected => false,
ToggleState::Indeterminate => { return; },
};
update_settings_file(fs.clone(), cx, move |setting, _| {
setting.telemetry.get_or_insert_default().metrics = Some(enabled);
});
// This telemetry event shouldn't fire when it's off. If it does we'll be alerted
// and can fix it in a timely manner to respect a user's choice.
telemetry::event!(
"Welcome Page Telemetry Metrics Toggled",
options = if enabled { "on" } else { "off" }
);
update_settings_file(
fs.clone(),
cx,
move |setting, _| {
setting.telemetry.get_or_insert_default().metrics = Some(enabled);
}
},
)
.tab_index({
*tab_index += 1;
*tab_index
}),
)
.child(
SwitchField::new(
"onboarding-telemetry-crash-reports",
None::<&str>,
Some(
"Help fix Zed by sending crash reports so we can fix critical issues fast"
.into(),
),
if TelemetrySettings::get_global(cx).diagnostics {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
{
let fs = fs.clone();
move |selection, _, cx| {
let enabled = match selection {
ToggleState::Selected => true,
ToggleState::Unselected => false,
ToggleState::Indeterminate => {
return;
}
};
,
);
update_settings_file(fs.clone(), cx, move |setting, _| {
// This telemetry event shouldn't fire when it's off. If it does we'll be alerted
// and can fix it in a timely manner to respect a user's choice.
telemetry::event!("Welcome Page Telemetry Metrics Toggled",
options = if enabled {
"on"
} else {
"off"
}
);
}},
).tab_index({
*tab_index += 1;
*tab_index
}))
.child(SwitchField::new(
"onboarding-telemetry-crash-reports",
"Help Fix Zed",
Some("Send crash reports so we can fix critical issues fast.".into()),
if TelemetrySettings::get_global(cx).diagnostics {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
{
let fs = fs.clone();
move |selection, _, cx| {
let enabled = match selection {
ToggleState::Selected => true,
ToggleState::Unselected => false,
ToggleState::Indeterminate => { return; },
};
update_settings_file(
fs.clone(),
cx,
move |setting, _| {
setting.telemetry.get_or_insert_default().diagnostics = Some(enabled);
});
},
// This telemetry event shouldn't fire when it's off. If it does we'll be alerted
// and can fix it in a timely manner to respect a user's choice.
telemetry::event!(
"Welcome Page Telemetry Diagnostics Toggled",
options = if enabled { "on" } else { "off" }
);
}
},
)
.tab_index({
*tab_index += 1;
*tab_index
}),
)
);
// This telemetry event shouldn't fire when it's off. If it does we'll be alerted
// and can fix it in a timely manner to respect a user's choice.
telemetry::event!("Welcome Page Telemetry Diagnostics Toggled",
options = if enabled {
"on"
} else {
"off"
}
);
}
}
).tab_index({
*tab_index += 1;
*tab_index
}))
}
fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
@@ -381,8 +380,8 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
};
SwitchField::new(
"onboarding-vim-mode",
Some("Vim Mode"),
Some("Coming from Neovim? Use our first-class implementation of Vim Mode".into()),
"Vim Mode",
Some("Coming from Neovim? Use our first-class implementation of Vim Mode.".into()),
toggle_state,
{
let fs = <dyn Fs>::global(cx);
@@ -411,79 +410,12 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
})
}
fn render_setting_import_button(
tab_index: isize,
label: SharedString,
action: &dyn Action,
imported: bool,
) -> impl IntoElement + 'static {
let action = action.boxed_clone();
h_flex().w_full().child(
ButtonLike::new(label.clone())
.style(ButtonStyle::OutlinedTransparent)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(imported)
.size(ButtonSize::Medium)
.tab_index(tab_index)
.child(
h_flex()
.w_full()
.justify_between()
.when(imported, |this| {
this.child(Icon::new(IconName::Check).color(Color::Success))
})
.child(Label::new(label.clone()).mx_2().size(LabelSize::Small)),
)
.on_click(move |_, window, cx| {
telemetry::event!("Welcome Import Settings", import_source = label,);
window.dispatch_action(action.boxed_clone(), cx);
}),
)
}
fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
let import_state = SettingsImportState::global(cx);
let imports: [(SharedString, &dyn Action, bool); 2] = [
(
"VS Code".into(),
&ImportVsCodeSettings { skip_prompt: false },
import_state.vscode,
),
(
"Cursor".into(),
&ImportCursorSettings { skip_prompt: false },
import_state.cursor,
),
];
let [vscode, cursor] = imports.map(|(label, action, imported)| {
*tab_index += 1;
render_setting_import_button(*tab_index - 1, label, action, imported)
});
h_flex()
.child(
v_flex()
.gap_0p5()
.max_w_5_6()
.child(Label::new("Import Settings"))
.child(
Label::new("Automatically pull your settings from other editors")
.color(Color::Muted),
),
)
.child(div().w_full())
.child(h_flex().gap_1().child(vscode).child(cursor))
}
pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement {
let mut tab_index = 0;
v_flex()
.id("basics-page")
.gap_6()
.child(render_theme_section(&mut tab_index, cx))
.child(render_base_keymap_section(&mut tab_index, cx))
.child(render_import_settings_section(&mut tab_index, cx))
.child(render_vim_mode_switch(&mut tab_index, cx))
.child(render_telemetry_section(&mut tab_index, cx))
}

View File

@@ -0,0 +1,612 @@
use std::sync::Arc;
use editor::{EditorSettings, ShowMinimap};
use fs::Fs;
use gpui::{Action, App, FontFeatures, IntoElement, Pixels, SharedString, Window};
use language::language_settings::{AllLanguageSettings, FormatOnSave};
use project::project_settings::ProjectSettings;
use settings::{Settings as _, update_settings_file};
use theme::{FontFamilyName, ThemeSettings};
use ui::{
ButtonLike, PopoverMenu, SwitchField, ToggleButtonGroup, ToggleButtonGroupStyle,
ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
};
use ui_input::{NumericStepper, font_picker};
use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState};
fn read_show_mini_map(cx: &App) -> ShowMinimap {
editor::EditorSettings::get_global(cx).minimap.show
}
fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
// This is used to speed up the UI
// the UI reads the current values to get what toggle state to show on buttons
// there's a slight delay if we just call update_settings_file so we manually set
// the value here then call update_settings file to get around the delay
let mut curr_settings = EditorSettings::get_global(cx).clone();
curr_settings.minimap.show = show;
EditorSettings::override_global(curr_settings, cx);
update_settings_file(fs, cx, move |settings, _| {
telemetry::event!(
"Welcome Minimap Clicked",
from = settings.editor.minimap.clone().unwrap_or_default(),
to = show
);
settings.editor.minimap.get_or_insert_default().show = Some(show);
});
}
fn read_inlay_hints(cx: &App) -> bool {
AllLanguageSettings::get_global(cx)
.defaults
.inlay_hints
.enabled
}
fn write_inlay_hints(enabled: bool, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
let mut curr_settings = AllLanguageSettings::get_global(cx).clone();
curr_settings.defaults.inlay_hints.enabled = enabled;
AllLanguageSettings::override_global(curr_settings, cx);
update_settings_file(fs, cx, move |settings, _cx| {
settings
.project
.all_languages
.defaults
.inlay_hints
.get_or_insert_default()
.enabled = Some(enabled);
});
}
fn read_git_blame(cx: &App) -> bool {
ProjectSettings::get_global(cx).git.inline_blame.enabled
}
fn write_git_blame(enabled: bool, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
let mut curr_settings = ProjectSettings::get_global(cx).clone();
curr_settings.git.inline_blame.enabled = enabled;
ProjectSettings::override_global(curr_settings, cx);
update_settings_file(fs, cx, move |settings, _| {
settings
.git
.get_or_insert_default()
.inline_blame
.get_or_insert_default()
.enabled = Some(enabled);
});
}
fn write_ui_font_family(font: SharedString, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
update_settings_file(fs, cx, move |settings, _| {
telemetry::event!(
"Welcome Font Changed",
type = "ui font",
old = settings.theme.ui_font_family,
new = font
);
settings.theme.ui_font_family = Some(FontFamilyName(font.into()));
});
}
fn write_ui_font_size(size: Pixels, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
update_settings_file(fs, cx, move |settings, _| {
settings.theme.ui_font_size = Some(size.into());
});
}
fn write_buffer_font_size(size: Pixels, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
update_settings_file(fs, cx, move |settings, _| {
settings.theme.buffer_font_size = Some(size.into());
});
}
fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
update_settings_file(fs, cx, move |settings, _| {
telemetry::event!(
"Welcome Font Changed",
type = "editor font",
old = settings.theme.buffer_font_family,
new = font_family
);
settings.theme.buffer_font_family = Some(FontFamilyName(font_family.into()));
});
}
fn read_font_ligatures(cx: &App) -> bool {
ThemeSettings::get_global(cx)
.buffer_font
.features
.is_calt_enabled()
.unwrap_or(true)
}
fn write_font_ligatures(enabled: bool, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
let bit = if enabled { 1 } else { 0 };
update_settings_file(fs, cx, move |settings, _| {
let mut features = settings
.theme
.buffer_font_features
.as_mut()
.map(|features| features.tag_value_list().to_vec())
.unwrap_or_default();
if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
features[calt_index].1 = bit;
} else {
features.push(("calt".into(), bit));
}
settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features)));
});
}
fn read_format_on_save(cx: &App) -> bool {
match AllLanguageSettings::get_global(cx).defaults.format_on_save {
FormatOnSave::On => true,
FormatOnSave::Off => false,
}
}
fn write_format_on_save(format_on_save: bool, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
update_settings_file(fs, cx, move |settings, _| {
settings.project.all_languages.defaults.format_on_save = Some(match format_on_save {
true => FormatOnSave::On,
false => FormatOnSave::Off,
});
});
}
fn render_setting_import_button(
tab_index: isize,
label: SharedString,
icon_name: IconName,
action: &dyn Action,
imported: bool,
) -> impl IntoElement {
let action = action.boxed_clone();
h_flex().w_full().child(
ButtonLike::new(label.clone())
.full_width()
.style(ButtonStyle::Outlined)
.size(ButtonSize::Large)
.tab_index(tab_index)
.child(
h_flex()
.w_full()
.justify_between()
.child(
h_flex()
.gap_1p5()
.px_1()
.child(
Icon::new(icon_name)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(Label::new(label.clone())),
)
.when(imported, |this| {
this.child(
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::XSmall),
)
.child(Label::new("Imported").size(LabelSize::Small)),
)
}),
)
.on_click(move |_, window, cx| {
telemetry::event!("Welcome Import Settings", import_source = label,);
window.dispatch_action(action.boxed_clone(), cx);
}),
)
}
fn render_import_settings_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
let import_state = SettingsImportState::global(cx);
let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [
(
"VS Code".into(),
IconName::EditorVsCode,
&ImportVsCodeSettings { skip_prompt: false },
import_state.vscode,
),
(
"Cursor".into(),
IconName::EditorCursor,
&ImportCursorSettings { skip_prompt: false },
import_state.cursor,
),
];
let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| {
*tab_index += 1;
render_setting_import_button(*tab_index - 1, label, icon_name, action, imported)
});
v_flex()
.gap_4()
.child(
v_flex()
.child(Label::new("Import Settings").size(LabelSize::Large))
.child(
Label::new("Automatically pull your settings from other editors.")
.color(Color::Muted),
),
)
.child(h_flex().w_full().gap_4().child(vscode).child(cursor))
}
fn render_font_customization_section(
tab_index: &mut isize,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let theme_settings = ThemeSettings::get_global(cx);
let ui_font_size = theme_settings.ui_font_size(cx);
let ui_font_family = theme_settings.ui_font.family.clone();
let buffer_font_family = theme_settings.buffer_font.family.clone();
let buffer_font_size = theme_settings.buffer_font_size(cx);
let ui_font_picker =
cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx));
let buffer_font_picker = cx.new(|cx| {
font_picker(
buffer_font_family.clone(),
write_buffer_font_family,
window,
cx,
)
});
let ui_font_handle = ui::PopoverMenuHandle::default();
let buffer_font_handle = ui::PopoverMenuHandle::default();
h_flex()
.w_full()
.gap_4()
.child(
v_flex()
.w_full()
.gap_1()
.child(Label::new("UI Font"))
.child(
h_flex()
.w_full()
.justify_between()
.gap_2()
.child(
PopoverMenu::new("ui-font-picker")
.menu({
let ui_font_picker = ui_font_picker;
move |_window, _cx| Some(ui_font_picker.clone())
})
.trigger(
ButtonLike::new("ui-font-family-button")
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.full_width()
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.child(
h_flex()
.w_full()
.justify_between()
.child(Label::new(ui_font_family))
.child(
Icon::new(IconName::ChevronUpDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.full_width(true)
.anchor(gpui::Corner::TopLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(4.0),
})
.with_handle(ui_font_handle),
)
.child(font_picker_stepper(
"ui-font-size",
&ui_font_size,
tab_index,
write_ui_font_size,
window,
cx,
)),
),
)
.child(
v_flex()
.w_full()
.gap_1()
.child(Label::new("Editor Font"))
.child(
h_flex()
.w_full()
.justify_between()
.gap_2()
.child(
PopoverMenu::new("buffer-font-picker")
.menu({
let buffer_font_picker = buffer_font_picker;
move |_window, _cx| Some(buffer_font_picker.clone())
})
.trigger(
ButtonLike::new("buffer-font-family-button")
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.full_width()
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.child(
h_flex()
.w_full()
.justify_between()
.child(Label::new(buffer_font_family))
.child(
Icon::new(IconName::ChevronUpDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.full_width(true)
.anchor(gpui::Corner::TopLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(4.0),
})
.with_handle(buffer_font_handle),
)
.child(font_picker_stepper(
"buffer-font-size",
&buffer_font_size,
tab_index,
write_buffer_font_size,
window,
cx,
)),
),
)
}
fn font_picker_stepper(
id: &'static str,
font_size: &Pixels,
tab_index: &mut isize,
write_font_size: fn(Pixels, &mut App),
window: &mut Window,
cx: &mut App,
) -> NumericStepper<u32> {
window.with_id(id, |window| {
let optimistic_font_size: gpui::Entity<Option<u32>> = window.use_state(cx, |_, _| None);
optimistic_font_size.update(cx, |optimistic_font_size, _| {
if let Some(optimistic_font_size_val) = optimistic_font_size {
if *optimistic_font_size_val == u32::from(font_size) {
*optimistic_font_size = None;
}
}
});
let stepper_font_size = optimistic_font_size
.read(cx)
.unwrap_or_else(|| font_size.into());
NumericStepper::new(
SharedString::new(format!("{}-stepper", id)),
stepper_font_size,
window,
cx,
)
.on_change(move |new_value, _, cx| {
optimistic_font_size.write(cx, Some(*new_value));
write_font_size(Pixels::from(*new_value), cx);
})
.format(|value| format!("{value}px"))
.style(ui_input::NumericStepperStyle::Outlined)
.tab_index({
*tab_index += 2;
*tab_index - 2
})
.min(6)
.max(32)
})
}
fn render_popular_settings_section(
tab_index: &mut isize,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
const LIGATURE_TOOLTIP: &str =
"Font ligatures combine two characters into one. For example, turning != into ≠.";
v_flex()
.pt_6()
.gap_4()
.border_t_1()
.border_color(cx.theme().colors().border_variant.opacity(0.5))
.child(Label::new("Popular Settings").size(LabelSize::Large))
.child(render_font_customization_section(tab_index, window, cx))
.child(
SwitchField::new(
"onboarding-font-ligatures",
"Font Ligatures",
Some("Combine text characters into their associated symbols.".into()),
if read_font_ligatures(cx) {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
|toggle_state, _, cx| {
let enabled = toggle_state == &ToggleState::Selected;
telemetry::event!(
"Welcome Font Ligature",
options = if enabled { "on" } else { "off" },
);
write_font_ligatures(enabled, cx);
},
)
.tab_index({
*tab_index += 1;
*tab_index - 1
})
.tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
)
.child(
SwitchField::new(
"onboarding-format-on-save",
"Format on Save",
Some("Format code automatically when saving.".into()),
if read_format_on_save(cx) {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
|toggle_state, _, cx| {
let enabled = toggle_state == &ToggleState::Selected;
telemetry::event!(
"Welcome Format On Save Changed",
options = if enabled { "on" } else { "off" },
);
write_format_on_save(enabled, cx);
},
)
.tab_index({
*tab_index += 1;
*tab_index - 1
}),
)
.child(
SwitchField::new(
"onboarding-enable-inlay-hints",
"Inlay Hints",
Some("See parameter names for function and method calls inline.".into()),
if read_inlay_hints(cx) {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
|toggle_state, _, cx| {
let enabled = toggle_state == &ToggleState::Selected;
telemetry::event!(
"Welcome Inlay Hints Changed",
options = if enabled { "on" } else { "off" },
);
write_inlay_hints(enabled, cx);
},
)
.tab_index({
*tab_index += 1;
*tab_index - 1
}),
)
.child(
SwitchField::new(
"onboarding-git-blame-switch",
"Inline Git Blame",
Some("See who committed each line on a given file.".into()),
if read_git_blame(cx) {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
|toggle_state, _, cx| {
let enabled = toggle_state == &ToggleState::Selected;
telemetry::event!(
"Welcome Git Blame Changed",
options = if enabled { "on" } else { "off" },
);
write_git_blame(enabled, cx);
},
)
.tab_index({
*tab_index += 1;
*tab_index - 1
}),
)
.child(
h_flex()
.items_start()
.justify_between()
.child(
v_flex().child(Label::new("Minimap")).child(
Label::new("See a high-level overview of your source code.")
.color(Color::Muted),
),
)
.child(
ToggleButtonGroup::single_row(
"onboarding-show-mini-map",
[
ToggleButtonSimple::new("Auto", |_, _, cx| {
write_show_mini_map(ShowMinimap::Auto, cx);
})
.tooltip(Tooltip::text(
"Show the minimap if the editor's scrollbar is visible.",
)),
ToggleButtonSimple::new("Always", |_, _, cx| {
write_show_mini_map(ShowMinimap::Always, cx);
}),
ToggleButtonSimple::new("Never", |_, _, cx| {
write_show_mini_map(ShowMinimap::Never, cx);
}),
],
)
.selected_index(match read_show_mini_map(cx) {
ShowMinimap::Auto => 0,
ShowMinimap::Always => 1,
ShowMinimap::Never => 2,
})
.tab_index(tab_index)
.style(ToggleButtonGroupStyle::Outlined)
.width(ui::rems_from_px(3. * 64.)),
),
)
}
pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
let mut tab_index = 0;
v_flex()
.gap_6()
.child(render_import_settings_section(&mut tab_index, cx))
.child(render_popular_settings_section(&mut tab_index, window, cx))
}

View File

@@ -14,8 +14,8 @@ use serde::Deserialize;
use settings::{SettingsStore, VsCodeSettingsSource};
use std::sync::Arc;
use ui::{
KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName,
WithScrollbar as _, prelude::*, rems_from_px,
Avatar, ButtonLike, FluentBuilder, Headline, KeyBinding, ParentElement as _,
StatefulInteractiveElement, Vector, VectorName, WithScrollbar, prelude::*, rems_from_px,
};
pub use ui_input::font_picker;
use workspace::{
@@ -26,8 +26,10 @@ use workspace::{
open_new, register_serializable_item, with_active_or_new_workspace,
};
mod ai_setup_page;
mod base_keymap_picker;
mod basics_page;
mod editing_page;
pub mod multibuffer_hint;
mod theme_preview;
mod welcome;
@@ -64,6 +66,12 @@ actions!(
actions!(
onboarding,
[
/// Activates the Basics page.
ActivateBasicsPage,
/// Activates the Editing page.
ActivateEditingPage,
/// Activates the AI Setup page.
ActivateAISetupPage,
/// Finish the onboarding process.
Finish,
/// Sign in while in the onboarding flow.
@@ -208,9 +216,27 @@ pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyh
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SelectedPage {
Basics,
Editing,
AiSetup,
}
impl SelectedPage {
fn name(&self) -> &'static str {
match self {
SelectedPage::Basics => "Basics",
SelectedPage::Editing => "Editing",
SelectedPage::AiSetup => "AI Setup",
}
}
}
struct Onboarding {
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
selected_page: SelectedPage,
user_store: Entity<UserStore>,
scroll_handle: ScrollHandle,
_settings_subscription: Subscription,
@@ -233,6 +259,7 @@ impl Onboarding {
workspace: workspace.weak_handle(),
focus_handle: cx.focus_handle(),
scroll_handle: ScrollHandle::new(),
selected_page: SelectedPage::Basics,
user_store: workspace.user_store().clone(),
_settings_subscription: cx
.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
@@ -240,8 +267,228 @@ impl Onboarding {
})
}
fn set_page(
&mut self,
page: SelectedPage,
clicked: Option<&'static str>,
cx: &mut Context<Self>,
) {
if let Some(click) = clicked {
telemetry::event!(
"Welcome Tab Clicked",
from = self.selected_page.name(),
to = page.name(),
clicked = click,
);
}
self.selected_page = page;
self.scroll_handle.set_offset(Default::default());
cx.notify();
cx.emit(ItemEvent::UpdateTab);
}
fn render_nav_buttons(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> [impl IntoElement; 3] {
let pages = [
SelectedPage::Basics,
SelectedPage::Editing,
SelectedPage::AiSetup,
];
let text = ["Basics", "Editing", "AI Setup"];
let actions: [&dyn Action; 3] = [
&ActivateBasicsPage,
&ActivateEditingPage,
&ActivateAISetupPage,
];
let mut binding = actions.map(|action| {
KeyBinding::for_action_in(action, &self.focus_handle, window, cx)
.map(|kb| kb.size(rems_from_px(12.)))
});
pages.map(|page| {
let i = page as usize;
let selected = self.selected_page == page;
h_flex()
.id(text[i])
.relative()
.w_full()
.gap_2()
.px_2()
.py_0p5()
.justify_between()
.rounded_sm()
.when(selected, |this| {
this.child(
div()
.h_4()
.w_px()
.bg(cx.theme().colors().text_accent)
.absolute()
.left_0(),
)
})
.hover(|style| style.bg(cx.theme().colors().element_hover))
.child(Label::new(text[i]).map(|this| {
if selected {
this.color(Color::Default)
} else {
this.color(Color::Muted)
}
}))
.child(binding[i].take().map_or(
gpui::Empty.into_any_element(),
IntoElement::into_any_element,
))
.on_click(cx.listener(move |this, click_event, _, cx| {
let click = match click_event {
gpui::ClickEvent::Mouse(_) => "mouse",
gpui::ClickEvent::Keyboard(_) => "keyboard",
};
this.set_page(page, Some(click), cx);
}))
})
}
fn render_nav(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.h_full()
.w(rems_from_px(220.))
.flex_shrink_0()
.gap_4()
.justify_between()
.child(
v_flex()
.gap_6()
.child(
h_flex()
.px_2()
.gap_4()
.child(Vector::square(VectorName::ZedLogo, rems(2.5)))
.child(
v_flex()
.child(
Headline::new("Welcome to Zed").size(HeadlineSize::Small),
)
.child(
Label::new("The editor for what's next")
.color(Color::Muted)
.size(LabelSize::Small)
.italic(),
),
),
)
.child(
v_flex()
.gap_4()
.child(
v_flex()
.py_4()
.border_y_1()
.border_color(cx.theme().colors().border_variant.opacity(0.5))
.gap_1()
.children(self.render_nav_buttons(window, cx)),
)
.map(|this| {
if let Some(user) = self.user_store.read(cx).current_user() {
this.child(
v_flex()
.gap_1()
.child(
h_flex()
.ml_2()
.gap_2()
.max_w_full()
.w_full()
.child(Avatar::new(user.avatar_uri.clone()))
.child(
Label::new(user.github_login.clone())
.truncate(),
),
)
.child(
ButtonLike::new("open_account")
.size(ButtonSize::Medium)
.child(
h_flex()
.ml_1()
.w_full()
.justify_between()
.child(Label::new("Open Account"))
.children(
KeyBinding::for_action_in(
&OpenAccount,
&self.focus_handle,
window,
cx,
)
.map(|kb| {
kb.size(rems_from_px(12.))
}),
),
)
.on_click(|_, window, cx| {
window.dispatch_action(
OpenAccount.boxed_clone(),
cx,
);
}),
),
)
} else {
this.child(
ButtonLike::new("sign_in")
.size(ButtonSize::Medium)
.child(
h_flex()
.ml_1()
.w_full()
.justify_between()
.child(Label::new("Sign In"))
.children(
KeyBinding::for_action_in(
&SignIn,
&self.focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
),
)
.on_click(|_, window, cx| {
telemetry::event!("Welcome Sign In Clicked");
window.dispatch_action(SignIn.boxed_clone(), cx);
}),
)
}
}),
),
)
.child({
Button::new("start_building", "Start Building")
.full_width()
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.key_binding(
KeyBinding::for_action_in(&Finish, &self.focus_handle, window, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
telemetry::event!("Welcome Start Building Clicked");
window.dispatch_action(Finish.boxed_clone(), cx);
})
})
}
fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) {
telemetry::event!("Finish Setup");
telemetry::event!("Welcome Skip Clicked");
go_to_welcome_page(cx);
}
@@ -262,14 +509,29 @@ impl Onboarding {
cx.open_url(&zed_urls::account_url(cx))
}
fn render_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
crate::basics_page::render_basics_page(cx).into_any_element()
fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let client = Client::global(cx);
match self.selected_page {
SelectedPage::Basics => crate::basics_page::render_basics_page(cx).into_any_element(),
SelectedPage::Editing => {
crate::editing_page::render_editing_page(window, cx).into_any_element()
}
SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page(
self.workspace.clone(),
self.user_store.clone(),
client,
window,
cx,
)
.into_any_element(),
}
}
}
impl Render for Onboarding {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
h_flex()
.image_cache(gpui::retain_all("onboarding-page"))
.key_context({
let mut ctx = KeyContext::new_with_defaults();
@@ -283,6 +545,15 @@ impl Render for Onboarding {
.on_action(Self::on_finish)
.on_action(Self::handle_sign_in)
.on_action(Self::handle_open_account)
.on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
this.set_page(SelectedPage::Basics, Some("action"), cx);
}))
.on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| {
this.set_page(SelectedPage::Editing, Some("action"), cx);
}))
.on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| {
this.set_page(SelectedPage::AiSetup, Some("action"), cx);
}))
.on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| {
window.focus_next();
cx.notify();
@@ -292,68 +563,35 @@ impl Render for Onboarding {
cx.notify();
}))
.child(
div()
.max_w(Rems(48.0))
.w_full()
.mx_auto()
h_flex()
.max_w(rems_from_px(1100.))
.max_h(rems_from_px(850.))
.size_full()
.gap_6()
.m_auto()
.py_20()
.px_12()
.items_start()
.gap_12()
.child(self.render_nav(window, cx))
.child(
v_flex()
.m_auto()
.id("page-content")
.gap_6()
div()
.size_full()
.max_w_full()
.min_w_0()
.p_12()
.border_color(cx.theme().colors().border_variant.opacity(0.5))
.overflow_y_scroll()
.pr_6()
.child(
h_flex()
.w_full()
.gap_4()
.child(Vector::square(VectorName::ZedLogo, rems(2.5)))
.child(
v_flex()
.child(
Headline::new("Welcome to Zed")
.size(HeadlineSize::Small),
)
.child(
Label::new("The editor for what's next")
.color(Color::Muted)
.size(LabelSize::Small)
.italic(),
),
)
.child(div().w_full())
.child({
Button::new("finish_setup", "Finish Setup")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.width(Rems(12.0))
.key_binding(
KeyBinding::for_action_in(
&Finish,
&self.focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(Finish.boxed_clone(), cx);
})
})
.pb_6()
.border_b_1()
.border_color(cx.theme().colors().border_variant.opacity(0.5)),
v_flex()
.id("page-content")
.size_full()
.max_w_full()
.min_w_0()
.pl_12()
.border_l_1()
.border_color(cx.theme().colors().border_variant.opacity(0.5))
.overflow_y_scroll()
.child(self.render_page(window, cx))
.track_scroll(&self.scroll_handle),
)
.child(self.render_page(cx))
.track_scroll(&self.scroll_handle),
)
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
),
)
}
}
@@ -390,6 +628,7 @@ impl Item for Onboarding {
Some(cx.new(|cx| Onboarding {
workspace: self.workspace.clone(),
user_store: self.user_store.clone(),
selected_page: self.selected_page,
scroll_handle: ScrollHandle::new(),
focus_handle: cx.focus_handle(),
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
@@ -575,10 +814,25 @@ impl workspace::SerializableItem for Onboarding {
cx: &mut App,
) -> gpui::Task<gpui::Result<Entity<Self>>> {
window.spawn(cx, async move |cx| {
if let Some(_) =
if let Some(page_number) =
persistence::ONBOARDING_PAGES.get_onboarding_page(item_id, workspace_id)?
{
workspace.update(cx, |workspace, cx| Onboarding::new(workspace, cx))
let page = match page_number {
0 => Some(SelectedPage::Basics),
1 => Some(SelectedPage::Editing),
2 => Some(SelectedPage::AiSetup),
_ => None,
};
workspace.update(cx, |workspace, cx| {
let onboarding_page = Onboarding::new(workspace, cx);
if let Some(page) = page {
zlog::info!("Onboarding page {page:?} loaded");
onboarding_page.update(cx, |onboarding_page, cx| {
onboarding_page.set_page(page, None, cx);
})
}
onboarding_page
})
} else {
Err(anyhow::anyhow!("No onboarding page to deserialize"))
}
@@ -594,10 +848,10 @@ impl workspace::SerializableItem for Onboarding {
cx: &mut ui::Context<Self>,
) -> Option<gpui::Task<gpui::Result<()>>> {
let workspace_id = workspace.database_id()?;
let page_number = self.selected_page as u16;
Some(cx.background_spawn(async move {
persistence::ONBOARDING_PAGES
.save_onboarding_page(item_id, workspace_id)
.save_onboarding_page(item_id, workspace_id, page_number)
.await
}))
}
@@ -620,32 +874,17 @@ mod persistence {
impl Domain for OnboardingPagesDb {
const NAME: &str = stringify!(OnboardingPagesDb);
const MIGRATIONS: &[&str] = &[
sql!(
CREATE TABLE onboarding_pages (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
page_number INTEGER,
const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE onboarding_pages (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
page_number INTEGER,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
),
sql!(
CREATE TABLE onboarding_pages_2 (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
INSERT INTO onboarding_pages_2 SELECT workspace_id, item_id FROM onboarding_pages;
DROP TABLE onboarding_pages;
ALTER TABLE onboarding_pages_2 RENAME TO onboarding_pages;
),
];
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
)];
}
db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
@@ -654,10 +893,11 @@ mod persistence {
query! {
pub async fn save_onboarding_page(
item_id: workspace::ItemId,
workspace_id: workspace::WorkspaceId
workspace_id: workspace::WorkspaceId,
page_number: u16
) -> Result<()> {
INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id)
VALUES (?, ?)
INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id, page_number)
VALUES (?, ?, ?)
}
}
@@ -665,8 +905,8 @@ mod persistence {
pub fn get_onboarding_page(
item_id: workspace::ItemId,
workspace_id: workspace::WorkspaceId
) -> Result<Option<workspace::ItemId>> {
SELECT item_id
) -> Result<Option<u16>> {
SELECT page_number
FROM onboarding_pages
WHERE item_id = ? AND workspace_id = ?
}

View File

@@ -9,7 +9,7 @@ use workspace::{
item::{Item, ItemEvent},
with_active_or_new_workspace,
};
use zed_actions::{Extensions, OpenSettingsEditor, agent, command_palette};
use zed_actions::{Extensions, OpenSettings, agent, command_palette};
use crate::{Onboarding, OpenOnboarding};
@@ -53,7 +53,7 @@ const CONTENT: (Section<4>, Section<3>) = (
SectionEntry {
icon: IconName::Settings,
title: "Open Settings",
action: &OpenSettingsEditor,
action: &OpenSettings,
},
SectionEntry {
icon: IconName::ZedAssistant,
@@ -151,7 +151,6 @@ impl SectionEntry {
}
pub struct WelcomePage {
first_paint: bool,
focus_handle: FocusHandle,
}
@@ -169,10 +168,6 @@ impl WelcomePage {
impl Render for WelcomePage {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.first_paint {
window.request_animation_frame();
self.first_paint = false;
}
let (first_section, second_section) = CONTENT;
let first_section_entries = first_section.entries.len();
let last_index = first_section_entries + second_section.entries.len();
@@ -316,10 +311,7 @@ impl WelcomePage {
cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
.detach();
WelcomePage {
first_paint: true,
focus_handle,
}
WelcomePage { focus_handle }
})
}
}

View File

@@ -90,6 +90,7 @@ which.workspace = true
worktree.workspace = true
zeroize.workspace = true
zlog.workspace = true
feature_flags.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

View File

@@ -8,13 +8,15 @@ use std::{
};
use anyhow::{Context as _, Result, bail};
use client::Client;
use collections::HashMap;
use feature_flags::FeatureFlagAppExt as _;
use fs::{Fs, RemoveOptions, RenameOptions};
use futures::StreamExt as _;
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
};
use http_client::{HttpClient, github::AssetKind};
use http_client::github::AssetKind;
use node_runtime::NodeRuntime;
use remote::RemoteClient;
use rpc::{AnyProtoClient, TypedEnvelope, proto};
@@ -112,7 +114,6 @@ enum AgentServerStoreState {
project_environment: Entity<ProjectEnvironment>,
downstream_client: Option<(u64, AnyProtoClient)>,
settings: Option<AllAgentServersSettings>,
http_client: Arc<dyn HttpClient>,
_subscriptions: [Subscription; 1],
},
Remote {
@@ -125,6 +126,7 @@ enum AgentServerStoreState {
pub struct AgentServerStore {
state: AgentServerStoreState,
external_agents: HashMap<ExternalAgentServerName, Box<dyn ExternalAgentServer>>,
_feature_flag_subscription: Option<gpui::Subscription>,
}
pub struct AgentServersUpdated;
@@ -172,7 +174,6 @@ impl AgentServerStore {
project_environment,
downstream_client,
settings: old_settings,
http_client,
..
} = &mut self.state
else {
@@ -204,19 +205,32 @@ impl AgentServerStore {
.unwrap_or(true),
}),
);
self.external_agents.insert(
CODEX_NAME.into(),
Box::new(LocalCodex {
fs: fs.clone(),
project_environment: project_environment.clone(),
custom_command: new_settings
.codex
.clone()
.and_then(|settings| settings.custom_command()),
http_client: http_client.clone(),
is_remote: downstream_client.is_some(),
}),
);
self.external_agents
.extend(new_settings.custom.iter().map(|(name, settings)| {
(
ExternalAgentServerName(name.clone()),
Box::new(LocalCustomAgent {
command: settings.command.clone(),
project_environment: project_environment.clone(),
}) as Box<dyn ExternalAgentServer>,
)
}));
use feature_flags::FeatureFlagAppExt as _;
if cx.has_flag::<feature_flags::CodexAcpFeatureFlag>() || new_settings.codex.is_some() {
self.external_agents.insert(
CODEX_NAME.into(),
Box::new(LocalCodex {
fs: fs.clone(),
project_environment: project_environment.clone(),
custom_command: new_settings
.codex
.clone()
.and_then(|settings| settings.custom_command()),
}),
);
}
self.external_agents.insert(
CLAUDE_CODE_NAME.into(),
Box::new(LocalClaudeCode {
@@ -229,16 +243,6 @@ impl AgentServerStore {
.and_then(|settings| settings.custom_command()),
}),
);
self.external_agents
.extend(new_settings.custom.iter().map(|(name, settings)| {
(
ExternalAgentServerName(name.clone()),
Box::new(LocalCustomAgent {
command: settings.command.clone(),
project_environment: project_environment.clone(),
}) as Box<dyn ExternalAgentServer>,
)
}));
*old_settings = Some(new_settings.clone());
@@ -249,6 +253,7 @@ impl AgentServerStore {
names: self
.external_agents
.keys()
.filter(|name| name.0 != CODEX_NAME)
.map(|name| name.to_string())
.collect(),
})
@@ -261,53 +266,43 @@ impl AgentServerStore {
node_runtime: NodeRuntime,
fs: Arc<dyn Fs>,
project_environment: Entity<ProjectEnvironment>,
http_client: Arc<dyn HttpClient>,
cx: &mut Context<Self>,
) -> Self {
let subscription = cx.observe_global::<SettingsStore>(|this, cx| {
this.agent_servers_settings_changed(cx);
});
let this_handle = cx.weak_entity();
let feature_flags_subscription =
cx.observe_flag::<feature_flags::CodexAcpFeatureFlag, _>(move |_enabled, cx| {
let _ = this_handle.update(cx, |this, cx| {
this.reregister_agents(cx);
});
});
let mut this = Self {
state: AgentServerStoreState::Local {
node_runtime,
fs,
project_environment,
http_client,
downstream_client: None,
settings: None,
_subscriptions: [subscription],
},
external_agents: Default::default(),
_feature_flag_subscription: Some(feature_flags_subscription),
};
this.agent_servers_settings_changed(cx);
this
}
pub(crate) fn remote(project_id: u64, upstream_client: Entity<RemoteClient>) -> Self {
pub(crate) fn remote(
project_id: u64,
upstream_client: Entity<RemoteClient>,
_cx: &mut Context<Self>,
) -> Self {
// Set up the builtin agents here so they're immediately available in
// remote projects--we know that the HeadlessProject on the other end
// will have them.
let external_agents = [
(
CLAUDE_CODE_NAME.into(),
Box::new(RemoteExternalAgentServer {
project_id,
upstream_client: upstream_client.clone(),
name: CLAUDE_CODE_NAME.into(),
status_tx: None,
new_version_available_tx: None,
}) as Box<dyn ExternalAgentServer>,
),
(
CODEX_NAME.into(),
Box::new(RemoteExternalAgentServer {
project_id,
upstream_client: upstream_client.clone(),
name: CODEX_NAME.into(),
status_tx: None,
new_version_available_tx: None,
}) as Box<dyn ExternalAgentServer>,
),
(
GEMINI_NAME.into(),
Box::new(RemoteExternalAgentServer {
@@ -318,6 +313,16 @@ impl AgentServerStore {
new_version_available_tx: None,
}) as Box<dyn ExternalAgentServer>,
),
(
CLAUDE_CODE_NAME.into(),
Box::new(RemoteExternalAgentServer {
project_id,
upstream_client: upstream_client.clone(),
name: CLAUDE_CODE_NAME.into(),
status_tx: None,
new_version_available_tx: None,
}) as Box<dyn ExternalAgentServer>,
),
]
.into_iter()
.collect();
@@ -328,6 +333,7 @@ impl AgentServerStore {
upstream_client,
},
external_agents,
_feature_flag_subscription: None,
}
}
@@ -335,6 +341,7 @@ impl AgentServerStore {
Self {
state: AgentServerStoreState::Collab,
external_agents: Default::default(),
_feature_flag_subscription: None,
}
}
@@ -996,9 +1003,7 @@ impl ExternalAgentServer for LocalClaudeCode {
struct LocalCodex {
fs: Arc<dyn Fs>,
project_environment: Entity<ProjectEnvironment>,
http_client: Arc<dyn HttpClient>,
custom_command: Option<AgentServerCommand>,
is_remote: bool,
}
impl ExternalAgentServer for LocalCodex {
@@ -1012,13 +1017,11 @@ impl ExternalAgentServer for LocalCodex {
) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
let fs = self.fs.clone();
let project_environment = self.project_environment.downgrade();
let http = self.http_client.clone();
let custom_command = self.custom_command.clone();
let root_dir: Arc<Path> = root_dir
.map(|root_dir| Path::new(root_dir))
.unwrap_or(paths::home_dir())
.into();
let is_remote = self.is_remote;
cx.spawn(async move |cx| {
let mut env = project_environment
@@ -1027,9 +1030,6 @@ impl ExternalAgentServer for LocalCodex {
})?
.await
.unwrap_or_default();
if is_remote {
env.insert("NO_BROWSER".to_owned(), "1".to_owned());
}
let mut command = if let Some(mut custom_command) = custom_command {
env.extend(custom_command.env.unwrap_or_default());
@@ -1040,6 +1040,7 @@ impl ExternalAgentServer for LocalCodex {
fs.create_dir(&dir).await?;
// Find or install the latest Codex release (no update checks for now).
let http = cx.update(|cx| Client::global(cx).http_client())?;
let release = ::http_client::github::latest_github_release(
CODEX_ACP_REPO,
true,

View File

@@ -1154,13 +1154,7 @@ impl Project {
});
let agent_server_store = cx.new(|cx| {
AgentServerStore::local(
node.clone(),
fs.clone(),
environment.clone(),
client.http_client(),
cx,
)
AgentServerStore::local(node.clone(), fs.clone(), environment.clone(), cx)
});
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
@@ -1327,7 +1321,7 @@ impl Project {
});
let agent_server_store =
cx.new(|_| AgentServerStore::remote(REMOTE_SERVER_PROJECT_ID, remote.clone()));
cx.new(|cx| AgentServerStore::remote(REMOTE_SERVER_PROJECT_ID, remote.clone(), cx));
cx.subscribe(&remote, Self::on_remote_client_event).detach();
@@ -3258,9 +3252,9 @@ impl Project {
self.buffers_needing_diff.insert(buffer.downgrade());
let first_insertion = self.buffers_needing_diff.len() == 1;
let settings = ProjectSettings::get_global(cx);
let delay = settings.git.gutter_debounce;
if delay == 0 {
let delay = if let Some(delay) = settings.git.gutter_debounce {
delay
} else {
if first_insertion {
let this = cx.weak_entity();
cx.defer(move |cx| {
@@ -3272,7 +3266,7 @@ impl Project {
});
}
return;
}
};
const MIN_DELAY: u64 = 50;
let delay = delay.max(MIN_DELAY);

View File

@@ -300,8 +300,8 @@ pub struct GitSettings {
pub git_gutter: settings::GitGutterSetting,
/// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
///
/// Default: 0
pub gutter_debounce: u64,
/// Default: null
pub gutter_debounce: Option<u64>,
/// Whether or not to show git blame data inline in
/// the currently focused line.
///
@@ -446,7 +446,7 @@ impl Settings for ProjectSettings {
let git = content.git.as_ref().unwrap();
let git_settings = GitSettings {
git_gutter: git.git_gutter.unwrap(),
gutter_debounce: git.gutter_debounce.unwrap_or_default(),
gutter_debounce: git.gutter_debounce,
inline_blame: {
let inline = git.inline_blame.unwrap();
InlineBlameSettings {

View File

@@ -201,24 +201,15 @@ impl Project {
},
None => match activation_script.clone() {
activation_script if !activation_script.is_empty() => {
let separator = shell_kind.sequential_commands_separator();
let activation_script =
activation_script.join(&format!("{separator} "));
let activation_script = activation_script.join("; ");
let to_run = format_to_run();
let mut arg = format!("{activation_script}{separator} {to_run}");
if shell_kind == ShellKind::Cmd {
// We need to put the entire command in quotes since otherwise CMD tries to execute them
// as separate commands rather than chaining one after another.
arg = format!("\"{arg}\"");
}
let args = shell_kind.args_for_shell(false, arg);
let arg = format!("{activation_script}; {to_run}");
(
Shell::WithArguments {
program: shell,
args,
args: vec!["-c".to_owned(), arg],
title_override: None,
},
env,
@@ -244,7 +235,7 @@ impl Project {
task_state,
shell,
env,
settings.cursor_shape,
settings.cursor_shape.unwrap_or_default(),
settings.alternate_scroll,
settings.max_scroll_history_lines,
is_via_remote,
@@ -374,7 +365,7 @@ impl Project {
None,
shell,
env,
settings.cursor_shape,
settings.cursor_shape.unwrap_or_default(),
settings.alternate_scroll,
settings.max_scroll_history_lines,
is_via_remote,

View File

@@ -643,17 +643,9 @@ impl ProjectPanel {
.as_ref()
.is_some_and(|state| state.processing_filename.is_none())
{
match project_panel.confirm_edit(window, cx) {
Some(task) => {
task.detach_and_notify_err(window, cx);
}
None => {
project_panel.state.edit_state = None;
project_panel
.update_visible_entries(None, false, false, window, cx);
cx.notify();
}
}
project_panel.state.edit_state = None;
project_panel.update_visible_entries(None, false, false, window, cx);
cx.notify();
}
}
_ => {}

View File

@@ -1026,7 +1026,7 @@ fn build_command(
) -> Result<CommandTemplate> {
use std::fmt::Write as _;
let mut exec = String::new();
let mut exec = String::from("exec env -C ");
if let Some(working_dir) = working_dir {
let working_dir = RemotePathBuf::new(working_dir, ssh_path_style).to_string();
@@ -1035,14 +1035,13 @@ fn build_command(
const TILDE_PREFIX: &'static str = "~/";
if working_dir.starts_with(TILDE_PREFIX) {
let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/");
write!(exec, "cd \"$HOME/{working_dir}\" && ",).unwrap();
write!(exec, "\"$HOME/{working_dir}\" ",).unwrap();
} else {
write!(exec, "cd \"{working_dir}\" && ",).unwrap();
write!(exec, "\"{working_dir}\" ",).unwrap();
}
} else {
write!(exec, "cd && ").unwrap();
write!(exec, "\"$HOME\" ").unwrap();
};
write!(exec, "exec env ").unwrap();
for (k, v) in input_env.iter() {
if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) {
@@ -1107,7 +1106,7 @@ mod tests {
"-p",
"2222",
"-t",
"cd \"$HOME/work\" && exec env INPUT_VA=val remote_program arg1 arg2"
"exec env -C \"$HOME/work\" INPUT_VA=val remote_program arg1 arg2"
]
);
assert_eq!(command.env, env);
@@ -1138,7 +1137,7 @@ mod tests {
"-L",
"1:foo:2",
"-t",
"cd && exec env INPUT_VA=val /bin/fish -l"
"exec env -C \"$HOME\" INPUT_VA=val /bin/fish -l"
]
);
assert_eq!(command.env, env);

View File

@@ -195,13 +195,8 @@ impl HeadlessProject {
});
let agent_server_store = cx.new(|cx| {
let mut agent_server_store = AgentServerStore::local(
node_runtime.clone(),
fs.clone(),
environment,
http_client.clone(),
cx,
);
let mut agent_server_store =
AgentServerStore::local(node_runtime.clone(), fs.clone(), environment, cx);
agent_server_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
agent_server_store
});

View File

@@ -1792,7 +1792,7 @@ async fn test_remote_external_agent_server(
.map(|name| name.to_string())
.collect::<Vec<_>>()
});
pretty_assertions::assert_eq!(names, ["codex", "gemini", "claude"]);
pretty_assertions::assert_eq!(names, ["gemini", "claude"]);
server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
settings_store
.set_server_settings(
@@ -1822,7 +1822,7 @@ async fn test_remote_external_agent_server(
.map(|name| name.to_string())
.collect::<Vec<_>>()
});
pretty_assertions::assert_eq!(names, ["gemini", "codex", "claude", "foo"]);
pretty_assertions::assert_eq!(names, ["gemini", "foo", "claude"]);
let (command, root, login) = project
.update(cx, |project, cx| {
project.agent_server_store().update(cx, |store, cx| {

View File

@@ -383,18 +383,7 @@ pub struct DebuggerSettingsContent {
/// The granularity of one 'step' in the stepping requests `next`, `stepIn`, `stepOut`, and `stepBack`.
#[derive(
PartialEq,
Eq,
Debug,
Hash,
Clone,
Copy,
Deserialize,
Serialize,
JsonSchema,
MergeFrom,
strum::VariantArray,
strum::VariantNames,
PartialEq, Eq, Debug, Hash, Clone, Copy, Deserialize, Serialize, JsonSchema, MergeFrom,
)]
#[serde(rename_all = "snake_case")]
pub enum SteppingGranularity {
@@ -787,9 +776,7 @@ pub enum ShowIndentGuides {
}
#[skip_serializing_none]
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
pub struct IndentGuidesSettingsContent {
/// When to show the scrollbar in the outline panel.
pub show: Option<ShowIndentGuides>,

View File

@@ -1,4 +1,3 @@
use std::fmt::Display;
use std::num;
use collections::HashMap;
@@ -154,8 +153,7 @@ pub struct EditorSettingsContent {
///
/// Values range from 0 to 106. Set to 0 to disable adjustments.
/// Default: 45
#[schemars(range(min = 0, max = 106))]
pub minimum_contrast_for_highlights: Option<MinimumContrast>,
pub minimum_contrast_for_highlights: Option<f32>,
/// Whether to follow-up empty go to definition responses from the language server or not.
/// `FindAllReferences` allows to look up references of the same symbol instead.
@@ -427,18 +425,7 @@ pub enum DoubleClickInMultibuffer {
///
/// Default: always
#[derive(
Copy,
Clone,
Debug,
Default,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
PartialEq,
Eq,
strum::VariantArray,
strum::VariantNames,
Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
)]
#[serde(rename_all = "snake_case")]
pub enum MinimapThumb {
@@ -453,18 +440,7 @@ pub enum MinimapThumb {
///
/// Default: left_open
#[derive(
Copy,
Clone,
Debug,
Default,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
PartialEq,
Eq,
strum::VariantArray,
strum::VariantNames,
Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
)]
#[serde(rename_all = "snake_case")]
pub enum MinimapThumbBorder {
@@ -484,19 +460,7 @@ pub enum MinimapThumbBorder {
/// Which diagnostic indicators to show in the scrollbar.
///
/// Default: all
#[derive(
Copy,
Clone,
Debug,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
PartialEq,
Eq,
strum::VariantArray,
strum::VariantNames,
)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ScrollbarDiagnostics {
/// Show all diagnostic levels: hint, information, warnings, error.
@@ -718,18 +682,7 @@ pub struct DragAndDropSelectionContent {
///
/// Default: never
#[derive(
Copy,
Clone,
Debug,
Default,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
PartialEq,
Eq,
strum::VariantArray,
strum::VariantNames,
Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
)]
#[serde(rename_all = "snake_case")]
pub enum ShowMinimap {
@@ -746,18 +699,7 @@ pub enum ShowMinimap {
///
/// Default: all_editors
#[derive(
Copy,
Clone,
Debug,
Default,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
PartialEq,
Eq,
strum::VariantArray,
strum::VariantNames,
Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
)]
#[serde(rename_all = "snake_case")]
pub enum DisplayIn {
@@ -767,28 +709,3 @@ pub enum DisplayIn {
#[default]
ActiveEditor,
}
/// Minimum APCA perceptual contrast for text over highlight backgrounds.
///
/// Valid range: 0.0 to 106.0
/// Default: 45.0
#[derive(
Clone,
Copy,
Debug,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
PartialEq,
PartialOrd,
derive_more::FromStr,
)]
#[serde(transparent)]
pub struct MinimumContrast(pub f32);
impl Display for MinimumContrast {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.1}", self.0)
}
}

View File

@@ -7,9 +7,7 @@ use serde_with::skip_serializing_none;
use settings_macros::MergeFrom;
use util::serde::default_true;
use crate::{
AllLanguageSettingsContent, ExtendingVec, ProjectTerminalSettingsContent, SlashCommandSettings,
};
use crate::{AllLanguageSettingsContent, ExtendingVec, SlashCommandSettings};
#[skip_serializing_none]
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
@@ -31,9 +29,6 @@ pub struct ProjectSettingsContent {
#[serde(default)]
pub lsp: HashMap<Arc<str>, LspSettings>,
#[serde(default)]
pub terminal: Option<ProjectTerminalSettingsContent>,
/// Configuration for Debugger-related features
#[serde(default)]
pub dap: HashMap<Arc<str>, DapSettingsContent>,
@@ -254,7 +249,7 @@ pub struct GitSettings {
pub git_gutter: Option<GitGutterSetting>,
/// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
///
/// Default: 0
/// Default: null
pub gutter_debounce: Option<u64>,
/// Whether or not to show git blame data inline in
/// the currently focused line.

View File

@@ -9,8 +9,9 @@ use settings_macros::MergeFrom;
use crate::FontFamilyName;
#[skip_serializing_none]
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct ProjectTerminalSettingsContent {
pub struct TerminalSettingsContent {
/// What shell to use when opening a terminal.
///
/// Default: system
@@ -19,24 +20,6 @@ pub struct ProjectTerminalSettingsContent {
///
/// Default: current_project_directory
pub working_directory: Option<WorkingDirectory>,
/// Any key-value pairs added to this list will be added to the terminal's
/// environment. Use `:` to separate multiple values.
///
/// Default: {}
pub env: Option<HashMap<String, String>>,
/// Activates the python virtual environment, if one is found, in the
/// terminal's working directory (as resolved by the working_directory
/// setting). Set this to "off" to disable this behavior.
///
/// Default: on
pub detect_venv: Option<VenvSettings>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct TerminalSettingsContent {
#[serde(flatten)]
pub project: ProjectTerminalSettingsContent,
/// Sets the terminal's font size.
///
/// If this option is not included,
@@ -62,10 +45,15 @@ pub struct TerminalSettingsContent {
pub font_features: Option<FontFeatures>,
/// Sets the terminal's font weight in CSS weight units 0-900.
pub font_weight: Option<f32>,
/// Any key-value pairs added to this list will be added to the terminal's
/// environment. Use `:` to separate multiple values.
///
/// Default: {}
pub env: Option<HashMap<String, String>>,
/// Default cursor shape for the terminal.
/// Can be "bar", "block", "underline", or "hollow".
///
/// Default: "block"
/// Default: None
pub cursor_shape: Option<CursorShapeContent>,
/// Sets the cursor blinking behavior in the terminal.
///
@@ -89,7 +77,7 @@ pub struct TerminalSettingsContent {
pub copy_on_select: Option<bool>,
/// Whether to keep the text selection after copying it to the clipboard.
///
/// Default: true
/// Default: false
pub keep_selection_on_copy: Option<bool>,
/// Whether to show the terminal button in the status bar.
///
@@ -104,6 +92,12 @@ pub struct TerminalSettingsContent {
///
/// Default: 320
pub default_height: Option<f32>,
/// Activates the python virtual environment, if one is found, in the
/// terminal's working directory (as resolved by the working_directory
/// setting). Set this to "off" to disable this behavior.
///
/// Default: on
pub detect_venv: Option<VenvSettings>,
/// The maximum number of lines to keep in the scrollback history.
/// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
/// 0 disables the scrolling.
@@ -170,9 +164,7 @@ pub enum WorkingDirectory {
}
#[skip_serializing_none]
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
pub struct ScrollbarSettingsContent {
/// When to show the scrollbar in the terminal.
///
@@ -207,25 +199,11 @@ impl TerminalLineHeight {
/// When to show the scrollbar.
///
/// Default: auto
#[derive(
Copy,
Clone,
Debug,
Default,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
PartialEq,
Eq,
strum::VariantArray,
strum::VariantNames,
)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ShowScrollbar {
/// Show the scrollbar if there's important information or
/// follow the system's configured behavior.
#[default]
Auto,
/// Match the system's configured behavior.
System,
@@ -236,18 +214,7 @@ pub enum ShowScrollbar {
}
#[derive(
Clone,
Copy,
Debug,
Default,
Serialize,
Deserialize,
PartialEq,
Eq,
JsonSchema,
MergeFrom,
strum::VariantArray,
strum::VariantNames,
Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom,
)]
#[serde(rename_all = "snake_case")]
// todo() -> combine with CursorShape
@@ -263,19 +230,7 @@ pub enum CursorShapeContent {
Hollow,
}
#[derive(
Copy,
Clone,
Debug,
Serialize,
Deserialize,
PartialEq,
Eq,
JsonSchema,
MergeFrom,
strum::VariantArray,
strum::VariantNames,
)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
#[serde(rename_all = "snake_case")]
pub enum TerminalBlink {
/// Never blink the cursor, ignoring the terminal mode.
@@ -287,19 +242,7 @@ pub enum TerminalBlink {
On,
}
#[derive(
Clone,
Copy,
Debug,
Serialize,
Deserialize,
PartialEq,
Eq,
JsonSchema,
MergeFrom,
strum::VariantArray,
strum::VariantNames,
)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
#[serde(rename_all = "snake_case")]
pub enum AlternateScroll {
On,
@@ -389,33 +332,3 @@ pub enum ActivateScript {
PowerShell,
Pyenv,
}
#[cfg(test)]
mod test {
use serde_json::json;
use crate::{ProjectSettingsContent, Shell, UserSettingsContent};
#[test]
fn test_project_settings() {
let project_content =
json!({"terminal": {"shell": {"program": "/bin/project"}}, "option_as_meta": true});
let user_content =
json!({"terminal": {"shell": {"program": "/bin/user"}}, "option_as_meta": false});
let user_settings = serde_json::from_value::<UserSettingsContent>(user_content).unwrap();
let project_settings =
serde_json::from_value::<ProjectSettingsContent>(project_content).unwrap();
assert_eq!(
user_settings.content.terminal.unwrap().project.shell,
Some(Shell::Program("/bin/user".to_owned()))
);
assert_eq!(user_settings.content.project.terminal, None);
assert_eq!(
project_settings.terminal.unwrap().shell,
Some(Shell::Program("/bin/project".to_owned()))
);
}
}

View File

@@ -57,7 +57,7 @@ pub struct ThemeSettingsContent {
/// The font size for agent responses in the agent panel. Falls back to the UI font size if unset.
#[serde(default)]
pub agent_ui_font_size: Option<f32>,
/// The font size for user messages in the agent panel.
/// The font size for user messages in the agent panel. Falls back to the buffer font size if unset.
#[serde(default)]
pub agent_buffer_font_size: Option<f32>,
/// The name of the Zed theme to use.

View File

@@ -418,7 +418,7 @@ pub enum PaneSplitDirectionVertical {
}
#[skip_serializing_none]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
#[serde(rename_all = "snake_case")]
pub struct CenteredLayoutSettings {
/// The relative width of the left padding of the central pane from the
@@ -564,9 +564,7 @@ pub enum ProjectPanelEntrySpacing {
}
#[skip_serializing_none]
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
pub struct ProjectPanelIndentGuidesSettings {
pub show: Option<ShowIndentGuides>,
}

View File

@@ -162,8 +162,8 @@ pub enum SettingsFile {
User,
Server,
Default,
/// Represents project settings in ssh projects as well as local projects
Project((WorktreeId, Arc<RelPath>)),
/// Local also represents project settings in ssh projects as well as local projects
Local((WorktreeId, Arc<RelPath>)),
}
#[derive(Clone)]
@@ -469,7 +469,7 @@ impl SettingsStore {
// rev because these are sorted by path, so highest precedence is last
.rev()
.cloned()
.map(SettingsFile::Project),
.map(SettingsFile::Local),
);
if self.server_settings.is_some() {
@@ -496,7 +496,7 @@ impl SettingsStore {
.map(|settings| settings.content.as_ref()),
SettingsFile::Default => Some(self.default_settings.as_ref()),
SettingsFile::Server => self.server_settings.as_deref(),
SettingsFile::Project(ref key) => self.local_settings.get(key),
SettingsFile::Local(ref key) => self.local_settings.get(key),
}
}
@@ -515,8 +515,8 @@ impl SettingsStore {
continue;
}
if let SettingsFile::Project((wt_id, ref path)) = file
&& let SettingsFile::Project((target_wt_id, ref target_path)) = target_file
if let SettingsFile::Local((wt_id, ref path)) = file
&& let SettingsFile::Local((target_wt_id, ref target_path)) = target_file
&& (wt_id != target_wt_id || !target_path.starts_with(path))
{
// if requesting value from a local file, don't return values from local files in different worktrees
@@ -534,16 +534,12 @@ impl SettingsStore {
overrides
}
/// Checks the given file, and files that the passed file overrides for the given field.
/// Returns the first file found that contains the value.
/// The value will only be None if no file contains the value.
/// I.e. if no file contains the value, returns `(File::Default, None)`
pub fn get_value_from_file<T>(
&self,
target_file: SettingsFile,
pick: fn(&SettingsContent) -> &Option<T>,
) -> (SettingsFile, Option<&T>) {
// todo(settings_ui): Add a metadata field for overriding the "overrides" tag, for contextually different settings
) -> (SettingsFile, &T) {
// TODO: Add a metadata field for overriding the "overrides" tag, for contextually different settings
// e.g. disable AI isn't overridden, or a vec that gets extended instead or some such
// todo(settings_ui) cache all files
@@ -556,9 +552,9 @@ impl SettingsStore {
}
found_file = true;
if let SettingsFile::Project((worktree_id, ref path)) = file
&& let SettingsFile::Project((target_worktree_id, ref target_path)) = target_file
&& (worktree_id != target_worktree_id || !target_path.starts_with(&path))
if let SettingsFile::Local((wt_id, ref path)) = file
&& let SettingsFile::Local((target_wt_id, ref target_path)) = target_file
&& (wt_id != target_wt_id || !target_path.starts_with(&path))
{
// if requesting value from a local file, don't return values from local files in different worktrees
continue;
@@ -568,11 +564,11 @@ impl SettingsStore {
continue;
};
if let Some(value) = pick(content).as_ref() {
return (file, Some(value));
return (file, value);
}
}
(SettingsFile::Default, None)
unreachable!("All values should have defaults");
}
}
@@ -1718,17 +1714,17 @@ mod tests {
let default_value = get(&store.default_settings).unwrap();
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local.clone()), get),
(SettingsFile::User, Some(&0))
store.get_value_from_file(SettingsFile::Local(local.clone()), get),
(SettingsFile::User, &0)
);
assert_eq!(
store.get_value_from_file(SettingsFile::User, get),
(SettingsFile::User, Some(&0))
(SettingsFile::User, &0)
);
store.set_user_settings(r#"{}"#, cx).unwrap();
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local.clone()), get),
(SettingsFile::Default, Some(&default_value))
store.get_value_from_file(SettingsFile::Local(local.clone()), get),
(SettingsFile::Default, &default_value)
);
store
.set_local_settings(
@@ -1740,12 +1736,12 @@ mod tests {
)
.unwrap();
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local.clone()), get),
(SettingsFile::Project(local), Some(&80))
store.get_value_from_file(SettingsFile::Local(local.clone()), get),
(SettingsFile::Local(local), &80)
);
assert_eq!(
store.get_value_from_file(SettingsFile::User, get),
(SettingsFile::Default, Some(&default_value))
(SettingsFile::Default, &default_value)
);
}
@@ -1821,12 +1817,12 @@ mod tests {
// each local child should only inherit from it's parent
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local_2_child), get),
(SettingsFile::Project(local_2), Some(&2))
store.get_value_from_file(SettingsFile::Local(local_2_child), get),
(SettingsFile::Local(local_2), &2)
);
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local_1_child.clone()), get),
(SettingsFile::Project(local_1.clone()), Some(&1))
store.get_value_from_file(SettingsFile::Local(local_1_child.clone()), get),
(SettingsFile::Local(local_1.clone()), &1)
);
// adjacent children should be treated as siblings not inherit from each other
@@ -1851,8 +1847,8 @@ mod tests {
.unwrap();
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local_1_adjacent_child.clone()), get),
(SettingsFile::Project(local_1.clone()), Some(&1))
store.get_value_from_file(SettingsFile::Local(local_1_adjacent_child.clone()), get),
(SettingsFile::Local(local_1.clone()), &1)
);
store
.set_local_settings(
@@ -1873,8 +1869,8 @@ mod tests {
)
.unwrap();
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local_1_child), get),
(SettingsFile::Project(local_1), Some(&1))
store.get_value_from_file(SettingsFile::Local(local_1_child), get),
(SettingsFile::Local(local_1), &1)
);
}
@@ -1950,9 +1946,9 @@ mod tests {
overrides,
vec![
SettingsFile::User,
SettingsFile::Project(wt0_root.clone()),
SettingsFile::Project(wt0_child1.clone()),
SettingsFile::Project(wt1_root.clone()),
SettingsFile::Local(wt0_root.clone()),
SettingsFile::Local(wt0_child1.clone()),
SettingsFile::Local(wt1_root.clone()),
]
);
@@ -1960,26 +1956,25 @@ mod tests {
assert_eq!(
overrides,
vec![
SettingsFile::Project(wt0_root.clone()),
SettingsFile::Project(wt0_child1.clone()),
SettingsFile::Project(wt1_root.clone()),
SettingsFile::Local(wt0_root.clone()),
SettingsFile::Local(wt0_child1.clone()),
SettingsFile::Local(wt1_root.clone()),
]
);
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_root), get);
let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_root), get);
assert_eq!(overrides, vec![]);
let overrides =
store.get_overrides_for_field(SettingsFile::Project(wt0_child1.clone()), get);
let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_child1.clone()), get);
assert_eq!(overrides, vec![]);
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_child2), get);
let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_child2), get);
assert_eq!(overrides, vec![]);
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt1_root), get);
let overrides = store.get_overrides_for_field(SettingsFile::Local(wt1_root), get);
assert_eq!(overrides, vec![]);
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt1_subdir), get);
let overrides = store.get_overrides_for_field(SettingsFile::Local(wt1_subdir), get);
assert_eq!(overrides, vec![]);
let wt0_deep_child = (
@@ -1996,10 +1991,10 @@ mod tests {
)
.unwrap();
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_deep_child), get);
let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_deep_child), get);
assert_eq!(overrides, vec![]);
let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_child1), get);
let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_child1), get);
assert_eq!(overrides, vec![]);
}
}

View File

@@ -17,7 +17,7 @@ test-support = []
[dependencies]
anyhow.workspace = true
heck.workspace = true
command_palette_hooks.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
@@ -38,7 +38,6 @@ util.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
log.workspace = true
[dev-dependencies]
assets.workspace = true
@@ -52,3 +51,7 @@ session.workspace = true
settings.workspace = true
zlog.workspace = true
pretty_assertions.workspace = true
[[example]]
name = "ui"
path = "examples/ui.rs"

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,113 @@
use std::sync::Arc;
use futures::StreamExt;
use gpui::AppContext as _;
use settings::{DEFAULT_KEYMAP_PATH, KeymapFile, SettingsStore, watch_config_file};
use settings_ui::open_settings_editor;
use ui::BorrowAppContext;
fn merge_paths(a: &std::path::Path, b: &std::path::Path) -> std::path::PathBuf {
let a_parts: Vec<_> = a.components().collect();
let b_parts: Vec<_> = b.components().collect();
let mut overlap = 0;
for i in 0..=a_parts.len().min(b_parts.len()) {
if a_parts[a_parts.len() - i..] == b_parts[..i] {
overlap = i;
}
}
let mut result = std::path::PathBuf::new();
for part in &a_parts {
result.push(part.as_os_str());
}
for part in &b_parts[overlap..] {
result.push(part.as_os_str());
}
result
}
fn main() {
zlog::init();
zlog::init_output_stderr();
let [crate_path, file_path] = [env!("CARGO_MANIFEST_DIR"), file!()].map(std::path::Path::new);
let example_dir_abs_path = merge_paths(crate_path, file_path)
.parent()
.unwrap()
.to_path_buf();
let app = gpui::Application::new().with_assets(assets::Assets);
let fs = Arc::new(fs::RealFs::new(None, app.background_executor()));
let mut user_settings_file_rx = watch_config_file(
&app.background_executor(),
fs.clone(),
paths::settings_file().clone(),
);
app.run(move |cx| {
<dyn fs::Fs>::set_global(fs.clone(), cx);
settings::init(cx);
settings_ui::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
client::init_settings(cx);
workspace::init_settings(cx);
// production client because fake client requires gpui/test-support
// and that causes issues with the real stuff we want to do
let client = client::Client::production(cx);
let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
let languages = Arc::new(language::LanguageRegistry::new(
cx.background_executor().clone(),
));
client::init(&client, cx);
project::Project::init(&client, cx);
zlog::info!(
"Creating fake worktree in {}",
example_dir_abs_path.display(),
);
let project = project::Project::local(
client.clone(),
node_runtime::NodeRuntime::unavailable(),
user_store,
languages,
fs.clone(),
Some(Default::default()), // WARN: if None is passed here, prepare to be process bombed
cx,
);
let worktree_task = project.update(cx, |project, cx| {
project.create_worktree(example_dir_abs_path, true, cx)
});
cx.spawn(async move |_| {
let worktree = worktree_task.await.unwrap();
std::mem::forget(worktree);
})
.detach();
std::mem::forget(project);
language::init(cx);
editor::init(cx);
menu::init();
let keybindings =
KeymapFile::load_asset_allow_partial_failure(DEFAULT_KEYMAP_PATH, cx).unwrap();
cx.bind_keys(keybindings);
cx.spawn(async move |cx| {
while let Some(content) = user_settings_file_rx.next().await {
cx.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.set_user_settings(&content, cx).unwrap()
})
})
.ok();
}
})
.detach();
open_settings_editor(cx).unwrap();
cx.activate(true);
});
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -49,7 +49,7 @@ impl ShellBuilder {
format!("{} -C '{}'", self.program, command_to_use_in_label)
}
ShellKind::Cmd => {
format!("{} /C \"{}\"", self.program, command_to_use_in_label)
format!("{} /C '{}'", self.program, command_to_use_in_label)
}
ShellKind::Posix
| ShellKind::Nushell

View File

@@ -345,7 +345,6 @@ impl Shell {
Shell::System => get_system_shell(),
}
}
pub fn program_and_args(&self) -> (String, &[String]) {
match self {
Shell::Program(program) => (program.clone(), &[]),
@@ -353,14 +352,6 @@ impl Shell {
Shell::System => (get_system_shell(), &[]),
}
}
pub fn shell_kind(&self) -> ShellKind {
match self {
Shell::Program(program) => ShellKind::new(program),
Shell::WithArguments { program, .. } => ShellKind::new(program),
Shell::System => ShellKind::system(),
}
}
}
type VsCodeEnvVariable = String;

View File

@@ -409,7 +409,6 @@ impl TerminalBuilder {
events_rx,
})
}
pub fn new(
working_directory: Option<PathBuf>,
task: Option<TaskState>,
@@ -495,8 +494,6 @@ impl TerminalBuilder {
.unwrap_or(params.program.clone())
});
let shell_kind = shell.shell_kind();
let pty_options = {
let alac_shell = shell_params.as_ref().map(|params| {
alacritty_terminal::tty::Shell::new(
@@ -510,10 +507,8 @@ impl TerminalBuilder {
working_directory: working_directory.clone(),
drain_on_exit: true,
env: env.clone().into_iter().collect(),
// We do not want to escape arguments if we are using CMD as our shell.
// If we do we end up with too many quotes/escaped quotes for CMD to handle.
#[cfg(windows)]
escape_args: shell_kind != util::shell::ShellKind::Cmd,
escape_args: true,
}
};
@@ -583,7 +578,7 @@ impl TerminalBuilder {
let no_task = task.is_none();
let terminal = Terminal {
let mut terminal = Terminal {
task,
terminal_type: TerminalType::Pty {
pty_tx: Notifier(pty_tx),
@@ -623,23 +618,14 @@ impl TerminalBuilder {
if !activation_script.is_empty() && no_task {
for activation_script in activation_script {
terminal.write_to_pty(activation_script.into_bytes());
// Simulate enter key press
// NOTE(PowerShell): using `\r\n` will put PowerShell in a continuation mode (infamous >> character)
// and generally mess up the rendering.
terminal.write_to_pty(b"\x0d");
terminal.input(activation_script.into_bytes());
terminal.write_to_pty(if cfg!(windows) {
b"\r\n" as &[_]
} else {
b"\n"
});
}
// In order to clear the screen at this point, we have two options:
// 1. We can send a shell-specific command such as "clear" or "cls"
// 2. We can "echo" a marker message that we will then catch when handling a Wakeup event
// and clear the screen using `terminal.clear()` method
// We cannot issue a `terminal.clear()` command at this point as alacritty is evented
// and while we have sent the activation script to the pty, it will be executed asynchronously.
// Therefore, we somehow need to wait for the activation script to finish executing before we
// can proceed with clearing the screen.
terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes());
// Simulate enter key press
terminal.write_to_pty(b"\x0d");
terminal.clear();
}
Ok(TerminalBuilder {

View File

@@ -10,7 +10,6 @@ pub use settings::AlternateScroll;
use settings::{
CursorShapeContent, SettingsContent, ShowScrollbar, TerminalBlink, TerminalDockPosition,
TerminalLineHeight, TerminalSettingsContent, VenvSettings, WorkingDirectory,
merge_from::MergeFrom,
};
use task::Shell;
use theme::FontFamilyName;
@@ -31,7 +30,7 @@ pub struct TerminalSettings {
pub font_weight: Option<FontWeight>,
pub line_height: TerminalLineHeight,
pub env: HashMap<String, String>,
pub cursor_shape: CursorShape,
pub cursor_shape: Option<CursorShape>,
pub blinking: TerminalBlink,
pub alternate_scroll: AlternateScroll,
pub option_as_meta: bool,
@@ -74,16 +73,13 @@ fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell {
impl settings::Settings for TerminalSettings {
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
let user_content = content.terminal.clone().unwrap();
// Note: we allow a subset of "terminal" settings in the project files.
let mut project_content = user_content.project.clone();
project_content.merge_from_option(content.project.terminal.as_ref());
let content = content.terminal.clone().unwrap();
TerminalSettings {
shell: settings_shell_to_task_shell(project_content.shell.unwrap()),
working_directory: project_content.working_directory.unwrap(),
font_size: user_content.font_size.map(px),
font_family: user_content.font_family,
font_fallbacks: user_content.font_fallbacks.map(|fallbacks| {
shell: settings_shell_to_task_shell(content.shell.unwrap()),
working_directory: content.working_directory.unwrap(),
font_size: content.font_size.map(px),
font_family: content.font_family,
font_fallbacks: content.font_fallbacks.map(|fallbacks| {
FontFallbacks::from_fonts(
fallbacks
.into_iter()
@@ -91,29 +87,29 @@ impl settings::Settings for TerminalSettings {
.collect(),
)
}),
font_features: user_content.font_features,
font_weight: user_content.font_weight.map(FontWeight),
line_height: user_content.line_height.unwrap(),
env: project_content.env.unwrap(),
cursor_shape: user_content.cursor_shape.unwrap().into(),
blinking: user_content.blinking.unwrap(),
alternate_scroll: user_content.alternate_scroll.unwrap(),
option_as_meta: user_content.option_as_meta.unwrap(),
copy_on_select: user_content.copy_on_select.unwrap(),
keep_selection_on_copy: user_content.keep_selection_on_copy.unwrap(),
button: user_content.button.unwrap(),
dock: user_content.dock.unwrap(),
default_width: px(user_content.default_width.unwrap()),
default_height: px(user_content.default_height.unwrap()),
detect_venv: project_content.detect_venv.unwrap(),
max_scroll_history_lines: user_content.max_scroll_history_lines,
font_features: content.font_features,
font_weight: content.font_weight.map(FontWeight),
line_height: content.line_height.unwrap(),
env: content.env.unwrap(),
cursor_shape: content.cursor_shape.map(Into::into),
blinking: content.blinking.unwrap(),
alternate_scroll: content.alternate_scroll.unwrap(),
option_as_meta: content.option_as_meta.unwrap(),
copy_on_select: content.copy_on_select.unwrap(),
keep_selection_on_copy: content.keep_selection_on_copy.unwrap(),
button: content.button.unwrap(),
dock: content.dock.unwrap(),
default_width: px(content.default_width.unwrap()),
default_height: px(content.default_height.unwrap()),
detect_venv: content.detect_venv.unwrap(),
max_scroll_history_lines: content.max_scroll_history_lines,
toolbar: Toolbar {
breadcrumbs: user_content.toolbar.unwrap().breadcrumbs.unwrap(),
breadcrumbs: content.toolbar.unwrap().breadcrumbs.unwrap(),
},
scrollbar: ScrollbarSettings {
show: user_content.scrollbar.unwrap().show,
show: content.scrollbar.unwrap().show,
},
minimum_contrast: user_content.minimum_contrast.unwrap(),
minimum_contrast: content.minimum_contrast.unwrap(),
}
}
@@ -164,7 +160,7 @@ impl settings::Settings for TerminalSettings {
// TODO: handle arguments
let shell_name = format!("{platform}Exec");
if let Some(s) = vscode.read_string(&name(&shell_name)) {
current.project.shell = Some(settings::Shell::Program(s.to_owned()))
current.shell = Some(settings::Shell::Program(s.to_owned()))
}
if let Some(env) = vscode
@@ -173,15 +169,15 @@ impl settings::Settings for TerminalSettings {
{
for (k, v) in env {
if v.is_null()
&& let Some(zed_env) = current.project.env.as_mut()
&& let Some(zed_env) = current.env.as_mut()
{
zed_env.remove(k);
}
let Some(v) = v.as_str() else { continue };
if let Some(zed_env) = current.project.env.as_mut() {
if let Some(zed_env) = current.env.as_mut() {
zed_env.insert(k.clone(), v.to_owned());
} else {
current.project.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
current.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
}
}
}

View File

@@ -234,7 +234,9 @@ impl TerminalView {
terminal_view.focus_out(window, cx);
},
);
let cursor_shape = TerminalSettings::get_global(cx).cursor_shape;
let cursor_shape = TerminalSettings::get_global(cx)
.cursor_shape
.unwrap_or_default();
let scroll_handle = TerminalScrollHandle::new(terminal.read(cx));
@@ -425,7 +427,7 @@ impl TerminalView {
let breadcrumb_visibility_changed = self.show_breadcrumbs != settings.toolbar.breadcrumbs;
self.show_breadcrumbs = settings.toolbar.breadcrumbs;
let new_cursor_shape = settings.cursor_shape;
let new_cursor_shape = settings.cursor_shape.unwrap_or_default();
let old_cursor_shape = self.cursor_shape;
if old_cursor_shape != new_cursor_shape {
self.cursor_shape = new_cursor_shape;

View File

@@ -116,7 +116,7 @@ pub struct ThemeSettings {
pub buffer_font: Font,
/// The agent font size. Determines the size of text in the agent panel. Falls back to the UI font size if unset.
agent_ui_font_size: Option<Pixels>,
/// The agent buffer font size. Determines the size of user messages in the agent panel.
/// The agent buffer font size. Determines the size of user messages in the agent panel. Falls back to the buffer font size if unset.
agent_buffer_font_size: Option<Pixels>,
/// The line height for buffers, and the terminal.
///
@@ -549,7 +549,7 @@ impl ThemeSettings {
.unwrap_or_else(|| self.ui_font_size(cx))
}
/// Returns the agent panel buffer font size.
/// Returns the agent panel buffer font size. Falls back to the buffer font size if unset.
pub fn agent_buffer_font_size(&self, cx: &App) -> Pixels {
cx.try_global::<AgentFontSize>()
.map(|size| size.0)

View File

@@ -474,6 +474,7 @@ impl RenderOnce for Button {
}
}
// View this component preview using `workspace: open component-preview`
impl Component for Button {
fn scope() -> ComponentScope {
ComponentScope::Input

View File

@@ -135,12 +135,6 @@ pub enum ButtonStyle {
/// a fully transparent button.
Outlined,
/// Transparent button that always has an outline.
OutlinedTransparent,
/// A more de-emphasized version of the outlined button.
OutlinedGhost,
/// The default button style, used for most buttons. Has a transparent background,
/// but has a background color to indicate states like hover and active.
#[default]
@@ -152,38 +146,11 @@ pub enum ButtonStyle {
Transparent,
}
/// Rounding for a button that may have straight edges.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub(crate) struct ButtonLikeRounding {
/// Top-left corner rounding
pub top_left: bool,
/// Top-right corner rounding
pub top_right: bool,
/// Bottom-right corner rounding
pub bottom_right: bool,
/// Bottom-left corner rounding
pub bottom_left: bool,
}
impl ButtonLikeRounding {
pub const ALL: Self = Self {
top_left: true,
top_right: true,
bottom_right: true,
bottom_left: true,
};
pub const LEFT: Self = Self {
top_left: true,
top_right: false,
bottom_right: false,
bottom_left: true,
};
pub const RIGHT: Self = Self {
top_left: false,
top_right: true,
bottom_right: true,
bottom_left: false,
};
pub(crate) enum ButtonLikeRounding {
All,
Left,
Right,
}
#[derive(Debug, Clone)]
@@ -228,18 +195,6 @@ impl ButtonStyle {
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_background,
border_color: cx.theme().colors().border_variant,
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::OutlinedGhost => ButtonLikeStyles {
background: transparent_black(),
border_color: cx.theme().colors().border_variant,
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::Subtle => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_background,
border_color: transparent_black(),
@@ -285,18 +240,6 @@ impl ButtonStyle {
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_hover,
border_color: cx.theme().colors().border,
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::OutlinedGhost => ButtonLikeStyles {
background: transparent_black(),
border_color: cx.theme().colors().border,
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::Subtle => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_hover,
border_color: transparent_black(),
@@ -335,18 +278,6 @@ impl ButtonStyle {
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_active,
border_color: cx.theme().colors().border_variant,
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::OutlinedGhost => ButtonLikeStyles {
background: transparent_black(),
border_color: cx.theme().colors().border_variant,
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::Transparent => ButtonLikeStyles {
background: transparent_black(),
border_color: transparent_black(),
@@ -380,18 +311,6 @@ impl ButtonStyle {
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_background,
border_color: cx.theme().colors().border,
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::OutlinedGhost => ButtonLikeStyles {
background: transparent_black(),
border_color: cx.theme().colors().border,
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::Transparent => ButtonLikeStyles {
background: transparent_black(),
border_color: cx.theme().colors().border_focused,
@@ -428,18 +347,6 @@ impl ButtonStyle {
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_disabled,
border_color: cx.theme().colors().border_disabled,
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::OutlinedGhost => ButtonLikeStyles {
background: transparent_black(),
border_color: cx.theme().colors().border_disabled,
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
ButtonStyle::Transparent => ButtonLikeStyles {
background: transparent_black(),
border_color: transparent_black(),
@@ -515,7 +422,7 @@ impl ButtonLike {
width: None,
height: None,
size: ButtonSize::Default,
rounding: Some(ButtonLikeRounding::ALL),
rounding: Some(ButtonLikeRounding::All),
tooltip: None,
hoverable_tooltip: None,
children: SmallVec::new(),
@@ -529,15 +436,15 @@ impl ButtonLike {
}
pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
Self::new(id).rounding(ButtonLikeRounding::LEFT)
Self::new(id).rounding(ButtonLikeRounding::Left)
}
pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
Self::new(id).rounding(ButtonLikeRounding::RIGHT)
Self::new(id).rounding(ButtonLikeRounding::Right)
}
pub fn new_rounded_all(id: impl Into<ElementId>) -> Self {
Self::new(id).rounding(ButtonLikeRounding::ALL)
Self::new(id).rounding(ButtonLikeRounding::All)
}
pub fn opacity(mut self, opacity: f32) -> Self {
@@ -687,20 +594,13 @@ impl RenderOnce for ButtonLike {
.when_some(self.width, |this, width| {
this.w(width).justify_center().text_center()
})
.when(
matches!(
self.style,
ButtonStyle::Outlined
| ButtonStyle::OutlinedTransparent
| ButtonStyle::OutlinedGhost
),
|this| this.border_1(),
)
.when_some(self.rounding, |this, rounding| {
this.when(rounding.top_left, |this| this.rounded_tl_sm())
.when(rounding.top_right, |this| this.rounded_tr_sm())
.when(rounding.bottom_right, |this| this.rounded_br_sm())
.when(rounding.bottom_left, |this| this.rounded_bl_sm())
.when(matches!(self.style, ButtonStyle::Outlined), |this| {
this.border_1()
})
.when_some(self.rounding, |this, rounding| match rounding {
ButtonLikeRounding::All => this.rounded_sm(),
ButtonLikeRounding::Left => this.rounded_l_sm(),
ButtonLikeRounding::Right => this.rounded_r_sm(),
})
.gap(DynamicSpacing::Base04.rems(cx))
.map(|this| match self.size {
@@ -725,13 +625,7 @@ impl RenderOnce for ButtonLike {
|refinement: StyleRefinement| refinement.bg(hovered_style.background);
this.cursor(self.cursor_style)
.hover(focus_color)
.map(|this| {
if matches!(self.style, ButtonStyle::Outlined) {
this.focus(|s| s.border_color(cx.theme().colors().border_focused))
} else {
this.focus(focus_color)
}
})
.focus(focus_color)
.active(|active| active.bg(style.active(cx).background))
})
.when_some(

View File

@@ -6,41 +6,15 @@ use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, Tooltip,
/// The position of a [`ToggleButton`] within a group of buttons.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct ToggleButtonPosition {
/// The toggle button is one of the leftmost of the group.
leftmost: bool,
/// The toggle button is one of the rightmost of the group.
rightmost: bool,
/// The toggle button is one of the topmost of the group.
topmost: bool,
/// The toggle button is one of the bottommost of the group.
bottommost: bool,
}
pub enum ToggleButtonPosition {
/// The toggle button is first in the group.
First,
impl ToggleButtonPosition {
pub const HORIZONTAL_FIRST: Self = Self {
leftmost: true,
..Self::HORIZONTAL_MIDDLE
};
pub const HORIZONTAL_MIDDLE: Self = Self {
leftmost: false,
rightmost: false,
topmost: true,
bottommost: true,
};
pub const HORIZONTAL_LAST: Self = Self {
rightmost: true,
..Self::HORIZONTAL_MIDDLE
};
/// The toggle button is in the middle of the group (i.e., it is not the first or last toggle button).
Middle,
pub(crate) fn to_rounding(self) -> ButtonLikeRounding {
ButtonLikeRounding {
top_left: self.topmost && self.leftmost,
top_right: self.topmost && self.rightmost,
bottom_right: self.bottommost && self.rightmost,
bottom_left: self.bottommost && self.leftmost,
}
}
/// The toggle button is last in the group.
Last,
}
#[derive(IntoElement, RegisterComponent)]
@@ -72,15 +46,15 @@ impl ToggleButton {
}
pub fn first(self) -> Self {
self.position_in_group(ToggleButtonPosition::HORIZONTAL_FIRST)
self.position_in_group(ToggleButtonPosition::First)
}
pub fn middle(self) -> Self {
self.position_in_group(ToggleButtonPosition::HORIZONTAL_MIDDLE)
self.position_in_group(ToggleButtonPosition::Middle)
}
pub fn last(self) -> Self {
self.position_in_group(ToggleButtonPosition::HORIZONTAL_LAST)
self.position_in_group(ToggleButtonPosition::Last)
}
}
@@ -179,8 +153,10 @@ impl RenderOnce for ToggleButton {
};
self.base
.when_some(self.position_in_group, |this, position| {
this.rounding(position.to_rounding())
.when_some(self.position_in_group, |this, position| match position {
ToggleButtonPosition::First => this.rounding(ButtonLikeRounding::Left),
ToggleButtonPosition::Middle => this.rounding(None),
ToggleButtonPosition::Last => this.rounding(ButtonLikeRounding::Right),
})
.child(
Label::new(self.label)
@@ -559,15 +535,7 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
ButtonLike::new((group_name.clone(), entry_index))
.full_width()
.rounding(Some(
ToggleButtonPosition {
leftmost: col_index == 0,
rightmost: col_index == COLS - 1,
topmost: row_index == 0,
bottommost: row_index == ROWS - 1,
}
.to_rounding(),
))
.rounding(None)
.when_some(self.tab_index, |this, tab_index| {
this.tab_index(tab_index + entry_index as isize)
})

View File

@@ -1,5 +1,5 @@
use crate::KeyBinding;
use crate::prelude::*;
use crate::{h_flex, prelude::*};
use gpui::{AnyElement, App, BoxShadow, FontStyle, Hsla, IntoElement, Window, point};
use theme::Appearance;
@@ -206,23 +206,20 @@ impl KeybindingHint {
impl RenderOnce for KeybindingHint {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let colors = cx.theme().colors();
let colors = cx.theme().colors().clone();
let is_light = cx.theme().appearance() == Appearance::Light;
let border_color =
self.background_color
.blend(colors.text.alpha(if is_light { 0.08 } else { 0.16 }));
let bg_color = self
.background_color
.blend(colors.text_accent.alpha(if is_light { 0.05 } else { 0.1 }));
let bg_color =
self.background_color
.blend(colors.text.alpha(if is_light { 0.06 } else { 0.12 }));
let shadow_color = colors.text.alpha(if is_light { 0.04 } else { 0.08 });
let size = self
.size
.unwrap_or(TextSize::Small.rems(cx).to_pixels(window.rem_size()));
let kb_size = size - px(2.0);
let mut base = h_flex();
@@ -231,13 +228,15 @@ impl RenderOnce for KeybindingHint {
.get_or_insert_with(Default::default)
.font_style = Some(FontStyle::Italic);
base.gap_1()
base.items_center()
.gap_0p5()
.font_buffer(cx)
.text_size(size)
.text_color(colors.text_disabled)
.children(self.prefix)
.child(
h_flex()
.items_center()
.rounded_sm()
.px_0p5()
.mr_0p5()

View File

@@ -585,7 +585,7 @@ impl RenderOnce for Switch {
///
/// let switch_field = SwitchField::new(
/// "feature-toggle",
/// Some("Enable feature"),
/// "Enable feature",
/// Some("This feature adds new functionality to the app.".into()),
/// ToggleState::Unselected,
/// |state, window, cx| {
@@ -596,7 +596,7 @@ impl RenderOnce for Switch {
#[derive(IntoElement, RegisterComponent)]
pub struct SwitchField {
id: ElementId,
label: Option<SharedString>,
label: SharedString,
description: Option<SharedString>,
toggle_state: ToggleState,
on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
@@ -609,14 +609,14 @@ pub struct SwitchField {
impl SwitchField {
pub fn new(
id: impl Into<ElementId>,
label: Option<impl Into<SharedString>>,
label: impl Into<SharedString>,
description: Option<SharedString>,
toggle_state: impl Into<ToggleState>,
on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
label: label.map(Into::into),
label: label.into(),
description,
toggle_state: toggle_state.into(),
on_click: Arc::new(on_click),
@@ -657,11 +657,11 @@ impl SwitchField {
impl RenderOnce for SwitchField {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let tooltip = self
.tooltip
.zip(self.label.clone())
.map(|(tooltip_fn, label)| {
h_flex().gap_0p5().child(Label::new(label)).child(
let tooltip = self.tooltip.map(|tooltip_fn| {
h_flex()
.gap_0p5()
.child(Label::new(self.label.clone()))
.child(
IconButton::new("tooltip_button", IconName::Info)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
@@ -673,7 +673,7 @@ impl RenderOnce for SwitchField {
})
.on_click(|_, _, _| {}), // Intentional empty on click handler so that clicking on the info tooltip icon doesn't trigger the switch toggle
)
});
});
h_flex()
.id((self.id.clone(), "container"))
@@ -694,17 +694,11 @@ impl RenderOnce for SwitchField {
(Some(description), None) => v_flex()
.gap_0p5()
.max_w_5_6()
.when_some(self.label, |this, label| this.child(Label::new(label)))
.child(Label::new(self.label.clone()))
.child(Label::new(description.clone()).color(Color::Muted))
.into_any_element(),
(None, Some(tooltip)) => tooltip.into_any_element(),
(None, None) => {
if let Some(label) = self.label.clone() {
Label::new(label).into_any_element()
} else {
gpui::Empty.into_any_element()
}
}
(None, None) => Label::new(self.label.clone()).into_any_element(),
})
.child(
Switch::new((self.id.clone(), "switch"), self.toggle_state)
@@ -754,7 +748,7 @@ impl Component for SwitchField {
"Unselected",
SwitchField::new(
"switch_field_unselected",
Some("Enable notifications"),
"Enable notifications",
Some("Receive notifications when new messages arrive.".into()),
ToggleState::Unselected,
|_, _, _| {},
@@ -765,7 +759,7 @@ impl Component for SwitchField {
"Selected",
SwitchField::new(
"switch_field_selected",
Some("Enable notifications"),
"Enable notifications",
Some("Receive notifications when new messages arrive.".into()),
ToggleState::Selected,
|_, _, _| {},
@@ -781,7 +775,7 @@ impl Component for SwitchField {
"Default",
SwitchField::new(
"switch_field_default",
Some("Default color"),
"Default color",
Some("This uses the default switch color.".into()),
ToggleState::Selected,
|_, _, _| {},
@@ -792,7 +786,7 @@ impl Component for SwitchField {
"Accent",
SwitchField::new(
"switch_field_accent",
Some("Accent color"),
"Accent color",
Some("This uses the accent color scheme.".into()),
ToggleState::Selected,
|_, _, _| {},
@@ -808,7 +802,7 @@ impl Component for SwitchField {
"Disabled",
SwitchField::new(
"switch_field_disabled",
Some("Disabled field"),
"Disabled field",
Some("This field is disabled and cannot be toggled.".into()),
ToggleState::Selected,
|_, _, _| {},
@@ -823,7 +817,7 @@ impl Component for SwitchField {
"No Description",
SwitchField::new(
"switch_field_disabled",
Some("Disabled field"),
"Disabled field",
None,
ToggleState::Selected,
|_, _, _| {},
@@ -838,7 +832,7 @@ impl Component for SwitchField {
"Tooltip with Description",
SwitchField::new(
"switch_field_tooltip_with_desc",
Some("Nice Feature"),
"Nice Feature",
Some("Enable advanced configuration options.".into()),
ToggleState::Unselected,
|_, _, _| {},
@@ -850,7 +844,7 @@ impl Component for SwitchField {
"Tooltip without Description",
SwitchField::new(
"switch_field_tooltip_no_desc",
Some("Nice Feature"),
"Nice Feature",
None,
ToggleState::Selected,
|_, _, _| {},

View File

@@ -21,7 +21,6 @@ pub struct TreeViewItem {
on_toggle: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>>,
tab_index: Option<isize>,
focus_handle: Option<gpui::FocusHandle>,
}
impl TreeViewItem {
@@ -42,7 +41,6 @@ impl TreeViewItem {
on_toggle: None,
on_secondary_mouse_down: None,
tab_index: None,
focus_handle: None,
}
}
@@ -109,11 +107,6 @@ impl TreeViewItem {
self.focused = focused;
self
}
pub fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
self.focus_handle = Some(focus_handle.clone());
self
}
}
impl Disableable for TreeViewItem {
@@ -133,12 +126,11 @@ impl Toggleable for TreeViewItem {
impl RenderOnce for TreeViewItem {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let selected_bg = cx.theme().colors().element_active.opacity(0.5);
let transparent_border = cx.theme().colors().border.opacity(0.);
let selected_border = cx.theme().colors().border.opacity(0.6);
let focused_border = cx.theme().colors().border_focused;
let transparent_border = cx.theme().colors().border_transparent;
let item_size = rems_from_px(28.);
let indentation_line = h_flex().size(item_size).flex_none().justify_center().child(
div()
.w_px()
@@ -153,27 +145,26 @@ impl RenderOnce for TreeViewItem {
.child(
h_flex()
.id("inner_tree_view_item")
.group("tree_view_item")
.cursor_pointer()
.size_full()
.relative()
.when_some(self.tab_index, |this, index| this.tab_index(index))
.map(|this| {
let label = self.label;
if self.root_item {
this.h(item_size)
.px_1()
.mb_1()
.gap_2p5()
.rounded_sm()
.border_1()
.focus(|s| s.border_color(focused_border))
.border_color(transparent_border)
.when(self.selected, |this| {
this.border_color(selected_border).bg(selected_bg)
})
.focus(|s| s.border_color(focused_border))
.hover(|s| s.bg(cx.theme().colors().element_hover))
.when_some(self.focus_handle, |this, handle| {
this.track_focus(&handle)
})
.when_some(self.tab_index, |this, index| this.tab_index(index))
.child(
Disclosure::new("toggle", self.expanded)
.when_some(
@@ -189,18 +180,6 @@ impl RenderOnce for TreeViewItem {
Label::new(label)
.when(!self.selected, |this| this.color(Color::Muted)),
)
.when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover))
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| this.on_click(on_click),
)
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
this.on_mouse_down(
MouseButton::Right,
move |event, window, cx| (on_mouse_down)(event, window, cx),
)
})
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
} else {
this.child(indentation_line).child(
h_flex()
@@ -210,42 +189,44 @@ impl RenderOnce for TreeViewItem {
.px_1()
.rounded_sm()
.border_1()
.focusable()
.in_focus(|s| s.border_color(focused_border))
.border_color(transparent_border)
.when(self.selected, |this| {
this.border_color(selected_border).bg(selected_bg)
})
.focus(|s| s.border_color(focused_border))
.hover(|s| s.bg(cx.theme().colors().element_hover))
.when_some(self.focus_handle, |this, handle| {
this.track_focus(&handle)
})
.when_some(self.tab_index, |this, index| this.tab_index(index))
.child(
Label::new(label)
.when(!self.selected, |this| this.color(Color::Muted)),
)
.when_some(self.on_hover, |this, on_hover| {
this.on_hover(on_hover)
})
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| this.on_click(on_click),
)
.when_some(
self.on_secondary_mouse_down,
|this, on_mouse_down| {
this.on_mouse_down(
MouseButton::Right,
move |event, window, cx| {
(on_mouse_down)(event, window, cx)
},
)
},
)
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)),
),
)
}
}),
})
.when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover))
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| {
if self.root_item
&& let Some(on_toggle) = self.on_toggle.clone()
{
this.on_click(move |event, window, cx| {
if !event.is_keyboard() {
on_click(event, window, cx);
}
on_toggle(event, window, cx);
})
} else {
this.on_click(on_click)
}
},
)
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
this.on_mouse_down(MouseButton::Right, move |event, window, cx| {
(on_mouse_down)(event, window, cx)
})
})
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)),
)
}
}

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window};
use gpui::{AnyElement, App, Context, SharedString, Task, Window};
use picker::{Picker, PickerDelegate};
use theme::FontFamilyCache;
use ui::{ListItem, ListItemSpacing, prelude::*};
@@ -139,12 +139,7 @@ impl PickerDelegate for FontPickerDelegate {
}
}
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<FontPicker>) {
cx.defer_in(window, |picker, window, cx| {
picker.set_query("", window, cx);
});
cx.emit(DismissEvent);
}
fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<FontPicker>) {}
fn render_match(
&self,
@@ -177,5 +172,5 @@ pub fn font_picker(
Picker::uniform_list(delegate, window, cx)
.show_scrollbar(true)
.width(rems_from_px(210.))
.max_height(Some(rems(18.).into()))
.max_height(Some(rems(20.).into()))
}

View File

@@ -1,6 +1,6 @@
use std::{
fmt::Display,
num::{NonZero, NonZeroU32, NonZeroU64},
num::{NonZeroU32, NonZeroU64},
rc::Rc,
str::FromStr,
};
@@ -8,17 +8,26 @@ use std::{
use editor::{Editor, EditorStyle};
use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers};
use settings::{CodeFade, MinimumContrast};
use ui::prelude::*;
use settings::CodeFade;
use ui::{IconButtonShape, prelude::*};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NumberFieldMode {
pub enum NumericStepperStyle {
Outlined,
#[default]
Ghost,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NumericStepperMode {
#[default]
Read,
Edit,
}
pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr + 'static {
pub trait NumericStepperType:
Display + Copy + Clone + Sized + PartialOrd + FromStr + 'static
{
fn default_format(value: &Self) -> String {
format!("{}", value)
}
@@ -31,7 +40,7 @@ pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr
fn saturating_sub(self, rhs: Self) -> Self;
}
impl NumberFieldType for gpui::FontWeight {
impl NumericStepperType for gpui::FontWeight {
fn default_step() -> Self {
FontWeight(10.0)
}
@@ -55,7 +64,7 @@ impl NumberFieldType for gpui::FontWeight {
}
}
impl NumberFieldType for settings::CodeFade {
impl NumericStepperType for settings::CodeFade {
fn default_step() -> Self {
CodeFade(0.10)
}
@@ -79,33 +88,9 @@ impl NumberFieldType for settings::CodeFade {
}
}
impl NumberFieldType for settings::MinimumContrast {
fn default_step() -> Self {
MinimumContrast(1.0)
}
fn large_step() -> Self {
MinimumContrast(10.0)
}
fn small_step() -> Self {
MinimumContrast(0.5)
}
fn min_value() -> Self {
MinimumContrast(0.0)
}
fn max_value() -> Self {
MinimumContrast(106.0)
}
fn saturating_add(self, rhs: Self) -> Self {
MinimumContrast((self.0 + rhs.0).min(Self::max_value().0))
}
fn saturating_sub(self, rhs: Self) -> Self {
MinimumContrast((self.0 - rhs.0).max(Self::min_value().0))
}
}
macro_rules! impl_numeric_stepper_int {
($type:ident) => {
impl NumberFieldType for $type {
impl NumericStepperType for $type {
fn default_step() -> Self {
1
}
@@ -139,7 +124,7 @@ macro_rules! impl_numeric_stepper_int {
macro_rules! impl_numeric_stepper_nonzero_int {
($nonzero:ty, $inner:ty) => {
impl NumberFieldType for $nonzero {
impl NumericStepperType for $nonzero {
fn default_step() -> Self {
<$nonzero>::new(1).unwrap()
}
@@ -175,7 +160,7 @@ macro_rules! impl_numeric_stepper_nonzero_int {
macro_rules! impl_numeric_stepper_float {
($type:ident) => {
impl NumberFieldType for $type {
impl NumericStepperType for $type {
fn default_format(value: &Self) -> String {
format!("{:.2}", value)
}
@@ -222,14 +207,14 @@ impl_numeric_stepper_int!(u64);
impl_numeric_stepper_nonzero_int!(NonZeroU32, u32);
impl_numeric_stepper_nonzero_int!(NonZeroU64, u64);
impl_numeric_stepper_nonzero_int!(NonZero<usize>, usize);
#[derive(RegisterComponent)]
pub struct NumberField<T = usize> {
pub struct NumericStepper<T = usize> {
id: ElementId,
value: T,
style: NumericStepperStyle,
focus_handle: FocusHandle,
mode: Entity<NumberFieldMode>,
mode: Entity<NumericStepperMode>,
format: Box<dyn FnOnce(&T) -> String>,
large_step: T,
small_step: T,
@@ -241,12 +226,12 @@ pub struct NumberField<T = usize> {
tab_index: Option<isize>,
}
impl<T: NumberFieldType> NumberField<T> {
impl<T: NumericStepperType> NumericStepper<T> {
pub fn new(id: impl Into<ElementId>, value: T, window: &mut Window, cx: &mut App) -> Self {
let id = id.into();
let (mode, focus_handle) = window.with_id(id.clone(), |window| {
let mode = window.use_state(cx, |_, _| NumberFieldMode::default());
let mode = window.use_state(cx, |_, _| NumericStepperMode::default());
let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle());
(mode, focus_handle)
});
@@ -256,6 +241,7 @@ impl<T: NumberFieldType> NumberField<T> {
mode,
value,
focus_handle: focus_handle.read(cx).clone(),
style: NumericStepperStyle::default(),
format: Box::new(T::default_format),
large_step: T::large_step(),
step: T::default_step(),
@@ -298,6 +284,11 @@ impl<T: NumberFieldType> NumberField<T> {
self
}
pub fn style(mut self, style: NumericStepperStyle) -> Self {
self.style = style;
self
}
pub fn on_reset(
mut self,
on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
@@ -317,7 +308,7 @@ impl<T: NumberFieldType> NumberField<T> {
}
}
impl<T: NumberFieldType> IntoElement for NumberField<T> {
impl<T: NumericStepperType> IntoElement for NumericStepper<T> {
type Element = gpui::Component<Self>;
fn into_element(self) -> Self::Element {
@@ -325,8 +316,12 @@ impl<T: NumberFieldType> IntoElement for NumberField<T> {
}
}
impl<T: NumberFieldType> RenderOnce for NumberField<T> {
impl<T: NumericStepperType> RenderOnce for NumericStepper<T> {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let shape = IconButtonShape::Square;
let icon_size = IconSize::Small;
let is_outlined = matches!(self.style, NumericStepperStyle::Outlined);
let mut tab_index = self.tab_index;
let get_step = {
@@ -344,27 +339,6 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
}
};
let bg_color = cx.theme().colors().surface_background;
let hover_bg_color = cx.theme().colors().element_hover;
let border_color = cx.theme().colors().border_variant;
let focus_border_color = cx.theme().colors().border_focused;
let base_button = |icon: IconName| {
h_flex()
.cursor_pointer()
.p_1p5()
.size_full()
.justify_center()
.overflow_hidden()
.border_1()
.border_color(border_color)
.bg(bg_color)
.hover(|s| s.bg(hover_bg_color))
.focus(|s| s.border_color(focus_border_color).bg(hover_bg_color))
.child(Icon::new(icon).size(IconSize::Small))
};
h_flex()
.id(self.id.clone())
.track_focus(&self.focus_handle)
@@ -372,7 +346,8 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
.when_some(self.on_reset, |this, on_reset| {
this.child(
IconButton::new("reset", IconName::RotateCcw)
.icon_size(IconSize::Small)
.shape(shape)
.icon_size(icon_size)
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1)
@@ -382,6 +357,18 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
})
.child(
h_flex()
.gap_1()
.rounded_sm()
.map(|this| {
if is_outlined {
this.overflow_hidden()
.bg(cx.theme().colors().surface_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
} else {
this.px_1().bg(cx.theme().colors().editor_background)
}
})
.map(|decrement| {
let decrement_handler = {
let value = self.value;
@@ -396,46 +383,72 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
}
};
decrement.child(
base_button(IconName::Dash)
.id("decrement_button")
.rounded_tl_sm()
.rounded_bl_sm()
.tab_index(
tab_index
.as_mut()
.map(|tab_index| {
*tab_index += 1;
*tab_index - 1
if is_outlined {
decrement.child(
h_flex()
.id("decrement_button")
.p_1p5()
.size_full()
.justify_center()
.hover(|s| {
s.bg(cx.theme().colors().element_hover)
.cursor(gpui::CursorStyle::PointingHand)
})
.border_r_1()
.border_color(cx.theme().colors().border_variant)
.child(Icon::new(IconName::Dash).size(IconSize::Small))
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1).focus(|style| {
style.bg(cx.theme().colors().element_hover)
})
.unwrap_or(0),
)
.on_click(decrement_handler),
)
})
.on_click(decrement_handler),
)
} else {
decrement.child(
IconButton::new("decrement", IconName::Dash)
.shape(shape)
.icon_size(icon_size)
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1)
})
.on_click(decrement_handler),
)
}
})
.child(
h_flex()
.min_w_16()
.size_full()
.border_y_1()
.border_color(border_color)
.bg(bg_color)
.in_focus(|this| this.border_color(focus_border_color))
.w_full()
.border_1()
.border_color(cx.theme().colors().border_transparent)
.in_focus(|this| this.border_color(cx.theme().colors().border_focused))
.child(match *self.mode.read(cx) {
NumberFieldMode::Read => h_flex()
NumericStepperMode::Read => h_flex()
.id("numeric_stepper_label")
.px_1()
.flex_1()
.justify_center()
.child(Label::new((self.format)(&self.value)))
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1).focus(|style| {
style.bg(cx.theme().colors().element_hover)
})
})
.on_click({
let _mode = self.mode.clone();
move |click, _, _cx| {
if click.click_count() == 2 || click.is_keyboard() {
// Edit mode is disabled until we implement center text alignment for editor
// mode.write(cx, NumericStepperMode::Edit);
}
}
})
.into_any_element(),
// Edit mode is disabled until we implement center text alignment for editor
// mode.write(cx, NumberFieldMode::Edit);
//
// When we get to making Edit mode work, we shouldn't even focus the decrement/increment buttons.
// Focus should go instead straight to the editor, avoiding any double-step focus.
// In this world, the buttons become a mouse-only interaction, given users should be able
// to do everything they'd do with the buttons straight in the editor anyway.
NumberFieldMode::Edit => h_flex()
NumericStepperMode::Edit => h_flex()
.flex_1()
.child(window.use_state(cx, {
|window, cx| {
@@ -470,7 +483,7 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
}
on_change(&new_value, window, cx);
};
mode.write(cx, NumberFieldMode::Read);
mode.write(cx, NumericStepperMode::Read);
}
})
.detach();
@@ -501,34 +514,52 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
}
};
increment.child(
base_button(IconName::Plus)
.id("increment_button")
.rounded_tr_sm()
.rounded_br_sm()
.tab_index(
tab_index
.as_mut()
.map(|tab_index| {
*tab_index += 1;
*tab_index - 1
if is_outlined {
increment.child(
h_flex()
.id("increment_button")
.p_1p5()
.size_full()
.justify_center()
.hover(|s| {
s.bg(cx.theme().colors().element_hover)
.cursor(gpui::CursorStyle::PointingHand)
})
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.child(Icon::new(IconName::Plus).size(IconSize::Small))
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1).focus(|style| {
style.bg(cx.theme().colors().element_hover)
})
.unwrap_or(0),
)
.on_click(increment_handler),
)
})
.on_click(increment_handler),
)
} else {
increment.child(
IconButton::new("increment", IconName::Plus)
.shape(shape)
.icon_size(icon_size)
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1)
})
.on_click(increment_handler),
)
}
}),
)
}
}
impl Component for NumberField<usize> {
impl Component for NumericStepper<usize> {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn name() -> &'static str {
"Number Field"
"Numeric Stepper"
}
fn sort_name() -> &'static str {
@@ -536,30 +567,50 @@ impl Component for NumberField<usize> {
}
fn description() -> Option<&'static str> {
Some("A numeric input element with increment and decrement buttons.")
Some("A button used to increment or decrement a numeric value.")
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let stepper_example = window.use_state(cx, |_, _| 100.0);
let first_stepper = window.use_state(cx, |_, _| 100usize);
let second_stepper = window.use_state(cx, |_, _| 100.0);
Some(
v_flex()
.gap_6()
.children(vec![single_example(
"Default Numeric Stepper",
NumberField::new(
"numeric-stepper-component-preview",
*stepper_example.read(cx),
window,
cx,
)
.on_change({
let stepper_example = stepper_example.clone();
move |value, _, cx| stepper_example.write(cx, *value)
})
.min(1.0)
.max(100.0)
.into_any_element(),
.children(vec![example_group_with_title(
"Styles",
vec![
single_example(
"Default",
NumericStepper::new(
"numeric-stepper-component-preview",
*first_stepper.read(cx),
window,
cx,
)
.on_change({
let first_stepper = first_stepper.clone();
move |value, _, cx| first_stepper.write(cx, *value)
})
.into_any_element(),
),
single_example(
"Outlined",
NumericStepper::new(
"numeric-stepper-with-border-component-preview",
*second_stepper.read(cx),
window,
cx,
)
.on_change({
let second_stepper = second_stepper.clone();
move |value, _, cx| second_stepper.write(cx, *value)
})
.min(1.0)
.max(100.0)
.style(NumericStepperStyle::Outlined)
.into_any_element(),
),
],
)])
.into_any_element(),
)

View File

@@ -5,13 +5,13 @@
//! It can't be located in the `ui` crate because it depends on `editor`.
//!
mod font_picker;
mod number_field;
mod numeric_stepper;
use component::{example_group, single_example};
use editor::{Editor, EditorElement, EditorStyle};
pub use font_picker::*;
use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, Length, TextStyle};
pub use number_field::*;
pub use numeric_stepper::*;
use settings::Settings;
use std::sync::Arc;
use theme::ThemeSettings;

View File

@@ -353,7 +353,7 @@ impl ShellKind {
}
}
pub const fn command_prefix(&self) -> Option<char> {
pub fn command_prefix(&self) -> Option<char> {
match self {
ShellKind::PowerShell => Some('&'),
ShellKind::Nushell => Some('^'),
@@ -361,13 +361,6 @@ impl ShellKind {
}
}
pub const fn sequential_commands_separator(&self) -> char {
match self {
ShellKind::Cmd => '&',
_ => ';',
}
}
pub fn try_quote<'a>(&self, arg: &'a str) -> Option<Cow<'a, str>> {
shlex::try_quote(arg).ok().map(|arg| match self {
// If we are running in PowerShell, we want to take extra care when escaping strings.
@@ -377,23 +370,4 @@ impl ShellKind {
_ => arg,
})
}
pub const fn activate_keyword(&self) -> &'static str {
match self {
ShellKind::Cmd => "",
ShellKind::Nushell => "overlay use",
ShellKind::PowerShell => ".",
ShellKind::Fish => "source",
ShellKind::Csh => "source",
ShellKind::Tcsh => "source",
ShellKind::Posix | ShellKind::Rc => "source",
}
}
pub const fn clear_screen_command(&self) -> &'static str {
match self {
ShellKind::Cmd => "cls",
_ => "clear",
}
}
}

View File

@@ -2388,7 +2388,6 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
let display_point = map.clip_at_line_end(display_point);
let point = display_point.to_point(map);
let offset = point.to_offset(&map.buffer_snapshot());
let snapshot = map.buffer_snapshot();
// Ensure the range is contained by the current line.
let mut line_end = map.next_line_boundary(point).0;
@@ -2396,19 +2395,10 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
line_end = map.max_point().to_point(map);
}
// Attempt to find the smallest enclosing bracket range that also contains
// the offset, which only happens if the cursor is currently in a bracket.
let range_filter = |_buffer: &language::BufferSnapshot,
opening_range: Range<usize>,
closing_range: Range<usize>| {
opening_range.contains(&offset) || closing_range.contains(&offset)
};
let bracket_ranges = snapshot
.innermost_enclosing_bracket_ranges(offset..offset, Some(&range_filter))
.or_else(|| snapshot.innermost_enclosing_bracket_ranges(offset..offset, None));
if let Some((opening_range, closing_range)) = bracket_ranges {
if let Some((opening_range, closing_range)) = map
.buffer_snapshot()
.innermost_enclosing_bracket_ranges(offset..offset, None)
{
if opening_range.contains(&offset) {
return closing_range.start.to_display_point(map);
} else if closing_range.contains(&offset) {
@@ -2450,6 +2440,7 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
if distance < closest_distance {
closest_pair_destination = Some(close_range.start);
closest_distance = distance;
continue;
}
}
@@ -2460,6 +2451,7 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
if distance < closest_distance {
closest_pair_destination = Some(open_range.start);
closest_distance = distance;
continue;
}
}
@@ -3399,22 +3391,6 @@ mod test {
}"});
}
#[gpui::test]
async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new_tsx(cx).await;
cx.set_shared_state(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"})
.await;
cx.simulate_shared_keystrokes("%").await;
cx.shared_state()
.await
.assert_eq(indoc! {r"<Button onClick={() => {}ˇ}></Button>"});
cx.simulate_shared_keystrokes("%").await;
cx.shared_state()
.await
.assert_eq(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"});
}
#[gpui::test]
async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;

View File

@@ -207,26 +207,6 @@ impl NeovimBackedTestContext {
}
}
pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext {
#[cfg(feature = "neovim")]
cx.executor().allow_parking();
let thread = thread::current();
let test_name = thread
.name()
.expect("thread is not named")
.split(':')
.next_back()
.unwrap()
.to_string();
Self {
cx: VimTestContext::new_tsx(cx).await,
neovim: NeovimConnection::new(test_name).await,
last_set_state: None,
recent_keystrokes: Default::default(),
}
}
pub async fn set_shared_state(&mut self, marked_text: &str) {
let mode = if marked_text.contains('»') {
Mode::Visual

View File

@@ -66,28 +66,6 @@ impl VimTestContext {
)
}
pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> VimTestContext {
Self::init(cx);
Self::new_with_lsp(
EditorLspTestContext::new_tsx(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..Default::default()
}),
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
..Default::default()
},
cx,
)
.await,
true,
)
}
pub fn init_keybindings(enabled: bool, cx: &mut App) {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |s| s.vim_mode = Some(enabled));

View File

@@ -1,5 +0,0 @@
{"Put":{"state":"<Button onClick=ˇ{() => {}}></Button>"}}
{"Key":"%"}
{"Get":{"state":"<Button onClick={() => {}ˇ}></Button>","mode":"Normal"}}
{"Key":"%"}
{"Get":{"state":"<Button onClick=ˇ{() => {}}></Button>","mode":"Normal"}}

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.208.2"
version = "0.208.0"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
preview
dev

View File

@@ -224,9 +224,7 @@ pub fn main() {
Ok(path) => askpass::set_askpass_program(path),
Err(err) => {
eprintln!("Error: {}", err);
if std::option_env!("ZED_BUNDLE").is_some() {
process::exit(1);
}
process::exit(1);
}
}

View File

@@ -63,26 +63,22 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
MenuItem::submenu(Menu {
name: "Settings".into(),
items: vec![
MenuItem::action("Open Settings", zed_actions::OpenSettingsEditor),
MenuItem::action("Open Settings JSON", super::OpenSettings),
MenuItem::action("Open Project Settings", super::OpenProjectSettings),
MenuItem::action("Open Settings", super::OpenSettings),
MenuItem::action("Open Key Bindings", zed_actions::OpenKeymapEditor),
MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
MenuItem::separator(),
MenuItem::action("Open Keymap Editor", zed_actions::OpenKeymapEditor),
MenuItem::action("Open Keymap JSON", zed_actions::OpenKeymap),
MenuItem::action(
"Open Default Key Bindings",
zed_actions::OpenDefaultKeymap,
),
MenuItem::separator(),
MenuItem::action("Open Project Settings", super::OpenProjectSettings),
MenuItem::action(
"Select Settings Profile...",
zed_actions::settings_profile_selector::Toggle,
),
MenuItem::action(
"Select Theme...",
zed_actions::theme_selector::Toggle::default(),
),
MenuItem::action(
"Select Icon Theme...",
zed_actions::icon_theme_selector::Toggle::default(),
),
],
}),
MenuItem::separator(),

View File

@@ -3512,7 +3512,7 @@ List of `integer` column numbers
"alternate_scroll": "off",
"blinking": "terminal_controlled",
"copy_on_select": false,
"keep_selection_on_copy": true,
"keep_selection_on_copy": false,
"dock": "bottom",
"default_width": 640,
"default_height": 320,
@@ -3690,7 +3690,7 @@ List of `integer` column numbers
- Description: Whether or not to keep the selection in the terminal after copying text.
- Setting: `keep_selection_on_copy`
- Default: `true`
- Default: `false`
**Options**
@@ -3701,7 +3701,7 @@ List of `integer` column numbers
```json
{
"terminal": {
"keep_selection_on_copy": false
"keep_selection_on_copy": true
}
}
```