input: Add context menu for cut, copy, paste. (#1279)
Close #27 <img width="756" height="674" alt="image" src="https://github.com/user-attachments/assets/07884a7e-366b-47b8-8bd7-a88f3cae52b8" />
This commit is contained in:
parent
993ce34bd8
commit
bf5650b5db
12 changed files with 342 additions and 71 deletions
|
|
@ -160,9 +160,31 @@ Input:
|
||||||
en: Replace
|
en: Replace
|
||||||
zh-CN: 替换
|
zh-CN: 替换
|
||||||
zh-HK: 替換
|
zh-HK: 替換
|
||||||
it: Sostituisci
|
|
||||||
Replace All:
|
Replace All:
|
||||||
en: Replace All
|
en: Replace All
|
||||||
zh-CN: 全部替换
|
zh-CN: 全部替换
|
||||||
zh-HK: 全部替換
|
zh-HK: 全部替換
|
||||||
it: Sostituisci tutto
|
Cut:
|
||||||
|
en: Cut
|
||||||
|
zh-CN: 剪切
|
||||||
|
zh-HK: 剪切
|
||||||
|
Copy:
|
||||||
|
en: Copy
|
||||||
|
zh-CN: 复制
|
||||||
|
zh-HK: 複製
|
||||||
|
Paste:
|
||||||
|
en: Paste
|
||||||
|
zh-CN: 粘贴
|
||||||
|
zh-HK: 貼上
|
||||||
|
Select All:
|
||||||
|
en: Select All
|
||||||
|
zh-CN: 全选
|
||||||
|
zh-HK: 全選
|
||||||
|
Go to Definition:
|
||||||
|
en: Go to Definition
|
||||||
|
zh-CN: 跳转到定义
|
||||||
|
zh-HK: 跳轉到定義
|
||||||
|
Show Code Actions:
|
||||||
|
en: Show Code Actions
|
||||||
|
zh-CN: 显示代码操作
|
||||||
|
zh-HK: 顯示代碼操作
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ impl Selection {
|
||||||
self.start = 0;
|
self.start = 0;
|
||||||
self.end = 0;
|
self.end = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if the given offset is within the selection range.
|
||||||
|
pub fn contains(&self, offset: usize) -> bool {
|
||||||
|
offset >= self.start && offset < self.end
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Range<usize>> for Selection {
|
impl From<Range<usize>> for Selection {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::ops::Range;
|
||||||
|
|
||||||
use crate::input::{
|
use crate::input::{
|
||||||
popovers::{CodeActionItem, CodeActionMenu, ContextMenu},
|
popovers::{CodeActionItem, CodeActionMenu, ContextMenu},
|
||||||
InputState,
|
InputState, ToggleCodeActions,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait CodeActionProvider {
|
pub trait CodeActionProvider {
|
||||||
|
|
@ -37,6 +37,15 @@ pub trait CodeActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputState {
|
impl InputState {
|
||||||
|
pub(crate) fn on_action_toggle_code_actions(
|
||||||
|
&mut self,
|
||||||
|
_: &ToggleCodeActions,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.handle_code_action_trigger(window, cx)
|
||||||
|
}
|
||||||
|
|
||||||
/// Show code actions for the cursor.
|
/// Show code actions for the cursor.
|
||||||
pub(crate) fn handle_code_action_trigger(
|
pub(crate) fn handle_code_action_trigger(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use rope::Rope;
|
||||||
use std::{ops::Range, rc::Rc};
|
use std::{ops::Range, rc::Rc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
input::{element::TextElement, InputState, RopeExt},
|
input::{element::TextElement, GoToDefinition, InputState, RopeExt},
|
||||||
ActiveTheme,
|
ActiveTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -31,14 +31,31 @@ pub(crate) struct HoverDefinition {
|
||||||
/// The range of the symbol that triggered the hover.
|
/// The range of the symbol that triggered the hover.
|
||||||
symbol_range: Range<usize>,
|
symbol_range: Range<usize>,
|
||||||
pub(crate) locations: Rc<Vec<lsp_types::LocationLink>>,
|
pub(crate) locations: Rc<Vec<lsp_types::LocationLink>>,
|
||||||
|
last_location: Option<(Range<usize>, Rc<Vec<lsp_types::LocationLink>>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HoverDefinition {
|
impl HoverDefinition {
|
||||||
pub(crate) fn new(symbol_range: Range<usize>, locations: Vec<lsp_types::LocationLink>) -> Self {
|
pub(crate) fn update(
|
||||||
Self {
|
&mut self,
|
||||||
symbol_range,
|
symbol_range: Range<usize>,
|
||||||
locations: Rc::new(locations),
|
locations: Vec<lsp_types::LocationLink>,
|
||||||
|
) {
|
||||||
|
self.clear();
|
||||||
|
self.symbol_range = symbol_range;
|
||||||
|
self.locations = Rc::new(locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_empty(&self) -> bool {
|
||||||
|
self.locations.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear(&mut self) {
|
||||||
|
if !self.locations.is_empty() {
|
||||||
|
self.last_location = Some((self.symbol_range.clone(), self.locations.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.symbol_range = 0..0;
|
||||||
|
self.locations = Rc::new(vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_same(&self, offset: usize) -> bool {
|
pub(crate) fn is_same(&self, offset: usize) -> bool {
|
||||||
|
|
@ -47,19 +64,18 @@ impl HoverDefinition {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputState {
|
impl InputState {
|
||||||
pub(super) fn handle_hover_definition(
|
pub(crate) fn handle_hover_definition(
|
||||||
&mut self,
|
&mut self,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<InputState>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let Some(provider) = self.lsp.definition_provider.clone() else {
|
let Some(provider) = self.lsp.definition_provider.clone() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Some(hover_definition) = self.hover_definition.as_ref() {
|
|
||||||
if hover_definition.is_same(offset) {
|
if self.hover_definition.is_same(offset) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently not implemented.
|
// Currently not implemented.
|
||||||
|
|
@ -71,7 +87,7 @@ impl InputState {
|
||||||
|
|
||||||
_ = editor.update(cx, |editor, cx| {
|
_ = editor.update(cx, |editor, cx| {
|
||||||
if locations.is_empty() {
|
if locations.is_empty() {
|
||||||
editor.hover_definition = None;
|
editor.hover_definition.clear();
|
||||||
} else {
|
} else {
|
||||||
if let Some(location) = locations.first() {
|
if let Some(location) = locations.first() {
|
||||||
if let Some(range) = location.origin_selection_range {
|
if let Some(range) = location.origin_selection_range {
|
||||||
|
|
@ -81,7 +97,9 @@ impl InputState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.hover_definition = Some(HoverDefinition::new(symbol_range, locations));
|
editor
|
||||||
|
.hover_definition
|
||||||
|
.update(symbol_range.clone(), locations.clone());
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
@ -90,6 +108,24 @@ impl InputState {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_action_go_to_definition(
|
||||||
|
&mut self,
|
||||||
|
_: &GoToDefinition,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let offset = self.cursor();
|
||||||
|
if let Some((symbol_range, locations)) = self.hover_definition.last_location.clone() {
|
||||||
|
if !(symbol_range.start..=symbol_range.end).contains(&offset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(location) = locations.first().cloned() {
|
||||||
|
self.go_to_definition(&location, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return true if handled.
|
/// Return true if handled.
|
||||||
pub(crate) fn handle_click_hover_definition(
|
pub(crate) fn handle_click_hover_definition(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -102,17 +138,27 @@ impl InputState {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(hover_definition) = self.hover_definition.as_ref() else {
|
if self.hover_definition.is_empty() {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if !hover_definition.is_same(offset) {
|
if !self.hover_definition.is_same(offset) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(location) = hover_definition.locations.first().cloned() else {
|
let Some(location) = self.hover_definition.locations.first().cloned() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.go_to_definition(&location, cx);
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn go_to_definition(
|
||||||
|
&mut self,
|
||||||
|
location: &lsp_types::LocationLink,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
if location
|
if location
|
||||||
.target_uri
|
.target_uri
|
||||||
.scheme()
|
.scheme()
|
||||||
|
|
@ -129,8 +175,6 @@ impl InputState {
|
||||||
self.move_to(start, cx);
|
self.move_to(start, cx);
|
||||||
self.select_to(end, cx);
|
self.select_to(end, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,7 +188,7 @@ impl TextElement {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(hover_definition) = editor.hover_definition.as_ref() else {
|
if editor.hover_definition.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -160,7 +204,10 @@ impl TextElement {
|
||||||
..UnderlineStyle::default()
|
..UnderlineStyle::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
Some((hover_definition.symbol_range.clone(), highlight_style))
|
Some((
|
||||||
|
editor.hover_definition.symbol_range.clone(),
|
||||||
|
highlight_style,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn layout_hover_definition_hitbox(
|
pub(crate) fn layout_hover_definition_hitbox(
|
||||||
|
|
@ -173,11 +220,11 @@ impl TextElement {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(hover_definition) = editor.hover_definition.as_ref() else {
|
if editor.hover_definition.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(bounds) = editor.range_to_bounds(&hover_definition.symbol_range) else {
|
let Some(bounds) = editor.range_to_bounds(&editor.hover_definition.symbol_range) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ impl InputState {
|
||||||
handled = menu.handle_action(action, window, cx)
|
handled = menu.handle_action(action, window, cx)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
ContextMenu::MouseContext(..) => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
handled
|
handled
|
||||||
|
|
@ -114,7 +115,7 @@ impl InputState {
|
||||||
self.hover_popover = None;
|
self.hover_popover = None;
|
||||||
self.handle_hover_definition(offset, window, cx);
|
self.handle_hover_definition(offset, window, cx);
|
||||||
} else {
|
} else {
|
||||||
self.hover_definition = None;
|
self.hover_definition.clear();
|
||||||
self.handle_hover_popover(offset, window, cx);
|
self.handle_hover_popover(offset, window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ mod mask_pattern;
|
||||||
mod mode;
|
mod mode;
|
||||||
mod number_input;
|
mod number_input;
|
||||||
mod otp_input;
|
mod otp_input;
|
||||||
mod popovers;
|
pub(crate) mod popovers;
|
||||||
mod rope_ext;
|
mod rope_ext;
|
||||||
mod search;
|
mod search;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
|
||||||
143
crates/ui/src/input/popovers/context_menu.rs
Normal file
143
crates/ui/src/input/popovers/context_menu.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
use gpui::{
|
||||||
|
anchored, deferred, div, prelude::FluentBuilder as _, px, App, AppContext as _, Context,
|
||||||
|
DismissEvent, Entity, IntoElement, MouseDownEvent, ParentElement as _, Pixels, Point, Render,
|
||||||
|
Styled, Subscription, Window,
|
||||||
|
};
|
||||||
|
use rust_i18n::t;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
input::{self, popovers::ContextMenu, InputState},
|
||||||
|
popup_menu::PopupMenu,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Context menu for mouse right clicks.
|
||||||
|
pub(crate) struct MouseContextMenu {
|
||||||
|
editor: Entity<InputState>,
|
||||||
|
menu: Entity<PopupMenu>,
|
||||||
|
mouse_position: Point<Pixels>,
|
||||||
|
open: bool,
|
||||||
|
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
pub(crate) fn handle_right_click_menu(
|
||||||
|
&mut self,
|
||||||
|
event: &MouseDownEvent,
|
||||||
|
offset: usize,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
// Show Mouse context menu
|
||||||
|
if !self.selected_range.contains(offset) {
|
||||||
|
self.move_to(offset, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.context_menu = Some(ContextMenu::MouseContext(self.mouse_context_menu.clone()));
|
||||||
|
|
||||||
|
let is_code_editor = self.mode.is_code_editor();
|
||||||
|
if is_code_editor {
|
||||||
|
self.handle_hover_definition(offset, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_goto_definition = self.lsp.definition_provider.is_some();
|
||||||
|
let has_code_action = !self.lsp.code_action_providers.is_empty();
|
||||||
|
let is_selected = !self.selected_range.is_empty();
|
||||||
|
let has_paste = cx.read_from_clipboard().is_some();
|
||||||
|
|
||||||
|
self.mouse_context_menu.update(cx, |this, cx| {
|
||||||
|
this.mouse_position = event.position;
|
||||||
|
this.menu.update(cx, |menu, cx| {
|
||||||
|
let new_menu = PopupMenu::new(cx)
|
||||||
|
.when(is_code_editor, |m| {
|
||||||
|
m.menu_with_enable(
|
||||||
|
t!("Input.Go to Definition"),
|
||||||
|
Box::new(input::GoToDefinition),
|
||||||
|
has_goto_definition,
|
||||||
|
)
|
||||||
|
.menu_with_enable(
|
||||||
|
t!("Input.Show Code Actions"),
|
||||||
|
Box::new(input::ToggleCodeActions),
|
||||||
|
has_code_action,
|
||||||
|
)
|
||||||
|
.separator()
|
||||||
|
})
|
||||||
|
.menu_with_enable(t!("Input.Cut"), Box::new(input::Cut), is_selected)
|
||||||
|
.menu_with_enable(t!("Input.Copy"), Box::new(input::Copy), is_selected)
|
||||||
|
.menu_with_enable(t!("Input.Paste"), Box::new(input::Paste), has_paste)
|
||||||
|
.separator()
|
||||||
|
.menu(t!("Input.Select All"), Box::new(input::SelectAll));
|
||||||
|
|
||||||
|
menu.menu_items = new_menu.menu_items;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
this.open = true;
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MouseContextMenu {
|
||||||
|
pub(crate) fn new(
|
||||||
|
editor: Entity<InputState>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Entity<Self> {
|
||||||
|
cx.new(|cx| {
|
||||||
|
let menu = cx.new(|cx| PopupMenu::new(cx).small());
|
||||||
|
|
||||||
|
let _subscriptions = vec![cx.subscribe_in(&menu, window, {
|
||||||
|
move |this: &mut Self, _, _: &DismissEvent, window, cx| {
|
||||||
|
this.close(window, cx);
|
||||||
|
}
|
||||||
|
})];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
editor,
|
||||||
|
menu,
|
||||||
|
mouse_position: Point::default(),
|
||||||
|
open: false,
|
||||||
|
_subscriptions,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn is_open(&self) -> bool {
|
||||||
|
self.open
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.open = false;
|
||||||
|
self.editor.update(cx, |this, cx| {
|
||||||
|
this.focus(window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for MouseContextMenu {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
if !self.open {
|
||||||
|
return div().into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
let pos = self.mouse_position;
|
||||||
|
|
||||||
|
deferred(
|
||||||
|
anchored()
|
||||||
|
.snap_to_window_with_margin(px(8.))
|
||||||
|
.anchor(gpui::Corner::TopLeft)
|
||||||
|
.position(pos)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.font_family(".SystemUIFont")
|
||||||
|
.text_size(px(14.))
|
||||||
|
.cursor_default()
|
||||||
|
.child(self.menu.clone()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
mod code_action_menu;
|
mod code_action_menu;
|
||||||
mod completion_menu;
|
mod completion_menu;
|
||||||
|
mod context_menu;
|
||||||
mod diagnostic_popover;
|
mod diagnostic_popover;
|
||||||
mod hover_popover;
|
mod hover_popover;
|
||||||
|
|
||||||
pub(crate) use code_action_menu::*;
|
pub(crate) use code_action_menu::*;
|
||||||
pub(crate) use completion_menu::*;
|
pub(crate) use completion_menu::*;
|
||||||
|
pub(crate) use context_menu::*;
|
||||||
pub(crate) use diagnostic_popover::*;
|
pub(crate) use diagnostic_popover::*;
|
||||||
pub(crate) use hover_popover::*;
|
pub(crate) use hover_popover::*;
|
||||||
|
|
||||||
|
|
@ -21,6 +23,7 @@ use crate::{
|
||||||
pub(crate) enum ContextMenu {
|
pub(crate) enum ContextMenu {
|
||||||
Completion(Entity<CompletionMenu>),
|
Completion(Entity<CompletionMenu>),
|
||||||
CodeAction(Entity<CodeActionMenu>),
|
CodeAction(Entity<CodeActionMenu>),
|
||||||
|
MouseContext(Entity<MouseContextMenu>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextMenu {
|
impl ContextMenu {
|
||||||
|
|
@ -28,6 +31,7 @@ impl ContextMenu {
|
||||||
match self {
|
match self {
|
||||||
ContextMenu::Completion(menu) => menu.read(cx).is_open(),
|
ContextMenu::Completion(menu) => menu.read(cx).is_open(),
|
||||||
ContextMenu::CodeAction(menu) => menu.read(cx).is_open(),
|
ContextMenu::CodeAction(menu) => menu.read(cx).is_open(),
|
||||||
|
ContextMenu::MouseContext(menu) => menu.read(cx).is_open(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,6 +39,7 @@ impl ContextMenu {
|
||||||
match self {
|
match self {
|
||||||
ContextMenu::Completion(menu) => menu.clone().into_any_element(),
|
ContextMenu::Completion(menu) => menu.clone().into_any_element(),
|
||||||
ContextMenu::CodeAction(menu) => menu.clone().into_any_element(),
|
ContextMenu::CodeAction(menu) => menu.clone().into_any_element(),
|
||||||
|
ContextMenu::MouseContext(menu) => menu.clone().into_any_element(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ use super::{
|
||||||
text_wrapper::TextWrapper,
|
text_wrapper::TextWrapper,
|
||||||
};
|
};
|
||||||
use crate::input::{
|
use crate::input::{
|
||||||
popovers::{ContextMenu, DiagnosticPopover, HoverPopover},
|
popovers::{ContextMenu, DiagnosticPopover, HoverPopover, MouseContextMenu},
|
||||||
search::{self, SearchPanel},
|
search::{self, SearchPanel},
|
||||||
HoverDefinition, Lsp, Position,
|
HoverDefinition, Lsp, Position,
|
||||||
};
|
};
|
||||||
|
|
@ -92,6 +92,7 @@ actions!(
|
||||||
Escape,
|
Escape,
|
||||||
ToggleCodeActions,
|
ToggleCodeActions,
|
||||||
Search,
|
Search,
|
||||||
|
GoToDefinition,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -298,11 +299,12 @@ pub struct InputState {
|
||||||
diagnostic_popover: Option<Entity<DiagnosticPopover>>,
|
diagnostic_popover: Option<Entity<DiagnosticPopover>>,
|
||||||
/// Completion/CodeAction context menu
|
/// Completion/CodeAction context menu
|
||||||
pub(super) context_menu: Option<ContextMenu>,
|
pub(super) context_menu: Option<ContextMenu>,
|
||||||
|
pub(super) mouse_context_menu: Entity<MouseContextMenu>,
|
||||||
/// A flag to indicate if we are currently inserting a completion item.
|
/// A flag to indicate if we are currently inserting a completion item.
|
||||||
pub(super) completion_inserting: bool,
|
pub(super) completion_inserting: bool,
|
||||||
pub(super) hover_popover: Option<Entity<HoverPopover>>,
|
pub(super) hover_popover: Option<Entity<HoverPopover>>,
|
||||||
/// The LSP definitions locations for "Go to Definition" feature.
|
/// The LSP definitions locations for "Go to Definition" feature.
|
||||||
pub(super) hover_definition: Option<HoverDefinition>,
|
pub(super) hover_definition: HoverDefinition,
|
||||||
|
|
||||||
pub lsp: Lsp,
|
pub lsp: Lsp,
|
||||||
|
|
||||||
|
|
@ -349,6 +351,7 @@ impl InputState {
|
||||||
];
|
];
|
||||||
|
|
||||||
let text_style = window.text_style();
|
let text_style = window.text_style();
|
||||||
|
let mouse_context_menu = MouseContextMenu::new(cx.entity(), window, cx);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
focus_handle: focus_handle.clone(),
|
focus_handle: focus_handle.clone(),
|
||||||
|
|
@ -390,9 +393,10 @@ impl InputState {
|
||||||
lsp: Lsp::default(),
|
lsp: Lsp::default(),
|
||||||
diagnostic_popover: None,
|
diagnostic_popover: None,
|
||||||
context_menu: None,
|
context_menu: None,
|
||||||
|
mouse_context_menu,
|
||||||
completion_inserting: false,
|
completion_inserting: false,
|
||||||
hover_popover: None,
|
hover_popover: None,
|
||||||
hover_definition: None,
|
hover_definition: HoverDefinition::default(),
|
||||||
silent_replace_text: false,
|
silent_replace_text: false,
|
||||||
_subscriptions,
|
_subscriptions,
|
||||||
_context_menu_task: Task::ready(Ok(())),
|
_context_menu_task: Task::ready(Ok(())),
|
||||||
|
|
@ -1530,15 +1534,6 @@ impl InputState {
|
||||||
cx.propagate();
|
cx.propagate();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn toggle_code_actions(
|
|
||||||
&mut self,
|
|
||||||
_: &ToggleCodeActions,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.handle_code_action_trigger(window, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn on_mouse_down(
|
pub(super) fn on_mouse_down(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &MouseDownEvent,
|
event: &MouseDownEvent,
|
||||||
|
|
@ -1566,6 +1561,12 @@ impl InputState {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show Mouse context menu
|
||||||
|
if event.button == MouseButton::Right {
|
||||||
|
self.handle_right_click_menu(event, offset, window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if event.modifiers.shift {
|
if event.modifiers.shift {
|
||||||
self.select_to(offset, cx);
|
self.select_to(offset, cx);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,9 @@ impl RenderOnce for TextInput {
|
||||||
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
||||||
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
||||||
})
|
})
|
||||||
.on_action(window.listener_for(&self.state, InputState::toggle_code_actions))
|
.on_action(
|
||||||
|
window.listener_for(&self.state, InputState::on_action_toggle_code_actions),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.on_action(window.listener_for(&self.state, InputState::left))
|
.on_action(window.listener_for(&self.state, InputState::left))
|
||||||
.on_action(window.listener_for(&self.state, InputState::right))
|
.on_action(window.listener_for(&self.state, InputState::right))
|
||||||
|
|
@ -291,6 +293,9 @@ impl RenderOnce for TextInput {
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_down))
|
.on_action(window.listener_for(&self.state, InputState::select_down))
|
||||||
.on_action(window.listener_for(&self.state, InputState::page_up))
|
.on_action(window.listener_for(&self.state, InputState::page_up))
|
||||||
.on_action(window.listener_for(&self.state, InputState::page_down))
|
.on_action(window.listener_for(&self.state, InputState::page_down))
|
||||||
|
.on_action(
|
||||||
|
window.listener_for(&self.state, InputState::on_action_go_to_definition),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_all))
|
.on_action(window.listener_for(&self.state, InputState::select_all))
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
|
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
|
||||||
|
|
@ -313,10 +318,18 @@ impl RenderOnce for TextInput {
|
||||||
MouseButton::Left,
|
MouseButton::Left,
|
||||||
window.listener_for(&self.state, InputState::on_mouse_down),
|
window.listener_for(&self.state, InputState::on_mouse_down),
|
||||||
)
|
)
|
||||||
|
.on_mouse_down(
|
||||||
|
MouseButton::Right,
|
||||||
|
window.listener_for(&self.state, InputState::on_mouse_down),
|
||||||
|
)
|
||||||
.on_mouse_up(
|
.on_mouse_up(
|
||||||
MouseButton::Left,
|
MouseButton::Left,
|
||||||
window.listener_for(&self.state, InputState::on_mouse_up),
|
window.listener_for(&self.state, InputState::on_mouse_up),
|
||||||
)
|
)
|
||||||
|
.on_mouse_up(
|
||||||
|
MouseButton::Right,
|
||||||
|
window.listener_for(&self.state, InputState::on_mouse_up),
|
||||||
|
)
|
||||||
.on_mouse_move(window.listener_for(&self.state, InputState::on_mouse_move))
|
.on_mouse_move(window.listener_for(&self.state, InputState::on_mouse_move))
|
||||||
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
||||||
.size_full()
|
.size_full()
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, deferred, div, prelude::FluentBuilder, px, relative, AnyElement, App, Context,
|
anchored, deferred, div, prelude::FluentBuilder, px, relative, AnyElement, App, Context,
|
||||||
Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable, GlobalElementId,
|
Corner, DismissEvent, Element, ElementId, Entity, Focusable, GlobalElementId,
|
||||||
InspectorElementId, InteractiveElement, IntoElement, MouseButton, MouseDownEvent,
|
InspectorElementId, InteractiveElement, IntoElement, MouseButton, MouseDownEvent,
|
||||||
ParentElement, Pixels, Point, Position, Stateful, Style, Window,
|
ParentElement, Pixels, Point, Position, Stateful, Style, Window,
|
||||||
};
|
};
|
||||||
|
|
@ -224,7 +224,7 @@ impl Element for ContextMenu {
|
||||||
|
|
||||||
// When right mouse click, to build content menu, and show it at the mouse position.
|
// When right mouse click, to build content menu, and show it at the mouse position.
|
||||||
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
||||||
if phase == DispatchPhase::Bubble
|
if phase.bubble()
|
||||||
&& event.button == MouseButton::Right
|
&& event.button == MouseButton::Right
|
||||||
&& bounds.contains(&event.position)
|
&& bounds.contains(&event.position)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,17 @@ use crate::{
|
||||||
button::Button, h_flex, popover::Popover, v_flex, ActiveTheme, Icon, IconName, Selectable,
|
button::Button, h_flex, popover::Popover, v_flex, ActiveTheme, Icon, IconName, Selectable,
|
||||||
Sizable as _,
|
Sizable as _,
|
||||||
};
|
};
|
||||||
use crate::{Kbd, Side, StyledExt};
|
use crate::{Kbd, Side, Size, StyledExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App, AppContext,
|
anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App, AppContext,
|
||||||
Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable,
|
Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, ScrollHandle,
|
InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, ScrollHandle,
|
||||||
SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
|
SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use gpui::{AsKeystroke, MouseDownEvent, Subscription};
|
use gpui::{AsKeystroke, Half, MouseDownEvent, Subscription};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
const ITEM_HEIGHT: Pixels = px(26.);
|
|
||||||
const CONTEXT: &str = "PopupMenu";
|
const CONTEXT: &str = "PopupMenu";
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
|
|
@ -61,7 +60,7 @@ pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement +
|
||||||
}
|
}
|
||||||
impl PopupMenuExt for Button {}
|
impl PopupMenuExt for Button {}
|
||||||
|
|
||||||
enum PopupMenuItem {
|
pub(crate) enum PopupMenuItem {
|
||||||
Separator,
|
Separator,
|
||||||
Label(SharedString),
|
Label(SharedString),
|
||||||
Item {
|
Item {
|
||||||
|
|
@ -113,13 +112,14 @@ pub struct PopupMenu {
|
||||||
/// The parent menu of this menu, if this is a submenu
|
/// The parent menu of this menu, if this is a submenu
|
||||||
parent_menu: Option<WeakEntity<Self>>,
|
parent_menu: Option<WeakEntity<Self>>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
menu_items: Vec<PopupMenuItem>,
|
pub(crate) menu_items: Vec<PopupMenuItem>,
|
||||||
has_icon: bool,
|
has_icon: bool,
|
||||||
selected_index: Option<usize>,
|
selected_index: Option<usize>,
|
||||||
min_width: Option<Pixels>,
|
min_width: Option<Pixels>,
|
||||||
max_width: Option<Pixels>,
|
max_width: Option<Pixels>,
|
||||||
max_height: Option<Pixels>,
|
max_height: Option<Pixels>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
|
size: Size,
|
||||||
|
|
||||||
scrollable: bool,
|
scrollable: bool,
|
||||||
external_link_icon: bool,
|
external_link_icon: bool,
|
||||||
|
|
@ -131,32 +131,35 @@ pub struct PopupMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PopupMenu {
|
impl PopupMenu {
|
||||||
|
pub(crate) fn new(cx: &mut App) -> Self {
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
previous_focus_handle: None,
|
||||||
|
parent_menu: None,
|
||||||
|
menu_items: Vec::new(),
|
||||||
|
selected_index: None,
|
||||||
|
min_width: None,
|
||||||
|
max_width: None,
|
||||||
|
max_height: None,
|
||||||
|
has_icon: false,
|
||||||
|
bounds: Bounds::default(),
|
||||||
|
scrollable: false,
|
||||||
|
scroll_handle: ScrollHandle::default(),
|
||||||
|
scroll_state: ScrollbarState::default(),
|
||||||
|
external_link_icon: true,
|
||||||
|
size: Size::default(),
|
||||||
|
_subscriptions: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(
|
pub fn build(
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
f: impl FnOnce(Self, &mut Window, &mut Context<PopupMenu>) -> Self,
|
f: impl FnOnce(Self, &mut Window, &mut Context<PopupMenu>) -> Self,
|
||||||
) -> Entity<Self> {
|
) -> Entity<Self> {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let focus_handle = cx.focus_handle();
|
let mut menu = Self::new(cx);
|
||||||
let _subscriptions = vec![];
|
menu.previous_focus_handle = window.focused(cx);
|
||||||
|
|
||||||
let menu = Self {
|
|
||||||
focus_handle,
|
|
||||||
previous_focus_handle: window.focused(cx),
|
|
||||||
parent_menu: None,
|
|
||||||
menu_items: Vec::new(),
|
|
||||||
selected_index: None,
|
|
||||||
min_width: None,
|
|
||||||
max_width: None,
|
|
||||||
max_height: None,
|
|
||||||
has_icon: false,
|
|
||||||
bounds: Bounds::default(),
|
|
||||||
scrollable: false,
|
|
||||||
scroll_handle: ScrollHandle::default(),
|
|
||||||
scroll_state: ScrollbarState::default(),
|
|
||||||
external_link_icon: true,
|
|
||||||
_subscriptions,
|
|
||||||
};
|
|
||||||
f(menu, window, cx)
|
f(menu, window, cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -198,6 +201,17 @@ impl PopupMenu {
|
||||||
self.menu_with_disabled(label, action, false)
|
self.menu_with_disabled(label, action, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add Menu Item with enable state
|
||||||
|
pub fn menu_with_enable(
|
||||||
|
mut self,
|
||||||
|
label: impl Into<SharedString>,
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
enable: bool,
|
||||||
|
) -> Self {
|
||||||
|
self.add_menu_item(label, None, action, !enable);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Add Menu Item with disabled state
|
/// Add Menu Item with disabled state
|
||||||
pub fn menu_with_disabled(
|
pub fn menu_with_disabled(
|
||||||
mut self,
|
mut self,
|
||||||
|
|
@ -428,6 +442,12 @@ impl PopupMenu {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Use small size, the menu item will have smaller height.
|
||||||
|
pub(crate) fn small(mut self) -> Self {
|
||||||
|
self.size = Size::Small;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a separator Menu Item
|
/// Add a separator Menu Item
|
||||||
pub fn separator(mut self) -> Self {
|
pub fn separator(mut self) -> Self {
|
||||||
if self.menu_items.is_empty() {
|
if self.menu_items.is_empty() {
|
||||||
|
|
@ -805,12 +825,17 @@ impl PopupMenu {
|
||||||
const EDGE_PADDING: Pixels = px(8.);
|
const EDGE_PADDING: Pixels = px(8.);
|
||||||
const INNER_PADDING: Pixels = px(4.);
|
const INNER_PADDING: Pixels = px(4.);
|
||||||
|
|
||||||
|
let (item_height, radius) = match self.size {
|
||||||
|
Size::Small => (px(20.), state.radius.half()),
|
||||||
|
_ => (px(26.), state.radius),
|
||||||
|
};
|
||||||
|
|
||||||
let this = MenuItem::new(ix)
|
let this = MenuItem::new(ix)
|
||||||
.relative()
|
.relative()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.py_0()
|
.py_0()
|
||||||
.px(INNER_PADDING)
|
.px(INNER_PADDING)
|
||||||
.rounded(state.radius)
|
.rounded(radius)
|
||||||
.items_center()
|
.items_center()
|
||||||
.hovered(selected)
|
.hovered(selected)
|
||||||
.on_mouse_enter(cx.listener(move |this, _, _, cx| {
|
.on_mouse_enter(cx.listener(move |this, _, _, cx| {
|
||||||
|
|
@ -853,7 +878,7 @@ impl PopupMenu {
|
||||||
.disabled(*disabled)
|
.disabled(*disabled)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.min_h(ITEM_HEIGHT)
|
.min_h(item_height)
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_x_1()
|
.gap_x_1()
|
||||||
.children(Self::render_icon(has_icon, icon.clone(), window, cx))
|
.children(Self::render_icon(has_icon, icon.clone(), window, cx))
|
||||||
|
|
@ -877,7 +902,7 @@ impl PopupMenu {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.disabled(*disabled)
|
.disabled(*disabled)
|
||||||
.h(ITEM_HEIGHT)
|
.h(item_height)
|
||||||
.children(Self::render_icon(has_icon, icon.clone(), window, cx))
|
.children(Self::render_icon(has_icon, icon.clone(), window, cx))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
@ -914,7 +939,7 @@ impl PopupMenu {
|
||||||
.items_start()
|
.items_start()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.min_h(ITEM_HEIGHT)
|
.min_h(item_height)
|
||||||
.size_full()
|
.size_full()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_x_1()
|
.gap_x_1()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue