Merge branch 'main' into theme-improvements
This commit is contained in:
commit
bcc554a3db
76 changed files with 4053 additions and 1557 deletions
|
@ -23,6 +23,7 @@ test-support = [
|
|||
text = { path = "../text" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
|
|
|
@ -4,6 +4,7 @@ mod highlight_matching_bracket;
|
|||
mod hover_popover;
|
||||
pub mod items;
|
||||
mod link_go_to_definition;
|
||||
mod mouse_context_menu;
|
||||
pub mod movement;
|
||||
mod multi_buffer;
|
||||
pub mod selections_collection;
|
||||
|
@ -29,11 +30,12 @@ use gpui::{
|
|||
impl_actions, impl_internal_actions,
|
||||
platform::CursorStyle,
|
||||
text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
|
||||
ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
pub use language::{char_kind, CharKind};
|
||||
use language::{
|
||||
BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity,
|
||||
|
@ -319,6 +321,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
|
||||
hover_popover::init(cx);
|
||||
link_go_to_definition::init(cx);
|
||||
mouse_context_menu::init(cx);
|
||||
|
||||
workspace::register_project_item::<Editor>(cx);
|
||||
workspace::register_followable_item::<Editor>(cx);
|
||||
|
@ -425,6 +428,7 @@ pub struct Editor {
|
|||
background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
context_menu: Option<ContextMenu>,
|
||||
mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
|
||||
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
|
||||
next_completion_id: CompletionId,
|
||||
available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
|
||||
|
@ -703,7 +707,7 @@ impl CompletionsMenu {
|
|||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_mouse_down(move |_, cx| {
|
||||
.on_mouse_down(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ConfirmCompletion {
|
||||
item_ix: Some(item_ix),
|
||||
});
|
||||
|
@ -836,7 +840,7 @@ impl CodeActionsMenu {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_mouse_down(move |_, cx| {
|
||||
.on_mouse_down(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
});
|
||||
|
@ -1010,11 +1014,11 @@ impl Editor {
|
|||
background_highlights: Default::default(),
|
||||
nav_history: None,
|
||||
context_menu: None,
|
||||
mouse_context_menu: cx.add_view(|cx| context_menu::ContextMenu::new(cx)),
|
||||
completion_tasks: Default::default(),
|
||||
next_completion_id: 0,
|
||||
available_code_actions: Default::default(),
|
||||
code_actions_task: Default::default(),
|
||||
|
||||
document_highlights_task: Default::default(),
|
||||
pending_rename: Default::default(),
|
||||
searchable: true,
|
||||
|
@ -1070,7 +1074,7 @@ impl Editor {
|
|||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &AppContext) -> String {
|
||||
pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> {
|
||||
self.buffer().read(cx).title(cx)
|
||||
}
|
||||
|
||||
|
@ -1596,7 +1600,7 @@ impl Editor {
|
|||
s.delete(newest_selection.id)
|
||||
}
|
||||
|
||||
s.set_pending_range(start..end, mode);
|
||||
s.set_pending_anchor_range(start..end, mode);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1937,6 +1941,10 @@ impl Editor {
|
|||
}
|
||||
|
||||
fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||
if !cx.global::<Settings>().show_completions_on_input {
|
||||
return;
|
||||
}
|
||||
|
||||
let selection = self.selections.newest_anchor();
|
||||
if self
|
||||
.buffer
|
||||
|
@ -2666,7 +2674,7 @@ impl Editor {
|
|||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_padding(Padding::uniform(3.))
|
||||
.on_mouse_down(|_, cx| {
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(ToggleCodeActions {
|
||||
deployed_from_indicator: true,
|
||||
});
|
||||
|
@ -2901,9 +2909,17 @@ impl Editor {
|
|||
if selections.iter().all(|s| s.is_empty()) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
let mut prev_cursor_row = 0;
|
||||
let mut row_delta = 0;
|
||||
for selection in &mut selections {
|
||||
let language_name =
|
||||
buffer.language_at(selection.start, cx).map(|l| l.name());
|
||||
let mut cursor = selection.start;
|
||||
if cursor.row != prev_cursor_row {
|
||||
row_delta = 0;
|
||||
prev_cursor_row = cursor.row;
|
||||
}
|
||||
cursor.column += row_delta;
|
||||
|
||||
let language_name = buffer.language_at(cursor, cx).map(|l| l.name());
|
||||
let settings = cx.global::<Settings>();
|
||||
let tab_size = if settings.hard_tabs(language_name.as_deref()) {
|
||||
IndentSize::tab()
|
||||
|
@ -2911,21 +2927,18 @@ impl Editor {
|
|||
let tab_size = settings.tab_size(language_name.as_deref()).get();
|
||||
let char_column = buffer
|
||||
.read(cx)
|
||||
.text_for_range(Point::new(selection.start.row, 0)..selection.start)
|
||||
.text_for_range(Point::new(cursor.row, 0)..cursor)
|
||||
.flat_map(str::chars)
|
||||
.count();
|
||||
let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size);
|
||||
IndentSize::spaces(chars_to_next_tab_stop)
|
||||
};
|
||||
buffer.edit(
|
||||
[(
|
||||
selection.start..selection.start,
|
||||
tab_size.chars().collect::<String>(),
|
||||
)],
|
||||
cx,
|
||||
);
|
||||
selection.start.column += tab_size.len;
|
||||
selection.end = selection.start;
|
||||
buffer.edit([(cursor..cursor, tab_size.chars().collect::<String>())], cx);
|
||||
cursor.column += tab_size.len;
|
||||
selection.start = cursor;
|
||||
selection.end = cursor;
|
||||
|
||||
row_delta += tab_size.len;
|
||||
}
|
||||
});
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
|
@ -5780,7 +5793,12 @@ impl View for Editor {
|
|||
});
|
||||
}
|
||||
|
||||
EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed()
|
||||
Stack::new()
|
||||
.with_child(
|
||||
EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed(),
|
||||
)
|
||||
.with_child(ChildView::new(&self.mouse_context_menu).boxed())
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn ui_name() -> &'static str {
|
||||
|
@ -6225,7 +6243,8 @@ pub fn styled_runs_for_code_label<'a>(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::{
|
||||
assert_text_with_selections, build_editor, select_ranges, EditorTestContext,
|
||||
assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
|
||||
EditorTestContext,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
@ -6236,7 +6255,6 @@ mod tests {
|
|||
};
|
||||
use indoc::indoc;
|
||||
use language::{FakeLspAdapter, LanguageConfig};
|
||||
use lsp::FakeLanguageServer;
|
||||
use project::FakeFs;
|
||||
use settings::EditorSettings;
|
||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||
|
@ -6244,7 +6262,9 @@ mod tests {
|
|||
use unindent::Unindent;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
|
||||
test::{
|
||||
marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker,
|
||||
},
|
||||
};
|
||||
use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
|
||||
|
||||
|
@ -7551,6 +7571,27 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, _| {
|
||||
settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap());
|
||||
});
|
||||
});
|
||||
cx.set_state(indoc! {"
|
||||
|ab|c
|
||||
|🏀|🏀|efg
|
||||
d|
|
||||
"});
|
||||
cx.update_editor(|e, cx| e.tab(&Tab, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|ab |c
|
||||
|🏀 |🏀 |efg
|
||||
d |
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
@ -9524,199 +9565,182 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
||||
let text = "
|
||||
one
|
||||
two
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.background().clone());
|
||||
fs.insert_file("/file.rs", text).await;
|
||||
|
||||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.project = Some(project);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(0, 3)..Point::new(0, 3)])
|
||||
});
|
||||
editor.handle_input(&Input(".".to_string()), cx);
|
||||
});
|
||||
|
||||
handle_completion_request(
|
||||
&mut fake_server,
|
||||
"/file.rs",
|
||||
Point::new(0, 4),
|
||||
vec![
|
||||
(Point::new(0, 4)..Point::new(0, 4), "first_completion"),
|
||||
(Point::new(0, 4)..Point::new(0, 4), "second_completion"),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
editor
|
||||
.condition(&cx, |editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
let apply_additional_edits = editor.update(cx, |editor, cx| {
|
||||
cx.set_state(indoc! {"
|
||||
one|
|
||||
two
|
||||
three"});
|
||||
cx.simulate_keystroke(".");
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
two
|
||||
three"},
|
||||
vec!["first_completion", "second_completion"],
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
let apply_additional_edits = cx.update_editor(|editor, cx| {
|
||||
editor.move_down(&MoveDown, cx);
|
||||
let apply_additional_edits = editor
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"
|
||||
one.second_completion
|
||||
two
|
||||
three
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
apply_additional_edits
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion|
|
||||
two
|
||||
three"});
|
||||
|
||||
handle_resolve_completion_request(
|
||||
&mut fake_server,
|
||||
Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")),
|
||||
)
|
||||
.await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
assert_eq!(
|
||||
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||
"
|
||||
one.second_completion
|
||||
two
|
||||
three
|
||||
additional edit
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([
|
||||
Point::new(1, 3)..Point::new(1, 3),
|
||||
Point::new(2, 5)..Point::new(2, 5),
|
||||
])
|
||||
});
|
||||
|
||||
editor.handle_input(&Input(" ".to_string()), cx);
|
||||
assert!(editor.context_menu.is_none());
|
||||
editor.handle_input(&Input("s".to_string()), cx);
|
||||
assert!(editor.context_menu.is_none());
|
||||
});
|
||||
|
||||
handle_completion_request(
|
||||
&mut fake_server,
|
||||
"/file.rs",
|
||||
Point::new(2, 7),
|
||||
vec![
|
||||
(Point::new(2, 6)..Point::new(2, 7), "fourth_completion"),
|
||||
(Point::new(2, 6)..Point::new(2, 7), "fifth_completion"),
|
||||
(Point::new(2, 6)..Point::new(2, 7), "sixth_completion"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
editor
|
||||
.condition(&cx, |editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.handle_input(&Input("i".to_string()), cx);
|
||||
});
|
||||
|
||||
handle_completion_request(
|
||||
&mut fake_server,
|
||||
"/file.rs",
|
||||
Point::new(2, 8),
|
||||
vec![
|
||||
(Point::new(2, 6)..Point::new(2, 8), "fourth_completion"),
|
||||
(Point::new(2, 6)..Point::new(2, 8), "fifth_completion"),
|
||||
(Point::new(2, 6)..Point::new(2, 8), "sixth_completion"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
editor
|
||||
.condition(&cx, |editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
let apply_additional_edits = editor.update(cx, |editor, cx| {
|
||||
let apply_additional_edits = editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"
|
||||
&mut cx,
|
||||
Some((
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two sixth_completion
|
||||
three sixth_completion
|
||||
additional edit
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
apply_additional_edits
|
||||
two
|
||||
three<>"},
|
||||
"\nadditional edit",
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion|
|
||||
two
|
||||
three
|
||||
additional edit"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
one.second_completion
|
||||
two|
|
||||
three|
|
||||
additional edit"});
|
||||
cx.simulate_keystroke(" ");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
cx.simulate_keystroke("s");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion
|
||||
two s|
|
||||
three s|
|
||||
additional edit"});
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two s
|
||||
three <s|>
|
||||
additional edit"},
|
||||
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
cx.simulate_keystroke("i");
|
||||
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two si
|
||||
three <si|>
|
||||
additional edit"},
|
||||
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
let apply_additional_edits = cx.update_editor(|editor, cx| {
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
handle_resolve_completion_request(&mut fake_server, None).await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion
|
||||
two sixth_completion|
|
||||
three sixth_completion|
|
||||
additional edit"});
|
||||
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
|
||||
async fn handle_completion_request(
|
||||
fake: &mut FakeLanguageServer,
|
||||
path: &'static str,
|
||||
position: Point,
|
||||
completions: Vec<(Range<Point>, &'static str)>,
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, _| {
|
||||
settings.show_completions_on_input = false;
|
||||
})
|
||||
});
|
||||
cx.set_state("editor|");
|
||||
cx.simulate_keystroke(".");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
cx.simulate_keystrokes(["c", "l", "o"]);
|
||||
cx.assert_editor_state("editor.clo|");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.show_completions(&ShowCompletions, cx);
|
||||
});
|
||||
handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
let apply_additional_edits = cx.update_editor(|editor, cx| {
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state("editor.close|");
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
|
||||
// Handle completion request passing a marked string specifying where the completion
|
||||
// should be triggered from using '|' character, what range should be replaced, and what completions
|
||||
// should be returned using '<' and '>' to delimit the range
|
||||
async fn handle_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
marked_string: &str,
|
||||
completions: Vec<&'static str>,
|
||||
) {
|
||||
fake.handle_request::<lsp::request::Completion, _, _>(move |params, _| {
|
||||
let complete_from_marker: TextRangeMarker = '|'.into();
|
||||
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
||||
let (_, mut marked_ranges) = marked_text_ranges_by(
|
||||
marked_string,
|
||||
vec![complete_from_marker.clone(), replace_range_marker.clone()],
|
||||
);
|
||||
|
||||
let complete_from_position =
|
||||
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
|
||||
let replace_range =
|
||||
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
||||
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
lsp::Url::from_file_path(path).unwrap()
|
||||
);
|
||||
assert_eq!(params.text_document_position.text_document.uri, url.clone());
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
lsp::Position::new(position.row, position.column)
|
||||
complete_from_position
|
||||
);
|
||||
Ok(Some(lsp::CompletionResponse::Array(
|
||||
completions
|
||||
.iter()
|
||||
.map(|(range, new_text)| lsp::CompletionItem {
|
||||
label: new_text.to_string(),
|
||||
.map(|completion_text| lsp::CompletionItem {
|
||||
label: completion_text.to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(range.start.row, range.start.column),
|
||||
lsp::Position::new(range.start.row, range.start.column),
|
||||
),
|
||||
new_text: new_text.to_string(),
|
||||
range: replace_range.clone(),
|
||||
new_text: completion_text.to_string(),
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
|
@ -9728,23 +9752,26 @@ mod tests {
|
|||
.await;
|
||||
}
|
||||
|
||||
async fn handle_resolve_completion_request(
|
||||
fake: &mut FakeLanguageServer,
|
||||
edit: Option<(Range<Point>, &'static str)>,
|
||||
async fn handle_resolve_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
edit: Option<(&'static str, &'static str)>,
|
||||
) {
|
||||
fake.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _| {
|
||||
let edit = edit.map(|(marked_string, new_text)| {
|
||||
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
||||
let (_, mut marked_ranges) =
|
||||
marked_text_ranges_by(marked_string, vec![replace_range_marker.clone()]);
|
||||
|
||||
let replace_range = cx
|
||||
.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
||||
|
||||
vec![lsp::TextEdit::new(replace_range, new_text.to_string())]
|
||||
});
|
||||
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||||
let edit = edit.clone();
|
||||
async move {
|
||||
Ok(lsp::CompletionItem {
|
||||
additional_text_edits: edit.map(|(range, new_text)| {
|
||||
vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(
|
||||
lsp::Position::new(range.start.row, range.start.column),
|
||||
lsp::Position::new(range.end.row, range.end.column),
|
||||
),
|
||||
new_text.to_string(),
|
||||
)]
|
||||
}),
|
||||
additional_text_edits: edit,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use crate::{
|
|||
display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
|
||||
hover_popover::HoverAt,
|
||||
link_go_to_definition::{CmdChanged, GoToFetchedDefinition, UpdateGoToDefinitionLink},
|
||||
mouse_context_menu::DeployMouseContextMenu,
|
||||
EditorStyle,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
|
@ -24,7 +25,7 @@ use gpui::{
|
|||
platform::CursorStyle,
|
||||
text_layout::{self, Line, RunStyle, TextLayoutCache},
|
||||
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, KeyDownEvent,
|
||||
LayoutContext, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent,
|
||||
LayoutContext, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent,
|
||||
MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext,
|
||||
WeakViewHandle,
|
||||
};
|
||||
|
@ -152,6 +153,24 @@ impl EditorElement {
|
|||
true
|
||||
}
|
||||
|
||||
fn mouse_right_down(
|
||||
&self,
|
||||
position: Vector2F,
|
||||
layout: &mut LayoutState,
|
||||
paint: &mut PaintState,
|
||||
cx: &mut EventContext,
|
||||
) -> bool {
|
||||
if !paint.text_bounds.contains_point(position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let snapshot = self.snapshot(cx.app);
|
||||
let (point, _) = paint.point_for_position(&snapshot, layout, position);
|
||||
|
||||
cx.dispatch_action(DeployMouseContextMenu { position, point });
|
||||
true
|
||||
}
|
||||
|
||||
fn mouse_up(&self, _position: Vector2F, cx: &mut EventContext) -> bool {
|
||||
if self.view(cx.app.as_ref()).is_selecting() {
|
||||
cx.dispatch_action(Select(SelectPhase::End));
|
||||
|
@ -949,7 +968,9 @@ impl EditorElement {
|
|||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, _, cx| cx.dispatch_action(jump_action.clone()))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(jump_action.clone())
|
||||
})
|
||||
.with_tooltip::<JumpIcon, _>(
|
||||
*key,
|
||||
"Jump to Buffer".to_string(),
|
||||
|
@ -1464,7 +1485,7 @@ impl Element for EditorElement {
|
|||
}
|
||||
|
||||
match event {
|
||||
Event::MouseDown(MouseEvent {
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
button: MouseButton::Left,
|
||||
position,
|
||||
cmd,
|
||||
|
@ -1482,7 +1503,12 @@ impl Element for EditorElement {
|
|||
paint,
|
||||
cx,
|
||||
),
|
||||
Event::MouseUp(MouseEvent {
|
||||
Event::MouseDown(MouseButtonEvent {
|
||||
button: MouseButton::Right,
|
||||
position,
|
||||
..
|
||||
}) => self.mouse_right_down(*position, layout, paint, cx),
|
||||
Event::MouseUp(MouseButtonEvent {
|
||||
button: MouseButton::Left,
|
||||
position,
|
||||
..
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToPoint as _};
|
||||
use crate::{
|
||||
Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer, NavigationData, ToPoint as _,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
|
@ -10,12 +12,18 @@ use project::{File, Project, ProjectEntryId, ProjectPath};
|
|||
use rpc::proto::{self, update_view};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{fmt::Write, path::PathBuf, time::Duration};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
use text::{Point, Selection};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView};
|
||||
|
||||
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
pub const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
|
||||
impl FollowableItem for Editor {
|
||||
fn from_state_proto(
|
||||
|
@ -292,9 +300,44 @@ impl Item for Editor {
|
|||
}
|
||||
}
|
||||
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
|
||||
let title = self.title(cx);
|
||||
Label::new(title, style.label.clone()).boxed()
|
||||
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
|
||||
match path_for_buffer(&self.buffer, detail, true, cx)? {
|
||||
Cow::Borrowed(path) => Some(path.to_string_lossy()),
|
||||
Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
detail: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(self.title(cx).into(), style.label.clone())
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(detail.and_then(|detail| {
|
||||
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
|
||||
let description = path.to_string_lossy();
|
||||
Some(
|
||||
Label::new(
|
||||
if description.len() > MAX_TAB_TITLE_LEN {
|
||||
description[..MAX_TAB_TITLE_LEN].to_string() + "…"
|
||||
} else {
|
||||
description.into()
|
||||
},
|
||||
style.description.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.description.container)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
}))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
|
@ -534,3 +577,42 @@ impl StatusItemView for CursorPosition {
|
|||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn path_for_buffer<'a>(
|
||||
buffer: &ModelHandle<MultiBuffer>,
|
||||
mut height: usize,
|
||||
include_filename: bool,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<Cow<'a, Path>> {
|
||||
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
|
||||
// Ensure we always render at least the filename.
|
||||
height += 1;
|
||||
|
||||
let mut prefix = file.path().as_ref();
|
||||
while height > 0 {
|
||||
if let Some(parent) = prefix.parent() {
|
||||
prefix = parent;
|
||||
height -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Here we could have just always used `full_path`, but that is very
|
||||
// allocation-heavy and so we try to use a `Cow<Path>` if we haven't
|
||||
// traversed all the way up to the worktree's root.
|
||||
if height > 0 {
|
||||
let full_path = file.full_path(cx);
|
||||
if include_filename {
|
||||
Some(full_path.into())
|
||||
} else {
|
||||
Some(full_path.parent().unwrap().to_path_buf().into())
|
||||
}
|
||||
} else {
|
||||
let mut path = file.path().strip_prefix(prefix).unwrap();
|
||||
if !include_filename {
|
||||
path = path.parent().unwrap();
|
||||
}
|
||||
Some(path.into())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -342,17 +342,16 @@ mod tests {
|
|||
test();"});
|
||||
|
||||
let mut requests =
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
origin_selection_range: Some(symbol_range),
|
||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
origin_selection_range: Some(symbol_range),
|
||||
target_uri: url.clone(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
|
@ -387,18 +386,17 @@ mod tests {
|
|||
// Response without source range still highlights word
|
||||
cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
|
||||
let mut requests =
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
// No origin range
|
||||
origin_selection_range: None,
|
||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
// No origin range
|
||||
origin_selection_range: None,
|
||||
target_uri: url.clone(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
|
@ -495,17 +493,16 @@ mod tests {
|
|||
test();"});
|
||||
|
||||
let mut requests =
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
origin_selection_range: Some(symbol_range),
|
||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
origin_selection_range: Some(symbol_range),
|
||||
target_uri: url,
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.update_editor(|editor, cx| {
|
||||
cmd_changed(editor, &CmdChanged { cmd_down: true }, cx);
|
||||
});
|
||||
|
@ -584,17 +581,16 @@ mod tests {
|
|||
test();"});
|
||||
|
||||
let mut requests =
|
||||
cx.lsp
|
||||
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
origin_selection_range: None,
|
||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
|
||||
lsp::LocationLink {
|
||||
origin_selection_range: None,
|
||||
target_uri: url,
|
||||
target_range,
|
||||
target_selection_range: target_range,
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.update_workspace(|workspace, cx| {
|
||||
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
|
||||
});
|
||||
|
|
103
crates/editor/src/mouse_context_menu.rs
Normal file
103
crates/editor/src/mouse_context_menu.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use context_menu::ContextMenuItem;
|
||||
use gpui::{geometry::vector::Vector2F, impl_internal_actions, MutableAppContext, ViewContext};
|
||||
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, Rename, SelectMode,
|
||||
ToggleCodeActions,
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployMouseContextMenu {
|
||||
pub position: Vector2F,
|
||||
pub point: DisplayPoint,
|
||||
}
|
||||
|
||||
impl_internal_actions!(editor, [DeployMouseContextMenu]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(deploy_context_menu);
|
||||
}
|
||||
|
||||
pub fn deploy_context_menu(
|
||||
editor: &mut Editor,
|
||||
&DeployMouseContextMenu { position, point }: &DeployMouseContextMenu,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
// Don't show context menu for inline editors
|
||||
if editor.mode() != EditorMode::Full {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show the context menu if there isn't a project associated with this editor
|
||||
if editor.project.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the cursor to the clicked location so that dispatched actions make sense
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.clear_disjoint();
|
||||
s.set_pending_display_range(point..point, SelectMode::Character);
|
||||
});
|
||||
|
||||
editor.mouse_context_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
position,
|
||||
vec![
|
||||
ContextMenuItem::item("Rename Symbol", Rename),
|
||||
ContextMenuItem::item("Go To Definition", GoToDefinition),
|
||||
ContextMenuItem::item("Find All References", FindAllReferences),
|
||||
ContextMenuItem::item(
|
||||
"Code Actions",
|
||||
ToggleCodeActions {
|
||||
deployed_from_indicator: false,
|
||||
},
|
||||
),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::test::EditorLspTestContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
fn te|st()
|
||||
do_work();"});
|
||||
let point = cx.display_point(indoc! {"
|
||||
fn test()
|
||||
do_w|ork();"});
|
||||
cx.update_editor(|editor, cx| {
|
||||
deploy_context_menu(
|
||||
editor,
|
||||
&DeployMouseContextMenu {
|
||||
position: Default::default(),
|
||||
point,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn test()
|
||||
do_w|ork();"});
|
||||
cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible()));
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ use language::{
|
|||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Ref, RefCell},
|
||||
cmp, fmt, io,
|
||||
iter::{self, FromIterator},
|
||||
|
@ -1194,14 +1195,14 @@ impl MultiBuffer {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &AppContext) -> String {
|
||||
if let Some(title) = self.title.clone() {
|
||||
return title;
|
||||
pub fn title<'a>(&'a self, cx: &'a AppContext) -> Cow<'a, str> {
|
||||
if let Some(title) = self.title.as_ref() {
|
||||
return title.into();
|
||||
}
|
||||
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file() {
|
||||
return file.file_name(cx).to_string_lossy().into();
|
||||
return file.file_name(cx).to_string_lossy();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -384,7 +384,7 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_pending_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
|
||||
pub fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
|
||||
self.collection.pending = Some(PendingSelection {
|
||||
selection: Selection {
|
||||
id: post_inc(&mut self.collection.next_selection_id),
|
||||
|
@ -398,6 +398,42 @@ impl<'a> MutableSelectionsCollection<'a> {
|
|||
self.selections_changed = true;
|
||||
}
|
||||
|
||||
pub fn set_pending_display_range(&mut self, range: Range<DisplayPoint>, mode: SelectMode) {
|
||||
let (start, end, reversed) = {
|
||||
let display_map = self.display_map();
|
||||
let buffer = self.buffer();
|
||||
let mut start = range.start;
|
||||
let mut end = range.end;
|
||||
let reversed = if start > end {
|
||||
mem::swap(&mut start, &mut end);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let end_bias = if end > start { Bias::Left } else { Bias::Right };
|
||||
(
|
||||
buffer.anchor_before(start.to_point(&display_map)),
|
||||
buffer.anchor_at(end.to_point(&display_map), end_bias),
|
||||
reversed,
|
||||
)
|
||||
};
|
||||
|
||||
let new_pending = PendingSelection {
|
||||
selection: Selection {
|
||||
id: post_inc(&mut self.collection.next_selection_id),
|
||||
start,
|
||||
end,
|
||||
reversed,
|
||||
goal: SelectionGoal::None,
|
||||
},
|
||||
mode,
|
||||
};
|
||||
|
||||
self.collection.pending = Some(new_pending);
|
||||
self.selections_changed = true;
|
||||
}
|
||||
|
||||
pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
|
||||
self.collection.pending = Some(PendingSelection { selection, mode });
|
||||
self.selections_changed = true;
|
||||
|
|
|
@ -4,12 +4,14 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
use futures::StreamExt;
|
||||
use anyhow::Result;
|
||||
use futures::{Future, StreamExt};
|
||||
use indoc::indoc;
|
||||
|
||||
use collections::BTreeMap;
|
||||
use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle};
|
||||
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection};
|
||||
use lsp::request;
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use util::{
|
||||
|
@ -110,6 +112,13 @@ impl<'a> EditorTestContext<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn condition(
|
||||
&self,
|
||||
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
||||
) -> impl Future<Output = ()> {
|
||||
self.editor.condition(self.cx, predicate)
|
||||
}
|
||||
|
||||
pub fn editor<F, T>(&mut self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Editor, &AppContext) -> T,
|
||||
|
@ -424,6 +433,7 @@ pub struct EditorLspTestContext<'a> {
|
|||
pub cx: EditorTestContext<'a>,
|
||||
pub lsp: lsp::FakeLanguageServer,
|
||||
pub workspace: ViewHandle<Workspace>,
|
||||
pub editor_lsp_url: lsp::Url,
|
||||
}
|
||||
|
||||
impl<'a> EditorLspTestContext<'a> {
|
||||
|
@ -497,6 +507,7 @@ impl<'a> EditorLspTestContext<'a> {
|
|||
},
|
||||
lsp,
|
||||
workspace,
|
||||
editor_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -520,11 +531,15 @@ impl<'a> EditorLspTestContext<'a> {
|
|||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||
let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
|
||||
assert_eq!(unmarked, self.cx.buffer_text());
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
|
||||
let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
|
||||
let start_point = offset_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end_point = offset_range.end.to_point(&snapshot.buffer_snapshot);
|
||||
self.to_lsp_range(offset_range)
|
||||
}
|
||||
|
||||
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
self.editor(|editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let start = point_to_lsp(
|
||||
|
@ -546,12 +561,45 @@ impl<'a> EditorLspTestContext<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let point = offset.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
self.editor(|editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
point_to_lsp(
|
||||
buffer
|
||||
.point_to_buffer_offset(point, cx)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_point_utf16(&buffer.read(cx)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_workspace<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||
{
|
||||
self.workspace.update(self.cx.cx, update)
|
||||
}
|
||||
|
||||
pub fn handle_request<T, F, Fut>(
|
||||
&self,
|
||||
mut handler: F,
|
||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
T::Params: 'static + Send,
|
||||
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
||||
{
|
||||
let url = self.editor_lsp_url.clone();
|
||||
self.lsp.handle_request::<T, _, _>(move |params, cx| {
|
||||
let url = url.clone();
|
||||
handler(url, params, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EditorLspTestContext<'a> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue