Merge remote-tracking branch 'origin' into storybook

This commit is contained in:
Nathan Sobo 2023-09-01 10:08:48 -06:00
commit 2fa9c037c8
110 changed files with 4110 additions and 1580 deletions

View file

@ -37,10 +37,7 @@ impl BlinkManager {
}
pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
if !self.visible {
self.visible = true;
cx.notify();
}
self.show_cursor(cx);
let epoch = self.next_blink_epoch();
let interval = self.blink_interval;
@ -82,7 +79,13 @@ impl BlinkManager {
})
.detach();
}
} else if !self.visible {
} else {
self.show_cursor(cx);
}
}
pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) {
if !self.visible {
self.visible = true;
cx.notify();
}

View file

@ -44,7 +44,7 @@ use gpui::{
elements::*,
executor,
fonts::{self, HighlightStyle, TextStyle},
geometry::vector::Vector2F,
geometry::vector::{vec2f, Vector2F},
impl_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton},
@ -820,6 +820,7 @@ struct CompletionsMenu {
id: CompletionId,
initial_position: Anchor,
buffer: ModelHandle<Buffer>,
project: Option<ModelHandle<Project>>,
completions: Arc<[Completion]>,
match_candidates: Vec<StringMatchCandidate>,
matches: Arc<[StringMatch]>,
@ -863,6 +864,48 @@ impl CompletionsMenu {
fn render(&self, style: EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
enum CompletionTag {}
let language_servers = self.project.as_ref().map(|project| {
project
.read(cx)
.language_servers_for_buffer(self.buffer.read(cx), cx)
.filter(|(_, server)| server.capabilities().completion_provider.is_some())
.map(|(adapter, server)| (server.server_id(), adapter.short_name))
.collect::<Vec<_>>()
});
let needs_server_name = language_servers
.as_ref()
.map_or(false, |servers| servers.len() > 1);
let get_server_name =
move |lookup_server_id: lsp::LanguageServerId| -> Option<&'static str> {
language_servers
.iter()
.flatten()
.find_map(|(server_id, server_name)| {
if *server_id == lookup_server_id {
Some(*server_name)
} else {
None
}
})
};
let widest_completion_ix = self
.matches
.iter()
.enumerate()
.max_by_key(|(_, mat)| {
let completion = &self.completions[mat.candidate_id];
let mut len = completion.label.text.chars().count();
if let Some(server_name) = get_server_name(completion.server_id) {
len += server_name.chars().count();
}
len
})
.map(|(ix, _)| ix);
let completions = self.completions.clone();
let matches = self.matches.clone();
let selected_item = self.selected_item;
@ -889,19 +932,83 @@ impl CompletionsMenu {
style.autocomplete.item
};
Text::new(completion.label.text.clone(), style.text.clone())
.with_soft_wrap(false)
.with_highlights(combine_syntax_and_fuzzy_match_highlights(
&completion.label.text,
style.text.color.into(),
styled_runs_for_code_label(
&completion.label,
&style.syntax,
),
&mat.positions,
))
.contained()
.with_style(item_style)
let completion_label =
Text::new(completion.label.text.clone(), style.text.clone())
.with_soft_wrap(false)
.with_highlights(
combine_syntax_and_fuzzy_match_highlights(
&completion.label.text,
style.text.color.into(),
styled_runs_for_code_label(
&completion.label,
&style.syntax,
),
&mat.positions,
),
);
if let Some(server_name) = get_server_name(completion.server_id) {
Flex::row()
.with_child(completion_label)
.with_children((|| {
if !needs_server_name {
return None;
}
let text_style = TextStyle {
color: style.autocomplete.server_name_color,
font_size: style.text.font_size
* style.autocomplete.server_name_size_percent,
..style.text.clone()
};
let label = Text::new(server_name, text_style)
.aligned()
.constrained()
.dynamically(move |constraint, _, _| {
gpui::SizeConstraint {
min: constraint.min,
max: vec2f(
constraint.max.x(),
constraint.min.y(),
),
}
});
if Some(item_ix) == widest_completion_ix {
Some(
label
.contained()
.with_style(
style
.autocomplete
.server_name_container,
)
.into_any(),
)
} else {
Some(label.flex_float().into_any())
}
})())
.into_any()
} else {
completion_label.into_any()
}
.contained()
.with_style(item_style)
.constrained()
.dynamically(
move |constraint, _, _| {
if Some(item_ix) == widest_completion_ix {
constraint
} else {
gpui::SizeConstraint {
min: constraint.min,
max: constraint.min,
}
}
},
)
},
)
.with_cursor_style(CursorStyle::PointingHand)
@ -918,19 +1025,7 @@ impl CompletionsMenu {
}
},
)
.with_width_from_item(
self.matches
.iter()
.enumerate()
.max_by_key(|(_, mat)| {
self.completions[mat.candidate_id]
.label
.text
.chars()
.count()
})
.map(|(ix, _)| ix),
)
.with_width_from_item(widest_completion_ix)
.contained()
.with_style(container_style)
.into_any()
@ -1454,6 +1549,16 @@ impl Editor {
cx.observe(&display_map, Self::on_display_map_changed),
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
cx.observe_global::<SettingsStore, _>(Self::settings_changed),
cx.observe_window_activation(|editor, active, cx| {
editor.blink_manager.update(cx, |blink_manager, cx| {
if active {
blink_manager.enable(cx);
} else {
blink_manager.show_cursor(cx);
blink_manager.disable(cx);
}
});
}),
],
};
@ -1625,6 +1730,15 @@ impl Editor {
self.read_only = read_only;
}
pub fn set_field_editor_style(
&mut self,
style: Option<Arc<GetFieldEditorTheme>>,
cx: &mut ViewContext<Self>,
) {
self.get_field_editor_theme = style;
cx.notify();
}
pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
self.replica_id_mapping.as_ref()
}
@ -2964,6 +3078,7 @@ impl Editor {
});
let id = post_inc(&mut self.next_completion_id);
let project = self.project.clone();
let task = cx.spawn(|this, mut cx| {
async move {
let menu = if let Some(completions) = completions.await.log_err() {
@ -2982,6 +3097,7 @@ impl Editor {
})
.collect(),
buffer,
project,
completions: completions.into(),
matches: Vec::new().into(),
selected_item: 0,
@ -4979,6 +5095,9 @@ impl Editor {
self.unmark_text(cx);
self.refresh_copilot_suggestions(true, cx);
cx.emit(Event::Edited);
cx.emit(Event::TransactionUndone {
transaction_id: tx_id,
});
}
}
@ -8418,6 +8537,9 @@ pub enum Event {
local: bool,
autoscroll: bool,
},
TransactionUndone {
transaction_id: TransactionId,
},
Closed,
}
@ -8458,7 +8580,7 @@ impl View for Editor {
"Editor"
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
let focused_event = EditorFocused(cx.handle());
cx.emit(Event::Focused);
@ -8466,7 +8588,7 @@ impl View for Editor {
}
if let Some(rename) = self.pending_rename.as_ref() {
cx.focus(&rename.editor);
} else {
} else if cx.is_self_focused() || !focused.is::<Editor>() {
if !self.focused {
self.blink_manager.update(cx, BlinkManager::enable);
}
@ -9161,6 +9283,7 @@ pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str
None
})
.flat_map(|word| word.split_inclusive('_'))
.flat_map(|word| word.split_inclusive('-'))
}
trait RangeToAnchorExt {

View file

@ -19,7 +19,8 @@ use gpui::{
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
Override, Point,
};
use parking_lot::Mutex;
use project::project_settings::{LspSettings, ProjectSettings};
@ -7688,6 +7689,105 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
}
#[gpui::test]
async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new(
Language::new(
LanguageConfig {
path_suffixes: vec!["jsx".into()],
overrides: [(
"element".into(),
LanguageConfigOverride {
word_characters: Override::Set(['-'].into_iter().collect()),
..Default::default()
},
)]
.into_iter()
.collect(),
..Default::default()
},
Some(tree_sitter_typescript::language_tsx()),
)
.with_override_query("(jsx_self_closing_element) @element")
.unwrap(),
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
cx.lsp
.handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
label: "bg-blue".into(),
..Default::default()
},
lsp::CompletionItem {
label: "bg-red".into(),
..Default::default()
},
lsp::CompletionItem {
label: "bg-yellow".into(),
..Default::default()
},
])))
});
cx.set_state(r#"<p class="bgˇ" />"#);
// Trigger completion when typing a dash, because the dash is an extra
// word character in the 'element' scope, which contains the cursor.
cx.simulate_keystroke("-");
cx.foreground().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-red", "bg-blue", "bg-yellow"]
);
} else {
panic!("expected completion menu to be open");
}
});
cx.simulate_keystroke("l");
cx.foreground().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-blue", "bg-yellow"]
);
} else {
panic!("expected completion menu to be open");
}
});
// When filtering completions, consider the character after the '-' to
// be the start of a subword.
cx.set_state(r#"<p class="yelˇ" />"#);
cx.simulate_keystroke("l");
cx.foreground().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-yellow"]
);
} else {
panic!("expected completion menu to be open");
}
});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point

