
### TODO - [x] Make sure keybinding shows up in pane + menu - [x] Selection tool in the editor toolbar - [x] Application Menu - [x] Add more options to pane + menu - Go to File... - Go to Symbol in Project... - [x] Add go items to the selection tool in the editor: - Go to Symbol in Editor... - Go to Line/Column... - Next Problem - Previous Problem - [x] Fix a bug where modals opened from a context menu aren't focused correclty - [x] Determine if or what needs to be done with project actions: - Difficulty is that these are exposed in the UI via clicking the project name in the titlebar or by right clicking the root entry in the project panel. But they require reading and are two clicks away. Is that sufficient? - Add Folder to Project - Open a new project - Open recent - [x] Get a style pass - [x] Implement style pass - [x] Fix the wrong actions in the selection menu - [x] Show selection tool toggle in the 'editor settings' thing - [x] Put preferences section from the app menu onto the right hand user menu - [x] Add Project menu into app menu to replace 'preferences' section, and put the rest of the actions there - [ ] ~~Adopt `...` convention for opening a surface~~ uncertain what this convention is. - [x] Adopt link styling for webview actions - [x] Set lucide hamburger for menu icon - [x] Gate application menu to only show on Linux and Windows Release Notes: - Added a 'selection and movement' tool to the Editor's toolbar, as well as controls to toggle it and a setting to remove it (`"toolbar": {"selections_menu": true/false }`) - Changed the behavior of the `+` menu in the tab bar to use standard actions and keybindings. Replaced 'New Center Terminal' with 'New Terminal', and 'New Search', with the usual 'Deploy Search'. Also added item-creating actions to this menu. - Added an 'application' menu to the titlebar to Linux and Windows builds of Zed
199 lines
6.7 KiB
Rust
199 lines
6.7 KiB
Rust
use editor::{Editor, ToPoint};
|
|
use gpui::{AppContext, Subscription, View, WeakView};
|
|
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
use settings::{Settings, SettingsSources};
|
|
use std::fmt::Write;
|
|
use text::{Point, Selection};
|
|
use ui::{
|
|
div, Button, ButtonCommon, Clickable, FluentBuilder, IntoElement, LabelSize, ParentElement,
|
|
Render, Tooltip, ViewContext,
|
|
};
|
|
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
|
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
|
|
|
#[derive(Copy, Clone, Default, PartialOrd, PartialEq)]
|
|
struct SelectionStats {
|
|
lines: usize,
|
|
characters: usize,
|
|
selections: usize,
|
|
}
|
|
|
|
pub struct CursorPosition {
|
|
position: Option<Point>,
|
|
selected_count: SelectionStats,
|
|
workspace: WeakView<Workspace>,
|
|
_observe_active_editor: Option<Subscription>,
|
|
}
|
|
|
|
impl CursorPosition {
|
|
pub fn new(workspace: &Workspace) -> Self {
|
|
Self {
|
|
position: None,
|
|
selected_count: Default::default(),
|
|
workspace: workspace.weak_handle(),
|
|
_observe_active_editor: None,
|
|
}
|
|
}
|
|
|
|
fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
|
let editor = editor.read(cx);
|
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
|
|
|
self.selected_count = Default::default();
|
|
self.selected_count.selections = editor.selections.count();
|
|
let mut last_selection: Option<Selection<usize>> = None;
|
|
for selection in editor.selections.all::<usize>(cx) {
|
|
self.selected_count.characters += selection.end - selection.start;
|
|
if last_selection
|
|
.as_ref()
|
|
.map_or(true, |last_selection| selection.id > last_selection.id)
|
|
{
|
|
last_selection = Some(selection);
|
|
}
|
|
}
|
|
for selection in editor.selections.all::<Point>(cx) {
|
|
if selection.end != selection.start {
|
|
self.selected_count.lines += (selection.end.row - selection.start.row) as usize;
|
|
if selection.end.column != 0 {
|
|
self.selected_count.lines += 1;
|
|
}
|
|
}
|
|
}
|
|
self.position = last_selection.map(|s| s.head().to_point(&buffer));
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
fn write_position(&self, text: &mut String, cx: &AppContext) {
|
|
if self.selected_count
|
|
<= (SelectionStats {
|
|
selections: 1,
|
|
..Default::default()
|
|
})
|
|
{
|
|
// Do not write out anything if we have just one empty selection.
|
|
return;
|
|
}
|
|
let SelectionStats {
|
|
lines,
|
|
characters,
|
|
selections,
|
|
} = self.selected_count;
|
|
let format = LineIndicatorFormat::get(None, cx);
|
|
let is_short_format = format == &LineIndicatorFormat::Short;
|
|
let lines = (lines > 1).then_some((lines, "line"));
|
|
let selections = (selections > 1).then_some((selections, "selection"));
|
|
let characters = (characters > 0).then_some((characters, "character"));
|
|
if (None, None, None) == (characters, selections, lines) {
|
|
// Nothing to display.
|
|
return;
|
|
}
|
|
write!(text, " (").unwrap();
|
|
let mut wrote_once = false;
|
|
for (count, name) in [selections, lines, characters].into_iter().flatten() {
|
|
if wrote_once {
|
|
write!(text, ", ").unwrap();
|
|
}
|
|
let name = if is_short_format { &name[..1] } else { &name };
|
|
let plural_suffix = if count > 1 && !is_short_format {
|
|
"s"
|
|
} else {
|
|
""
|
|
};
|
|
write!(text, "{count} {name}{plural_suffix}").unwrap();
|
|
wrote_once = true;
|
|
}
|
|
text.push(')');
|
|
}
|
|
}
|
|
|
|
impl Render for CursorPosition {
|
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
div().when_some(self.position, |el, position| {
|
|
let mut text = format!(
|
|
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
|
|
position.row + 1,
|
|
position.column + 1
|
|
);
|
|
self.write_position(&mut text, cx);
|
|
|
|
el.child(
|
|
Button::new("go-to-line-column", text)
|
|
.label_size(LabelSize::Small)
|
|
.on_click(cx.listener(|this, _, cx| {
|
|
if let Some(workspace) = this.workspace.upgrade() {
|
|
workspace.update(cx, |workspace, cx| {
|
|
if let Some(editor) = workspace
|
|
.active_item(cx)
|
|
.and_then(|item| item.act_as::<Editor>(cx))
|
|
{
|
|
workspace
|
|
.toggle_modal(cx, |cx| crate::GoToLine::new(editor, cx))
|
|
}
|
|
});
|
|
}
|
|
}))
|
|
.tooltip(|cx| {
|
|
Tooltip::for_action(
|
|
"Go to Line/Column",
|
|
&editor::actions::ToggleGoToLine,
|
|
cx,
|
|
)
|
|
}),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
impl StatusItemView for CursorPosition {
|
|
fn set_active_pane_item(
|
|
&mut self,
|
|
active_pane_item: Option<&dyn ItemHandle>,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
|
|
self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
|
|
self.update_position(editor, cx);
|
|
} else {
|
|
self.position = None;
|
|
self._observe_active_editor = None;
|
|
}
|
|
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Default, PartialEq, JsonSchema, Deserialize, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub(crate) enum LineIndicatorFormat {
|
|
Short,
|
|
#[default]
|
|
Long,
|
|
}
|
|
|
|
/// Whether or not to automatically check for updates.
|
|
///
|
|
/// Values: short, long
|
|
/// Default: short
|
|
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
|
|
#[serde(transparent)]
|
|
pub(crate) struct LineIndicatorFormatContent(LineIndicatorFormat);
|
|
|
|
impl Settings for LineIndicatorFormat {
|
|
const KEY: Option<&'static str> = Some("line_indicator_format");
|
|
|
|
type FileContent = Option<LineIndicatorFormatContent>;
|
|
|
|
fn load(
|
|
sources: SettingsSources<Self::FileContent>,
|
|
_: &mut AppContext,
|
|
) -> anyhow::Result<Self> {
|
|
let format = [sources.release_channel, sources.user]
|
|
.into_iter()
|
|
.find_map(|value| value.copied().flatten())
|
|
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
|
|
|
|
Ok(format.0)
|
|
}
|
|
}
|