Compare commits
5 Commits
v0.208.2-p
...
settings-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bcb4bd400 | ||
|
|
98f26b2b43 | ||
|
|
9b3bb0b4aa | ||
|
|
5b85cdcf3c | ||
|
|
557199052e |
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
)],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"));
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
427
crates/onboarding/src/ai_setup_page.rs
Normal file
427
crates/onboarding/src/ai_setup_page.rs
Normal 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),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
612
crates/onboarding/src/editing_page.rs
Normal file
612
crates/onboarding/src/editing_page.rs
Normal 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))
|
||||
}
|
||||
@@ -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 = ?
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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![]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
1
crates/settings_ui/examples/.zed/settings.json
Normal file
1
crates/settings_ui/examples/.zed/settings.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
113
crates/settings_ui/examples/ui.rs
Normal file
113
crates/settings_ui/examples/ui.rs
Normal 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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|_, _, _| {},
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"}}
|
||||
@@ -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>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
preview
|
||||
dev
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user