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:
Jason Lee 2025-09-19 13:49:24 +08:00 committed by GitHub
parent 6a817d6a05
commit 5e7a2cb37f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 841 additions and 487 deletions

View file

@ -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

View file

@ -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
});

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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(())
});
}
}

View 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();
}
}

View 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(())
});
}
}

View 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))
}
}

View 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(())
});
}
}

View 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);
}
}
}

View file

@ -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 {