View file

@ -2251,7 +2251,7 @@ impl Element<Editor> for EditorElement {
let replica_id = if let Some(mapping) = &editor.replica_id_mapping {
mapping.get(&replica_id).copied()
} else {
None
Some(replica_id)
};
// The local selections match the leader's selections.

View file

@ -177,20 +177,20 @@ pub fn line_end(
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let language = map.buffer_snapshot.language_at(raw_point);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_preceding_boundary(map, point, |left, right| {
(char_kind(language, left) != char_kind(language, right) && !right.is_whitespace())
(char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
|| left == '\n'
})
}
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let language = map.buffer_snapshot.language_at(raw_point);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_preceding_boundary(map, point, |left, right| {
let is_word_start =
char_kind(language, left) != char_kind(language, right) && !right.is_whitespace();
char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
let is_subword_start =
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start || left == '\n'
@ -199,19 +199,19 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let language = map.buffer_snapshot.language_at(raw_point);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_boundary(map, point, |left, right| {
(char_kind(language, left) != char_kind(language, right) && !left.is_whitespace())
(char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
|| right == '\n'
})
}
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let language = map.buffer_snapshot.language_at(raw_point);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_boundary(map, point, |left, right| {
let is_word_end =
(char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace();
(char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
let is_subword_end =
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end || right == '\n'
@ -399,14 +399,14 @@ pub fn find_boundary_in_line(
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
let raw_point = point.to_point(map);
let language = map.buffer_snapshot.language_at(raw_point);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
let text = &map.buffer_snapshot;
let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c));
let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c));
let prev_char_kind = text
.reversed_chars_at(ix)
.next()
.map(|c| char_kind(language, c));
.map(|c| char_kind(&scope, c));
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
}

View file

@ -617,6 +617,42 @@ impl MultiBuffer {
}
}
pub fn merge_transactions(
&mut self,
transaction: TransactionId,
destination: TransactionId,
cx: &mut ModelContext<Self>,
) {
if let Some(buffer) = self.as_singleton() {
buffer.update(cx, |buffer, _| {
buffer.merge_transactions(transaction, destination)
});
} else {
if let Some(transaction) = self.history.forget(transaction) {
if let Some(destination) = self.history.transaction_mut(destination) {
for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions {
if let Some(destination_buffer_transaction_id) =
destination.buffer_transactions.get(&buffer_id)
{
if let Some(state) = self.buffers.borrow().get(&buffer_id) {
state.buffer.update(cx, |buffer, _| {
buffer.merge_transactions(
buffer_transaction_id,
*destination_buffer_transaction_id,
)
});
}
} else {
destination
.buffer_transactions
.insert(buffer_id, buffer_transaction_id);
}
}
}
}
}
}
pub fn finalize_last_transaction(&mut self, cx: &mut ModelContext<Self>) {
self.history.finalize_last_transaction();
for BufferState { buffer, .. } in self.buffers.borrow().values() {
@ -788,6 +824,20 @@ impl MultiBuffer {
None
}
pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut ModelContext<Self>) {
if let Some(buffer) = self.as_singleton() {
buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
} else if let Some(transaction) = self.history.remove_from_undo(transaction_id) {
for (buffer_id, transaction_id) in &transaction.buffer_transactions {
if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) {
buffer.update(cx, |buffer, cx| {
buffer.undo_transaction(*transaction_id, cx)
});
}
}
}
}
pub fn stream_excerpts_with_context_lines(
&mut self,
buffer: ModelHandle<Buffer>,
@ -1367,13 +1417,13 @@ impl MultiBuffer {
return false;
}
let language = self.language_at(position.clone(), cx);
if char_kind(language.as_ref(), char) == CharKind::Word {
let snapshot = self.snapshot(cx);
let position = position.to_offset(&snapshot);
let scope = snapshot.language_scope_at(position);
if char_kind(&scope, char) == CharKind::Word {
return true;
}
let snapshot = self.snapshot(cx);
let anchor = snapshot.anchor_before(position);
anchor
.buffer_id
@ -1875,8 +1925,8 @@ impl MultiBufferSnapshot {
let mut next_chars = self.chars_at(start).peekable();
let mut prev_chars = self.reversed_chars_at(start).peekable();
let language = self.language_at(start);
let kind = |c| char_kind(language, c);
let scope = self.language_scope_at(start);
let kind = |c| char_kind(&scope, c);
let word_kind = cmp::max(
prev_chars.peek().copied().map(kind),
next_chars.peek().copied().map(kind),
@ -2316,6 +2366,16 @@ impl MultiBufferSnapshot {
}
}
pub fn prev_non_blank_row(&self, mut row: u32) -> Option<u32> {
while row > 0 {
row -= 1;
if !self.is_line_blank(row) {
return Some(row);
}
}
None
}
pub fn line_len(&self, row: u32) -> u32 {
if let Some((_, range)) = self.buffer_line_for_row(row) {
range.end.column - range.start.column
@ -3347,6 +3407,35 @@ impl History {
}
}
fn forget(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
if let Some(ix) = self
.undo_stack
.iter()
.rposition(|transaction| transaction.id == transaction_id)
{
Some(self.undo_stack.remove(ix))
} else if let Some(ix) = self
.redo_stack
.iter()
.rposition(|transaction| transaction.id == transaction_id)
{
Some(self.redo_stack.remove(ix))
} else {
None
}
}
fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> {
self.undo_stack
.iter_mut()
.find(|transaction| transaction.id == transaction_id)
.or_else(|| {
self.redo_stack
.iter_mut()
.find(|transaction| transaction.id == transaction_id)
})
}
fn pop_undo(&mut self) -> Option<&mut Transaction> {
assert_eq!(self.transaction_depth, 0);
if let Some(transaction) = self.undo_stack.pop() {
@ -3367,6 +3456,16 @@ impl History {
}
}
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
let ix = self
.undo_stack
.iter()
.rposition(|transaction| transaction.id == transaction_id)?;
let transaction = self.undo_stack.remove(ix);
self.redo_stack.push(transaction);
self.redo_stack.last()
}
fn group(&mut self) -> Option<TransactionId> {
let mut count = 0;
let mut transactions = self.undo_stack.iter();

View file

@ -51,7 +51,7 @@ impl<'a> EditorLspTestContext<'a> {
language
.path_suffixes()
.first()
.unwrap_or(&"txt".to_string())
.expect("language must have a path suffix for EditorLspTestContext")
);
let mut fake_servers = language