Merge branch 'main' into theme-improvements

This commit is contained in:
Nate Butler 2022-07-19 20:12:02 -04:00
commit bcc554a3db
76 changed files with 4053 additions and 1557 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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