Compare commits
15 Commits
ex-local
...
remote-pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d46eca9d3 | ||
|
|
f92403a6dd | ||
|
|
7adb4fb73d | ||
|
|
cedca47a82 | ||
|
|
ef3065abe7 | ||
|
|
8bfa78d34c | ||
|
|
115431d917 | ||
|
|
334ca21857 | ||
|
|
f58278aaf4 | ||
|
|
e10b9b70ef | ||
|
|
098adf3bdd | ||
|
|
a85c508f69 | ||
|
|
2a713c546b | ||
|
|
f937c1931f | ||
|
|
7a62f01ea5 |
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -12477,7 +12477,6 @@ dependencies = [
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"db",
|
||||
"encoding_rs",
|
||||
"extension",
|
||||
"fancy-regex",
|
||||
"fs",
|
||||
@@ -12648,6 +12647,8 @@ dependencies = [
|
||||
"paths",
|
||||
"rope",
|
||||
"serde",
|
||||
"strum 0.27.2",
|
||||
"tempfile",
|
||||
"text",
|
||||
"util",
|
||||
"uuid",
|
||||
|
||||
@@ -1321,6 +1321,14 @@
|
||||
"hidden_files": ["**/.*"],
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Global switch to enable or disable all git integration features.
|
||||
// If set to true, disables all git integration features.
|
||||
// If set to false, individual git integration features below will be independently enabled or disabled.
|
||||
"disable_git": false,
|
||||
// Whether to enable git status tracking.
|
||||
"enable_status": true,
|
||||
// Whether to enable git diff display.
|
||||
"enable_diff": true,
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
|
||||
@@ -426,7 +426,7 @@ impl NativeAgent {
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(UserRulesContext {
|
||||
uuid: prompt_metadata.id.user_id()?,
|
||||
uuid: prompt_metadata.id.as_user()?,
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
|
||||
@@ -338,7 +338,13 @@ impl AcpThreadView {
|
||||
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
|
||||
let available_commands = Rc::new(RefCell::new(vec![]));
|
||||
|
||||
let placeholder = placeholder_text(agent.name().as_ref(), false);
|
||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||
let agent_display_name = agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(agent.name()))
|
||||
.unwrap_or_else(|| agent.name());
|
||||
|
||||
let placeholder = placeholder_text(agent_display_name.as_ref(), false);
|
||||
|
||||
let message_editor = cx.new(|cx| {
|
||||
let mut editor = MessageEditor::new(
|
||||
@@ -377,7 +383,6 @@ impl AcpThreadView {
|
||||
)
|
||||
});
|
||||
|
||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||
let subscriptions = [
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
|
||||
cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
|
||||
@@ -1498,7 +1503,13 @@ impl AcpThreadView {
|
||||
let has_commands = !available_commands.is_empty();
|
||||
self.available_commands.replace(available_commands);
|
||||
|
||||
let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands);
|
||||
let agent_display_name = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
|
||||
.unwrap_or_else(|| self.agent.name());
|
||||
|
||||
let new_placeholder = placeholder_text(agent_display_name.as_ref(), has_commands);
|
||||
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(&new_placeholder, window, cx);
|
||||
|
||||
@@ -1586,7 +1586,7 @@ pub(crate) fn search_rules(
|
||||
None
|
||||
} else {
|
||||
Some(RulesContextEntry {
|
||||
prompt_id: metadata.id.user_id()?,
|
||||
prompt_id: metadata.id.as_user()?,
|
||||
title: metadata.title?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
|
||||
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
|
||||
pub const COMPLETION_MENU_MIN_WIDTH: Pixels = px(280.);
|
||||
pub const COMPLETION_MENU_MAX_WIDTH: Pixels = px(540.);
|
||||
pub const CODE_ACTION_MENU_MIN_WIDTH: Pixels = px(220.);
|
||||
pub const CODE_ACTION_MENU_MAX_WIDTH: Pixels = px(540.);
|
||||
|
||||
// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
|
||||
// documentation not yet being parsed.
|
||||
@@ -179,7 +181,7 @@ impl CodeContextMenu {
|
||||
) -> Option<AnyElement> {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
|
||||
CodeContextMenu::CodeActions(_) => None,
|
||||
CodeContextMenu::CodeActions(menu) => menu.render_aside(max_size, window, cx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1419,26 +1421,6 @@ pub enum CodeActionsItem {
|
||||
}
|
||||
|
||||
impl CodeActionsItem {
|
||||
fn as_task(&self) -> Option<&ResolvedTask> {
|
||||
let Self::Task(_, task) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(task)
|
||||
}
|
||||
|
||||
fn as_code_action(&self) -> Option<&CodeAction> {
|
||||
let Self::CodeAction { action, .. } = self else {
|
||||
return None;
|
||||
};
|
||||
Some(action)
|
||||
}
|
||||
fn as_debug_scenario(&self) -> Option<&DebugScenario> {
|
||||
let Self::DebugScenario(scenario) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(scenario)
|
||||
}
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
|
||||
@@ -1446,6 +1428,14 @@ impl CodeActionsItem {
|
||||
Self::DebugScenario(scenario) => scenario.label.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn menu_label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
|
||||
Self::Task(_, task) => task.resolved_label.replace("\n", ""),
|
||||
Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodeActionsMenu {
|
||||
@@ -1555,60 +1545,33 @@ impl CodeActionsMenu {
|
||||
let item_ix = range.start + ix;
|
||||
let selected = item_ix == selected_item;
|
||||
let colors = cx.theme().colors();
|
||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
||||
ListItem::new(item_ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.when_some(action.as_code_action(), |this, action| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
action.lsp_action.title().replace("\n", ""),
|
||||
)
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(action.as_task(), |this, task| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(task.resolved_label.replace("\n", ""))
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(action.as_debug_scenario(), |this, scenario| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child("debug: ")
|
||||
.child(scenario.label.clone())
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |editor, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
})),
|
||||
)
|
||||
|
||||
ListItem::new(item_ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.overflow_x()
|
||||
.child(
|
||||
div()
|
||||
.min_w(CODE_ACTION_MENU_MIN_WIDTH)
|
||||
.max_w(CODE_ACTION_MENU_MAX_WIDTH)
|
||||
.overflow_hidden()
|
||||
.text_ellipsis()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.when(selected, |this| this.text_color(colors.text_accent))
|
||||
.child(action.menu_label()),
|
||||
)
|
||||
.on_click(cx.listener(move |editor, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
@@ -1635,4 +1598,42 @@ impl CodeActionsMenu {
|
||||
|
||||
Popover::new().child(list).into_any_element()
|
||||
}
|
||||
|
||||
fn render_aside(
|
||||
&mut self,
|
||||
max_size: Size<Pixels>,
|
||||
window: &mut Window,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
let Some(action) = self.actions.get(self.selected_item) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let label = action.menu_label();
|
||||
let text_system = window.text_system();
|
||||
let mut line_wrapper = text_system.line_wrapper(
|
||||
window.text_style().font(),
|
||||
window.text_style().font_size.to_pixels(window.rem_size()),
|
||||
);
|
||||
let is_truncated =
|
||||
line_wrapper.should_truncate_line(&label, CODE_ACTION_MENU_MAX_WIDTH, "…");
|
||||
|
||||
if is_truncated.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
Popover::new()
|
||||
.child(
|
||||
div()
|
||||
.child(label)
|
||||
.id("code_actions_menu_extended")
|
||||
.px(MENU_ASIDE_X_PADDING / 2.)
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.occlude(),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,8 @@ impl Settings for EditorSettings {
|
||||
},
|
||||
scrollbar: Scrollbar {
|
||||
show: scrollbar.show.map(Into::into).unwrap(),
|
||||
git_diff: scrollbar.git_diff.unwrap(),
|
||||
git_diff: scrollbar.git_diff.unwrap()
|
||||
&& content.git.unwrap().enabled.unwrap().is_git_diff_enabled(),
|
||||
selected_text: scrollbar.selected_text.unwrap(),
|
||||
selected_symbol: scrollbar.selected_symbol.unwrap(),
|
||||
search_results: scrollbar.search_results.unwrap(),
|
||||
|
||||
@@ -1 +1 @@
|
||||
LICENSE-GPL
|
||||
../../LICENSE-GPL
|
||||
@@ -58,7 +58,7 @@ use project::{
|
||||
git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},
|
||||
project_settings::{GitPathStyle, ProjectSettings},
|
||||
};
|
||||
use prompt_store::{PromptId, PromptStore, RULES_FILE_NAMES};
|
||||
use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore, StatusStyle};
|
||||
use std::future::Future;
|
||||
@@ -2579,25 +2579,26 @@ impl GitPanel {
|
||||
is_using_legacy_zed_pro: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> String {
|
||||
const DEFAULT_PROMPT: &str = include_str!("commit_message_prompt.txt");
|
||||
|
||||
// Remove this once we stop supporting legacy Zed Pro
|
||||
// In legacy Zed Pro, Git commit summary generation did not count as a
|
||||
// prompt. If the user changes the prompt, our classification will fail,
|
||||
// meaning that users will be charged for generating commit messages.
|
||||
if is_using_legacy_zed_pro {
|
||||
return DEFAULT_PROMPT.to_string();
|
||||
return BuiltInPrompt::CommitMessage.default_content().to_string();
|
||||
}
|
||||
|
||||
let load = async {
|
||||
let store = cx.update(|cx| PromptStore::global(cx)).ok()?.await.ok()?;
|
||||
store
|
||||
.update(cx, |s, cx| s.load(PromptId::CommitMessage, cx))
|
||||
.update(cx, |s, cx| {
|
||||
s.load(PromptId::BuiltIn(BuiltInPrompt::CommitMessage), cx)
|
||||
})
|
||||
.ok()?
|
||||
.await
|
||||
.ok()
|
||||
};
|
||||
load.await.unwrap_or_else(|| DEFAULT_PROMPT.to_string())
|
||||
load.await
|
||||
.unwrap_or_else(|| BuiltInPrompt::CommitMessage.default_content().to_string())
|
||||
}
|
||||
|
||||
/// Generates a commit message using an LLM.
|
||||
|
||||
@@ -316,6 +316,7 @@ impl SystemWindowTabController {
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
|
||||
|
||||
let current_group = current_group?;
|
||||
// TODO: `.keys()` returns arbitrary order, what does "next" mean?
|
||||
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
|
||||
let idx = group_ids.iter().position(|g| *g == current_group)?;
|
||||
let next_idx = (idx + 1) % group_ids.len();
|
||||
@@ -340,6 +341,7 @@ impl SystemWindowTabController {
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
|
||||
|
||||
let current_group = current_group?;
|
||||
// TODO: `.keys()` returns arbitrary order, what does "previous" mean?
|
||||
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
|
||||
let idx = group_ids.iter().position(|g| *g == current_group)?;
|
||||
let prev_idx = if idx == 0 {
|
||||
@@ -361,12 +363,9 @@ impl SystemWindowTabController {
|
||||
|
||||
/// Get all tabs in the same window.
|
||||
pub fn tabs(&self, id: WindowId) -> Option<&Vec<SystemWindowTab>> {
|
||||
let tab_group = self
|
||||
.tab_groups
|
||||
.iter()
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group))?;
|
||||
|
||||
self.tab_groups.get(&tab_group)
|
||||
self.tab_groups
|
||||
.values()
|
||||
.find(|tabs| tabs.iter().any(|tab| tab.id == id))
|
||||
}
|
||||
|
||||
/// Initialize the visibility of the system window tab controller.
|
||||
@@ -441,7 +440,7 @@ impl SystemWindowTabController {
|
||||
/// Insert a tab into a tab group.
|
||||
pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec<SystemWindowTab>) {
|
||||
let mut controller = cx.global_mut::<SystemWindowTabController>();
|
||||
let Some(tab) = tabs.clone().into_iter().find(|tab| tab.id == id) else {
|
||||
let Some(tab) = tabs.iter().find(|tab| tab.id == id).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -504,16 +503,14 @@ impl SystemWindowTabController {
|
||||
return;
|
||||
};
|
||||
|
||||
let initial_tabs_len = initial_tabs.len();
|
||||
let mut all_tabs = initial_tabs.clone();
|
||||
for tabs in controller.tab_groups.values() {
|
||||
all_tabs.extend(
|
||||
tabs.iter()
|
||||
.filter(|tab| !initial_tabs.contains(tab))
|
||||
.cloned(),
|
||||
);
|
||||
|
||||
for (_, mut tabs) in controller.tab_groups.drain() {
|
||||
tabs.retain(|tab| !all_tabs[..initial_tabs_len].contains(tab));
|
||||
all_tabs.extend(tabs);
|
||||
}
|
||||
|
||||
controller.tab_groups.clear();
|
||||
controller.tab_groups.insert(0, all_tabs);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@ pub unsafe fn new_renderer(
|
||||
_native_window: *mut c_void,
|
||||
_native_view: *mut c_void,
|
||||
_bounds: crate::Size<f32>,
|
||||
_transparent: bool,
|
||||
transparent: bool,
|
||||
) -> Renderer {
|
||||
MetalRenderer::new(context)
|
||||
MetalRenderer::new(context, transparent)
|
||||
}
|
||||
|
||||
pub(crate) struct InstanceBufferPool {
|
||||
@@ -128,7 +128,7 @@ pub struct PathRasterizationVertex {
|
||||
}
|
||||
|
||||
impl MetalRenderer {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>, transparent: bool) -> Self {
|
||||
// Prefer low‐power integrated GPUs on Intel Mac. On Apple
|
||||
// Silicon, there is only ever one GPU, so this is equivalent to
|
||||
// `metal::Device::system_default()`.
|
||||
@@ -152,8 +152,13 @@ impl MetalRenderer {
|
||||
let layer = metal::MetalLayer::new();
|
||||
layer.set_device(&device);
|
||||
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
|
||||
layer.set_opaque(false);
|
||||
// Support direct-to-display rendering if the window is not transparent
|
||||
// https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos
|
||||
layer.set_opaque(!transparent);
|
||||
layer.set_maximum_drawable_count(3);
|
||||
// We already present at display sync with the display link
|
||||
// This allows to use direct-to-display even in window mode
|
||||
layer.set_display_sync_enabled(false);
|
||||
unsafe {
|
||||
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
|
||||
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
|
||||
@@ -352,8 +357,8 @@ impl MetalRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_transparency(&self, _transparent: bool) {
|
||||
// todo(mac)?
|
||||
pub fn update_transparency(&self, transparent: bool) {
|
||||
self.layer.set_opaque(!transparent);
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
|
||||
@@ -128,22 +128,21 @@ impl LineWrapper {
|
||||
})
|
||||
}
|
||||
|
||||
/// Truncate a line of text to the given width with this wrapper's font and font size.
|
||||
pub fn truncate_line<'a>(
|
||||
/// Determines if a line should be truncated based on its width.
|
||||
pub fn should_truncate_line(
|
||||
&mut self,
|
||||
line: SharedString,
|
||||
line: &str,
|
||||
truncate_width: Pixels,
|
||||
truncation_suffix: &str,
|
||||
runs: &'a [TextRun],
|
||||
) -> (SharedString, Cow<'a, [TextRun]>) {
|
||||
) -> Option<usize> {
|
||||
let mut width = px(0.);
|
||||
let mut suffix_width = truncation_suffix
|
||||
let suffix_width = truncation_suffix
|
||||
.chars()
|
||||
.map(|c| self.width_for_char(c))
|
||||
.fold(px(0.0), |a, x| a + x);
|
||||
let mut char_indices = line.char_indices();
|
||||
let mut truncate_ix = 0;
|
||||
for (ix, c) in char_indices {
|
||||
|
||||
for (ix, c) in line.char_indices() {
|
||||
if width + suffix_width < truncate_width {
|
||||
truncate_ix = ix;
|
||||
}
|
||||
@@ -152,16 +151,32 @@ impl LineWrapper {
|
||||
width += char_width;
|
||||
|
||||
if width.floor() > truncate_width {
|
||||
let result =
|
||||
SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
|
||||
let mut runs = runs.to_vec();
|
||||
update_runs_after_truncation(&result, truncation_suffix, &mut runs);
|
||||
|
||||
return (result, Cow::Owned(runs));
|
||||
return Some(truncate_ix);
|
||||
}
|
||||
}
|
||||
|
||||
(line, Cow::Borrowed(runs))
|
||||
None
|
||||
}
|
||||
|
||||
/// Truncate a line of text to the given width with this wrapper's font and font size.
|
||||
pub fn truncate_line<'a>(
|
||||
&mut self,
|
||||
line: SharedString,
|
||||
truncate_width: Pixels,
|
||||
truncation_suffix: &str,
|
||||
runs: &'a [TextRun],
|
||||
) -> (SharedString, Cow<'a, [TextRun]>) {
|
||||
if let Some(truncate_ix) =
|
||||
self.should_truncate_line(&line, truncate_width, truncation_suffix)
|
||||
{
|
||||
let result =
|
||||
SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
|
||||
let mut runs = runs.to_vec();
|
||||
update_runs_after_truncation(&result, truncation_suffix, &mut runs);
|
||||
(result, Cow::Owned(runs))
|
||||
} else {
|
||||
(line, Cow::Borrowed(runs))
|
||||
}
|
||||
}
|
||||
|
||||
/// Any character in this list should be treated as a word character,
|
||||
|
||||
@@ -50,7 +50,13 @@ impl Settings for OutlinePanelSettings {
|
||||
dock: panel.dock.unwrap(),
|
||||
file_icons: panel.file_icons.unwrap(),
|
||||
folder_icons: panel.folder_icons.unwrap(),
|
||||
git_status: panel.git_status.unwrap(),
|
||||
git_status: panel.git_status.unwrap()
|
||||
&& content
|
||||
.git
|
||||
.unwrap()
|
||||
.enabled
|
||||
.unwrap()
|
||||
.is_git_status_enabled(),
|
||||
indent_size: panel.indent_size.unwrap(),
|
||||
indent_guides: IndentGuidesSettings {
|
||||
show: panel.indent_guides.unwrap().show.unwrap(),
|
||||
|
||||
@@ -40,7 +40,6 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
dap.workspace = true
|
||||
encoding_rs.workspace = true
|
||||
extension.workspace = true
|
||||
fancy-regex.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -65,7 +65,7 @@ use debugger::{
|
||||
dap_store::{DapStore, DapStoreEvent},
|
||||
session::Session,
|
||||
};
|
||||
use encoding_rs;
|
||||
|
||||
pub use environment::ProjectEnvironment;
|
||||
#[cfg(test)]
|
||||
use futures::future::join_all;
|
||||
@@ -5444,48 +5444,6 @@ impl Project {
|
||||
worktree.read(cx).entry_for_path(rel_path).is_some()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_local_settings_file(
|
||||
&self,
|
||||
worktree_id: WorktreeId,
|
||||
rel_path: Arc<RelPath>,
|
||||
cx: &mut App,
|
||||
update: impl 'static + Send + FnOnce(&mut settings::SettingsContent, &App),
|
||||
) {
|
||||
let Some(worktree) = self.worktree_for_id(worktree_id, cx) else {
|
||||
// todo(settings_ui) error?
|
||||
return;
|
||||
};
|
||||
cx.spawn(async move |cx| {
|
||||
let file = worktree
|
||||
.update(cx, |worktree, cx| worktree.load_file(&rel_path, cx))?
|
||||
.await
|
||||
.context("Failed to load settings file")?;
|
||||
|
||||
let has_bom = file.has_bom;
|
||||
|
||||
let new_text = cx.read_global::<SettingsStore, _>(|store, cx| {
|
||||
store.new_text_for_update(file.text, move |settings| update(settings, cx))
|
||||
})?;
|
||||
worktree
|
||||
.update(cx, |worktree, cx| {
|
||||
let line_ending = text::LineEnding::detect(&new_text);
|
||||
worktree.write_file(
|
||||
rel_path.clone(),
|
||||
new_text.into(),
|
||||
line_ending,
|
||||
encoding_rs::UTF_8,
|
||||
has_bom,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
.context("Failed to write settings file")?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PathMatchCandidateSet {
|
||||
|
||||
@@ -332,6 +332,10 @@ impl GoToDiagnosticSeverityFilter {
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct GitSettings {
|
||||
/// Whether or not git integration is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
pub enabled: GitEnabledSettings,
|
||||
/// Whether or not to show the git gutter.
|
||||
///
|
||||
/// Default: tracked_files
|
||||
@@ -361,6 +365,18 @@ pub struct GitSettings {
|
||||
pub path_style: GitPathStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GitEnabledSettings {
|
||||
/// Whether git integration is enabled for showing git status.
|
||||
///
|
||||
/// Default: true
|
||||
pub status: bool,
|
||||
/// Whether git integration is enabled for showing diffs.
|
||||
///
|
||||
/// Default: true
|
||||
pub diff: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||
pub enum GitPathStyle {
|
||||
#[default]
|
||||
@@ -502,7 +518,14 @@ impl Settings for ProjectSettings {
|
||||
let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
|
||||
|
||||
let git = content.git.as_ref().unwrap();
|
||||
let git_enabled = {
|
||||
GitEnabledSettings {
|
||||
status: git.enabled.as_ref().unwrap().is_git_status_enabled(),
|
||||
diff: git.enabled.as_ref().unwrap().is_git_diff_enabled(),
|
||||
}
|
||||
};
|
||||
let git_settings = GitSettings {
|
||||
enabled: git_enabled,
|
||||
git_gutter: git.git_gutter.unwrap(),
|
||||
gutter_debounce: git.gutter_debounce.unwrap_or_default(),
|
||||
inline_blame: {
|
||||
|
||||
@@ -92,7 +92,13 @@ impl Settings for ProjectPanelSettings {
|
||||
entry_spacing: project_panel.entry_spacing.unwrap(),
|
||||
file_icons: project_panel.file_icons.unwrap(),
|
||||
folder_icons: project_panel.folder_icons.unwrap(),
|
||||
git_status: project_panel.git_status.unwrap(),
|
||||
git_status: project_panel.git_status.unwrap()
|
||||
&& content
|
||||
.git
|
||||
.unwrap()
|
||||
.enabled
|
||||
.unwrap()
|
||||
.is_git_status_enabled(),
|
||||
indent_size: project_panel.indent_size.unwrap(),
|
||||
indent_guides: IndentGuidesSettings {
|
||||
show: project_panel.indent_guides.unwrap().show.unwrap(),
|
||||
|
||||
@@ -28,6 +28,11 @@ parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
rope.workspace = true
|
||||
serde.workspace = true
|
||||
strum.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod prompts;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt as _;
|
||||
@@ -23,6 +23,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use strum::{EnumIter, IntoEnumIterator as _};
|
||||
use text::LineEnding;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
@@ -51,11 +52,51 @@ pub struct PromptMetadata {
|
||||
pub saved_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl PromptMetadata {
|
||||
fn builtin(builtin: BuiltInPrompt) -> Self {
|
||||
Self {
|
||||
id: PromptId::BuiltIn(builtin),
|
||||
title: Some(builtin.title().into()),
|
||||
default: false,
|
||||
saved_at: DateTime::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Built-in prompts that have default content and can be customized by users.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter)]
|
||||
pub enum BuiltInPrompt {
|
||||
CommitMessage,
|
||||
}
|
||||
|
||||
impl BuiltInPrompt {
|
||||
pub fn title(&self) -> &'static str {
|
||||
match self {
|
||||
Self::CommitMessage => "Commit message",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default content for this built-in prompt.
|
||||
pub fn default_content(&self) -> &'static str {
|
||||
match self {
|
||||
Self::CommitMessage => include_str!("../../git_ui/src/commit_message_prompt.txt"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BuiltInPrompt {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CommitMessage => write!(f, "Commit message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum PromptId {
|
||||
User { uuid: UserPromptId },
|
||||
CommitMessage,
|
||||
BuiltIn(BuiltInPrompt),
|
||||
}
|
||||
|
||||
impl PromptId {
|
||||
@@ -63,31 +104,37 @@ impl PromptId {
|
||||
UserPromptId::new().into()
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> Option<UserPromptId> {
|
||||
pub fn as_user(&self) -> Option<UserPromptId> {
|
||||
match self {
|
||||
Self::User { uuid } => Some(*uuid),
|
||||
_ => None,
|
||||
Self::BuiltIn { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_built_in(&self) -> Option<BuiltInPrompt> {
|
||||
match self {
|
||||
Self::User { .. } => None,
|
||||
Self::BuiltIn(builtin) => Some(*builtin),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_built_in(&self) -> bool {
|
||||
match self {
|
||||
Self::User { .. } => false,
|
||||
Self::CommitMessage => true,
|
||||
}
|
||||
matches!(self, Self::BuiltIn { .. })
|
||||
}
|
||||
|
||||
pub fn can_edit(&self) -> bool {
|
||||
match self {
|
||||
Self::User { .. } | Self::CommitMessage => true,
|
||||
Self::User { .. } => true,
|
||||
Self::BuiltIn(builtin) => match builtin {
|
||||
BuiltInPrompt::CommitMessage => true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_content(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::User { .. } => None,
|
||||
Self::CommitMessage => Some(include_str!("../../git_ui/src/commit_message_prompt.txt")),
|
||||
}
|
||||
impl From<BuiltInPrompt> for PromptId {
|
||||
fn from(builtin: BuiltInPrompt) -> Self {
|
||||
PromptId::BuiltIn(builtin)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +164,7 @@ impl std::fmt::Display for PromptId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PromptId::User { uuid } => write!(f, "{}", uuid.0),
|
||||
PromptId::CommitMessage => write!(f, "Commit message"),
|
||||
PromptId::BuiltIn(builtin) => write!(f, "{}", builtin),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,6 +197,16 @@ impl MetadataCache {
|
||||
cache.metadata.push(metadata.clone());
|
||||
cache.metadata_by_id.insert(prompt_id, metadata);
|
||||
}
|
||||
|
||||
// Insert all the built-in prompts that were not customized by the user
|
||||
for builtin in BuiltInPrompt::iter() {
|
||||
let builtin_id = PromptId::BuiltIn(builtin);
|
||||
if !cache.metadata_by_id.contains_key(&builtin_id) {
|
||||
let metadata = PromptMetadata::builtin(builtin);
|
||||
cache.metadata.push(metadata.clone());
|
||||
cache.metadata_by_id.insert(builtin_id, metadata);
|
||||
}
|
||||
}
|
||||
cache.sort();
|
||||
Ok(cache)
|
||||
}
|
||||
@@ -198,10 +255,6 @@ impl PromptStore {
|
||||
let mut txn = db_env.write_txn()?;
|
||||
let metadata = db_env.create_database(&mut txn, Some("metadata.v2"))?;
|
||||
let bodies = db_env.create_database(&mut txn, Some("bodies.v2"))?;
|
||||
|
||||
metadata.delete(&mut txn, &PromptId::CommitMessage)?;
|
||||
bodies.delete(&mut txn, &PromptId::CommitMessage)?;
|
||||
|
||||
txn.commit()?;
|
||||
|
||||
Self::upgrade_dbs(&db_env, metadata, bodies).log_err();
|
||||
@@ -294,7 +347,16 @@ impl PromptStore {
|
||||
let bodies = self.bodies;
|
||||
cx.background_spawn(async move {
|
||||
let txn = env.read_txn()?;
|
||||
let mut prompt = bodies.get(&txn, &id)?.context("prompt not found")?.into();
|
||||
let mut prompt: String = match bodies.get(&txn, &id)? {
|
||||
Some(body) => body.into(),
|
||||
None => {
|
||||
if let Some(built_in) = id.as_built_in() {
|
||||
built_in.default_content().into()
|
||||
} else {
|
||||
anyhow::bail!("prompt not found")
|
||||
}
|
||||
}
|
||||
};
|
||||
LineEnding::normalize(&mut prompt);
|
||||
Ok(prompt)
|
||||
})
|
||||
@@ -339,11 +401,6 @@ impl PromptStore {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the number of prompts in the store.
|
||||
pub fn prompt_count(&self) -> usize {
|
||||
self.metadata_cache.read().metadata.len()
|
||||
}
|
||||
|
||||
pub fn metadata(&self, id: PromptId) -> Option<PromptMetadata> {
|
||||
self.metadata_cache.read().metadata_by_id.get(&id).cloned()
|
||||
}
|
||||
@@ -412,23 +469,38 @@ impl PromptStore {
|
||||
return Task::ready(Err(anyhow!("this prompt cannot be edited")));
|
||||
}
|
||||
|
||||
let prompt_metadata = PromptMetadata {
|
||||
id,
|
||||
title,
|
||||
default,
|
||||
saved_at: Utc::now(),
|
||||
let body = body.to_string();
|
||||
let is_default_content = id
|
||||
.as_built_in()
|
||||
.is_some_and(|builtin| body.trim() == builtin.default_content().trim());
|
||||
|
||||
let metadata = if let Some(builtin) = id.as_built_in() {
|
||||
PromptMetadata::builtin(builtin)
|
||||
} else {
|
||||
PromptMetadata {
|
||||
id,
|
||||
title,
|
||||
default,
|
||||
saved_at: Utc::now(),
|
||||
}
|
||||
};
|
||||
self.metadata_cache.write().insert(prompt_metadata.clone());
|
||||
|
||||
self.metadata_cache.write().insert(metadata.clone());
|
||||
|
||||
let db_connection = self.env.clone();
|
||||
let bodies = self.bodies;
|
||||
let metadata = self.metadata;
|
||||
let metadata_db = self.metadata;
|
||||
|
||||
let task = cx.background_spawn(async move {
|
||||
let mut txn = db_connection.write_txn()?;
|
||||
|
||||
metadata.put(&mut txn, &id, &prompt_metadata)?;
|
||||
bodies.put(&mut txn, &id, &body.to_string())?;
|
||||
if is_default_content {
|
||||
metadata_db.delete(&mut txn, &id)?;
|
||||
bodies.delete(&mut txn, &id)?;
|
||||
} else {
|
||||
metadata_db.put(&mut txn, &id, &metadata)?;
|
||||
bodies.put(&mut txn, &id, &body)?;
|
||||
}
|
||||
|
||||
txn.commit()?;
|
||||
|
||||
@@ -490,3 +562,122 @@ impl PromptStore {
|
||||
pub struct GlobalPromptStore(Shared<Task<Result<Entity<PromptStore>, Arc<anyhow::Error>>>>);
|
||||
|
||||
impl Global for GlobalPromptStore {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_built_in_prompt_load_save(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db_path = temp_dir.path().join("prompts-db");
|
||||
|
||||
let store = cx.update(|cx| PromptStore::new(db_path, cx)).await.unwrap();
|
||||
let store = cx.new(|_cx| store);
|
||||
|
||||
let commit_message_id = PromptId::BuiltIn(BuiltInPrompt::CommitMessage);
|
||||
|
||||
let loaded_content = store
|
||||
.update(cx, |store, cx| store.load(commit_message_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut expected_content = BuiltInPrompt::CommitMessage.default_content().to_string();
|
||||
LineEnding::normalize(&mut expected_content);
|
||||
assert_eq!(
|
||||
loaded_content.trim(),
|
||||
expected_content.trim(),
|
||||
"Loading a built-in prompt not in DB should return default content"
|
||||
);
|
||||
|
||||
let metadata = store.read_with(cx, |store, _| store.metadata(commit_message_id));
|
||||
assert!(
|
||||
metadata.is_some(),
|
||||
"Built-in prompt should always have metadata"
|
||||
);
|
||||
assert!(
|
||||
store.read_with(cx, |store, _| {
|
||||
store
|
||||
.metadata_cache
|
||||
.read()
|
||||
.metadata_by_id
|
||||
.contains_key(&commit_message_id)
|
||||
}),
|
||||
"Built-in prompt should always be in cache"
|
||||
);
|
||||
|
||||
let custom_content = "Custom commit message prompt";
|
||||
store
|
||||
.update(cx, |store, cx| {
|
||||
store.save(
|
||||
commit_message_id,
|
||||
Some("Commit message".into()),
|
||||
false,
|
||||
Rope::from(custom_content),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let loaded_custom = store
|
||||
.update(cx, |store, cx| store.load(commit_message_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
loaded_custom.trim(),
|
||||
custom_content.trim(),
|
||||
"Custom content should be loaded after saving"
|
||||
);
|
||||
|
||||
assert!(
|
||||
store
|
||||
.read_with(cx, |store, _| store.metadata(commit_message_id))
|
||||
.is_some(),
|
||||
"Built-in prompt should have metadata after customization"
|
||||
);
|
||||
|
||||
store
|
||||
.update(cx, |store, cx| {
|
||||
store.save(
|
||||
commit_message_id,
|
||||
Some("Commit message".into()),
|
||||
false,
|
||||
Rope::from(BuiltInPrompt::CommitMessage.default_content()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let metadata_after_reset =
|
||||
store.read_with(cx, |store, _| store.metadata(commit_message_id));
|
||||
assert!(
|
||||
metadata_after_reset.is_some(),
|
||||
"Built-in prompt should still have metadata after reset"
|
||||
);
|
||||
assert_eq!(
|
||||
metadata_after_reset
|
||||
.as_ref()
|
||||
.and_then(|m| m.title.as_ref().map(|t| t.as_ref())),
|
||||
Some("Commit message"),
|
||||
"Built-in prompt should have default title after reset"
|
||||
);
|
||||
|
||||
let loaded_after_reset = store
|
||||
.update(cx, |store, cx| store.load(commit_message_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut expected_content_after_reset =
|
||||
BuiltInPrompt::CommitMessage.default_content().to_string();
|
||||
LineEnding::normalize(&mut expected_content_after_reset);
|
||||
assert_eq!(
|
||||
loaded_after_reset.trim(),
|
||||
expected_content_after_reset.trim(),
|
||||
"After saving default content, load should return default"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use collections::{HashMap, HashSet};
|
||||
use editor::{CompletionProvider, SelectionEffects};
|
||||
use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
|
||||
use gpui::{
|
||||
Action, App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable,
|
||||
PromptLevel, Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle,
|
||||
WindowOptions, actions, point, size, transparent_black,
|
||||
App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel,
|
||||
Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions,
|
||||
actions, point, size, transparent_black,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry, language_settings::SoftWrap};
|
||||
use language_model::{
|
||||
@@ -21,7 +21,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use title_bar::platform_title_bar::PlatformTitleBar;
|
||||
use ui::{Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
|
||||
use ui::{Divider, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{Workspace, WorkspaceSettings, client_side_decorations};
|
||||
use zed_actions::assistant::InlineAssist;
|
||||
@@ -206,13 +206,8 @@ impl PickerDelegate for RulePickerDelegate {
|
||||
self.filtered_entries.len()
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, cx: &mut App) -> Option<SharedString> {
|
||||
let text = if self.store.read(cx).prompt_count() == 0 {
|
||||
"No rules.".into()
|
||||
} else {
|
||||
"No rules found matching your search.".into()
|
||||
};
|
||||
Some(text)
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
Some("No rules found matching your search.".into())
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
@@ -680,13 +675,13 @@ impl RulesLibrary {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(default_content) = prompt_id.default_content() else {
|
||||
let Some(built_in) = prompt_id.as_built_in() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(rule_editor) = self.rule_editors.get(&prompt_id) {
|
||||
rule_editor.body_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(default_content, window, cx);
|
||||
editor.set_text(built_in.default_content(), window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1428,31 +1423,7 @@ impl Render for RulesLibrary {
|
||||
this.border_t_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(self.render_rule_list(cx))
|
||||
.map(|el| {
|
||||
if self.store.read(cx).prompt_count() == 0 {
|
||||
el.child(
|
||||
v_flex()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
Button::new("create-rule", "New Rule")
|
||||
.style(ButtonStyle::Outlined)
|
||||
.key_binding(KeyBinding::for_action(&NewRule, cx))
|
||||
.on_click(|_, window, cx| {
|
||||
window
|
||||
.dispatch_action(NewRule.boxed_clone(), cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
el.child(self.render_active_rule(cx))
|
||||
}
|
||||
}),
|
||||
.child(self.render_active_rule(cx)),
|
||||
),
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -288,6 +288,11 @@ impl std::fmt::Debug for ContextServerCommand {
|
||||
#[with_fallible_options]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
pub struct GitSettings {
|
||||
/// Whether or not to enable git integration.
|
||||
///
|
||||
/// Default: true
|
||||
#[serde(flatten)]
|
||||
pub enabled: Option<GitEnabledSettings>,
|
||||
/// Whether or not to show the git gutter.
|
||||
///
|
||||
/// Default: tracked_files
|
||||
@@ -317,6 +322,25 @@ pub struct GitSettings {
|
||||
pub path_style: Option<GitPathStyle>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct GitEnabledSettings {
|
||||
pub disable_git: Option<bool>,
|
||||
pub enable_status: Option<bool>,
|
||||
pub enable_diff: Option<bool>,
|
||||
}
|
||||
|
||||
impl GitEnabledSettings {
|
||||
pub fn is_git_status_enabled(&self) -> bool {
|
||||
!self.disable_git.unwrap_or(false) && self.enable_status.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn is_git_diff_enabled(&self) -> bool {
|
||||
!self.disable_git.unwrap_or(false) && self.enable_diff.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
|
||||
@@ -27,6 +27,7 @@ fs.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
heck.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
paths.workspace = true
|
||||
|
||||
@@ -17,7 +17,7 @@ where
|
||||
labels: &'static [&'static str],
|
||||
should_do_title_case: bool,
|
||||
tab_index: Option<isize>,
|
||||
on_change: Rc<dyn Fn(T, &mut App) + 'static>,
|
||||
on_change: Rc<dyn Fn(T, &mut ui::Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl<T> EnumVariantDropdown<T>
|
||||
@@ -29,7 +29,7 @@ where
|
||||
current_value: T,
|
||||
variants: &'static [T],
|
||||
labels: &'static [&'static str],
|
||||
on_change: impl Fn(T, &mut App) + 'static,
|
||||
on_change: impl Fn(T, &mut ui::Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
@@ -78,8 +78,8 @@ where
|
||||
value == current_value,
|
||||
IconPosition::End,
|
||||
None,
|
||||
move |_, cx| {
|
||||
on_change(value, cx);
|
||||
move |window, cx| {
|
||||
on_change(value, window, cx);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ pub struct FontPickerDelegate {
|
||||
filtered_fonts: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
current_font: SharedString,
|
||||
on_font_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
|
||||
on_font_changed: Arc<dyn Fn(SharedString, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl FontPickerDelegate {
|
||||
fn new(
|
||||
current_font: SharedString,
|
||||
on_font_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
on_font_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
cx: &mut Context<FontPicker>,
|
||||
) -> Self {
|
||||
let font_family_cache = FontFamilyCache::global(cx);
|
||||
@@ -132,10 +132,10 @@ impl PickerDelegate for FontPickerDelegate {
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<FontPicker>) {
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<FontPicker>) {
|
||||
if let Some(font_match) = self.filtered_fonts.get(self.selected_index) {
|
||||
let font = font_match.string.clone();
|
||||
(self.on_font_changed)(font.into(), cx);
|
||||
(self.on_font_changed)(font.into(), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ impl PickerDelegate for FontPickerDelegate {
|
||||
|
||||
pub fn font_picker(
|
||||
current_font: SharedString,
|
||||
on_font_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
on_font_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<FontPicker>,
|
||||
) -> FontPicker {
|
||||
|
||||
@@ -13,13 +13,13 @@ pub struct IconThemePickerDelegate {
|
||||
filtered_themes: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
|
||||
on_theme_changed: Arc<dyn Fn(SharedString, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl IconThemePickerDelegate {
|
||||
fn new(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
on_theme_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) -> Self {
|
||||
let theme_registry = ThemeRegistry::global(cx);
|
||||
@@ -32,15 +32,15 @@ impl IconThemePickerDelegate {
|
||||
|
||||
let selected_index = icon_themes
|
||||
.iter()
|
||||
.position(|icon_themes| *icon_themes == current_theme)
|
||||
.position(|icon_theme| *icon_theme == current_theme)
|
||||
.unwrap_or(0);
|
||||
|
||||
let filtered_themes = icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, icon_themes)| StringMatch {
|
||||
.map(|(index, theme)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: icon_themes.to_string(),
|
||||
string: theme.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
@@ -67,13 +67,18 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<IconThemePicker>) {
|
||||
self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1));
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
index: usize,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) {
|
||||
self.selected_index = index.min(self.filtered_themes.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Search icon theme…".into()
|
||||
"Search icon themes…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
@@ -89,9 +94,9 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, icon_theme)| StringMatch {
|
||||
.map(|(index, theme)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: icon_theme.to_string(),
|
||||
string: theme.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
@@ -100,16 +105,16 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
let _candidates: Vec<StringMatchCandidate> = icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, icon_theme)| StringMatchCandidate::new(id, icon_theme.as_ref()))
|
||||
.map(|(id, theme)| StringMatchCandidate::new(id, theme.as_ref()))
|
||||
.collect();
|
||||
|
||||
icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, icon_theme)| icon_theme.to_lowercase().contains(&query.to_lowercase()))
|
||||
.map(|(index, icon_theme)| StringMatch {
|
||||
.filter(|(_, theme)| theme.to_lowercase().contains(&query.to_lowercase()))
|
||||
.map(|(index, theme)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: icon_theme.to_string(),
|
||||
string: theme.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
@@ -119,7 +124,7 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
let selected_index = if query.is_empty() {
|
||||
icon_themes
|
||||
.iter()
|
||||
.position(|icon_theme| *icon_theme == current_theme)
|
||||
.position(|theme| *theme == current_theme)
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
matches
|
||||
@@ -138,12 +143,12 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) {
|
||||
if let Some(theme_match) = self.filtered_themes.get(self.selected_index) {
|
||||
let theme = theme_match.string.clone();
|
||||
(self.on_theme_changed)(theme.into(), cx);
|
||||
(self.on_theme_changed)(theme.into(), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,15 +161,15 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
index: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<IconThemePicker>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let theme_match = self.filtered_themes.get(ix)?;
|
||||
let theme_match = self.filtered_themes.get(index)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
ListItem::new(index)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
@@ -176,7 +181,7 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
|
||||
pub fn icon_theme_picker(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
on_theme_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) -> IconThemePicker {
|
||||
|
||||
@@ -9,7 +9,7 @@ use ui::{
|
||||
pub struct SettingsInputField {
|
||||
initial_text: Option<String>,
|
||||
placeholder: Option<&'static str>,
|
||||
confirm: Option<Box<dyn Fn(Option<String>, &mut App)>>,
|
||||
confirm: Option<Box<dyn Fn(Option<String>, &mut Window, &mut App)>>,
|
||||
tab_index: Option<isize>,
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ impl SettingsInputField {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_confirm(mut self, confirm: impl Fn(Option<String>, &mut App) + 'static) -> Self {
|
||||
pub fn on_confirm(
|
||||
mut self,
|
||||
confirm: impl Fn(Option<String>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.confirm = Some(Box::new(confirm));
|
||||
self
|
||||
}
|
||||
@@ -83,13 +86,13 @@ impl RenderOnce for SettingsInputField {
|
||||
.child(editor)
|
||||
.when_some(self.confirm, |this, confirm| {
|
||||
this.on_action::<menu::Confirm>({
|
||||
move |_, _, cx| {
|
||||
move |_, window, cx| {
|
||||
let Some(editor) = weak_editor.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let new_value = editor.read_with(cx, |editor, cx| editor.text(cx));
|
||||
let new_value = (!new_value.is_empty()).then_some(new_value);
|
||||
confirm(new_value, cx);
|
||||
confirm(new_value, window, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,13 +13,13 @@ pub struct ThemePickerDelegate {
|
||||
filtered_themes: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
|
||||
on_theme_changed: Arc<dyn Fn(SharedString, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl ThemePickerDelegate {
|
||||
fn new(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
on_theme_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
cx: &mut Context<ThemePicker>,
|
||||
) -> Self {
|
||||
let theme_registry = ThemeRegistry::global(cx);
|
||||
@@ -130,10 +130,10 @@ impl PickerDelegate for ThemePickerDelegate {
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<ThemePicker>) {
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<ThemePicker>) {
|
||||
if let Some(theme_match) = self.filtered_themes.get(self.selected_index) {
|
||||
let theme = theme_match.string.clone();
|
||||
(self.on_theme_changed)(theme.into(), cx);
|
||||
(self.on_theme_changed)(theme.into(), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ impl PickerDelegate for ThemePickerDelegate {
|
||||
|
||||
pub fn theme_picker(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
on_theme_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ThemePicker>,
|
||||
) -> ThemePicker {
|
||||
|
||||
@@ -5519,6 +5519,102 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
SettingsPage {
|
||||
title: "Version Control",
|
||||
items: vec![
|
||||
SettingsPageItem::SectionHeader("Git Integration"),
|
||||
SettingsPageItem::DynamicItem(DynamicItem {
|
||||
discriminant: SettingItem {
|
||||
files: USER,
|
||||
title: "Disable Git Integration",
|
||||
description: "Disable all Git integration features in Zed.",
|
||||
field: Box::new(SettingField::<bool> {
|
||||
json_path: Some("git.disable_git"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.disable_git
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.enabled
|
||||
.get_or_insert_default()
|
||||
.disable_git = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
},
|
||||
pick_discriminant: |settings_content| {
|
||||
let disabled = settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.disable_git
|
||||
.unwrap_or(false);
|
||||
Some(if disabled { 0 } else { 1 })
|
||||
},
|
||||
fields: vec![
|
||||
vec![],
|
||||
vec![
|
||||
SettingItem {
|
||||
files: USER,
|
||||
title: "Enable Git Status",
|
||||
description: "Show Git status information in the editor.",
|
||||
field: Box::new(SettingField::<bool> {
|
||||
json_path: Some("git.enable_status"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.enable_status
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.enabled
|
||||
.get_or_insert_default()
|
||||
.enable_status = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
},
|
||||
SettingItem {
|
||||
files: USER,
|
||||
title: "Enable Git Diff",
|
||||
description: "Show Git diff information in the editor.",
|
||||
field: Box::new(SettingField::<bool> {
|
||||
json_path: Some("git.enable_diff"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.enable_diff
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.enabled
|
||||
.get_or_insert_default()
|
||||
.enable_diff = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Git Gutter"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Visibility",
|
||||
|
||||
@@ -217,7 +217,7 @@ fn render_api_key_provider(
|
||||
SettingsInputField::new()
|
||||
.tab_index(0)
|
||||
.with_placeholder("xxxxxxxxxxxxxxxxxxxx")
|
||||
.on_confirm(move |api_key, cx| {
|
||||
.on_confirm(move |api_key, _window, cx| {
|
||||
write_key(api_key.filter(|key| !key.is_empty()), cx);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -2,7 +2,7 @@ mod components;
|
||||
mod page_data;
|
||||
mod pages;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -11,7 +11,9 @@ use gpui::{
|
||||
Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window, WindowBounds,
|
||||
WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, uniform_list,
|
||||
};
|
||||
use project::{Project, WorktreeId};
|
||||
|
||||
use language::Buffer;
|
||||
use project::{Project, ProjectPath, WorktreeId};
|
||||
use release_channel::ReleaseChannel;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -154,7 +156,7 @@ trait AnySettingField {
|
||||
current_file: &SettingsUiFile,
|
||||
file_set_in: &settings::SettingsFile,
|
||||
cx: &App,
|
||||
) -> Option<Box<dyn Fn(&mut App)>>;
|
||||
) -> Option<Box<dyn Fn(&mut Window, &mut App)>>;
|
||||
|
||||
fn json_path(&self) -> Option<&'static str>;
|
||||
}
|
||||
@@ -184,7 +186,7 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
|
||||
current_file: &SettingsUiFile,
|
||||
file_set_in: &settings::SettingsFile,
|
||||
cx: &App,
|
||||
) -> Option<Box<dyn Fn(&mut App)>> {
|
||||
) -> Option<Box<dyn Fn(&mut Window, &mut App)>> {
|
||||
if file_set_in == &settings::SettingsFile::Default {
|
||||
return None;
|
||||
}
|
||||
@@ -203,7 +205,7 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
|
||||
}
|
||||
let current_file = current_file.clone();
|
||||
|
||||
return Some(Box::new(move |cx| {
|
||||
return Some(Box::new(move |window, cx| {
|
||||
let store = SettingsStore::global(cx);
|
||||
let default_value = (this.pick)(store.raw_default_settings());
|
||||
let is_set_somewhere_other_than_default = store
|
||||
@@ -215,9 +217,15 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
|
||||
} else {
|
||||
None
|
||||
};
|
||||
update_settings_file(current_file.clone(), None, cx, move |settings, _| {
|
||||
(this.write)(settings, value_to_set);
|
||||
})
|
||||
update_settings_file(
|
||||
current_file.clone(),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _| {
|
||||
(this.write)(settings, value_to_set);
|
||||
},
|
||||
)
|
||||
// todo(settings_ui): Don't log err
|
||||
.log_err();
|
||||
}));
|
||||
@@ -575,7 +583,6 @@ pub fn open_settings_editor(
|
||||
}
|
||||
|
||||
// We have to defer this to get the workspace off the stack.
|
||||
|
||||
let path = path.map(ToOwned::to_owned);
|
||||
cx.defer(move |cx| {
|
||||
let current_rem_size: f32 = theme::ThemeSettings::get_global(cx).ui_font_size(cx).into();
|
||||
@@ -671,6 +678,8 @@ pub struct SettingsWindow {
|
||||
pages: Vec<SettingsPage>,
|
||||
search_bar: Entity<Editor>,
|
||||
search_task: Option<Task<()>>,
|
||||
/// Cached settings file buffers to avoid repeated disk I/O on each settings change
|
||||
project_setting_file_buffers: HashMap<ProjectPath, Entity<Buffer>>,
|
||||
/// Index into navbar_entries
|
||||
navbar_entry: usize,
|
||||
navbar_entries: Vec<NavBarEntry>,
|
||||
@@ -1069,8 +1078,8 @@ fn render_settings_item(
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Reset to Default"))
|
||||
.on_click({
|
||||
move |_, _, cx| {
|
||||
reset_to_default(cx);
|
||||
move |_, window, cx| {
|
||||
reset_to_default(window, cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -1499,6 +1508,7 @@ impl SettingsWindow {
|
||||
files: vec![],
|
||||
|
||||
current_file: current_file,
|
||||
project_setting_file_buffers: HashMap::default(),
|
||||
pages: vec![],
|
||||
navbar_entries: vec![],
|
||||
navbar_entry: 0,
|
||||
@@ -2026,14 +2036,9 @@ impl SettingsWindow {
|
||||
}
|
||||
|
||||
if let Some(worktree_id) = settings_ui_file.worktree_id() {
|
||||
let directory_name = all_projects(cx)
|
||||
let directory_name = all_projects(self.original_window.as_ref(), cx)
|
||||
.find_map(|project| project.read(cx).worktree_for_id(worktree_id, cx))
|
||||
.and_then(|worktree| worktree.read(cx).root_dir())
|
||||
.and_then(|root_dir| {
|
||||
root_dir
|
||||
.file_name()
|
||||
.map(|os_string| os_string.to_string_lossy().to_string())
|
||||
});
|
||||
.map(|worktree| worktree.read(cx).root_name());
|
||||
|
||||
let Some(directory_name) = directory_name else {
|
||||
log::error!(
|
||||
@@ -2043,7 +2048,8 @@ impl SettingsWindow {
|
||||
continue;
|
||||
};
|
||||
|
||||
self.worktree_root_dirs.insert(worktree_id, directory_name);
|
||||
self.worktree_root_dirs
|
||||
.insert(worktree_id, directory_name.as_unix_str().to_string());
|
||||
}
|
||||
|
||||
let focus_handle = prev_files
|
||||
@@ -2059,7 +2065,7 @@ impl SettingsWindow {
|
||||
|
||||
let mut missing_worktrees = Vec::new();
|
||||
|
||||
for worktree in all_projects(cx)
|
||||
for worktree in all_projects(self.original_window.as_ref(), cx)
|
||||
.flat_map(|project| project.read(cx).visible_worktrees(cx))
|
||||
.filter(|tree| !self.worktree_root_dirs.contains_key(&tree.read(cx).id()))
|
||||
{
|
||||
@@ -3525,7 +3531,10 @@ impl Render for SettingsWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn all_projects(cx: &App) -> impl Iterator<Item = Entity<project::Project>> {
|
||||
fn all_projects(
|
||||
window: Option<&WindowHandle<Workspace>>,
|
||||
cx: &App,
|
||||
) -> impl Iterator<Item = Entity<project::Project>> {
|
||||
workspace::AppState::global(cx)
|
||||
.upgrade()
|
||||
.map(|app_state| {
|
||||
@@ -3535,6 +3544,9 @@ fn all_projects(cx: &App) -> impl Iterator<Item = Entity<project::Project>> {
|
||||
.workspaces()
|
||||
.iter()
|
||||
.filter_map(|workspace| Some(workspace.read(cx).ok()?.project().clone()))
|
||||
.chain(
|
||||
window.and_then(|workspace| Some(workspace.read(cx).ok()?.project().clone())),
|
||||
)
|
||||
})
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -3543,6 +3555,7 @@ fn all_projects(cx: &App) -> impl Iterator<Item = Entity<project::Project>> {
|
||||
fn update_settings_file(
|
||||
file: SettingsUiFile,
|
||||
file_name: Option<&'static str>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
|
||||
) -> Result<()> {
|
||||
@@ -3551,41 +3564,11 @@ fn update_settings_file(
|
||||
match file {
|
||||
SettingsUiFile::Project((worktree_id, rel_path)) => {
|
||||
let rel_path = rel_path.join(paths::local_settings_file_relative_path());
|
||||
let Some((worktree, project)) = all_projects(cx).find_map(|project| {
|
||||
project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.zip(Some(project))
|
||||
}) else {
|
||||
anyhow::bail!("Could not find project with worktree id: {}", worktree_id);
|
||||
let Some(settings_window) = window.root::<SettingsWindow>().flatten() else {
|
||||
anyhow::bail!("No settings window found");
|
||||
};
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let task = if project.contains_local_settings_file(worktree_id, &rel_path, cx) {
|
||||
None
|
||||
} else {
|
||||
Some(worktree.update(cx, |worktree, cx| {
|
||||
worktree.create_entry(rel_path.clone(), false, None, cx)
|
||||
}))
|
||||
};
|
||||
|
||||
cx.spawn(async move |project, cx| {
|
||||
if let Some(task) = task
|
||||
&& task.await.is_err()
|
||||
{
|
||||
return;
|
||||
};
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.update_local_settings_file(worktree_id, rel_path, cx, update);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
update_project_setting_file(worktree_id, rel_path, update, settings_window, cx)
|
||||
}
|
||||
SettingsUiFile::User => {
|
||||
// todo(settings_ui) error?
|
||||
@@ -3596,6 +3579,86 @@ fn update_settings_file(
|
||||
}
|
||||
}
|
||||
|
||||
fn update_project_setting_file(
|
||||
worktree_id: WorktreeId,
|
||||
rel_path: Arc<RelPath>,
|
||||
update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
|
||||
settings_window: Entity<SettingsWindow>,
|
||||
cx: &mut App,
|
||||
) -> Result<()> {
|
||||
let Some((worktree, project)) =
|
||||
all_projects(settings_window.read(cx).original_window.as_ref(), cx).find_map(|project| {
|
||||
project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.zip(Some(project))
|
||||
})
|
||||
else {
|
||||
anyhow::bail!("Could not find project with worktree id: {}", worktree_id);
|
||||
};
|
||||
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: rel_path.clone(),
|
||||
};
|
||||
|
||||
let needs_creation = !project
|
||||
.read(cx)
|
||||
.contains_local_settings_file(worktree_id, &rel_path, cx);
|
||||
|
||||
let create_task = needs_creation.then(|| {
|
||||
worktree.update(cx, |worktree, cx| {
|
||||
worktree.create_entry(rel_path.clone(), false, None, cx)
|
||||
})
|
||||
});
|
||||
let buffer_store = project.read(cx).buffer_store().clone();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
if let Some(create_task) = create_task {
|
||||
create_task.await?;
|
||||
}
|
||||
let cached_buffer = settings_window.read_with(cx, |settings_window, _| {
|
||||
settings_window
|
||||
.project_setting_file_buffers
|
||||
.get(&project_path)
|
||||
.cloned()
|
||||
})?;
|
||||
let buffer = if let Some(cached_buffer) = cached_buffer {
|
||||
cached_buffer
|
||||
} else {
|
||||
let buffer = buffer_store
|
||||
.update(cx, |store, cx| store.open_buffer(project_path.clone(), cx))?
|
||||
.await
|
||||
.context("Failed to open settings file")?;
|
||||
|
||||
settings_window.update(cx, |this, _cx| {
|
||||
this.project_setting_file_buffers
|
||||
.insert(project_path, buffer.clone());
|
||||
})?;
|
||||
|
||||
buffer
|
||||
};
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let current_text = buffer.text();
|
||||
let new_text = cx
|
||||
.global::<SettingsStore>()
|
||||
.new_text_for_update(current_text, |settings| update(settings, cx));
|
||||
buffer.edit([(0..buffer.len(), new_text)], None, cx);
|
||||
})?;
|
||||
|
||||
buffer_store
|
||||
.update(cx, |store, cx| store.save_buffer(buffer, cx))?
|
||||
.await
|
||||
.context("Failed to save settings file")?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
|
||||
field: SettingField<T>,
|
||||
file: SettingsUiFile,
|
||||
@@ -3617,10 +3680,16 @@ fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
|
||||
|editor, placeholder| editor.with_placeholder(placeholder),
|
||||
)
|
||||
.on_confirm({
|
||||
move |new_text, cx| {
|
||||
update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
|
||||
(field.write)(settings, new_text.map(Into::into));
|
||||
})
|
||||
move |new_text, window, cx| {
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(settings, new_text.map(Into::into));
|
||||
},
|
||||
)
|
||||
.log_err(); // todo(settings_ui) don't log err
|
||||
}
|
||||
})
|
||||
@@ -3645,11 +3714,11 @@ fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
|
||||
Switch::new("toggle_button", toggle_state)
|
||||
.tab_index(0_isize)
|
||||
.on_click({
|
||||
move |state, _window, cx| {
|
||||
move |state, window, cx| {
|
||||
telemetry::event!("Settings Change", setting = field.json_path, type = file.setting_type());
|
||||
|
||||
let state = *state == ui::ToggleState::Selected;
|
||||
update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
|
||||
update_settings_file(file.clone(), field.json_path, window, cx, move |settings, _cx| {
|
||||
(field.write)(settings, Some(state.into()));
|
||||
})
|
||||
.log_err(); // todo(settings_ui) don't log err
|
||||
@@ -3669,11 +3738,17 @@ fn render_number_field<T: NumberFieldType + Send + Sync>(
|
||||
let value = value.copied().unwrap_or_else(T::min_value);
|
||||
NumberField::new("numeric_stepper", value, window, cx)
|
||||
.on_change({
|
||||
move |value, _window, cx| {
|
||||
move |value, window, cx| {
|
||||
let value = *value;
|
||||
update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
|
||||
(field.write)(settings, Some(value));
|
||||
})
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(settings, Some(value));
|
||||
},
|
||||
)
|
||||
.log_err(); // todo(settings_ui) don't log err
|
||||
}
|
||||
})
|
||||
@@ -3701,13 +3776,19 @@ where
|
||||
let current_value = current_value.copied().unwrap_or(variants()[0]);
|
||||
|
||||
EnumVariantDropdown::new("dropdown", current_value, variants(), labels(), {
|
||||
move |value, cx| {
|
||||
move |value, window, cx| {
|
||||
if value == current_value {
|
||||
return;
|
||||
}
|
||||
update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
|
||||
(field.write)(settings, Some(value));
|
||||
})
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(settings, Some(value));
|
||||
},
|
||||
)
|
||||
.log_err(); // todo(settings_ui) don't log err
|
||||
}
|
||||
})
|
||||
@@ -3752,10 +3833,11 @@ fn render_font_picker(
|
||||
Some(cx.new(move |cx| {
|
||||
font_picker(
|
||||
current_value.clone().into(),
|
||||
move |font_name, cx| {
|
||||
move |font_name, window, cx| {
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(settings, Some(font_name.into()));
|
||||
@@ -3801,10 +3883,11 @@ fn render_theme_picker(
|
||||
let current_value = current_value.clone();
|
||||
theme_picker(
|
||||
current_value,
|
||||
move |theme_name, cx| {
|
||||
move |theme_name, window, cx| {
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(
|
||||
@@ -3853,10 +3936,11 @@ fn render_icon_theme_picker(
|
||||
let current_value = current_value.clone();
|
||||
icon_theme_picker(
|
||||
current_value,
|
||||
move |theme_name, cx| {
|
||||
move |theme_name, window, cx| {
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(
|
||||
@@ -3970,6 +4054,7 @@ pub mod test {
|
||||
worktree_root_dirs: HashMap::default(),
|
||||
files: Vec::default(),
|
||||
current_file: crate::SettingsUiFile::User,
|
||||
project_setting_file_buffers: HashMap::default(),
|
||||
pages,
|
||||
search_bar: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
navbar_entry: selected_idx.expect("Must have a selected navbar entry"),
|
||||
|
||||
@@ -76,7 +76,13 @@ impl Settings for ItemSettings {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
let tabs = content.tabs.as_ref().unwrap();
|
||||
Self {
|
||||
git_status: tabs.git_status.unwrap(),
|
||||
git_status: tabs.git_status.unwrap()
|
||||
&& content
|
||||
.git
|
||||
.unwrap()
|
||||
.enabled
|
||||
.unwrap()
|
||||
.is_git_status_enabled(),
|
||||
close_position: tabs.close_position.unwrap(),
|
||||
activate_on_close: tabs.activate_on_close.unwrap(),
|
||||
file_icons: tabs.file_icons.unwrap(),
|
||||
|
||||
@@ -73,7 +73,7 @@ h_flex()
|
||||
|
||||
- `Window`: A struct in zed representing a zed window in your desktop environment (see image below). There can be multiple if you have multiple zed instances open. Mostly passed around for rendering.
|
||||
- `Modal`: A UI element that floats on top of the rest of the UI
|
||||
- `Picker`: A struct representing a list of items in floating on top of the UI (Modal). You can select an item and confirm. What happens on select or confirm is determined by the picker's delegate. (The 'Model' in the image below is a picker.)
|
||||
- `Picker`: A struct representing a list of items floating on top of the UI (Modal). You can select an item and confirm. What happens on select or confirm is determined by the picker's delegate. (The 'Modal' in the image below is a picker.)
|
||||
- `PickerDelegate`: A trait used to specialize behavior for a `Picker`. The `Picker` stores the `PickerDelegate` in the field delegate.
|
||||
- `Center`: The middle of the zed window, the center is split into multiple `Pane`s. In the codebase this is a field on the `Workspace` struct. (see image below).
|
||||
- `Pane`: An area in the `Center` where we can place items, such as an editor, multi-buffer or terminal (see image below).
|
||||
|
||||
Reference in New Issue
Block a user