editor: Add DefinitionProvider. (#1261)
https://github.com/user-attachments/assets/f8234a3f-3075-48ce-ae46-8ea869b6a13d - Hold `cmd` to hover on a word to trigger definition check.
This commit is contained in:
parent
6a817d6a05
commit
5e7a2cb37f
11 changed files with 841 additions and 487 deletions
|
|
@ -57,6 +57,7 @@ single_range_in_vec_init = "allow"
|
|||
style = { level = "allow", priority = -1 }
|
||||
todo = "deny"
|
||||
type_complexity = "allow"
|
||||
manual_is_multiple_of = "allow"
|
||||
|
||||
[profile.dev]
|
||||
codegen-units = 16
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ use gpui_component::{
|
|||
h_flex,
|
||||
highlighter::{Diagnostic, DiagnosticSeverity, Language, LanguageConfig, LanguageRegistry},
|
||||
input::{
|
||||
self, CodeActionProvider, CompletionProvider, HoverProvider, InputEvent, InputState,
|
||||
Position, Rope, RopeExt, TabSize, TextInput,
|
||||
self, CodeActionProvider, CompletionProvider, DefinitionProvider, HoverProvider,
|
||||
InputEvent, InputState, Position, Rope, RopeExt, TabSize, TextInput,
|
||||
},
|
||||
v_flex, ActiveTheme, ContextModal, IconName, IndexPath, Selectable, Sizable,
|
||||
};
|
||||
|
|
@ -301,6 +301,83 @@ impl HoverProvider for ExampleLspStore {
|
|||
}
|
||||
}
|
||||
|
||||
const RUST_DOC_URLS: &[(&str, &str)] = &[
|
||||
("String", "string/struct.String"),
|
||||
("Debug", "fmt/trait.Debug"),
|
||||
("Clone", "clone/trait.Clone"),
|
||||
("Option", "option/enum.Option"),
|
||||
("Result", "result/enum.Result"),
|
||||
("Vec", "vec/struct.Vec"),
|
||||
("HashMap", "collections/hash_map/struct.HashMap"),
|
||||
("HashSet", "collections/hash_set/struct.HashSet"),
|
||||
("Arc", "sync/struct.Arc"),
|
||||
("RwLock", "sync/struct.RwLock"),
|
||||
("Duration", "time/struct.Duration"),
|
||||
];
|
||||
|
||||
impl DefinitionProvider for ExampleLspStore {
|
||||
fn definitions(
|
||||
&self,
|
||||
text: &Rope,
|
||||
offset: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Vec<lsp_types::LocationLink>>> {
|
||||
let Some(word_range) = text.word_range(offset) else {
|
||||
return Task::ready(Ok(vec![]));
|
||||
};
|
||||
let word = text.slice(word_range.clone()).to_string();
|
||||
|
||||
let document_uri = lsp_types::Uri::from_str("file://example").unwrap();
|
||||
let start = text.offset_to_position(word_range.start);
|
||||
let end = text.offset_to_position(word_range.end);
|
||||
let symbol_range = lsp_types::Range { start, end };
|
||||
|
||||
if word == "Duration" {
|
||||
let target_range = lsp_types::Range {
|
||||
start: lsp_types::Position {
|
||||
line: 2,
|
||||
character: 4,
|
||||
},
|
||||
end: lsp_types::Position {
|
||||
line: 2,
|
||||
character: 23,
|
||||
},
|
||||
};
|
||||
return Task::ready(Ok(vec![lsp_types::LocationLink {
|
||||
target_uri: document_uri,
|
||||
target_range: target_range,
|
||||
target_selection_range: target_range,
|
||||
origin_selection_range: Some(symbol_range),
|
||||
}]));
|
||||
}
|
||||
|
||||
let names = RUST_DOC_URLS
|
||||
.iter()
|
||||
.map(|(name, _)| *name)
|
||||
.collect::<Vec<_>>();
|
||||
for (ix, t) in names.iter().enumerate() {
|
||||
if *t == word {
|
||||
let url = RUST_DOC_URLS[ix].1;
|
||||
let location = lsp_types::LocationLink {
|
||||
target_uri: lsp_types::Uri::from_str(&format!(
|
||||
"https://doc.rust-lang.org/std/{}.html",
|
||||
url
|
||||
))
|
||||
.unwrap(),
|
||||
target_selection_range: lsp_types::Range::default(),
|
||||
target_range: lsp_types::Range::default(),
|
||||
origin_selection_range: Some(symbol_range),
|
||||
};
|
||||
|
||||
return Task::ready(Ok(vec![location]));
|
||||
}
|
||||
}
|
||||
|
||||
Task::ready(Ok(vec![]))
|
||||
}
|
||||
}
|
||||
|
||||
struct TextConvertor;
|
||||
|
||||
impl CodeActionProvider for TextConvertor {
|
||||
|
|
@ -528,6 +605,7 @@ impl Example {
|
|||
editor.lsp.completion_provider = Some(lsp_store.clone());
|
||||
editor.lsp.code_action_providers = vec![lsp_store.clone(), Rc::new(TextConvertor)];
|
||||
editor.lsp.hover_provider = Some(lsp_store.clone());
|
||||
editor.lsp.definition_provider = Some(lsp_store.clone());
|
||||
|
||||
editor
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ impl HelloWorld {
|
|||
}
|
||||
|
||||
// Greets multiple people asynchronously with configurable delay
|
||||
//
|
||||
// Use `Command-click` on `Duration` will jump to its definition.
|
||||
// Use `Control-click` on `String`, `HashMap` or `Result` will open its documentation page.
|
||||
pub async fn greet<T: AsRef<str>>(&self, names: &[T]) -> Result<()> {
|
||||
for name in names {
|
||||
time::sleep(Duration::from_millis(100)).await;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::{ops::Range, rc::Rc};
|
|||
|
||||
use gpui::{
|
||||
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
|
||||
Entity, GlobalElementId, Half, HighlightStyle, IntoElement, LayoutId, MouseButton,
|
||||
Entity, GlobalElementId, Half, HighlightStyle, Hitbox, IntoElement, LayoutId, MouseButton,
|
||||
MouseMoveEvent, Path, Pixels, Point, SharedString, Size, Style, TextAlign, TextRun,
|
||||
UnderlineStyle, Window, WrappedLine,
|
||||
};
|
||||
|
|
@ -21,7 +21,7 @@ pub(super) const RIGHT_MARGIN: Pixels = px(10.);
|
|||
pub(super) const LINE_NUMBER_RIGHT_MARGIN: Pixels = px(10.);
|
||||
|
||||
pub(super) struct TextElement {
|
||||
state: Entity<InputState>,
|
||||
pub(crate) state: Entity<InputState>,
|
||||
placeholder: SharedString,
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +230,8 @@ impl TextElement {
|
|||
(cursor_bounds, scroll_offset, current_row)
|
||||
}
|
||||
|
||||
fn layout_match_range(
|
||||
/// Layout the match range to a Path.
|
||||
pub(crate) fn layout_match_range(
|
||||
range: Range<usize>,
|
||||
last_layout: &LastLayout,
|
||||
bounds: &mut Bounds<Pixels>,
|
||||
|
|
@ -516,6 +517,11 @@ impl TextElement {
|
|||
|
||||
let diagnostic_styles = diagnostics.styles_for_range(&visible_byte_range, cx);
|
||||
|
||||
// hover definition style
|
||||
if let Some(hover_style) = self.layout_hover_definition(cx) {
|
||||
styles.push(hover_style);
|
||||
}
|
||||
|
||||
// Combine marker styles
|
||||
styles = gpui::combine_highlights(diagnostic_styles, styles).collect();
|
||||
|
||||
|
|
@ -537,6 +543,7 @@ pub(super) struct PrepaintState {
|
|||
selection_path: Option<Path<Pixels>>,
|
||||
hover_highlight_path: Option<Path<Pixels>>,
|
||||
search_match_paths: Vec<(Path<Pixels>, bool)>,
|
||||
hover_definition_hitbox: Option<Hitbox>,
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
|
|
@ -914,6 +921,8 @@ impl Element for TextElement {
|
|||
None
|
||||
};
|
||||
|
||||
let hover_definition_hitbox = self.layout_hover_definition_hitbox(state, window, cx);
|
||||
|
||||
PrepaintState {
|
||||
bounds,
|
||||
last_layout,
|
||||
|
|
@ -925,6 +934,7 @@ impl Element for TextElement {
|
|||
selection_path,
|
||||
search_match_paths,
|
||||
hover_highlight_path,
|
||||
hover_definition_hitbox,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1117,6 +1127,10 @@ impl Element for TextElement {
|
|||
cx.notify();
|
||||
});
|
||||
|
||||
if let Some(hitbox) = prepaint.hover_definition_hitbox.as_ref() {
|
||||
window.set_cursor_style(gpui::CursorStyle::PointingHand, &hitbox);
|
||||
}
|
||||
|
||||
self.paint_mouse_listeners(window, cx);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,401 +0,0 @@
|
|||
use std::{cell::RefCell, ops::Range, rc::Rc};
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{App, Context, Entity, EntityInputHandler, SharedString, Task, Window};
|
||||
use lsp_types::{
|
||||
request::Completion, CodeAction, CompletionContext, CompletionItem, CompletionResponse,
|
||||
};
|
||||
use rope::Rope;
|
||||
|
||||
use crate::input::{
|
||||
popovers::{CodeActionItem, CodeActionMenu, CompletionMenu, ContextMenu, HoverPopover},
|
||||
InputState, RopeExt,
|
||||
};
|
||||
|
||||
/// LSP ServerCapabilities
|
||||
///
|
||||
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverCapabilities
|
||||
pub struct Lsp {
|
||||
/// The completion provider.
|
||||
pub completion_provider: Option<Rc<dyn CompletionProvider>>,
|
||||
/// The code action providers.
|
||||
pub code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
|
||||
/// The hover provider.
|
||||
pub hover_provider: Option<Rc<dyn HoverProvider>>,
|
||||
_hover_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
impl Default for Lsp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
completion_provider: None,
|
||||
code_action_providers: vec![],
|
||||
hover_provider: None,
|
||||
_hover_task: Task::ready(Ok(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for providing code completions based on the current input state and context.
|
||||
pub trait CompletionProvider {
|
||||
/// Fetches completions based on the given byte offset.
|
||||
fn completions(
|
||||
&self,
|
||||
text: &Rope,
|
||||
offset: usize,
|
||||
trigger: CompletionContext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<InputState>,
|
||||
) -> Task<Result<Vec<CompletionResponse>>>;
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
_completion_indices: Vec<usize>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_: &mut Context<InputState>,
|
||||
) -> Task<Result<bool>> {
|
||||
Task::ready(Ok(false))
|
||||
}
|
||||
|
||||
/// Determines if the completion should be triggered based on the given byte offset.
|
||||
///
|
||||
/// This is called on the main thread.
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
offset: usize,
|
||||
new_text: &str,
|
||||
cx: &mut Context<InputState>,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
pub trait CodeActionProvider {
|
||||
/// The id for this CodeAction.
|
||||
fn id(&self) -> SharedString;
|
||||
|
||||
/// Fetches code actions for the specified range.
|
||||
fn code_actions(
|
||||
&self,
|
||||
state: Entity<InputState>,
|
||||
range: Range<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<CodeAction>>>;
|
||||
|
||||
/// Performs the specified code action.
|
||||
fn perform_code_action(
|
||||
&self,
|
||||
state: Entity<InputState>,
|
||||
action: CodeAction,
|
||||
push_to_history: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
pub trait HoverProvider {
|
||||
/// Hover provider
|
||||
///
|
||||
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover
|
||||
fn hover(
|
||||
&self,
|
||||
_text: &Rope,
|
||||
_offset: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Option<lsp_types::Hover>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub(crate) fn hide_context_menu(&mut self, cx: &mut Context<Self>) {
|
||||
self.context_menu = None;
|
||||
self._context_menu_task = Task::ready(Ok(()));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn is_context_menu_open(&self, cx: &App) -> bool {
|
||||
let Some(menu) = self.context_menu.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
menu.is_open(cx)
|
||||
}
|
||||
|
||||
/// Handles an action for the completion menu, if it exists.
|
||||
///
|
||||
/// Return true if the action was handled, otherwise false.
|
||||
pub fn handle_action_for_context_menu(
|
||||
&mut self,
|
||||
action: Box<dyn gpui::Action>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let Some(menu) = self.context_menu.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut handled = false;
|
||||
|
||||
match menu {
|
||||
ContextMenu::Completion(menu) => {
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
handled = menu.handle_action(action, window, cx)
|
||||
});
|
||||
}
|
||||
ContextMenu::CodeAction(menu) => {
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
handled = menu.handle_action(action, window, cx)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handled
|
||||
}
|
||||
|
||||
pub fn handle_completion_trigger(
|
||||
&mut self,
|
||||
range: &Range<usize>,
|
||||
new_text: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.completion_inserting {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(provider) = self.lsp.completion_provider.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let start = range.end;
|
||||
let new_offset = self.cursor();
|
||||
|
||||
if !provider.is_completion_trigger(start, new_text, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let menu = match self.context_menu.as_ref() {
|
||||
Some(ContextMenu::Completion(menu)) => Some(menu),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// To create or get the existing completion menu.
|
||||
let menu = match menu {
|
||||
Some(menu) => menu.clone(),
|
||||
None => {
|
||||
let menu = CompletionMenu::new(cx.entity(), window, cx);
|
||||
self.context_menu = Some(ContextMenu::Completion(menu.clone()));
|
||||
menu
|
||||
}
|
||||
};
|
||||
|
||||
let start_offset = menu.read(cx).trigger_start_offset.unwrap_or(start);
|
||||
if new_offset < start_offset {
|
||||
return;
|
||||
}
|
||||
|
||||
let query = self
|
||||
.text_for_range(
|
||||
self.range_to_utf16(&(start_offset..new_offset)),
|
||||
&mut None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
_ = menu.update(cx, |menu, _| {
|
||||
menu.update_query(start_offset, query.clone());
|
||||
});
|
||||
|
||||
let completion_context = CompletionContext {
|
||||
trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||
trigger_character: Some(query),
|
||||
};
|
||||
|
||||
let provider_responses =
|
||||
provider.completions(&self.text, start_offset, completion_context, window, cx);
|
||||
self._context_menu_task = cx.spawn_in(window, async move |editor, cx| {
|
||||
let mut completions: Vec<CompletionItem> = vec![];
|
||||
if let Some(provider_responses) = provider_responses.await.ok() {
|
||||
for resp in provider_responses {
|
||||
match resp {
|
||||
CompletionResponse::Array(items) => completions.extend(items),
|
||||
CompletionResponse::List(list) => completions.extend(list.items),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if completions.is_empty() {
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
menu.hide(cx);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
if !editor.focus_handle.is_focused(window) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
menu.show(new_offset, completions, window, cx);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
/// Show code actions for the cursor.
|
||||
pub(super) fn handle_code_action_trigger(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let providers = self.lsp.code_action_providers.clone();
|
||||
let menu = match self.context_menu.as_ref() {
|
||||
Some(ContextMenu::CodeAction(menu)) => Some(menu),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let menu = match menu {
|
||||
Some(menu) => menu.clone(),
|
||||
None => {
|
||||
let menu = CodeActionMenu::new(cx.entity(), window, cx);
|
||||
self.context_menu = Some(ContextMenu::CodeAction(menu.clone()));
|
||||
menu
|
||||
}
|
||||
};
|
||||
|
||||
let range = self.selected_range.start..self.selected_range.end;
|
||||
|
||||
let state = cx.entity();
|
||||
self._context_menu_task = cx.spawn_in(window, async move |editor, cx| {
|
||||
let mut provider_responses = vec![];
|
||||
_ = cx.update(|window, cx| {
|
||||
for provider in providers {
|
||||
let task = provider.code_actions(state.clone(), range.clone(), window, cx);
|
||||
provider_responses.push((provider.id(), task));
|
||||
}
|
||||
});
|
||||
|
||||
let mut code_actions: Vec<CodeActionItem> = vec![];
|
||||
for (provider_id, provider_responses) in provider_responses {
|
||||
if let Some(responses) = provider_responses.await.ok() {
|
||||
code_actions.extend(responses.into_iter().map(|action| CodeActionItem {
|
||||
provider_id: provider_id.clone(),
|
||||
action,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if code_actions.is_empty() {
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
menu.hide(cx);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
if !editor.focus_handle.is_focused(window) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
menu.show(editor.cursor(), code_actions, window, cx);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn perform_code_action(
|
||||
&mut self,
|
||||
item: &CodeActionItem,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let providers = self.lsp.code_action_providers.clone();
|
||||
let Some(provider) = providers
|
||||
.iter()
|
||||
.find(|provider| provider.id() == item.provider_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let state = cx.entity();
|
||||
let task = provider.perform_code_action(state, item.action.clone(), true, window, cx);
|
||||
|
||||
cx.spawn_in(window, async move |_, _| {
|
||||
let _ = task.await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Apply a list of [`lsp_types::TextEdit`] to mutate the text.
|
||||
pub fn apply_lsp_edits(
|
||||
&mut self,
|
||||
text_edits: &Vec<lsp_types::TextEdit>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
for edit in text_edits {
|
||||
let start = self.text.position_to_offset(&edit.range.start);
|
||||
let end = self.text.position_to_offset(&edit.range.end);
|
||||
|
||||
let range_utf16 = self.range_to_utf16(&(start..end));
|
||||
self.replace_text_in_range(Some(range_utf16), &edit.new_text, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle hover trigger LSP request.
|
||||
pub(super) fn handle_hover(
|
||||
&mut self,
|
||||
offset: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<InputState>,
|
||||
) {
|
||||
let Some(provider) = self.lsp.hover_provider.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(hover_popover) = self.hover_popover.as_ref() {
|
||||
if hover_popover.read(cx).is_same(offset) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Currently not implemented.
|
||||
let task = provider.hover(&self.text, offset, window, cx);
|
||||
let word_range = self.text.word_range(offset).unwrap_or(offset..offset);
|
||||
let editor = cx.entity();
|
||||
self.lsp._hover_task = cx.spawn_in(window, async move |_, cx| {
|
||||
let result = task.await?;
|
||||
|
||||
_ = editor.update(cx, |editor, cx| match result {
|
||||
Some(hover) => {
|
||||
let hover_popover = HoverPopover::new(cx.entity(), word_range, &hover, cx);
|
||||
editor.hover_popover = Some(hover_popover);
|
||||
}
|
||||
None => {
|
||||
editor.hover_popover = None;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
131
crates/ui/src/input/lsp/code_actions.rs
Normal file
131
crates/ui/src/input/lsp/code_actions.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
use anyhow::Result;
|
||||
use gpui::{App, Context, Entity, SharedString, Task, Window};
|
||||
use lsp_types::CodeAction;
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::input::{
|
||||
popovers::{CodeActionItem, CodeActionMenu, ContextMenu},
|
||||
InputState,
|
||||
};
|
||||
|
||||
pub trait CodeActionProvider {
|
||||
/// The id for this CodeAction.
|
||||
fn id(&self) -> SharedString;
|
||||
|
||||
/// Fetches code actions for the specified range.
|
||||
///
|
||||
/// textDocument/codeAction
|
||||
///
|
||||
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction
|
||||
fn code_actions(
|
||||
&self,
|
||||
state: Entity<InputState>,
|
||||
range: Range<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<CodeAction>>>;
|
||||
|
||||
/// Performs the specified code action.
|
||||
fn perform_code_action(
|
||||
&self,
|
||||
state: Entity<InputState>,
|
||||
action: CodeAction,
|
||||
push_to_history: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
/// Show code actions for the cursor.
|
||||
pub(crate) fn handle_code_action_trigger(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let providers = self.lsp.code_action_providers.clone();
|
||||
let menu = match self.context_menu.as_ref() {
|
||||
Some(ContextMenu::CodeAction(menu)) => Some(menu),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let menu = match menu {
|
||||
Some(menu) => menu.clone(),
|
||||
None => {
|
||||
let menu = CodeActionMenu::new(cx.entity(), window, cx);
|
||||
self.context_menu = Some(ContextMenu::CodeAction(menu.clone()));
|
||||
menu
|
||||
}
|
||||
};
|
||||
|
||||
let range = self.selected_range.start..self.selected_range.end;
|
||||
|
||||
let state = cx.entity();
|
||||
self._context_menu_task = cx.spawn_in(window, async move |editor, cx| {
|
||||
let mut provider_responses = vec![];
|
||||
_ = cx.update(|window, cx| {
|
||||
for provider in providers {
|
||||
let task = provider.code_actions(state.clone(), range.clone(), window, cx);
|
||||
provider_responses.push((provider.id(), task));
|
||||
}
|
||||
});
|
||||
|
||||
let mut code_actions: Vec<CodeActionItem> = vec![];
|
||||
for (provider_id, provider_responses) in provider_responses {
|
||||
if let Some(responses) = provider_responses.await.ok() {
|
||||
code_actions.extend(responses.into_iter().map(|action| CodeActionItem {
|
||||
provider_id: provider_id.clone(),
|
||||
action,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if code_actions.is_empty() {
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
menu.hide(cx);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
if !editor.focus_handle.is_focused(window) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
menu.show(editor.cursor(), code_actions, window, cx);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn perform_code_action(
|
||||
&mut self,
|
||||
item: &CodeActionItem,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let providers = self.lsp.code_action_providers.clone();
|
||||
let Some(provider) = providers
|
||||
.iter()
|
||||
.find(|provider| provider.id() == item.provider_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let state = cx.entity();
|
||||
let task = provider.perform_code_action(state, item.action.clone(), true, window, cx);
|
||||
|
||||
cx.spawn_in(window, async move |_, _| {
|
||||
let _ = task.await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
148
crates/ui/src/input/lsp/completions.rs
Normal file
148
crates/ui/src/input/lsp/completions.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
use anyhow::Result;
|
||||
use gpui::{Context, EntityInputHandler, Task, Window};
|
||||
use lsp_types::{request::Completion, CompletionContext, CompletionItem, CompletionResponse};
|
||||
use rope::Rope;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc};
|
||||
|
||||
use crate::input::{
|
||||
popovers::{CompletionMenu, ContextMenu},
|
||||
InputState,
|
||||
};
|
||||
|
||||
/// A trait for providing code completions based on the current input state and context.
|
||||
pub trait CompletionProvider {
|
||||
/// Fetches completions based on the given byte offset.
|
||||
///
|
||||
/// textDocument/completion
|
||||
///
|
||||
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
|
||||
fn completions(
|
||||
&self,
|
||||
text: &Rope,
|
||||
offset: usize,
|
||||
trigger: CompletionContext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<InputState>,
|
||||
) -> Task<Result<Vec<CompletionResponse>>>;
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
_completion_indices: Vec<usize>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_: &mut Context<InputState>,
|
||||
) -> Task<Result<bool>> {
|
||||
Task::ready(Ok(false))
|
||||
}
|
||||
|
||||
/// Determines if the completion should be triggered based on the given byte offset.
|
||||
///
|
||||
/// This is called on the main thread.
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
offset: usize,
|
||||
new_text: &str,
|
||||
cx: &mut Context<InputState>,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub(crate) fn handle_completion_trigger(
|
||||
&mut self,
|
||||
range: &Range<usize>,
|
||||
new_text: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.completion_inserting {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(provider) = self.lsp.completion_provider.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let start = range.end;
|
||||
let new_offset = self.cursor();
|
||||
|
||||
if !provider.is_completion_trigger(start, new_text, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let menu = match self.context_menu.as_ref() {
|
||||
Some(ContextMenu::Completion(menu)) => Some(menu),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// To create or get the existing completion menu.
|
||||
let menu = match menu {
|
||||
Some(menu) => menu.clone(),
|
||||
None => {
|
||||
let menu = CompletionMenu::new(cx.entity(), window, cx);
|
||||
self.context_menu = Some(ContextMenu::Completion(menu.clone()));
|
||||
menu
|
||||
}
|
||||
};
|
||||
|
||||
let start_offset = menu.read(cx).trigger_start_offset.unwrap_or(start);
|
||||
if new_offset < start_offset {
|
||||
return;
|
||||
}
|
||||
|
||||
let query = self
|
||||
.text_for_range(
|
||||
self.range_to_utf16(&(start_offset..new_offset)),
|
||||
&mut None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
_ = menu.update(cx, |menu, _| {
|
||||
menu.update_query(start_offset, query.clone());
|
||||
});
|
||||
|
||||
let completion_context = CompletionContext {
|
||||
trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||
trigger_character: Some(query),
|
||||
};
|
||||
|
||||
let provider_responses =
|
||||
provider.completions(&self.text, start_offset, completion_context, window, cx);
|
||||
self._context_menu_task = cx.spawn_in(window, async move |editor, cx| {
|
||||
let mut completions: Vec<CompletionItem> = vec![];
|
||||
if let Some(provider_responses) = provider_responses.await.ok() {
|
||||
for resp in provider_responses {
|
||||
match resp {
|
||||
CompletionResponse::Array(items) => completions.extend(items),
|
||||
CompletionResponse::List(list) => completions.extend(list.items),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if completions.is_empty() {
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
menu.hide(cx);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
if !editor.focus_handle.is_focused(window) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
menu.show(new_offset, completions, window, cx);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
186
crates/ui/src/input/lsp/definitions.rs
Normal file
186
crates/ui/src/input/lsp/definitions.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
use anyhow::Result;
|
||||
use gpui::{
|
||||
px, App, Context, HighlightStyle, Hitbox, MouseDownEvent, Task, UnderlineStyle, Window,
|
||||
};
|
||||
use rope::Rope;
|
||||
use std::{ops::Range, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
input::{element::TextElement, InputState, RopeExt},
|
||||
ActiveTheme,
|
||||
};
|
||||
|
||||
/// Definition provider
|
||||
///
|
||||
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition
|
||||
pub trait DefinitionProvider {
|
||||
/// textDocument/definition
|
||||
///
|
||||
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition
|
||||
fn definitions(
|
||||
&self,
|
||||
_text: &Rope,
|
||||
_offset: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Vec<lsp_types::LocationLink>>>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct HoverDefinition {
|
||||
/// The range of the symbol that triggered the hover.
|
||||
symbol_range: Range<usize>,
|
||||
pub(crate) locations: Rc<Vec<lsp_types::LocationLink>>,
|
||||
}
|
||||
|
||||
impl HoverDefinition {
|
||||
pub(crate) fn new(symbol_range: Range<usize>, locations: Vec<lsp_types::LocationLink>) -> Self {
|
||||
Self {
|
||||
symbol_range,
|
||||
locations: Rc::new(locations),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_same(&self, offset: usize) -> bool {
|
||||
self.symbol_range.contains(&offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub(super) fn handle_hover_definition(
|
||||
&mut self,
|
||||
offset: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<InputState>,
|
||||
) {
|
||||
let Some(provider) = self.lsp.definition_provider.clone() else {
|
||||
return;
|
||||
};
|
||||
if let Some(hover_definition) = self.hover_definition.as_ref() {
|
||||
if hover_definition.is_same(offset) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Currently not implemented.
|
||||
let task = provider.definitions(&self.text, offset, window, cx);
|
||||
let mut symbol_range = self.text.word_range(offset).unwrap_or(offset..offset);
|
||||
let editor = cx.entity();
|
||||
self.lsp._hover_task = cx.spawn_in(window, async move |_, cx| {
|
||||
let locations = task.await?;
|
||||
|
||||
_ = editor.update(cx, |editor, cx| {
|
||||
if locations.is_empty() {
|
||||
editor.hover_definition = None;
|
||||
} else {
|
||||
if let Some(location) = locations.first() {
|
||||
if let Some(range) = location.origin_selection_range {
|
||||
let start = editor.text.position_to_offset(&range.start);
|
||||
let end = editor.text.position_to_offset(&range.end);
|
||||
symbol_range = start..end;
|
||||
}
|
||||
}
|
||||
|
||||
editor.hover_definition = Some(HoverDefinition::new(symbol_range, locations));
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
/// Return true if handled.
|
||||
pub(crate) fn handle_click_hover_definition(
|
||||
&mut self,
|
||||
event: &MouseDownEvent,
|
||||
offset: usize,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<InputState>,
|
||||
) -> bool {
|
||||
if !event.modifiers.secondary() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(hover_definition) = self.hover_definition.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
if !hover_definition.is_same(offset) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(location) = hover_definition.locations.first().cloned() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if location
|
||||
.target_uri
|
||||
.scheme()
|
||||
.map(|s| s.as_str() == "https" || s.as_str() == "http")
|
||||
== Some(true)
|
||||
{
|
||||
cx.open_url(&location.target_uri.to_string());
|
||||
} else {
|
||||
// Move to the location.
|
||||
let target_range = location.target_range;
|
||||
let start = self.text.position_to_offset(&target_range.start);
|
||||
let end = self.text.position_to_offset(&target_range.end);
|
||||
|
||||
self.move_to(start, cx);
|
||||
self.select_to(end, cx);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl TextElement {
|
||||
pub(crate) fn layout_hover_definition(
|
||||
&self,
|
||||
cx: &App,
|
||||
) -> Option<(Range<usize>, HighlightStyle)> {
|
||||
let editor = self.state.read(cx);
|
||||
if !editor.mode.is_code_editor() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(hover_definition) = editor.hover_definition.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut highlight_style: HighlightStyle = cx
|
||||
.theme()
|
||||
.highlight_theme
|
||||
.link_text
|
||||
.map(|style| style.into())
|
||||
.unwrap_or_default();
|
||||
|
||||
highlight_style.underline = Some(UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
..UnderlineStyle::default()
|
||||
});
|
||||
|
||||
Some((hover_definition.symbol_range.clone(), highlight_style))
|
||||
}
|
||||
|
||||
pub(crate) fn layout_hover_definition_hitbox(
|
||||
&self,
|
||||
editor: &InputState,
|
||||
window: &mut Window,
|
||||
_cx: &App,
|
||||
) -> Option<Hitbox> {
|
||||
if !editor.mode.is_code_editor() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(hover_definition) = editor.hover_definition.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Some(bounds) = editor.range_to_bounds(&hover_definition.symbol_range) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal))
|
||||
}
|
||||
}
|
||||
66
crates/ui/src/input/lsp/hover.rs
Normal file
66
crates/ui/src/input/lsp/hover.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
use anyhow::Result;
|
||||
use gpui::{App, Context, Task, Window};
|
||||
use rope::Rope;
|
||||
|
||||
use crate::input::{popovers::HoverPopover, InputState, RopeExt};
|
||||
|
||||
/// Hover provider
|
||||
///
|
||||
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover
|
||||
pub trait HoverProvider {
|
||||
/// textDocument/hover
|
||||
///
|
||||
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover
|
||||
fn hover(
|
||||
&self,
|
||||
_text: &Rope,
|
||||
_offset: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<Option<lsp_types::Hover>>>;
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
/// Handle hover trigger LSP request.
|
||||
pub(super) fn handle_hover_popover(
|
||||
&mut self,
|
||||
offset: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<InputState>,
|
||||
) {
|
||||
let Some(provider) = self.lsp.hover_provider.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(hover_popover) = self.hover_popover.as_ref() {
|
||||
if hover_popover.read(cx).is_same(offset) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Currently not implemented.
|
||||
let task = provider.hover(&self.text, offset, window, cx);
|
||||
let mut symbol_range = self.text.word_range(offset).unwrap_or(offset..offset);
|
||||
let editor = cx.entity();
|
||||
self.lsp._hover_task = cx.spawn_in(window, async move |_, cx| {
|
||||
let result = task.await?;
|
||||
|
||||
_ = editor.update(cx, |editor, cx| match result {
|
||||
Some(hover) => {
|
||||
if let Some(range) = hover.range {
|
||||
let start = editor.text.position_to_offset(&range.start);
|
||||
let end = editor.text.position_to_offset(&range.end);
|
||||
symbol_range = start..end;
|
||||
}
|
||||
let hover_popover = HoverPopover::new(cx.entity(), symbol_range, &hover, cx);
|
||||
editor.hover_popover = Some(hover_popover);
|
||||
}
|
||||
None => {
|
||||
editor.hover_popover = None;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
121
crates/ui/src/input/lsp/mod.rs
Normal file
121
crates/ui/src/input/lsp/mod.rs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
use anyhow::Result;
|
||||
use gpui::{App, Context, EntityInputHandler, MouseMoveEvent, Task, Window};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::input::{popovers::ContextMenu, InputState, RopeExt};
|
||||
|
||||
mod code_actions;
|
||||
mod completions;
|
||||
mod definitions;
|
||||
mod hover;
|
||||
|
||||
pub use code_actions::*;
|
||||
pub use completions::*;
|
||||
pub use definitions::*;
|
||||
pub use hover::*;
|
||||
|
||||
/// LSP ServerCapabilities
|
||||
///
|
||||
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverCapabilities
|
||||
pub struct Lsp {
|
||||
/// The completion provider.
|
||||
pub completion_provider: Option<Rc<dyn CompletionProvider>>,
|
||||
/// The code action providers.
|
||||
pub code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
|
||||
/// The hover provider.
|
||||
pub hover_provider: Option<Rc<dyn HoverProvider>>,
|
||||
/// The definition provider.
|
||||
pub definition_provider: Option<Rc<dyn DefinitionProvider>>,
|
||||
_hover_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
impl Default for Lsp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
completion_provider: None,
|
||||
code_action_providers: vec![],
|
||||
hover_provider: None,
|
||||
definition_provider: None,
|
||||
_hover_task: Task::ready(Ok(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub(crate) fn hide_context_menu(&mut self, cx: &mut Context<Self>) {
|
||||
self.context_menu = None;
|
||||
self._context_menu_task = Task::ready(Ok(()));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn is_context_menu_open(&self, cx: &App) -> bool {
|
||||
let Some(menu) = self.context_menu.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
menu.is_open(cx)
|
||||
}
|
||||
|
||||
/// Handles an action for the completion menu, if it exists.
|
||||
///
|
||||
/// Return true if the action was handled, otherwise false.
|
||||
pub fn handle_action_for_context_menu(
|
||||
&mut self,
|
||||
action: Box<dyn gpui::Action>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let Some(menu) = self.context_menu.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut handled = false;
|
||||
|
||||
match menu {
|
||||
ContextMenu::Completion(menu) => {
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
handled = menu.handle_action(action, window, cx)
|
||||
});
|
||||
}
|
||||
ContextMenu::CodeAction(menu) => {
|
||||
_ = menu.update(cx, |menu, cx| {
|
||||
handled = menu.handle_action(action, window, cx)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handled
|
||||
}
|
||||
|
||||
/// Apply a list of [`lsp_types::TextEdit`] to mutate the text.
|
||||
pub fn apply_lsp_edits(
|
||||
&mut self,
|
||||
text_edits: &Vec<lsp_types::TextEdit>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
for edit in text_edits {
|
||||
let start = self.text.position_to_offset(&edit.range.start);
|
||||
let end = self.text.position_to_offset(&edit.range.end);
|
||||
|
||||
let range_utf16 = self.range_to_utf16(&(start..end));
|
||||
self.replace_text_in_range(Some(range_utf16), &edit.new_text, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_mouse_move(
|
||||
&mut self,
|
||||
offset: usize,
|
||||
event: &MouseMoveEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<InputState>,
|
||||
) {
|
||||
if event.modifiers.secondary() {
|
||||
self.hover_popover = None;
|
||||
self.handle_hover_definition(offset, window, cx);
|
||||
} else {
|
||||
self.hover_definition = None;
|
||||
self.handle_hover_popover(offset, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ use super::{
|
|||
use crate::input::{
|
||||
popovers::{ContextMenu, DiagnosticPopover, HoverPopover},
|
||||
search::{self, SearchPanel},
|
||||
Lsp, Position,
|
||||
HoverDefinition, Lsp, Position,
|
||||
};
|
||||
use crate::input::{RopeExt as _, Selection};
|
||||
use crate::{highlighter::DiagnosticSet, input::text_wrapper::LineItem};
|
||||
|
|
@ -299,6 +299,8 @@ pub struct InputState {
|
|||
/// A flag to indicate if we are currently inserting a completion item.
|
||||
pub(super) completion_inserting: bool,
|
||||
pub(super) hover_popover: Option<Entity<HoverPopover>>,
|
||||
/// The LSP definitions locations for "Go to Definition" feature.
|
||||
pub(super) hover_definition: Option<HoverDefinition>,
|
||||
|
||||
pub lsp: Lsp,
|
||||
|
||||
|
|
@ -384,6 +386,7 @@ impl InputState {
|
|||
context_menu: None,
|
||||
completion_inserting: false,
|
||||
hover_popover: None,
|
||||
hover_definition: None,
|
||||
_subscriptions,
|
||||
_context_menu_task: Task::ready(Ok(())),
|
||||
}
|
||||
|
|
@ -614,7 +617,7 @@ impl InputState {
|
|||
/// Move the cursor vertically by one line (up or down) while preserving the column if possible.
|
||||
///
|
||||
/// move_lines: Number of lines to move vertically (positive for down, negative for up).
|
||||
fn move_vertical(&mut self, move_lines: isize, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn move_vertical(&mut self, move_lines: isize, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.mode.is_single_line() {
|
||||
return;
|
||||
}
|
||||
|
|
@ -653,7 +656,7 @@ impl InputState {
|
|||
}
|
||||
}
|
||||
self.pause_blink_cursor(cx);
|
||||
self.move_to(new_offset, window, cx);
|
||||
self.move_to(new_offset, cx);
|
||||
// Set back the preferred_column
|
||||
self.preferred_column = was_preferred_column;
|
||||
cx.notify();
|
||||
|
|
@ -861,7 +864,7 @@ impl InputState {
|
|||
|
||||
// TODO: Scroll to make the row in center of viewport.
|
||||
|
||||
self.move_to(offset, window, cx);
|
||||
self.move_to(offset, cx);
|
||||
self.update_preferred_column();
|
||||
self.focus(window, cx);
|
||||
}
|
||||
|
|
@ -874,21 +877,21 @@ impl InputState {
|
|||
});
|
||||
}
|
||||
|
||||
pub(super) fn left(&mut self, _: &MoveLeft, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(super) fn left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.pause_blink_cursor(cx);
|
||||
if self.selected_range.is_empty() {
|
||||
self.move_to(self.previous_boundary(self.cursor()), window, cx);
|
||||
self.move_to(self.previous_boundary(self.cursor()), cx);
|
||||
} else {
|
||||
self.move_to(self.selected_range.start, window, cx)
|
||||
self.move_to(self.selected_range.start, cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn right(&mut self, _: &MoveRight, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(super) fn right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.pause_blink_cursor(cx);
|
||||
if self.selected_range.is_empty() {
|
||||
self.move_to(self.next_boundary(self.selected_range.end), window, cx);
|
||||
self.move_to(self.next_boundary(self.selected_range.end), cx);
|
||||
} else {
|
||||
self.move_to(self.selected_range.end, window, cx)
|
||||
self.move_to(self.selected_range.end, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -904,7 +907,6 @@ impl InputState {
|
|||
if !self.selected_range.is_empty() {
|
||||
self.move_to(
|
||||
self.previous_boundary(self.selected_range.start.saturating_sub(1)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
|
@ -924,7 +926,6 @@ impl InputState {
|
|||
if !self.selected_range.is_empty() {
|
||||
self.move_to(
|
||||
self.next_boundary(self.selected_range.end.saturating_sub(1)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
|
@ -964,162 +965,137 @@ impl InputState {
|
|||
self.move_vertical(display_lines, window, cx);
|
||||
}
|
||||
|
||||
pub(super) fn select_left(
|
||||
&mut self,
|
||||
_: &SelectLeft,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.select_to(self.previous_boundary(self.cursor()), window, cx);
|
||||
pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.select_to(self.previous_boundary(self.cursor()), cx);
|
||||
}
|
||||
|
||||
pub(super) fn select_right(
|
||||
&mut self,
|
||||
_: &SelectRight,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.select_to(self.next_boundary(self.cursor()), window, cx);
|
||||
pub(super) fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.select_to(self.next_boundary(self.cursor()), cx);
|
||||
}
|
||||
|
||||
pub(super) fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(super) fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.mode.is_single_line() {
|
||||
return;
|
||||
}
|
||||
let offset = self.start_of_line().saturating_sub(1);
|
||||
self.select_to(self.previous_boundary(offset), window, cx);
|
||||
self.select_to(self.previous_boundary(offset), cx);
|
||||
}
|
||||
|
||||
pub(super) fn select_down(
|
||||
&mut self,
|
||||
_: &SelectDown,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
pub(super) fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.mode.is_single_line() {
|
||||
return;
|
||||
}
|
||||
let offset = (self.end_of_line() + 1).min(self.text.len());
|
||||
self.select_to(self.next_boundary(offset), window, cx);
|
||||
self.select_to(self.next_boundary(offset), cx);
|
||||
}
|
||||
|
||||
pub(super) fn select_all(
|
||||
&mut self,
|
||||
_: &SelectAll,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.move_to(0, window, cx);
|
||||
self.select_to(self.text.len(), window, cx);
|
||||
pub(super) fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.move_to(0, cx);
|
||||
self.select_to(self.text.len(), cx);
|
||||
}
|
||||
|
||||
pub(super) fn home(&mut self, _: &MoveHome, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.pause_blink_cursor(cx);
|
||||
let offset = self.start_of_line();
|
||||
self.move_to(offset, window, cx);
|
||||
self.move_to(offset, cx);
|
||||
}
|
||||
|
||||
pub(super) fn end(&mut self, _: &MoveEnd, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(super) fn end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.pause_blink_cursor(cx);
|
||||
let offset = self.end_of_line();
|
||||
self.move_to(offset, window, cx);
|
||||
self.move_to(offset, cx);
|
||||
}
|
||||
|
||||
pub(super) fn move_to_start(
|
||||
&mut self,
|
||||
_: &MoveToStart,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.move_to(0, window, cx);
|
||||
self.move_to(0, cx);
|
||||
}
|
||||
|
||||
pub(super) fn move_to_end(
|
||||
&mut self,
|
||||
_: &MoveToEnd,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.move_to(self.text.len(), window, cx);
|
||||
pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.move_to(self.text.len(), cx);
|
||||
}
|
||||
|
||||
pub(super) fn move_to_previous_word(
|
||||
&mut self,
|
||||
_: &MoveToPreviousWord,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let offset = self.previous_start_of_word();
|
||||
self.move_to(offset, window, cx);
|
||||
self.move_to(offset, cx);
|
||||
}
|
||||
|
||||
pub(super) fn move_to_next_word(
|
||||
&mut self,
|
||||
_: &MoveToNextWord,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let offset = self.next_end_of_word();
|
||||
self.move_to(offset, window, cx);
|
||||
self.move_to(offset, cx);
|
||||
}
|
||||
|
||||
pub(super) fn select_to_start(
|
||||
&mut self,
|
||||
_: &SelectToStart,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.select_to(0, window, cx);
|
||||
self.select_to(0, cx);
|
||||
}
|
||||
|
||||
pub(super) fn select_to_end(
|
||||
&mut self,
|
||||
_: &SelectToEnd,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let end = self.text.len();
|
||||
self.select_to(end, window, cx);
|
||||
self.select_to(end, cx);
|
||||
}
|
||||
|
||||
pub(super) fn select_to_start_of_line(
|
||||
&mut self,
|
||||
_: &SelectToStartOfLine,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let offset = self.start_of_line();
|
||||
self.select_to(offset, window, cx);
|
||||
self.select_to(offset, cx);
|
||||
}
|
||||
|
||||
pub(super) fn select_to_end_of_line(
|
||||
&mut self,
|
||||
_: &SelectToEndOfLine,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let offset = self.end_of_line();
|
||||
self.select_to(offset, window, cx);
|
||||
self.select_to(offset, cx);
|
||||
}
|
||||
|
||||
pub(super) fn select_to_previous_word(
|
||||
&mut self,
|
||||
_: &SelectToPreviousWordStart,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let offset = self.previous_start_of_word();
|
||||
self.select_to(offset, window, cx);
|
||||
self.select_to(offset, cx);
|
||||
}
|
||||
|
||||
pub(super) fn select_to_next_word(
|
||||
&mut self,
|
||||
_: &SelectToNextWordEnd,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let offset = self.next_end_of_word();
|
||||
self.select_to(offset, window, cx);
|
||||
self.select_to(offset, cx);
|
||||
}
|
||||
|
||||
/// Return the start offset of the previous word.
|
||||
|
|
@ -1230,7 +1206,7 @@ impl InputState {
|
|||
|
||||
pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.select_to(self.previous_boundary(self.cursor()), window, cx)
|
||||
self.select_to(self.previous_boundary(self.cursor()), cx)
|
||||
}
|
||||
self.replace_text_in_range(None, "", window, cx);
|
||||
self.pause_blink_cursor(cx);
|
||||
|
|
@ -1238,7 +1214,7 @@ impl InputState {
|
|||
|
||||
pub(super) fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.select_to(self.next_boundary(self.cursor()), window, cx)
|
||||
self.select_to(self.next_boundary(self.cursor()), cx)
|
||||
}
|
||||
self.replace_text_in_range(None, "", window, cx);
|
||||
self.pause_blink_cursor(cx);
|
||||
|
|
@ -1546,6 +1522,11 @@ impl InputState {
|
|||
|
||||
self.selecting = true;
|
||||
let offset = self.index_for_mouse_position(event.position, window, cx);
|
||||
|
||||
if self.handle_click_hover_definition(event, offset, window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Double click to select word
|
||||
if event.button == MouseButton::Left && event.click_count == 2 {
|
||||
self.select_word(offset, window, cx);
|
||||
|
|
@ -1553,9 +1534,9 @@ impl InputState {
|
|||
}
|
||||
|
||||
if event.modifiers.shift {
|
||||
self.select_to(offset, window, cx);
|
||||
self.select_to(offset, cx);
|
||||
} else {
|
||||
self.move_to(offset, window, cx)
|
||||
self.move_to(offset, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1577,7 +1558,7 @@ impl InputState {
|
|||
) {
|
||||
// Show diagnostic popover on mouse move
|
||||
let offset = self.index_for_mouse_position(event.position, window, cx);
|
||||
self.handle_hover(offset, window, cx);
|
||||
self.handle_mouse_move(offset, event, window, cx);
|
||||
|
||||
if self.mode.is_code_editor() {
|
||||
if let Some(diagnostic) = self
|
||||
|
|
@ -1669,6 +1650,7 @@ impl InputState {
|
|||
// Scroll down
|
||||
scroll_offset.y = -(row_offset_y - bounds.size.height.half());
|
||||
}
|
||||
|
||||
self.update_scroll_offset(Some(scroll_offset), cx);
|
||||
}
|
||||
|
||||
|
|
@ -1751,7 +1733,7 @@ impl InputState {
|
|||
/// The offset is the UTF-8 offset.
|
||||
///
|
||||
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
|
||||
fn move_to(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(crate) fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
||||
let offset = offset.clamp(0, self.text.len());
|
||||
self.selected_range = (offset..offset).into();
|
||||
self.scroll_to(offset, cx);
|
||||
|
|
@ -1776,7 +1758,7 @@ impl InputState {
|
|||
}
|
||||
}
|
||||
|
||||
fn index_for_mouse_position(
|
||||
pub(crate) fn index_for_mouse_position(
|
||||
&self,
|
||||
position: Point<Pixels>,
|
||||
_window: &Window,
|
||||
|
|
@ -1895,7 +1877,7 @@ impl InputState {
|
|||
/// The offset is the UTF-8 offset.
|
||||
///
|
||||
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
|
||||
fn select_to(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(crate) fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
||||
let offset = offset.clamp(0, self.text.len());
|
||||
if self.selection_reversed {
|
||||
self.selected_range.start = offset
|
||||
|
|
@ -2107,7 +2089,7 @@ impl InputState {
|
|||
}
|
||||
|
||||
let offset = self.index_for_mouse_position(event.position, window, cx);
|
||||
self.select_to(offset, window, cx);
|
||||
self.select_to(offset, cx);
|
||||
}
|
||||
|
||||
fn is_valid_input(&self, new_text: &str, cx: &mut Context<Self>) -> bool {
|
||||
|
|
@ -2186,6 +2168,31 @@ impl InputState {
|
|||
pub(super) fn selected_text(&self) -> Rope {
|
||||
self.text.slice(self.selected_range.into())
|
||||
}
|
||||
|
||||
pub(crate) fn range_to_bounds(&self, range: &Range<usize>) -> Option<Bounds<Pixels>> {
|
||||
let Some(last_layout) = self.last_layout.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Some(last_bounds) = self.last_bounds else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let (_, _, start_pos) = self.line_and_position_for_offset(range.start);
|
||||
let (_, _, end_pos) = self.line_and_position_for_offset(range.end);
|
||||
|
||||
let Some(start_pos) = start_pos else {
|
||||
return None;
|
||||
};
|
||||
let Some(end_pos) = end_pos else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(Bounds::from_corners(
|
||||
last_bounds.origin + start_pos,
|
||||
last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityInputHandler for InputState {
|
||||
|
|
|
|||
Loading…
Reference in a new issue