mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
catchup with main
This commit is contained in:
commit
3a661c5977
585
Cargo.lock
generated
585
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
3
Procfile
3
Procfile
@ -1,3 +1,4 @@
|
||||
web: cd ../zed.dev && PORT=3000 npx vercel dev
|
||||
collab: cd crates/collab && cargo run serve
|
||||
livekit: livekit-server --dev
|
||||
livekit: livekit-server --dev
|
||||
postgrest: postgrest crates/collab/admin_api.conf
|
||||
|
@ -12,14 +12,14 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
|
||||
```
|
||||
sudo xcodebuild -license
|
||||
```
|
||||
|
||||
|
||||
* Install homebrew, node and rustup-init (rutup, rust, cargo, etc.)
|
||||
```
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
brew install node rustup-init
|
||||
rustup-init # follow the installation steps
|
||||
```
|
||||
|
||||
|
||||
* Install postgres and configure the database
|
||||
```
|
||||
brew install postgresql@15
|
||||
@ -27,11 +27,12 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
|
||||
psql -c "CREATE ROLE postgres SUPERUSER LOGIN" postgres
|
||||
psql -U postgres -c "CREATE DATABASE zed"
|
||||
```
|
||||
|
||||
* Install the `LiveKit` server and the `foreman` process supervisor:
|
||||
|
||||
* Install the `LiveKit` server, the `PostgREST` API server, and the `foreman` process supervisor:
|
||||
|
||||
```
|
||||
brew install livekit
|
||||
brew install postgrest
|
||||
brew install foreman
|
||||
```
|
||||
|
||||
|
@ -231,7 +231,14 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"context": "BufferSearchBar && in_replace",
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"cmd-enter": "search::ReplaceAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace > Editor",
|
||||
"bindings": {
|
||||
"up": "search::PreviousHistoryQuery",
|
||||
"down": "search::NextHistoryQuery"
|
||||
@ -533,7 +540,7 @@
|
||||
// TODO: Move this to a dock open action
|
||||
"cmd-shift-c": "collab_panel::ToggleFocus",
|
||||
"cmd-alt-i": "zed::DebugElements",
|
||||
"ctrl-:": "editor::ToggleInlayHints",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -32,6 +32,8 @@
|
||||
"right": "vim::Right",
|
||||
"$": "vim::EndOfLine",
|
||||
"^": "vim::FirstNonWhitespace",
|
||||
"_": "vim::StartOfLineDownward",
|
||||
"g _": "vim::EndOfLineDownward",
|
||||
"shift-g": "vim::EndOfDocument",
|
||||
"w": "vim::NextWordStart",
|
||||
"{": "vim::StartOfParagraph",
|
||||
@ -326,7 +328,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
".": "vim::Repeat",
|
||||
"c": [
|
||||
@ -389,7 +391,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == n",
|
||||
"context": "Editor && VimCount",
|
||||
"bindings": {
|
||||
"0": [
|
||||
"vim::Number",
|
||||
@ -497,7 +499,7 @@
|
||||
"around": true
|
||||
}
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -27,6 +27,7 @@ futures.workspace = true
|
||||
indoc.workspace = true
|
||||
isahc.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
@ -1,5 +1,6 @@
|
||||
pub mod assistant;
|
||||
mod assistant_settings;
|
||||
mod codegen;
|
||||
mod streaming_diff;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
@ -26,7 +27,7 @@ use util::paths::CONVERSATIONS_DIR;
|
||||
const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
|
||||
|
||||
// Data types for chat completion requests
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct OpenAIRequest {
|
||||
model: String,
|
||||
messages: Vec<RequestMessage>,
|
||||
|
@ -1,9 +1,8 @@
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
|
||||
stream_completion,
|
||||
streaming_diff::{Hunk, StreamingDiff},
|
||||
MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role,
|
||||
SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL,
|
||||
codegen::{self, Codegen, CodegenKind, OpenAICompletionProvider},
|
||||
stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage,
|
||||
Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Local};
|
||||
@ -13,10 +12,10 @@ use editor::{
|
||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
|
||||
},
|
||||
scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
|
||||
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{
|
||||
@ -30,17 +29,14 @@ use gpui::{
|
||||
ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
WindowContext,
|
||||
};
|
||||
use language::{
|
||||
language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, ToOffset as _,
|
||||
TransactionId,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
|
||||
use search::BufferSearchBar;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
cmp, env,
|
||||
fmt::Write,
|
||||
future, iter,
|
||||
iter,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
@ -266,23 +262,40 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
fn new_inline_assist(&mut self, editor: &ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
|
||||
api_key
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let provider = Arc::new(OpenAICompletionProvider::new(
|
||||
api_key,
|
||||
cx.background().clone(),
|
||||
));
|
||||
let selection = editor.read(cx).selections.newest_anchor().clone();
|
||||
let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot);
|
||||
let assist_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
|
||||
InlineAssistKind::Generate
|
||||
let codegen_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
|
||||
CodegenKind::Generate {
|
||||
position: selection.start,
|
||||
}
|
||||
} else {
|
||||
InlineAssistKind::Transform
|
||||
CodegenKind::Transform {
|
||||
range: selection.start..selection.end,
|
||||
}
|
||||
};
|
||||
let codegen = cx.add_model(|cx| {
|
||||
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
|
||||
});
|
||||
|
||||
let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
|
||||
let inline_assistant = cx.add_view(|cx| {
|
||||
let assistant = InlineAssistant::new(
|
||||
inline_assist_id,
|
||||
assist_kind,
|
||||
measurements.clone(),
|
||||
self.include_conversation_in_next_inline_assist,
|
||||
self.inline_prompt_history.clone(),
|
||||
codegen.clone(),
|
||||
cx,
|
||||
);
|
||||
cx.focus_self();
|
||||
@ -321,48 +334,66 @@ impl AssistantPanel {
|
||||
self.pending_inline_assists.insert(
|
||||
inline_assist_id,
|
||||
PendingInlineAssist {
|
||||
kind: assist_kind,
|
||||
editor: editor.downgrade(),
|
||||
range,
|
||||
highlighted_ranges: Default::default(),
|
||||
inline_assistant: Some((block_id, inline_assistant.clone())),
|
||||
code_generation: Task::ready(None),
|
||||
transaction_id: None,
|
||||
codegen: codegen.clone(),
|
||||
_subscriptions: vec![
|
||||
cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
|
||||
cx.subscribe(editor, {
|
||||
let inline_assistant = inline_assistant.downgrade();
|
||||
move |this, editor, event, cx| {
|
||||
move |_, editor, event, cx| {
|
||||
if let Some(inline_assistant) = inline_assistant.upgrade(cx) {
|
||||
match event {
|
||||
editor::Event::SelectionsChanged { local } => {
|
||||
if *local && inline_assistant.read(cx).has_focus {
|
||||
cx.focus(&editor);
|
||||
}
|
||||
if let editor::Event::SelectionsChanged { local } = event {
|
||||
if *local && inline_assistant.read(cx).has_focus {
|
||||
cx.focus(&editor);
|
||||
}
|
||||
editor::Event::TransactionUndone {
|
||||
transaction_id: tx_id,
|
||||
} => {
|
||||
if let Some(pending_assist) =
|
||||
this.pending_inline_assists.get(&inline_assist_id)
|
||||
{
|
||||
if pending_assist.transaction_id == Some(*tx_id) {
|
||||
// Notice we are supplying `undo: false` here. This
|
||||
// is because there's no need to undo the transaction
|
||||
// because the user just did so.
|
||||
this.close_inline_assist(
|
||||
inline_assist_id,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cx.observe(&codegen, {
|
||||
let editor = editor.downgrade();
|
||||
move |this, _, cx| {
|
||||
if let Some(editor) = editor.upgrade(cx) {
|
||||
this.update_highlights_for_editor(&editor, cx);
|
||||
}
|
||||
}
|
||||
}),
|
||||
cx.subscribe(&codegen, move |this, codegen, event, cx| match event {
|
||||
codegen::Event::Undone => {
|
||||
this.finish_inline_assist(inline_assist_id, false, cx)
|
||||
}
|
||||
codegen::Event::Finished => {
|
||||
let pending_assist = if let Some(pending_assist) =
|
||||
this.pending_inline_assists.get(&inline_assist_id)
|
||||
{
|
||||
pending_assist
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let error = codegen
|
||||
.read(cx)
|
||||
.error()
|
||||
.map(|error| format!("Inline assistant error: {}", error));
|
||||
if let Some(error) = error {
|
||||
if pending_assist.inline_assistant.is_none() {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(inline_assist_id, error),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
this.finish_inline_assist(inline_assist_id, false, cx);
|
||||
}
|
||||
} else {
|
||||
this.finish_inline_assist(inline_assist_id, false, cx);
|
||||
}
|
||||
}
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
@ -388,7 +419,7 @@ impl AssistantPanel {
|
||||
self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx);
|
||||
}
|
||||
InlineAssistantEvent::Canceled => {
|
||||
self.close_inline_assist(assist_id, true, cx);
|
||||
self.finish_inline_assist(assist_id, true, cx);
|
||||
}
|
||||
InlineAssistantEvent::Dismissed => {
|
||||
self.hide_inline_assist(assist_id, cx);
|
||||
@ -417,7 +448,7 @@ impl AssistantPanel {
|
||||
.get(&editor.downgrade())
|
||||
.and_then(|assist_ids| assist_ids.last().copied())
|
||||
{
|
||||
panel.close_inline_assist(assist_id, true, cx);
|
||||
panel.finish_inline_assist(assist_id, true, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@ -432,7 +463,7 @@ impl AssistantPanel {
|
||||
cx.propagate_action();
|
||||
}
|
||||
|
||||
fn close_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
|
||||
fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
|
||||
self.hide_inline_assist(assist_id, cx);
|
||||
|
||||
if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) {
|
||||
@ -450,13 +481,9 @@ impl AssistantPanel {
|
||||
self.update_highlights_for_editor(&editor, cx);
|
||||
|
||||
if undo {
|
||||
if let Some(transaction_id) = pending_assist.transaction_id {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
buffer.undo_transaction(transaction_id, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
pending_assist
|
||||
.codegen
|
||||
.update(cx, |codegen, cx| codegen.undo(cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -481,12 +508,6 @@ impl AssistantPanel {
|
||||
include_conversation: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
|
||||
api_key
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let conversation = if include_conversation {
|
||||
self.active_editor()
|
||||
.map(|editor| editor.read(cx).conversation.clone())
|
||||
@ -514,56 +535,9 @@ impl AssistantPanel {
|
||||
self.inline_prompt_history.pop_front();
|
||||
}
|
||||
|
||||
let range = pending_assist.range.clone();
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let selected_text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
.collect::<Rope>();
|
||||
|
||||
let selection_start = range.start.to_point(&snapshot);
|
||||
let selection_end = range.end.to_point(&snapshot);
|
||||
|
||||
let mut base_indent: Option<language::IndentSize> = None;
|
||||
let mut start_row = selection_start.row;
|
||||
if snapshot.is_line_blank(start_row) {
|
||||
if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) {
|
||||
start_row = prev_non_blank_row;
|
||||
}
|
||||
}
|
||||
for row in start_row..=selection_end.row {
|
||||
if snapshot.is_line_blank(row) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let line_indent = snapshot.indent_size_for_line(row);
|
||||
if let Some(base_indent) = base_indent.as_mut() {
|
||||
if line_indent.len < base_indent.len {
|
||||
*base_indent = line_indent;
|
||||
}
|
||||
} else {
|
||||
base_indent = Some(line_indent);
|
||||
}
|
||||
}
|
||||
|
||||
let mut normalized_selected_text = selected_text.clone();
|
||||
if let Some(base_indent) = base_indent {
|
||||
for row in selection_start.row..=selection_end.row {
|
||||
let selection_row = row - selection_start.row;
|
||||
let line_start =
|
||||
normalized_selected_text.point_to_offset(Point::new(selection_row, 0));
|
||||
let indent_len = if row == selection_start.row {
|
||||
base_indent.len.saturating_sub(selection_start.column)
|
||||
} else {
|
||||
let line_len = normalized_selected_text.line_len(selection_row);
|
||||
cmp::min(line_len, base_indent.len)
|
||||
};
|
||||
let indent_end = cmp::min(
|
||||
line_start + indent_len as usize,
|
||||
normalized_selected_text.len(),
|
||||
);
|
||||
normalized_selected_text.replace(line_start..indent_end, "");
|
||||
}
|
||||
}
|
||||
let range = pending_assist.codegen.read(cx).range();
|
||||
let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
|
||||
|
||||
let language = snapshot.language_at(range.start);
|
||||
let language_name = if let Some(language) = language.as_ref() {
|
||||
@ -581,8 +555,8 @@ impl AssistantPanel {
|
||||
if let Some(language_name) = language_name {
|
||||
writeln!(prompt, "You're an expert {language_name} engineer.").unwrap();
|
||||
}
|
||||
match pending_assist.kind {
|
||||
InlineAssistKind::Transform => {
|
||||
match pending_assist.codegen.read(cx).kind() {
|
||||
CodegenKind::Transform { .. } => {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You're currently working inside an editor on this file:"
|
||||
@ -608,7 +582,7 @@ impl AssistantPanel {
|
||||
} else {
|
||||
writeln!(prompt, "```").unwrap();
|
||||
}
|
||||
writeln!(prompt, "{normalized_selected_text}").unwrap();
|
||||
writeln!(prompt, "{selected_text}").unwrap();
|
||||
writeln!(prompt, "```").unwrap();
|
||||
writeln!(prompt).unwrap();
|
||||
writeln!(
|
||||
@ -622,7 +596,7 @@ impl AssistantPanel {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
InlineAssistKind::Generate => {
|
||||
CodegenKind::Generate { .. } => {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You're currently working inside an editor on this file:"
|
||||
@ -689,209 +663,9 @@ impl AssistantPanel {
|
||||
messages,
|
||||
stream: true,
|
||||
};
|
||||
let response = stream_completion(api_key, cx.background().clone(), request);
|
||||
let editor = editor.downgrade();
|
||||
|
||||
pending_assist.code_generation = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
let diff = cx.background().spawn(async move {
|
||||
let chunks = strip_markdown_codeblock(response.await?.filter_map(
|
||||
|message| async move {
|
||||
match message {
|
||||
Ok(mut message) => Some(Ok(message.choices.pop()?.delta.content?)),
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
},
|
||||
));
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
let mut indent_len;
|
||||
let indent_text;
|
||||
if let Some(base_indent) = base_indent {
|
||||
indent_len = base_indent.len;
|
||||
indent_text = match base_indent.kind {
|
||||
language::IndentKind::Space => " ",
|
||||
language::IndentKind::Tab => "\t",
|
||||
};
|
||||
} else {
|
||||
indent_len = 0;
|
||||
indent_text = "";
|
||||
};
|
||||
|
||||
let mut first_line_len = 0;
|
||||
let mut first_line_non_whitespace_char_ix = None;
|
||||
let mut first_line = true;
|
||||
let mut new_text = String::new();
|
||||
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
let chunk = chunk?;
|
||||
|
||||
let mut lines = chunk.split('\n');
|
||||
if let Some(mut line) = lines.next() {
|
||||
if first_line {
|
||||
if first_line_non_whitespace_char_ix.is_none() {
|
||||
if let Some(mut char_ix) =
|
||||
line.find(|ch: char| !ch.is_whitespace())
|
||||
{
|
||||
line = &line[char_ix..];
|
||||
char_ix += first_line_len;
|
||||
first_line_non_whitespace_char_ix = Some(char_ix);
|
||||
let first_line_indent = char_ix
|
||||
.saturating_sub(selection_start.column as usize)
|
||||
as usize;
|
||||
new_text.push_str(&indent_text.repeat(first_line_indent));
|
||||
indent_len = indent_len.saturating_sub(char_ix as u32);
|
||||
}
|
||||
}
|
||||
first_line_len += line.len();
|
||||
}
|
||||
|
||||
if first_line_non_whitespace_char_ix.is_some() {
|
||||
new_text.push_str(line);
|
||||
}
|
||||
}
|
||||
|
||||
for line in lines {
|
||||
first_line = false;
|
||||
new_text.push('\n');
|
||||
if !line.is_empty() {
|
||||
new_text.push_str(&indent_text.repeat(indent_len as usize));
|
||||
}
|
||||
new_text.push_str(line);
|
||||
}
|
||||
|
||||
let hunks = diff.push_new(&new_text);
|
||||
hunks_tx.send(hunks).await?;
|
||||
new_text.clear();
|
||||
}
|
||||
hunks_tx.send(diff.finish()).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
while let Some(hunks) = hunks_rx.next().await {
|
||||
let editor = if let Some(editor) = editor.upgrade(&cx) {
|
||||
editor
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
let this = if let Some(this) = this.upgrade(&cx) {
|
||||
this
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let pending_assist = if let Some(pending_assist) =
|
||||
this.pending_inline_assists.get_mut(&inline_assist_id)
|
||||
{
|
||||
pending_assist
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
pending_assist.highlighted_ranges.clear();
|
||||
editor.update(cx, |editor, cx| {
|
||||
let transaction = editor.buffer().update(cx, |buffer, cx| {
|
||||
// Avoid grouping assistant edits with user edits.
|
||||
buffer.finalize_last_transaction(cx);
|
||||
|
||||
buffer.start_transaction(cx);
|
||||
buffer.edit(
|
||||
hunks.into_iter().filter_map(|hunk| match hunk {
|
||||
Hunk::Insert { text } => {
|
||||
let edit_start = snapshot.anchor_after(edit_start);
|
||||
Some((edit_start..edit_start, text))
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
Some((edit_range, String::new()))
|
||||
}
|
||||
Hunk::Keep { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start += len;
|
||||
pending_assist.highlighted_ranges.push(edit_range);
|
||||
None
|
||||
}
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
buffer.end_transaction(cx)
|
||||
});
|
||||
|
||||
if let Some(transaction) = transaction {
|
||||
if let Some(first_transaction) = pending_assist.transaction_id {
|
||||
// Group all assistant edits into the first transaction.
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
buffer.merge_transactions(
|
||||
transaction,
|
||||
first_transaction,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
} else {
|
||||
pending_assist.transaction_id = Some(transaction);
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.update_highlights_for_editor(&editor, cx);
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(error) = diff.await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let pending_assist = if let Some(pending_assist) =
|
||||
this.pending_inline_assists.get_mut(&inline_assist_id)
|
||||
{
|
||||
pending_assist
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some((_, inline_assistant)) =
|
||||
pending_assist.inline_assistant.as_ref()
|
||||
{
|
||||
inline_assistant.update(cx, |inline_assistant, cx| {
|
||||
inline_assistant.set_error(error, cx);
|
||||
});
|
||||
} else if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
inline_assist_id,
|
||||
format!("Inline assistant error: {}", error),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
})?;
|
||||
} else {
|
||||
let _ = this.update(&mut cx, |this, cx| {
|
||||
this.close_inline_assist(inline_assist_id, false, cx)
|
||||
});
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
pending_assist
|
||||
.codegen
|
||||
.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
}
|
||||
|
||||
fn update_highlights_for_editor(
|
||||
@ -909,8 +683,9 @@ impl AssistantPanel {
|
||||
|
||||
for inline_assist_id in inline_assist_ids {
|
||||
if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) {
|
||||
background_ranges.push(pending_assist.range.clone());
|
||||
foreground_ranges.extend(pending_assist.highlighted_ranges.iter().cloned());
|
||||
let codegen = pending_assist.codegen.read(cx);
|
||||
background_ranges.push(codegen.range());
|
||||
foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
|
||||
}
|
||||
}
|
||||
|
||||
@ -929,7 +704,7 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
if foreground_ranges.is_empty() {
|
||||
editor.clear_text_highlights::<PendingInlineAssist>(cx);
|
||||
editor.clear_highlights::<PendingInlineAssist>(cx);
|
||||
} else {
|
||||
editor.highlight_text::<PendingInlineAssist>(
|
||||
foreground_ranges,
|
||||
@ -2887,12 +2662,6 @@ enum InlineAssistantEvent {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum InlineAssistKind {
|
||||
Transform,
|
||||
Generate,
|
||||
}
|
||||
|
||||
struct InlineAssistant {
|
||||
id: usize,
|
||||
prompt_editor: ViewHandle<Editor>,
|
||||
@ -2900,11 +2669,11 @@ struct InlineAssistant {
|
||||
has_focus: bool,
|
||||
include_conversation: bool,
|
||||
measurements: Rc<Cell<BlockMeasurements>>,
|
||||
error: Option<anyhow::Error>,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_history_ix: Option<usize>,
|
||||
pending_prompt: String,
|
||||
_subscription: Subscription,
|
||||
codegen: ModelHandle<Codegen>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl Entity for InlineAssistant {
|
||||
@ -2933,7 +2702,7 @@ impl View for InlineAssistant {
|
||||
.element()
|
||||
.aligned(),
|
||||
)
|
||||
.with_children(if let Some(error) = self.error.as_ref() {
|
||||
.with_children(if let Some(error) = self.codegen.read(cx).error() {
|
||||
Some(
|
||||
Svg::new("icons/circle_x_mark_12.svg")
|
||||
.with_color(theme.assistant.error_icon.color)
|
||||
@ -3007,10 +2776,10 @@ impl View for InlineAssistant {
|
||||
impl InlineAssistant {
|
||||
fn new(
|
||||
id: usize,
|
||||
kind: InlineAssistKind,
|
||||
measurements: Rc<Cell<BlockMeasurements>>,
|
||||
include_conversation: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
codegen: ModelHandle<Codegen>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let prompt_editor = cx.add_view(|cx| {
|
||||
@ -3018,14 +2787,17 @@ impl InlineAssistant {
|
||||
Some(Arc::new(|theme| theme.assistant.inline.editor.clone())),
|
||||
cx,
|
||||
);
|
||||
let placeholder = match kind {
|
||||
InlineAssistKind::Transform => "Enter transformation prompt…",
|
||||
InlineAssistKind::Generate => "Enter generation prompt…",
|
||||
let placeholder = match codegen.read(cx).kind() {
|
||||
CodegenKind::Transform { .. } => "Enter transformation prompt…",
|
||||
CodegenKind::Generate { .. } => "Enter generation prompt…",
|
||||
};
|
||||
editor.set_placeholder_text(placeholder, cx);
|
||||
editor
|
||||
});
|
||||
let subscription = cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events);
|
||||
let subscriptions = vec![
|
||||
cx.observe(&codegen, Self::handle_codegen_changed),
|
||||
cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
|
||||
];
|
||||
Self {
|
||||
id,
|
||||
prompt_editor,
|
||||
@ -3033,11 +2805,11 @@ impl InlineAssistant {
|
||||
has_focus: false,
|
||||
include_conversation,
|
||||
measurements,
|
||||
error: None,
|
||||
prompt_history,
|
||||
prompt_history_ix: None,
|
||||
pending_prompt: String::new(),
|
||||
_subscription: subscription,
|
||||
codegen,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
@ -3053,6 +2825,32 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_codegen_changed(&mut self, _: ModelHandle<Codegen>, cx: &mut ViewContext<Self>) {
|
||||
let is_read_only = !self.codegen.read(cx).idle();
|
||||
self.prompt_editor.update(cx, |editor, cx| {
|
||||
let was_read_only = editor.read_only();
|
||||
if was_read_only != is_read_only {
|
||||
if is_read_only {
|
||||
editor.set_read_only(true);
|
||||
editor.set_field_editor_style(
|
||||
Some(Arc::new(|theme| {
|
||||
theme.assistant.inline.disabled_editor.clone()
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
self.confirmed = false;
|
||||
editor.set_read_only(false);
|
||||
editor.set_field_editor_style(
|
||||
Some(Arc::new(|theme| theme.assistant.inline.editor.clone())),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(InlineAssistantEvent::Canceled);
|
||||
}
|
||||
@ -3076,7 +2874,6 @@ impl InlineAssistant {
|
||||
include_conversation: self.include_conversation,
|
||||
});
|
||||
self.confirmed = true;
|
||||
self.error = None;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@ -3093,19 +2890,6 @@ impl InlineAssistant {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_error(&mut self, error: anyhow::Error, cx: &mut ViewContext<Self>) {
|
||||
self.error = Some(error);
|
||||
self.confirmed = false;
|
||||
self.prompt_editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(false);
|
||||
editor.set_field_editor_style(
|
||||
Some(Arc::new(|theme| theme.assistant.inline.editor.clone())),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.prompt_history_ix {
|
||||
if ix > 0 {
|
||||
@ -3152,13 +2936,9 @@ struct BlockMeasurements {
|
||||
}
|
||||
|
||||
struct PendingInlineAssist {
|
||||
kind: InlineAssistKind,
|
||||
editor: WeakViewHandle<Editor>,
|
||||
range: Range<Anchor>,
|
||||
highlighted_ranges: Vec<Range<Anchor>>,
|
||||
inline_assistant: Option<(BlockId, ViewHandle<InlineAssistant>)>,
|
||||
code_generation: Task<Option<()>>,
|
||||
transaction_id: Option<TransactionId>,
|
||||
codegen: ModelHandle<Codegen>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@ -3184,65 +2964,10 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_markdown_codeblock(
|
||||
stream: impl Stream<Item = Result<String>>,
|
||||
) -> impl Stream<Item = Result<String>> {
|
||||
let mut first_line = true;
|
||||
let mut buffer = String::new();
|
||||
let mut starts_with_fenced_code_block = false;
|
||||
stream.filter_map(move |chunk| {
|
||||
let chunk = match chunk {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => return future::ready(Some(Err(err))),
|
||||
};
|
||||
buffer.push_str(&chunk);
|
||||
|
||||
if first_line {
|
||||
if buffer == "" || buffer == "`" || buffer == "``" {
|
||||
return future::ready(None);
|
||||
} else if buffer.starts_with("```") {
|
||||
starts_with_fenced_code_block = true;
|
||||
if let Some(newline_ix) = buffer.find('\n') {
|
||||
buffer.replace_range(..newline_ix + 1, "");
|
||||
first_line = false;
|
||||
} else {
|
||||
return future::ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let text = if starts_with_fenced_code_block {
|
||||
buffer
|
||||
.strip_suffix("\n```\n")
|
||||
.or_else(|| buffer.strip_suffix("\n```"))
|
||||
.or_else(|| buffer.strip_suffix("\n``"))
|
||||
.or_else(|| buffer.strip_suffix("\n`"))
|
||||
.or_else(|| buffer.strip_suffix('\n'))
|
||||
.unwrap_or(&buffer)
|
||||
} else {
|
||||
&buffer
|
||||
};
|
||||
|
||||
if text.contains('\n') {
|
||||
first_line = false;
|
||||
}
|
||||
|
||||
let remainder = buffer.split_off(text.len());
|
||||
let result = if buffer.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Ok(buffer.clone()))
|
||||
};
|
||||
buffer = remainder;
|
||||
future::ready(result)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::MessageId;
|
||||
use futures::stream;
|
||||
use gpui::AppContext;
|
||||
|
||||
#[gpui::test]
|
||||
@ -3611,62 +3336,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_markdown_codeblock() {
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"```js\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"``\nLorem ipsum dolor\n```"
|
||||
);
|
||||
|
||||
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
|
||||
stream::iter(
|
||||
text.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(size)
|
||||
.map(|chunk| Ok(chunk.iter().collect::<String>()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn messages(
|
||||
conversation: &ModelHandle<Conversation>,
|
||||
cx: &AppContext,
|
||||
|
704
crates/ai/src/codegen.rs
Normal file
704
crates/ai/src/codegen.rs
Normal file
@ -0,0 +1,704 @@
|
||||
use crate::{
|
||||
stream_completion,
|
||||
streaming_diff::{Hunk, StreamingDiff},
|
||||
OpenAIRequest,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use editor::{
|
||||
multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use futures::{
|
||||
channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, SinkExt, Stream, StreamExt,
|
||||
};
|
||||
use gpui::{executor::Background, Entity, ModelContext, ModelHandle, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
use std::{cmp, future, ops::Range, sync::Arc};
|
||||
|
||||
pub trait CompletionProvider {
|
||||
fn complete(
|
||||
&self,
|
||||
prompt: OpenAIRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
|
||||
}
|
||||
|
||||
pub struct OpenAICompletionProvider {
|
||||
api_key: String,
|
||||
executor: Arc<Background>,
|
||||
}
|
||||
|
||||
impl OpenAICompletionProvider {
|
||||
pub fn new(api_key: String, executor: Arc<Background>) -> Self {
|
||||
Self { api_key, executor }
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionProvider for OpenAICompletionProvider {
|
||||
fn complete(
|
||||
&self,
|
||||
prompt: OpenAIRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt);
|
||||
async move {
|
||||
let response = request.await?;
|
||||
let stream = response
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
Ok(stream)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Finished,
|
||||
Undone,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CodegenKind {
|
||||
Transform { range: Range<Anchor> },
|
||||
Generate { position: Anchor },
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
buffer: ModelHandle<MultiBuffer>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
kind: CodegenKind,
|
||||
last_equal_ranges: Vec<Range<Anchor>>,
|
||||
transaction_id: Option<TransactionId>,
|
||||
error: Option<anyhow::Error>,
|
||||
generation: Task<()>,
|
||||
idle: bool,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
impl Entity for Codegen {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: ModelHandle<MultiBuffer>,
|
||||
mut kind: CodegenKind,
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
match &mut kind {
|
||||
CodegenKind::Transform { range } => {
|
||||
let mut point_range = range.to_point(&snapshot);
|
||||
point_range.start.column = 0;
|
||||
if point_range.end.column > 0 || point_range.start.row == point_range.end.row {
|
||||
point_range.end.column = snapshot.line_len(point_range.end.row);
|
||||
}
|
||||
range.start = snapshot.anchor_before(point_range.start);
|
||||
range.end = snapshot.anchor_after(point_range.end);
|
||||
}
|
||||
CodegenKind::Generate { position } => {
|
||||
*position = position.bias_right(&snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
provider,
|
||||
buffer: buffer.clone(),
|
||||
snapshot,
|
||||
kind,
|
||||
last_equal_ranges: Default::default(),
|
||||
transaction_id: Default::default(),
|
||||
error: Default::default(),
|
||||
idle: true,
|
||||
generation: Task::ready(()),
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
_buffer: ModelHandle<MultiBuffer>,
|
||||
event: &multi_buffer::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
|
||||
if self.transaction_id == Some(*transaction_id) {
|
||||
self.transaction_id = None;
|
||||
self.generation = Task::ready(());
|
||||
cx.emit(Event::Undone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range(&self) -> Range<Anchor> {
|
||||
match &self.kind {
|
||||
CodegenKind::Transform { range } => range.clone(),
|
||||
CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &CodegenKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
|
||||
&self.last_equal_ranges
|
||||
}
|
||||
|
||||
pub fn idle(&self) -> bool {
|
||||
self.idle
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<&anyhow::Error> {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: OpenAIRequest, cx: &mut ModelContext<Self>) {
|
||||
let range = self.range();
|
||||
let snapshot = self.snapshot.clone();
|
||||
let selected_text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
.collect::<Rope>();
|
||||
|
||||
let selection_start = range.start.to_point(&snapshot);
|
||||
let suggested_line_indent = snapshot
|
||||
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
|
||||
.into_values()
|
||||
.next()
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row));
|
||||
|
||||
let response = self.provider.complete(prompt);
|
||||
self.generation = cx.spawn_weak(|this, mut cx| {
|
||||
async move {
|
||||
let generate = async {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
let diff = cx.background().spawn(async move {
|
||||
let chunks = strip_markdown_codeblock(response.await?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
let mut new_text = String::new();
|
||||
let mut base_indent = None;
|
||||
let mut line_indent = None;
|
||||
let mut first_line = true;
|
||||
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
let chunk = chunk?;
|
||||
|
||||
let mut lines = chunk.split('\n').peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
new_text.push_str(line);
|
||||
if line_indent.is_none() {
|
||||
if let Some(non_whitespace_ch_ix) =
|
||||
new_text.find(|ch: char| !ch.is_whitespace())
|
||||
{
|
||||
line_indent = Some(non_whitespace_ch_ix);
|
||||
base_indent = base_indent.or(line_indent);
|
||||
|
||||
let line_indent = line_indent.unwrap();
|
||||
let base_indent = base_indent.unwrap();
|
||||
let indent_delta = line_indent as i32 - base_indent as i32;
|
||||
let mut corrected_indent_len = cmp::max(
|
||||
0,
|
||||
suggested_line_indent.len as i32 + indent_delta,
|
||||
)
|
||||
as usize;
|
||||
if first_line {
|
||||
corrected_indent_len = corrected_indent_len
|
||||
.saturating_sub(selection_start.column as usize);
|
||||
}
|
||||
|
||||
let indent_char = suggested_line_indent.char();
|
||||
let mut indent_buffer = [0; 4];
|
||||
let indent_str =
|
||||
indent_char.encode_utf8(&mut indent_buffer);
|
||||
new_text.replace_range(
|
||||
..line_indent,
|
||||
&indent_str.repeat(corrected_indent_len),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if line_indent.is_some() {
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
new_text.clear();
|
||||
}
|
||||
|
||||
if lines.peek().is_some() {
|
||||
hunks_tx.send(diff.push_new("\n")).await?;
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
hunks_tx.send(diff.finish()).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
while let Some(hunks) = hunks_rx.next().await {
|
||||
let this = if let Some(this) = this.upgrade(&cx) {
|
||||
this
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
|
||||
let transaction = this.buffer.update(cx, |buffer, cx| {
|
||||
// Avoid grouping assistant edits with user edits.
|
||||
buffer.finalize_last_transaction(cx);
|
||||
|
||||
buffer.start_transaction(cx);
|
||||
buffer.edit(
|
||||
hunks.into_iter().filter_map(|hunk| match hunk {
|
||||
Hunk::Insert { text } => {
|
||||
let edit_start = snapshot.anchor_after(edit_start);
|
||||
Some((edit_start..edit_start, text))
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
Some((edit_range, String::new()))
|
||||
}
|
||||
Hunk::Keep { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
this.last_equal_ranges.push(edit_range);
|
||||
None
|
||||
}
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
buffer.end_transaction(cx)
|
||||
});
|
||||
|
||||
if let Some(transaction) = transaction {
|
||||
if let Some(first_transaction) = this.transaction_id {
|
||||
// Group all assistant edits into the first transaction.
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_transactions(
|
||||
transaction,
|
||||
first_transaction,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.transaction_id = Some(transaction);
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
diff.await?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = generate.await;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
this.idle = true;
|
||||
if let Err(error) = result {
|
||||
this.error = Some(error);
|
||||
}
|
||||
cx.emit(Event::Finished);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
self.error.take();
|
||||
self.idle = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction_id) = self.transaction_id {
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_markdown_codeblock(
|
||||
stream: impl Stream<Item = Result<String>>,
|
||||
) -> impl Stream<Item = Result<String>> {
|
||||
let mut first_line = true;
|
||||
let mut buffer = String::new();
|
||||
let mut starts_with_fenced_code_block = false;
|
||||
stream.filter_map(move |chunk| {
|
||||
let chunk = match chunk {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => return future::ready(Some(Err(err))),
|
||||
};
|
||||
buffer.push_str(&chunk);
|
||||
|
||||
if first_line {
|
||||
if buffer == "" || buffer == "`" || buffer == "``" {
|
||||
return future::ready(None);
|
||||
} else if buffer.starts_with("```") {
|
||||
starts_with_fenced_code_block = true;
|
||||
if let Some(newline_ix) = buffer.find('\n') {
|
||||
buffer.replace_range(..newline_ix + 1, "");
|
||||
first_line = false;
|
||||
} else {
|
||||
return future::ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let text = if starts_with_fenced_code_block {
|
||||
buffer
|
||||
.strip_suffix("\n```\n")
|
||||
.or_else(|| buffer.strip_suffix("\n```"))
|
||||
.or_else(|| buffer.strip_suffix("\n``"))
|
||||
.or_else(|| buffer.strip_suffix("\n`"))
|
||||
.or_else(|| buffer.strip_suffix('\n'))
|
||||
.unwrap_or(&buffer)
|
||||
} else {
|
||||
&buffer
|
||||
};
|
||||
|
||||
if text.contains('\n') {
|
||||
first_line = false;
|
||||
}
|
||||
|
||||
let remainder = buffer.split_off(text.len());
|
||||
let result = if buffer.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Ok(buffer.clone()))
|
||||
};
|
||||
buffer = remainder;
|
||||
future::ready(result)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::stream;
|
||||
use gpui::{executor::Deterministic, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use parking_lot::Mutex;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
deterministic: Arc<Deterministic>,
|
||||
) {
|
||||
cx.set_global(cx.read(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
let x = 0;
|
||||
for _ in 0..10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4))
|
||||
});
|
||||
let provider = Arc::new(TestCompletionProvider::new());
|
||||
let codegen = cx.add_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Transform { range },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
" let mut x = 0;\n",
|
||||
" while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
" }",
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
deterministic.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_past_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
deterministic: Arc<Deterministic>,
|
||||
) {
|
||||
cx.set_global(cx.read(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
le
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 6))
|
||||
});
|
||||
let provider = Arc::new(TestCompletionProvider::new());
|
||||
let codegen = cx.add_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"t mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
deterministic.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_before_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
deterministic: Arc<Deterministic>,
|
||||
) {
|
||||
cx.set_global(cx.read(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = concat!(
|
||||
"fn main() {\n",
|
||||
" \n",
|
||||
"}\n" //
|
||||
);
|
||||
let buffer =
|
||||
cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 2))
|
||||
});
|
||||
let provider = Arc::new(TestCompletionProvider::new());
|
||||
let codegen = cx.add_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"let mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
deterministic.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_markdown_codeblock() {
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"```js\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"``\nLorem ipsum dolor\n```"
|
||||
);
|
||||
|
||||
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
|
||||
stream::iter(
|
||||
text.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(size)
|
||||
.map(|chunk| Ok(chunk.iter().collect::<String>()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct TestCompletionProvider {
|
||||
last_completion_tx: Mutex<Option<mpsc::Sender<String>>>,
|
||||
}
|
||||
|
||||
impl TestCompletionProvider {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
last_completion_tx: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_completion(&self, completion: impl Into<String>) {
|
||||
let mut tx = self.last_completion_tx.lock();
|
||||
tx.as_mut().unwrap().try_send(completion.into()).unwrap();
|
||||
}
|
||||
|
||||
fn finish_completion(&self) {
|
||||
self.last_completion_tx.lock().take().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionProvider for TestCompletionProvider {
|
||||
fn complete(
|
||||
&self,
|
||||
_prompt: OpenAIRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
*self.last_completion_tx.lock() = Some(tx);
|
||||
async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
4
crates/collab/admin_api.conf
Normal file
4
crates/collab/admin_api.conf
Normal file
@ -0,0 +1,4 @@
|
||||
db-uri = "postgres://postgres@localhost/zed"
|
||||
server-port = 8081
|
||||
jwt-secret = "the-postgrest-jwt-secret-for-authorization"
|
||||
log-level = "info"
|
@ -3,6 +3,7 @@ apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: ${ZED_KUBE_NAMESPACE}
|
||||
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
@ -11,7 +12,7 @@ metadata:
|
||||
name: collab
|
||||
annotations:
|
||||
service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
|
||||
service.beta.kubernetes.io/do-loadbalancer-certificate-id: "08d9d8ce-761f-4ab3-bc78-4923ab5b0e33"
|
||||
service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID}
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
@ -21,6 +22,26 @@ spec:
|
||||
protocol: TCP
|
||||
port: 443
|
||||
targetPort: 8080
|
||||
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
namespace: ${ZED_KUBE_NAMESPACE}
|
||||
name: pgadmin
|
||||
annotations:
|
||||
service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
|
||||
service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID}
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
app: postgrest
|
||||
ports:
|
||||
- name: web
|
||||
protocol: TCP
|
||||
port: 443
|
||||
targetPort: 8080
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@ -117,3 +138,40 @@ spec:
|
||||
# FIXME - Switch to the more restrictive `PERFMON` capability.
|
||||
# This capability isn't yet available in a stable version of Debian.
|
||||
add: ["SYS_ADMIN"]
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: ${ZED_KUBE_NAMESPACE}
|
||||
name: postgrest
|
||||
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgrest
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgrest
|
||||
spec:
|
||||
containers:
|
||||
- name: postgrest
|
||||
image: "postgrest/postgrest"
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: PGRST_SERVER_PORT
|
||||
value: "8080"
|
||||
- name: PGRST_DB_URI
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: database
|
||||
key: url
|
||||
- name: PGRST_JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgrest
|
||||
key: jwt_secret
|
||||
|
@ -1,8 +1,7 @@
|
||||
use crate::{
|
||||
auth,
|
||||
db::{Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary},
|
||||
rpc::{self, ResultExt},
|
||||
AppState, Error, Result,
|
||||
db::{User, UserId},
|
||||
rpc, AppState, Error, Result,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
@ -11,7 +10,7 @@ use axum::{
|
||||
http::{self, Request, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::IntoResponse,
|
||||
routing::{get, post, put},
|
||||
routing::{get, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use axum_extra::response::ErasedJson;
|
||||
@ -23,18 +22,9 @@ use tracing::instrument;
|
||||
pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
|
||||
Router::new()
|
||||
.route("/user", get(get_authenticated_user))
|
||||
.route("/users", get(get_users).post(create_user))
|
||||
.route("/users/:id", put(update_user).delete(destroy_user))
|
||||
.route("/users/:id/access_tokens", post(create_access_token))
|
||||
.route("/users_with_no_invites", get(get_users_with_no_invites))
|
||||
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
||||
.route("/panic", post(trace_panic))
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.route("/signups", post(create_signup))
|
||||
.route("/signups_summary", get(get_waitlist_summary))
|
||||
.route("/user_invites", post(create_invite_from_code))
|
||||
.route("/unsent_invites", get(get_unsent_invites))
|
||||
.route("/sent_invites", post(record_sent_invites))
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(Extension(state))
|
||||
@ -104,28 +94,6 @@ async fn get_authenticated_user(
|
||||
return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GetUsersQueryParams {
|
||||
query: Option<String>,
|
||||
page: Option<u32>,
|
||||
limit: Option<u32>,
|
||||
}
|
||||
|
||||
async fn get_users(
|
||||
Query(params): Query<GetUsersQueryParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<User>>> {
|
||||
let limit = params.limit.unwrap_or(100);
|
||||
let users = if let Some(query) = params.query {
|
||||
app.db.fuzzy_search_users(&query, limit).await?
|
||||
} else {
|
||||
app.db
|
||||
.get_all_users(params.page.unwrap_or(0), limit)
|
||||
.await?
|
||||
};
|
||||
Ok(Json(users))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct CreateUserParams {
|
||||
github_user_id: i32,
|
||||
@ -145,119 +113,6 @@ struct CreateUserResponse {
|
||||
metrics_id: String,
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
Json(params): Json<CreateUserParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Extension(rpc_server): Extension<Arc<rpc::Server>>,
|
||||
) -> Result<Json<Option<CreateUserResponse>>> {
|
||||
let user = NewUserParams {
|
||||
github_login: params.github_login,
|
||||
github_user_id: params.github_user_id,
|
||||
invite_count: params.invite_count,
|
||||
};
|
||||
|
||||
// Creating a user via the normal signup process
|
||||
let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
|
||||
if let Some(result) = app
|
||||
.db
|
||||
.create_user_from_invite(
|
||||
&Invite {
|
||||
email_address: params.email_address,
|
||||
email_confirmation_code,
|
||||
},
|
||||
user,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
result
|
||||
} else {
|
||||
return Ok(Json(None));
|
||||
}
|
||||
}
|
||||
// Creating a user as an admin
|
||||
else if params.admin {
|
||||
app.db
|
||||
.create_user(¶ms.email_address, false, user)
|
||||
.await?
|
||||
} else {
|
||||
Err(Error::Http(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"email confirmation code is required".into(),
|
||||
))?
|
||||
};
|
||||
|
||||
if let Some(inviter_id) = result.inviting_user_id {
|
||||
rpc_server
|
||||
.invite_code_redeemed(inviter_id, result.user_id)
|
||||
.await
|
||||
.trace_err();
|
||||
}
|
||||
|
||||
let user = app
|
||||
.db
|
||||
.get_user_by_id(result.user_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
|
||||
|
||||
Ok(Json(Some(CreateUserResponse {
|
||||
user,
|
||||
metrics_id: result.metrics_id,
|
||||
signup_device_id: result.signup_device_id,
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UpdateUserParams {
|
||||
admin: Option<bool>,
|
||||
invite_count: Option<i32>,
|
||||
}
|
||||
|
||||
async fn update_user(
|
||||
Path(user_id): Path<i32>,
|
||||
Json(params): Json<UpdateUserParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Extension(rpc_server): Extension<Arc<rpc::Server>>,
|
||||
) -> Result<()> {
|
||||
let user_id = UserId(user_id);
|
||||
|
||||
if let Some(admin) = params.admin {
|
||||
app.db.set_user_is_admin(user_id, admin).await?;
|
||||
}
|
||||
|
||||
if let Some(invite_count) = params.invite_count {
|
||||
app.db
|
||||
.set_invite_count_for_user(user_id, invite_count)
|
||||
.await?;
|
||||
rpc_server.invite_count_updated(user_id).await.trace_err();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn destroy_user(
|
||||
Path(user_id): Path<i32>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<()> {
|
||||
app.db.destroy_user(UserId(user_id)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GetUsersWithNoInvites {
|
||||
invited_by_another_user: bool,
|
||||
}
|
||||
|
||||
async fn get_users_with_no_invites(
|
||||
Query(params): Query<GetUsersWithNoInvites>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<User>>> {
|
||||
Ok(Json(
|
||||
app.db
|
||||
.get_users_with_no_invites(params.invited_by_another_user)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Panic {
|
||||
version: String,
|
||||
@ -327,69 +182,3 @@ async fn create_access_token(
|
||||
encrypted_access_token,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_user_for_invite_code(
|
||||
Path(code): Path<String>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<User>> {
|
||||
Ok(Json(app.db.get_user_for_invite_code(&code).await?))
|
||||
}
|
||||
|
||||
async fn create_signup(
|
||||
Json(params): Json<NewSignup>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<()> {
|
||||
app.db.create_signup(¶ms).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_waitlist_summary(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<WaitlistSummary>> {
|
||||
Ok(Json(app.db.get_waitlist_summary().await?))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateInviteFromCodeParams {
|
||||
invite_code: String,
|
||||
email_address: String,
|
||||
device_id: Option<String>,
|
||||
#[serde(default)]
|
||||
added_to_mailing_list: bool,
|
||||
}
|
||||
|
||||
async fn create_invite_from_code(
|
||||
Json(params): Json<CreateInviteFromCodeParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Invite>> {
|
||||
Ok(Json(
|
||||
app.db
|
||||
.create_invite_from_code(
|
||||
¶ms.invite_code,
|
||||
¶ms.email_address,
|
||||
params.device_id.as_deref(),
|
||||
params.added_to_mailing_list,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetUnsentInvitesParams {
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
async fn get_unsent_invites(
|
||||
Query(params): Query<GetUnsentInvitesParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<Invite>>> {
|
||||
Ok(Json(app.db.get_unsent_invites(params.count).await?))
|
||||
}
|
||||
|
||||
async fn record_sent_invites(
|
||||
Json(params): Json<Vec<Invite>>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<()> {
|
||||
app.db.record_sent_invites(¶ms).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -7,5 +7,4 @@ pub mod contacts;
|
||||
pub mod projects;
|
||||
pub mod rooms;
|
||||
pub mod servers;
|
||||
pub mod signups;
|
||||
pub mod users;
|
||||
|
@ -1,349 +0,0 @@
|
||||
use super::*;
|
||||
use hyper::StatusCode;
|
||||
|
||||
impl Database {
|
||||
pub async fn create_invite_from_code(
|
||||
&self,
|
||||
code: &str,
|
||||
email_address: &str,
|
||||
device_id: Option<&str>,
|
||||
added_to_mailing_list: bool,
|
||||
) -> Result<Invite> {
|
||||
self.transaction(|tx| async move {
|
||||
let existing_user = user::Entity::find()
|
||||
.filter(user::Column::EmailAddress.eq(email_address))
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
|
||||
if existing_user.is_some() {
|
||||
Err(anyhow!("email address is already in use"))?;
|
||||
}
|
||||
|
||||
let inviting_user_with_invites = match user::Entity::find()
|
||||
.filter(
|
||||
user::Column::InviteCode
|
||||
.eq(code)
|
||||
.and(user::Column::InviteCount.gt(0)),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
{
|
||||
Some(inviting_user) => inviting_user,
|
||||
None => {
|
||||
return Err(Error::Http(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"unable to find an invite code with invites remaining".to_string(),
|
||||
))?
|
||||
}
|
||||
};
|
||||
user::Entity::update_many()
|
||||
.filter(
|
||||
user::Column::Id
|
||||
.eq(inviting_user_with_invites.id)
|
||||
.and(user::Column::InviteCount.gt(0)),
|
||||
)
|
||||
.col_expr(
|
||||
user::Column::InviteCount,
|
||||
Expr::col(user::Column::InviteCount).sub(1),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let signup = signup::Entity::insert(signup::ActiveModel {
|
||||
email_address: ActiveValue::set(email_address.into()),
|
||||
email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
|
||||
email_confirmation_sent: ActiveValue::set(false),
|
||||
inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)),
|
||||
platform_linux: ActiveValue::set(false),
|
||||
platform_mac: ActiveValue::set(false),
|
||||
platform_windows: ActiveValue::set(false),
|
||||
platform_unknown: ActiveValue::set(true),
|
||||
device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())),
|
||||
added_to_mailing_list: ActiveValue::set(added_to_mailing_list),
|
||||
..Default::default()
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::column(signup::Column::EmailAddress)
|
||||
.update_column(signup::Column::InvitingUserId)
|
||||
.to_owned(),
|
||||
)
|
||||
.exec_with_returning(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(Invite {
|
||||
email_address: signup.email_address,
|
||||
email_confirmation_code: signup.email_confirmation_code,
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_user_from_invite(
|
||||
&self,
|
||||
invite: &Invite,
|
||||
user: NewUserParams,
|
||||
) -> Result<Option<NewUserResult>> {
|
||||
self.transaction(|tx| async {
|
||||
let tx = tx;
|
||||
let signup = signup::Entity::find()
|
||||
.filter(
|
||||
signup::Column::EmailAddress
|
||||
.eq(invite.email_address.as_str())
|
||||
.and(
|
||||
signup::Column::EmailConfirmationCode
|
||||
.eq(invite.email_confirmation_code.as_str()),
|
||||
),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?;
|
||||
|
||||
if signup.user_id.is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let user = user::Entity::insert(user::ActiveModel {
|
||||
email_address: ActiveValue::set(Some(invite.email_address.clone())),
|
||||
github_login: ActiveValue::set(user.github_login.clone()),
|
||||
github_user_id: ActiveValue::set(Some(user.github_user_id)),
|
||||
admin: ActiveValue::set(false),
|
||||
invite_count: ActiveValue::set(user.invite_count),
|
||||
invite_code: ActiveValue::set(Some(random_invite_code())),
|
||||
metrics_id: ActiveValue::set(Uuid::new_v4()),
|
||||
..Default::default()
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::column(user::Column::GithubLogin)
|
||||
.update_columns([
|
||||
user::Column::EmailAddress,
|
||||
user::Column::GithubUserId,
|
||||
user::Column::Admin,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec_with_returning(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut signup = signup.into_active_model();
|
||||
signup.user_id = ActiveValue::set(Some(user.id));
|
||||
let signup = signup.update(&*tx).await?;
|
||||
|
||||
if let Some(inviting_user_id) = signup.inviting_user_id {
|
||||
let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id {
|
||||
(inviting_user_id, user.id, true)
|
||||
} else {
|
||||
(user.id, inviting_user_id, false)
|
||||
};
|
||||
|
||||
contact::Entity::insert(contact::ActiveModel {
|
||||
user_id_a: ActiveValue::set(user_id_a),
|
||||
user_id_b: ActiveValue::set(user_id_b),
|
||||
a_to_b: ActiveValue::set(a_to_b),
|
||||
should_notify: ActiveValue::set(true),
|
||||
accepted: ActiveValue::set(true),
|
||||
..Default::default()
|
||||
})
|
||||
.on_conflict(OnConflict::new().do_nothing().to_owned())
|
||||
.exec_without_returning(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Some(NewUserResult {
|
||||
user_id: user.id,
|
||||
metrics_id: user.metrics_id.to_string(),
|
||||
inviting_user_id: signup.inviting_user_id,
|
||||
signup_device_id: signup.device_id,
|
||||
}))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
if count > 0 {
|
||||
user::Entity::update_many()
|
||||
.filter(
|
||||
user::Column::Id
|
||||
.eq(id)
|
||||
.and(user::Column::InviteCode.is_null()),
|
||||
)
|
||||
.set(user::ActiveModel {
|
||||
invite_code: ActiveValue::set(Some(random_invite_code())),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
user::Entity::update_many()
|
||||
.filter(user::Column::Id.eq(id))
|
||||
.set(user::ActiveModel {
|
||||
invite_count: ActiveValue::set(count),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_invite_code_for_user(&self, id: UserId) -> Result<Option<(String, i32)>> {
|
||||
self.transaction(|tx| async move {
|
||||
match user::Entity::find_by_id(id).one(&*tx).await? {
|
||||
Some(user) if user.invite_code.is_some() => {
|
||||
Ok(Some((user.invite_code.unwrap(), user.invite_count)))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_user_for_invite_code(&self, code: &str) -> Result<User> {
|
||||
self.transaction(|tx| async move {
|
||||
user::Entity::find()
|
||||
.filter(user::Column::InviteCode.eq(code))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
Error::Http(
|
||||
StatusCode::NOT_FOUND,
|
||||
"that invite code does not exist".to_string(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
signup::Entity::insert(signup::ActiveModel {
|
||||
email_address: ActiveValue::set(signup.email_address.clone()),
|
||||
email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
|
||||
email_confirmation_sent: ActiveValue::set(false),
|
||||
platform_mac: ActiveValue::set(signup.platform_mac),
|
||||
platform_windows: ActiveValue::set(signup.platform_windows),
|
||||
platform_linux: ActiveValue::set(signup.platform_linux),
|
||||
platform_unknown: ActiveValue::set(false),
|
||||
editor_features: ActiveValue::set(Some(signup.editor_features.clone())),
|
||||
programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())),
|
||||
device_id: ActiveValue::set(signup.device_id.clone()),
|
||||
added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list),
|
||||
..Default::default()
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::column(signup::Column::EmailAddress)
|
||||
.update_columns([
|
||||
signup::Column::PlatformMac,
|
||||
signup::Column::PlatformWindows,
|
||||
signup::Column::PlatformLinux,
|
||||
signup::Column::EditorFeatures,
|
||||
signup::Column::ProgrammingLanguages,
|
||||
signup::Column::DeviceId,
|
||||
signup::Column::AddedToMailingList,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_signup(&self, email_address: &str) -> Result<signup::Model> {
|
||||
self.transaction(|tx| async move {
|
||||
let signup = signup::Entity::find()
|
||||
.filter(signup::Column::EmailAddress.eq(email_address))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
anyhow!("signup with email address {} doesn't exist", email_address)
|
||||
})?;
|
||||
|
||||
Ok(signup)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> {
|
||||
self.transaction(|tx| async move {
|
||||
let query = "
|
||||
SELECT
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count,
|
||||
COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count,
|
||||
COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count,
|
||||
COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM signups
|
||||
WHERE
|
||||
NOT email_confirmation_sent
|
||||
) AS unsent
|
||||
";
|
||||
Ok(
|
||||
WaitlistSummary::find_by_statement(Statement::from_sql_and_values(
|
||||
self.pool.get_database_backend(),
|
||||
query.into(),
|
||||
vec![],
|
||||
))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("invalid result"))?,
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> {
|
||||
let emails = invites
|
||||
.iter()
|
||||
.map(|s| s.email_address.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
self.transaction(|tx| async {
|
||||
let tx = tx;
|
||||
signup::Entity::update_many()
|
||||
.filter(signup::Column::EmailAddress.is_in(emails.iter().copied()))
|
||||
.set(signup::ActiveModel {
|
||||
email_confirmation_sent: ActiveValue::set(true),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_unsent_invites(&self, count: usize) -> Result<Vec<Invite>> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(signup::Entity::find()
|
||||
.select_only()
|
||||
.column(signup::Column::EmailAddress)
|
||||
.column(signup::Column::EmailConfirmationCode)
|
||||
.filter(
|
||||
signup::Column::EmailConfirmationSent.eq(false).and(
|
||||
signup::Column::PlatformMac
|
||||
.eq(true)
|
||||
.or(signup::Column::PlatformUnknown.eq(true)),
|
||||
),
|
||||
)
|
||||
.order_by_asc(signup::Column::CreatedAt)
|
||||
.limit(count as u64)
|
||||
.into_model()
|
||||
.all(&*tx)
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn random_invite_code() -> String {
|
||||
nanoid::nanoid!(16)
|
||||
}
|
||||
|
||||
fn random_email_confirmation_code() -> String {
|
||||
nanoid::nanoid!(64)
|
||||
}
|
@ -123,27 +123,6 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_users_with_no_invites(
|
||||
&self,
|
||||
invited_by_another_user: bool,
|
||||
) -> Result<Vec<User>> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(user::Entity::find()
|
||||
.filter(
|
||||
user::Column::InviteCount
|
||||
.eq(0)
|
||||
.and(if invited_by_another_user {
|
||||
user::Column::InviterId.is_not_null()
|
||||
} else {
|
||||
user::Column::InviterId.is_null()
|
||||
}),
|
||||
)
|
||||
.all(&*tx)
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryAs {
|
||||
@ -163,21 +142,6 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
user::Entity::update_many()
|
||||
.filter(user::Column::Id.eq(id))
|
||||
.set(user::ActiveModel {
|
||||
admin: ActiveValue::set(is_admin),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
user::Entity::update_many()
|
||||
|
@ -575,308 +575,6 @@ async fn test_fuzzy_search_users() {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_invite_codes() {
|
||||
let test_db = TestDb::postgres(build_background_executor());
|
||||
let db = test_db.db();
|
||||
|
||||
let NewUserResult { user_id: user1, .. } = db
|
||||
.create_user(
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user1".into(),
|
||||
github_user_id: 0,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Initially, user 1 has no invite code
|
||||
assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None);
|
||||
|
||||
// Setting invite count to 0 when no code is assigned does not assign a new code
|
||||
db.set_invite_count_for_user(user1, 0).await.unwrap();
|
||||
assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none());
|
||||
|
||||
// User 1 creates an invite code that can be used twice.
|
||||
db.set_invite_count_for_user(user1, 2).await.unwrap();
|
||||
let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
|
||||
assert_eq!(invite_count, 2);
|
||||
|
||||
// User 2 redeems the invite code and becomes a contact of user 1.
|
||||
let user2_invite = db
|
||||
.create_invite_from_code(
|
||||
&invite_code,
|
||||
"user2@example.com",
|
||||
Some("user-2-device-id"),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let NewUserResult {
|
||||
user_id: user2,
|
||||
inviting_user_id,
|
||||
signup_device_id,
|
||||
metrics_id,
|
||||
} = db
|
||||
.create_user_from_invite(
|
||||
&user2_invite,
|
||||
NewUserParams {
|
||||
github_login: "user2".into(),
|
||||
github_user_id: 2,
|
||||
invite_count: 7,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
|
||||
assert_eq!(invite_count, 1);
|
||||
assert_eq!(inviting_user_id, Some(user1));
|
||||
assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
|
||||
assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
|
||||
assert_eq!(
|
||||
db.get_contacts(user1).await.unwrap(),
|
||||
[Contact::Accepted {
|
||||
user_id: user2,
|
||||
should_notify: true,
|
||||
busy: false,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user2).await.unwrap(),
|
||||
[Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false,
|
||||
busy: false,
|
||||
}]
|
||||
);
|
||||
assert!(db.has_contact(user1, user2).await.unwrap());
|
||||
assert!(db.has_contact(user2, user1).await.unwrap());
|
||||
assert_eq!(
|
||||
db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
|
||||
7
|
||||
);
|
||||
|
||||
// User 3 redeems the invite code and becomes a contact of user 1.
|
||||
let user3_invite = db
|
||||
.create_invite_from_code(&invite_code, "user3@example.com", None, true)
|
||||
.await
|
||||
.unwrap();
|
||||
let NewUserResult {
|
||||
user_id: user3,
|
||||
inviting_user_id,
|
||||
signup_device_id,
|
||||
..
|
||||
} = db
|
||||
.create_user_from_invite(
|
||||
&user3_invite,
|
||||
NewUserParams {
|
||||
github_login: "user-3".into(),
|
||||
github_user_id: 3,
|
||||
invite_count: 3,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
|
||||
assert_eq!(invite_count, 0);
|
||||
assert_eq!(inviting_user_id, Some(user1));
|
||||
assert!(signup_device_id.is_none());
|
||||
assert_eq!(
|
||||
db.get_contacts(user1).await.unwrap(),
|
||||
[
|
||||
Contact::Accepted {
|
||||
user_id: user2,
|
||||
should_notify: true,
|
||||
busy: false,
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user3,
|
||||
should_notify: true,
|
||||
busy: false,
|
||||
}
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user3).await.unwrap(),
|
||||
[Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false,
|
||||
busy: false,
|
||||
}]
|
||||
);
|
||||
assert!(db.has_contact(user1, user3).await.unwrap());
|
||||
assert!(db.has_contact(user3, user1).await.unwrap());
|
||||
assert_eq!(
|
||||
db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
|
||||
3
|
||||
);
|
||||
|
||||
// Trying to reedem the code for the third time results in an error.
|
||||
db.create_invite_from_code(
|
||||
&invite_code,
|
||||
"user4@example.com",
|
||||
Some("user-4-device-id"),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// Invite count can be updated after the code has been created.
|
||||
db.set_invite_count_for_user(user1, 2).await.unwrap();
|
||||
let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
|
||||
assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
|
||||
assert_eq!(invite_count, 2);
|
||||
|
||||
// User 4 can now redeem the invite code and becomes a contact of user 1.
|
||||
let user4_invite = db
|
||||
.create_invite_from_code(
|
||||
&invite_code,
|
||||
"user4@example.com",
|
||||
Some("user-4-device-id"),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let user4 = db
|
||||
.create_user_from_invite(
|
||||
&user4_invite,
|
||||
NewUserParams {
|
||||
github_login: "user-4".into(),
|
||||
github_user_id: 4,
|
||||
invite_count: 5,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
|
||||
assert_eq!(invite_count, 1);
|
||||
assert_eq!(
|
||||
db.get_contacts(user1).await.unwrap(),
|
||||
[
|
||||
Contact::Accepted {
|
||||
user_id: user2,
|
||||
should_notify: true,
|
||||
busy: false,
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user3,
|
||||
should_notify: true,
|
||||
busy: false,
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user4,
|
||||
should_notify: true,
|
||||
busy: false,
|
||||
}
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user4).await.unwrap(),
|
||||
[Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: false,
|
||||
busy: false,
|
||||
}]
|
||||
);
|
||||
assert!(db.has_contact(user1, user4).await.unwrap());
|
||||
assert!(db.has_contact(user4, user1).await.unwrap());
|
||||
assert_eq!(
|
||||
db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
|
||||
5
|
||||
);
|
||||
|
||||
// An existing user cannot redeem invite codes.
|
||||
db.create_invite_from_code(
|
||||
&invite_code,
|
||||
"user2@example.com",
|
||||
Some("user-2-device-id"),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
|
||||
assert_eq!(invite_count, 1);
|
||||
|
||||
// A newer user can invite an existing one via a different email address
|
||||
// than the one they used to sign up.
|
||||
let user5 = db
|
||||
.create_user(
|
||||
"user5@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user5".into(),
|
||||
github_user_id: 5,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
db.set_invite_count_for_user(user5, 5).await.unwrap();
|
||||
let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap();
|
||||
let user5_invite_to_user1 = db
|
||||
.create_invite_from_code(&user5_invite_code, "user1@different.com", None, true)
|
||||
.await
|
||||
.unwrap();
|
||||
let user1_2 = db
|
||||
.create_user_from_invite(
|
||||
&user5_invite_to_user1,
|
||||
NewUserParams {
|
||||
github_login: "user1".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 5,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.user_id;
|
||||
assert_eq!(user1_2, user1);
|
||||
assert_eq!(
|
||||
db.get_contacts(user1).await.unwrap(),
|
||||
[
|
||||
Contact::Accepted {
|
||||
user_id: user2,
|
||||
should_notify: true,
|
||||
busy: false,
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user3,
|
||||
should_notify: true,
|
||||
busy: false,
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user4,
|
||||
should_notify: true,
|
||||
busy: false,
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user5,
|
||||
should_notify: false,
|
||||
busy: false,
|
||||
}
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user5).await.unwrap(),
|
||||
[Contact::Accepted {
|
||||
user_id: user1,
|
||||
should_notify: true,
|
||||
busy: false,
|
||||
}]
|
||||
);
|
||||
assert!(db.has_contact(user1, user5).await.unwrap());
|
||||
assert!(db.has_contact(user5, user1).await.unwrap());
|
||||
}
|
||||
|
||||
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
|
||||
|
||||
async fn test_channels(db: &Arc<Database>) {
|
||||
@ -1329,245 +1027,6 @@ async fn test_channel_renames(db: &Arc<Database>) {
|
||||
assert!(bad_name_rename.is_err())
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiple_signup_overwrite() {
|
||||
let test_db = TestDb::postgres(build_background_executor());
|
||||
let db = test_db.db();
|
||||
|
||||
let email_address = "user_1@example.com".to_string();
|
||||
|
||||
let initial_signup_created_at_milliseconds = 0;
|
||||
|
||||
let initial_signup = NewSignup {
|
||||
email_address: email_address.clone(),
|
||||
platform_mac: false,
|
||||
platform_linux: true,
|
||||
platform_windows: false,
|
||||
editor_features: vec!["speed".into()],
|
||||
programming_languages: vec!["rust".into(), "c".into()],
|
||||
device_id: Some(format!("device_id")),
|
||||
added_to_mailing_list: false,
|
||||
created_at: Some(
|
||||
DateTime::from_timestamp_millis(initial_signup_created_at_milliseconds).unwrap(),
|
||||
),
|
||||
};
|
||||
|
||||
db.create_signup(&initial_signup).await.unwrap();
|
||||
|
||||
let initial_signup_from_db = db.get_signup(&email_address).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
initial_signup_from_db.clone(),
|
||||
signup::Model {
|
||||
email_address: initial_signup.email_address,
|
||||
platform_mac: initial_signup.platform_mac,
|
||||
platform_linux: initial_signup.platform_linux,
|
||||
platform_windows: initial_signup.platform_windows,
|
||||
editor_features: Some(initial_signup.editor_features),
|
||||
programming_languages: Some(initial_signup.programming_languages),
|
||||
added_to_mailing_list: initial_signup.added_to_mailing_list,
|
||||
..initial_signup_from_db
|
||||
}
|
||||
);
|
||||
|
||||
let subsequent_signup = NewSignup {
|
||||
email_address: email_address.clone(),
|
||||
platform_mac: true,
|
||||
platform_linux: false,
|
||||
platform_windows: true,
|
||||
editor_features: vec!["git integration".into(), "clean design".into()],
|
||||
programming_languages: vec!["d".into(), "elm".into()],
|
||||
device_id: Some(format!("different_device_id")),
|
||||
added_to_mailing_list: true,
|
||||
// subsequent signup happens next day
|
||||
created_at: Some(
|
||||
DateTime::from_timestamp_millis(
|
||||
initial_signup_created_at_milliseconds + (1000 * 60 * 60 * 24),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
};
|
||||
|
||||
db.create_signup(&subsequent_signup).await.unwrap();
|
||||
|
||||
let subsequent_signup_from_db = db.get_signup(&email_address).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
subsequent_signup_from_db.clone(),
|
||||
signup::Model {
|
||||
platform_mac: subsequent_signup.platform_mac,
|
||||
platform_linux: subsequent_signup.platform_linux,
|
||||
platform_windows: subsequent_signup.platform_windows,
|
||||
editor_features: Some(subsequent_signup.editor_features),
|
||||
programming_languages: Some(subsequent_signup.programming_languages),
|
||||
device_id: subsequent_signup.device_id,
|
||||
added_to_mailing_list: subsequent_signup.added_to_mailing_list,
|
||||
// shouldn't overwrite their creation Datetime - user shouldn't lose their spot in line
|
||||
created_at: initial_signup_from_db.created_at,
|
||||
..subsequent_signup_from_db
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_signups() {
|
||||
let test_db = TestDb::postgres(build_background_executor());
|
||||
let db = test_db.db();
|
||||
|
||||
let usernames = (0..8).map(|i| format!("person-{i}")).collect::<Vec<_>>();
|
||||
|
||||
let all_signups = usernames
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, username)| NewSignup {
|
||||
email_address: format!("{username}@example.com"),
|
||||
platform_mac: true,
|
||||
platform_linux: i % 2 == 0,
|
||||
platform_windows: i % 4 == 0,
|
||||
editor_features: vec!["speed".into()],
|
||||
programming_languages: vec!["rust".into(), "c".into()],
|
||||
device_id: Some(format!("device_id_{i}")),
|
||||
added_to_mailing_list: i != 0, // One user failed to subscribe
|
||||
created_at: Some(DateTime::from_timestamp_millis(i as i64).unwrap()), // Signups are consecutive
|
||||
})
|
||||
.collect::<Vec<NewSignup>>();
|
||||
|
||||
// people sign up on the waitlist
|
||||
for signup in &all_signups {
|
||||
// users can sign up multiple times without issues
|
||||
for _ in 0..2 {
|
||||
db.create_signup(&signup).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
db.get_waitlist_summary().await.unwrap(),
|
||||
WaitlistSummary {
|
||||
count: 8,
|
||||
mac_count: 8,
|
||||
linux_count: 4,
|
||||
windows_count: 2,
|
||||
unknown_count: 0,
|
||||
}
|
||||
);
|
||||
|
||||
// retrieve the next batch of signup emails to send
|
||||
let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
|
||||
let addresses = signups_batch1
|
||||
.iter()
|
||||
.map(|s| &s.email_address)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
addresses,
|
||||
&[
|
||||
all_signups[0].email_address.as_str(),
|
||||
all_signups[1].email_address.as_str(),
|
||||
all_signups[2].email_address.as_str()
|
||||
]
|
||||
);
|
||||
assert_ne!(
|
||||
signups_batch1[0].email_confirmation_code,
|
||||
signups_batch1[1].email_confirmation_code
|
||||
);
|
||||
|
||||
// the waitlist isn't updated until we record that the emails
|
||||
// were successfully sent.
|
||||
let signups_batch = db.get_unsent_invites(3).await.unwrap();
|
||||
assert_eq!(signups_batch, signups_batch1);
|
||||
|
||||
// once the emails go out, we can retrieve the next batch
|
||||
// of signups.
|
||||
db.record_sent_invites(&signups_batch1).await.unwrap();
|
||||
let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
|
||||
let addresses = signups_batch2
|
||||
.iter()
|
||||
.map(|s| &s.email_address)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
addresses,
|
||||
&[
|
||||
all_signups[3].email_address.as_str(),
|
||||
all_signups[4].email_address.as_str(),
|
||||
all_signups[5].email_address.as_str()
|
||||
]
|
||||
);
|
||||
|
||||
// the sent invites are excluded from the summary.
|
||||
assert_eq!(
|
||||
db.get_waitlist_summary().await.unwrap(),
|
||||
WaitlistSummary {
|
||||
count: 5,
|
||||
mac_count: 5,
|
||||
linux_count: 2,
|
||||
windows_count: 1,
|
||||
unknown_count: 0,
|
||||
}
|
||||
);
|
||||
|
||||
// user completes the signup process by providing their
|
||||
// github account.
|
||||
let NewUserResult {
|
||||
user_id,
|
||||
inviting_user_id,
|
||||
signup_device_id,
|
||||
..
|
||||
} = db
|
||||
.create_user_from_invite(
|
||||
&Invite {
|
||||
..signups_batch1[0].clone()
|
||||
},
|
||||
NewUserParams {
|
||||
github_login: usernames[0].clone(),
|
||||
github_user_id: 0,
|
||||
invite_count: 5,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
|
||||
assert!(inviting_user_id.is_none());
|
||||
assert_eq!(user.github_login, usernames[0]);
|
||||
assert_eq!(
|
||||
user.email_address,
|
||||
Some(all_signups[0].email_address.clone())
|
||||
);
|
||||
assert_eq!(user.invite_count, 5);
|
||||
assert_eq!(signup_device_id.unwrap(), "device_id_0");
|
||||
|
||||
// cannot redeem the same signup again.
|
||||
assert!(db
|
||||
.create_user_from_invite(
|
||||
&Invite {
|
||||
email_address: signups_batch1[0].email_address.clone(),
|
||||
email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
|
||||
},
|
||||
NewUserParams {
|
||||
github_login: "some-other-github_account".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 5,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// cannot redeem a signup with the wrong confirmation code.
|
||||
db.create_user_from_invite(
|
||||
&Invite {
|
||||
email_address: signups_batch1[1].email_address.clone(),
|
||||
email_confirmation_code: "the-wrong-code".to_string(),
|
||||
},
|
||||
NewUserParams {
|
||||
github_login: usernames[1].clone(),
|
||||
github_user_id: 2,
|
||||
invite_count: 5,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
fn build_background_executor() -> Arc<Background> {
|
||||
Deterministic::new(0).build_background()
|
||||
}
|
||||
|
@ -553,9 +553,8 @@ impl Server {
|
||||
this.app_state.db.set_user_connected_once(user_id, true).await?;
|
||||
}
|
||||
|
||||
let (contacts, invite_code, channels_for_user, channel_invites) = future::try_join4(
|
||||
let (contacts, channels_for_user, channel_invites) = future::try_join3(
|
||||
this.app_state.db.get_contacts(user_id),
|
||||
this.app_state.db.get_invite_code_for_user(user_id),
|
||||
this.app_state.db.get_channels_for_user(user_id),
|
||||
this.app_state.db.get_channel_invites_for_user(user_id)
|
||||
).await?;
|
||||
@ -568,13 +567,6 @@ impl Server {
|
||||
channels_for_user,
|
||||
channel_invites
|
||||
))?;
|
||||
|
||||
if let Some((code, count)) = invite_code {
|
||||
this.peer.send(connection_id, proto::UpdateInviteInfo {
|
||||
url: format!("{}{}", this.app_state.config.invite_link_prefix, code),
|
||||
count: count as u32,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(incoming_call) = this.app_state.db.incoming_call_for_user(user_id).await? {
|
||||
|
@ -3146,6 +3146,7 @@ async fn test_local_settings(
|
||||
)
|
||||
.await;
|
||||
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
|
||||
deterministic.run_until_parked();
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
|
@ -32,7 +32,8 @@ impl DiagnosticIndicator {
|
||||
this.in_progress_checks.insert(*language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
|
||||
| project::Event::LanguageServerRemoved(language_server_id) => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
|
@ -5,11 +5,11 @@ mod tab_map;
|
||||
mod wrap_map;
|
||||
|
||||
use crate::{
|
||||
link_go_to_definition::{DocumentRange, InlayRange},
|
||||
Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer,
|
||||
MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
pub use block_map::{BlockMap, BlockPoint};
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use fold_map::FoldMap;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
@ -43,7 +43,8 @@ pub trait ToDisplayPoint {
|
||||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
|
||||
}
|
||||
|
||||
type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<DocumentRange>)>>;
|
||||
type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
|
||||
type InlayHighlights = BTreeMap<TypeId, HashMap<InlayId, (HighlightStyle, InlayHighlight)>>;
|
||||
|
||||
pub struct DisplayMap {
|
||||
buffer: ModelHandle<MultiBuffer>,
|
||||
@ -54,6 +55,7 @@ pub struct DisplayMap {
|
||||
wrap_map: ModelHandle<WrapMap>,
|
||||
block_map: BlockMap,
|
||||
text_highlights: TextHighlights,
|
||||
inlay_highlights: InlayHighlights,
|
||||
pub clip_at_line_ends: bool,
|
||||
}
|
||||
|
||||
@ -89,6 +91,7 @@ impl DisplayMap {
|
||||
wrap_map,
|
||||
block_map,
|
||||
text_highlights: Default::default(),
|
||||
inlay_highlights: Default::default(),
|
||||
clip_at_line_ends: false,
|
||||
}
|
||||
}
|
||||
@ -113,6 +116,7 @@ impl DisplayMap {
|
||||
wrap_snapshot,
|
||||
block_snapshot,
|
||||
text_highlights: self.text_highlights.clone(),
|
||||
inlay_highlights: self.inlay_highlights.clone(),
|
||||
clip_at_line_ends: self.clip_at_line_ends,
|
||||
}
|
||||
}
|
||||
@ -215,37 +219,32 @@ impl DisplayMap {
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
style: HighlightStyle,
|
||||
) {
|
||||
self.text_highlights.insert(
|
||||
Some(type_id),
|
||||
Arc::new((style, ranges.into_iter().map(DocumentRange::Text).collect())),
|
||||
);
|
||||
self.text_highlights
|
||||
.insert(Some(type_id), Arc::new((style, ranges)));
|
||||
}
|
||||
|
||||
pub fn highlight_inlays(
|
||||
&mut self,
|
||||
type_id: TypeId,
|
||||
ranges: Vec<InlayRange>,
|
||||
highlights: Vec<InlayHighlight>,
|
||||
style: HighlightStyle,
|
||||
) {
|
||||
self.text_highlights.insert(
|
||||
Some(type_id),
|
||||
Arc::new((
|
||||
style,
|
||||
ranges.into_iter().map(DocumentRange::Inlay).collect(),
|
||||
)),
|
||||
);
|
||||
for highlight in highlights {
|
||||
self.inlay_highlights
|
||||
.entry(type_id)
|
||||
.or_default()
|
||||
.insert(highlight.inlay, (style, highlight));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[DocumentRange])> {
|
||||
pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
|
||||
let highlights = self.text_highlights.get(&Some(type_id))?;
|
||||
Some((highlights.0, &highlights.1))
|
||||
}
|
||||
|
||||
pub fn clear_text_highlights(
|
||||
&mut self,
|
||||
type_id: TypeId,
|
||||
) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
|
||||
self.text_highlights.remove(&Some(type_id))
|
||||
pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
|
||||
let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some();
|
||||
cleared |= self.inlay_highlights.remove(&type_id).is_none();
|
||||
cleared
|
||||
}
|
||||
|
||||
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
|
||||
@ -309,6 +308,14 @@ impl DisplayMap {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Highlights<'a> {
|
||||
pub text_highlights: Option<&'a TextHighlights>,
|
||||
pub inlay_highlights: Option<&'a InlayHighlights>,
|
||||
pub inlay_highlight_style: Option<HighlightStyle>,
|
||||
pub suggestion_highlight_style: Option<HighlightStyle>,
|
||||
}
|
||||
|
||||
pub struct DisplaySnapshot {
|
||||
pub buffer_snapshot: MultiBufferSnapshot,
|
||||
pub fold_snapshot: fold_map::FoldSnapshot,
|
||||
@ -317,6 +324,7 @@ pub struct DisplaySnapshot {
|
||||
wrap_snapshot: wrap_map::WrapSnapshot,
|
||||
block_snapshot: block_map::BlockSnapshot,
|
||||
text_highlights: TextHighlights,
|
||||
inlay_highlights: InlayHighlights,
|
||||
clip_at_line_ends: bool,
|
||||
}
|
||||
|
||||
@ -422,15 +430,6 @@ impl DisplaySnapshot {
|
||||
.to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
|
||||
}
|
||||
|
||||
pub fn inlay_offset_to_display_point(&self, offset: InlayOffset, bias: Bias) -> DisplayPoint {
|
||||
let inlay_point = self.inlay_snapshot.to_point(offset);
|
||||
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
|
||||
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
|
||||
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
|
||||
let block_point = self.block_snapshot.to_block_point(wrap_point);
|
||||
DisplayPoint(block_point)
|
||||
}
|
||||
|
||||
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
|
||||
@ -463,9 +462,7 @@ impl DisplaySnapshot {
|
||||
.chunks(
|
||||
display_row..self.max_point().row() + 1,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|h| h.text)
|
||||
}
|
||||
@ -474,7 +471,7 @@ impl DisplaySnapshot {
|
||||
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||
(0..=display_row).into_iter().rev().flat_map(|row| {
|
||||
self.block_snapshot
|
||||
.chunks(row..row + 1, false, None, None, None)
|
||||
.chunks(row..row + 1, false, Highlights::default())
|
||||
.map(|h| h.text)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
@ -482,19 +479,22 @@ impl DisplaySnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn chunks(
|
||||
&self,
|
||||
pub fn chunks<'a>(
|
||||
&'a self,
|
||||
display_rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
inlay_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
) -> DisplayChunks<'_> {
|
||||
self.block_snapshot.chunks(
|
||||
display_rows,
|
||||
language_aware,
|
||||
Some(&self.text_highlights),
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
Highlights {
|
||||
text_highlights: Some(&self.text_highlights),
|
||||
inlay_highlights: Some(&self.inlay_highlights),
|
||||
inlay_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -752,12 +752,20 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn highlight_ranges<Tag: ?Sized + 'static>(
|
||||
pub fn text_highlight_ranges<Tag: ?Sized + 'static>(
|
||||
&self,
|
||||
) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
|
||||
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
|
||||
let type_id = TypeId::of::<Tag>();
|
||||
self.text_highlights.get(&Some(type_id)).cloned()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn inlay_highlights<Tag: ?Sized + 'static>(
|
||||
&self,
|
||||
) -> Option<&HashMap<InlayId, (HighlightStyle, InlayHighlight)>> {
|
||||
let type_id = TypeId::of::<Tag>();
|
||||
self.inlay_highlights.get(&type_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
|
@ -1,10 +1,10 @@
|
||||
use super::{
|
||||
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
|
||||
TextHighlights,
|
||||
Highlights,
|
||||
};
|
||||
use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
use gpui::{fonts::HighlightStyle, AnyElement, ViewContext};
|
||||
use gpui::{AnyElement, ViewContext};
|
||||
use language::{BufferSnapshot, Chunk, Patch, Point};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
@ -576,9 +576,7 @@ impl BlockSnapshot {
|
||||
self.chunks(
|
||||
0..self.transforms.summary().output_rows,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect()
|
||||
@ -588,9 +586,7 @@ impl BlockSnapshot {
|
||||
&'a self,
|
||||
rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
highlights: Highlights<'a>,
|
||||
) -> BlockChunks<'a> {
|
||||
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
|
||||
@ -622,9 +618,7 @@ impl BlockSnapshot {
|
||||
input_chunks: self.wrap_snapshot.chunks(
|
||||
input_start..input_end,
|
||||
language_aware,
|
||||
text_highlights,
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
highlights,
|
||||
),
|
||||
input_chunk: Default::default(),
|
||||
transforms: cursor,
|
||||
@ -1501,9 +1495,7 @@ mod tests {
|
||||
.chunks(
|
||||
start_row as u32..blocks_snapshot.max_point().row + 1,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect::<String>();
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
|
||||
TextHighlights,
|
||||
Highlights,
|
||||
};
|
||||
use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
|
||||
use gpui::{color::Color, fonts::HighlightStyle};
|
||||
@ -475,7 +475,7 @@ pub struct FoldSnapshot {
|
||||
impl FoldSnapshot {
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks(FoldOffset(0)..self.len(), false, None, None, None)
|
||||
self.chunks(FoldOffset(0)..self.len(), false, Highlights::default())
|
||||
.map(|c| c.text)
|
||||
.collect()
|
||||
}
|
||||
@ -651,9 +651,7 @@ impl FoldSnapshot {
|
||||
&'a self,
|
||||
range: Range<FoldOffset>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
highlights: Highlights<'a>,
|
||||
) -> FoldChunks<'a> {
|
||||
let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
|
||||
|
||||
@ -674,9 +672,7 @@ impl FoldSnapshot {
|
||||
inlay_chunks: self.inlay_snapshot.chunks(
|
||||
inlay_start..inlay_end,
|
||||
language_aware,
|
||||
text_highlights,
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
highlights,
|
||||
),
|
||||
inlay_chunk: None,
|
||||
inlay_offset: inlay_start,
|
||||
@ -687,8 +683,12 @@ impl FoldSnapshot {
|
||||
}
|
||||
|
||||
pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
|
||||
self.chunks(start.to_offset(self)..self.len(), false, None, None, None)
|
||||
.flat_map(|chunk| chunk.text.chars())
|
||||
self.chunks(
|
||||
start.to_offset(self)..self.len(),
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
.flat_map(|chunk| chunk.text.chars())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -1496,7 +1496,7 @@ mod tests {
|
||||
let text = &expected_text[start.0..end.0];
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.chunks(start..end, false, None, None, None)
|
||||
.chunks(start..end, false, Highlights::default())
|
||||
.map(|c| c.text)
|
||||
.collect::<String>(),
|
||||
text,
|
||||
|
@ -1,5 +1,4 @@
|
||||
use crate::{
|
||||
link_go_to_definition::DocumentRange,
|
||||
multi_buffer::{MultiBufferChunks, MultiBufferRows},
|
||||
Anchor, InlayId, MultiBufferSnapshot, ToOffset,
|
||||
};
|
||||
@ -11,12 +10,13 @@ use std::{
|
||||
cmp,
|
||||
iter::Peekable,
|
||||
ops::{Add, AddAssign, Range, Sub, SubAssign},
|
||||
sync::Arc,
|
||||
vec,
|
||||
};
|
||||
use sum_tree::{Bias, Cursor, SumTree};
|
||||
use sum_tree::{Bias, Cursor, SumTree, TreeMap};
|
||||
use text::{Patch, Rope};
|
||||
|
||||
use super::TextHighlights;
|
||||
use super::Highlights;
|
||||
|
||||
pub struct InlayMap {
|
||||
snapshot: InlaySnapshot,
|
||||
@ -214,10 +214,11 @@ pub struct InlayChunks<'a> {
|
||||
inlay_chunk: Option<&'a str>,
|
||||
output_offset: InlayOffset,
|
||||
max_output_offset: InlayOffset,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
inlay_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
|
||||
active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
|
||||
highlights: Highlights<'a>,
|
||||
snapshot: &'a InlaySnapshot,
|
||||
}
|
||||
|
||||
@ -293,8 +294,41 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
prefix
|
||||
}
|
||||
Transform::Inlay(inlay) => {
|
||||
let mut inlay_style_and_highlight = None;
|
||||
if let Some(inlay_highlights) = self.highlights.inlay_highlights {
|
||||
for (_, inlay_id_to_data) in inlay_highlights.iter() {
|
||||
let style_and_highlight = inlay_id_to_data.get(&inlay.id);
|
||||
if style_and_highlight.is_some() {
|
||||
inlay_style_and_highlight = style_and_highlight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut highlight_style = match inlay.id {
|
||||
InlayId::Suggestion(_) => self.suggestion_highlight_style,
|
||||
InlayId::Hint(_) => self.inlay_highlight_style,
|
||||
};
|
||||
let next_inlay_highlight_endpoint;
|
||||
let offset_in_inlay = self.output_offset - self.transforms.start().0;
|
||||
if let Some((style, highlight)) = inlay_style_and_highlight {
|
||||
let range = &highlight.range;
|
||||
if offset_in_inlay.0 < range.start {
|
||||
next_inlay_highlight_endpoint = range.start - offset_in_inlay.0;
|
||||
} else if offset_in_inlay.0 >= range.end {
|
||||
next_inlay_highlight_endpoint = usize::MAX;
|
||||
} else {
|
||||
next_inlay_highlight_endpoint = range.end - offset_in_inlay.0;
|
||||
highlight_style
|
||||
.get_or_insert_with(|| Default::default())
|
||||
.highlight(style.clone());
|
||||
}
|
||||
} else {
|
||||
next_inlay_highlight_endpoint = usize::MAX;
|
||||
}
|
||||
|
||||
let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
|
||||
let start = self.output_offset - self.transforms.start().0;
|
||||
let start = offset_in_inlay;
|
||||
let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0)
|
||||
- self.transforms.start().0;
|
||||
inlay.text.chunks_in_range(start.0..end.0)
|
||||
@ -302,21 +336,15 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
let inlay_chunk = self
|
||||
.inlay_chunk
|
||||
.get_or_insert_with(|| inlay_chunks.next().unwrap());
|
||||
let (chunk, remainder) = inlay_chunk.split_at(
|
||||
inlay_chunk
|
||||
.len()
|
||||
.min(next_highlight_endpoint.0 - self.output_offset.0),
|
||||
);
|
||||
let (chunk, remainder) =
|
||||
inlay_chunk.split_at(inlay_chunk.len().min(next_inlay_highlight_endpoint));
|
||||
*inlay_chunk = remainder;
|
||||
if inlay_chunk.is_empty() {
|
||||
self.inlay_chunk = None;
|
||||
}
|
||||
|
||||
self.output_offset.0 += chunk.len();
|
||||
let mut highlight_style = match inlay.id {
|
||||
InlayId::Suggestion(_) => self.suggestion_highlight_style,
|
||||
InlayId::Hint(_) => self.hint_highlight_style,
|
||||
};
|
||||
|
||||
if !self.active_highlights.is_empty() {
|
||||
for active_highlight in self.active_highlights.values() {
|
||||
highlight_style
|
||||
@ -625,18 +653,20 @@ impl InlayMap {
|
||||
.filter(|ch| *ch != '\r')
|
||||
.take(len)
|
||||
.collect::<String>();
|
||||
log::info!(
|
||||
"creating inlay at buffer offset {} with bias {:?} and text {:?}",
|
||||
position,
|
||||
bias,
|
||||
text
|
||||
);
|
||||
|
||||
let inlay_id = if i % 2 == 0 {
|
||||
InlayId::Hint(post_inc(next_inlay_id))
|
||||
} else {
|
||||
InlayId::Suggestion(post_inc(next_inlay_id))
|
||||
};
|
||||
log::info!(
|
||||
"creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}",
|
||||
inlay_id,
|
||||
position,
|
||||
bias,
|
||||
text
|
||||
);
|
||||
|
||||
to_insert.push(Inlay {
|
||||
id: inlay_id,
|
||||
position: snapshot.buffer.anchor_at(position, bias),
|
||||
@ -992,77 +1022,24 @@ impl InlaySnapshot {
|
||||
&'a self,
|
||||
range: Range<InlayOffset>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
highlights: Highlights<'a>,
|
||||
) -> InlayChunks<'a> {
|
||||
let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
|
||||
let mut highlight_endpoints = Vec::new();
|
||||
if let Some(text_highlights) = text_highlights {
|
||||
if let Some(text_highlights) = highlights.text_highlights {
|
||||
if !text_highlights.is_empty() {
|
||||
while cursor.start().0 < range.end {
|
||||
let transform_start = self.buffer.anchor_after(
|
||||
self.to_buffer_offset(cmp::max(range.start, cursor.start().0)),
|
||||
);
|
||||
let transform_start =
|
||||
self.to_inlay_offset(transform_start.to_offset(&self.buffer));
|
||||
|
||||
let transform_end = {
|
||||
let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
|
||||
self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
|
||||
cursor.end(&()).0,
|
||||
cursor.start().0 + overshoot,
|
||||
)))
|
||||
};
|
||||
let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer));
|
||||
|
||||
for (tag, text_highlights) in text_highlights.iter() {
|
||||
let style = text_highlights.0;
|
||||
let ranges = &text_highlights.1;
|
||||
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = self
|
||||
.document_to_inlay_range(probe)
|
||||
.end
|
||||
.cmp(&transform_start);
|
||||
if cmp.is_gt() {
|
||||
cmp::Ordering::Greater
|
||||
} else {
|
||||
cmp::Ordering::Less
|
||||
}
|
||||
}) {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
for range in &ranges[start_ix..] {
|
||||
let range = self.document_to_inlay_range(range);
|
||||
if range.start.cmp(&transform_end).is_ge() {
|
||||
break;
|
||||
}
|
||||
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: range.start,
|
||||
is_start: true,
|
||||
tag: *tag,
|
||||
style,
|
||||
});
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: range.end,
|
||||
is_start: false,
|
||||
tag: *tag,
|
||||
style,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cursor.next(&());
|
||||
}
|
||||
highlight_endpoints.sort();
|
||||
self.apply_text_highlights(
|
||||
&mut cursor,
|
||||
&range,
|
||||
text_highlights,
|
||||
&mut highlight_endpoints,
|
||||
);
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
}
|
||||
}
|
||||
|
||||
highlight_endpoints.sort();
|
||||
let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
|
||||
let buffer_chunks = self.buffer.chunks(buffer_range, language_aware);
|
||||
|
||||
@ -1074,29 +1051,76 @@ impl InlaySnapshot {
|
||||
buffer_chunk: None,
|
||||
output_offset: range.start,
|
||||
max_output_offset: range.end,
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
inlay_highlight_style: highlights.inlay_highlight_style,
|
||||
suggestion_highlight_style: highlights.suggestion_highlight_style,
|
||||
highlight_endpoints: highlight_endpoints.into_iter().peekable(),
|
||||
active_highlights: Default::default(),
|
||||
highlights,
|
||||
snapshot: self,
|
||||
}
|
||||
}
|
||||
|
||||
fn document_to_inlay_range(&self, range: &DocumentRange) -> Range<InlayOffset> {
|
||||
match range {
|
||||
DocumentRange::Text(text_range) => {
|
||||
self.to_inlay_offset(text_range.start.to_offset(&self.buffer))
|
||||
..self.to_inlay_offset(text_range.end.to_offset(&self.buffer))
|
||||
}
|
||||
DocumentRange::Inlay(inlay_range) => {
|
||||
inlay_range.highlight_start..inlay_range.highlight_end
|
||||
fn apply_text_highlights(
|
||||
&self,
|
||||
cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>,
|
||||
range: &Range<InlayOffset>,
|
||||
text_highlights: &TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
|
||||
highlight_endpoints: &mut Vec<HighlightEndpoint>,
|
||||
) {
|
||||
while cursor.start().0 < range.end {
|
||||
let transform_start = self
|
||||
.buffer
|
||||
.anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0)));
|
||||
let transform_end =
|
||||
{
|
||||
let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
|
||||
self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
|
||||
cursor.end(&()).0,
|
||||
cursor.start().0 + overshoot,
|
||||
)))
|
||||
};
|
||||
|
||||
for (tag, text_highlights) in text_highlights.iter() {
|
||||
let style = text_highlights.0;
|
||||
let ranges = &text_highlights.1;
|
||||
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = probe.end.cmp(&transform_start, &self.buffer);
|
||||
if cmp.is_gt() {
|
||||
cmp::Ordering::Greater
|
||||
} else {
|
||||
cmp::Ordering::Less
|
||||
}
|
||||
}) {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
for range in &ranges[start_ix..] {
|
||||
if range.start.cmp(&transform_end, &self.buffer).is_ge() {
|
||||
break;
|
||||
}
|
||||
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)),
|
||||
is_start: true,
|
||||
tag: *tag,
|
||||
style,
|
||||
});
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
|
||||
is_start: false,
|
||||
tag: *tag,
|
||||
style,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cursor.next(&());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks(Default::default()..self.len(), false, None, None, None)
|
||||
self.chunks(Default::default()..self.len(), false, Highlights::default())
|
||||
.map(|chunk| chunk.text)
|
||||
.collect()
|
||||
}
|
||||
@ -1144,7 +1168,11 @@ fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{link_go_to_definition::InlayRange, InlayId, MultiBuffer};
|
||||
use crate::{
|
||||
display_map::{InlayHighlights, TextHighlights},
|
||||
link_go_to_definition::InlayHighlight,
|
||||
InlayId, MultiBuffer,
|
||||
};
|
||||
use gpui::AppContext;
|
||||
use project::{InlayHint, InlayHintLabel, ResolveState};
|
||||
use rand::prelude::*;
|
||||
@ -1619,8 +1647,8 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut expected_text = Rope::from(buffer_snapshot.text());
|
||||
for (offset, inlay) in inlays.into_iter().rev() {
|
||||
expected_text.replace(offset..offset, &inlay.text.to_string());
|
||||
for (offset, inlay) in inlays.iter().rev() {
|
||||
expected_text.replace(*offset..*offset, &inlay.text.to_string());
|
||||
}
|
||||
assert_eq!(inlay_snapshot.text(), expected_text.to_string());
|
||||
|
||||
@ -1640,51 +1668,87 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
let mut highlights = TextHighlights::default();
|
||||
let highlight_count = rng.gen_range(0_usize..10);
|
||||
let mut highlight_ranges = (0..highlight_count)
|
||||
let mut text_highlights = TextHighlights::default();
|
||||
let text_highlight_count = rng.gen_range(0_usize..10);
|
||||
let mut text_highlight_ranges = (0..text_highlight_count)
|
||||
.map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
|
||||
.collect::<Vec<_>>();
|
||||
highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
|
||||
log::info!("highlighting ranges {:?}", highlight_ranges);
|
||||
let highlight_ranges = if rng.gen_bool(0.5) {
|
||||
highlight_ranges
|
||||
.into_iter()
|
||||
.map(|range| InlayRange {
|
||||
inlay_position: buffer_snapshot.anchor_before(range.start),
|
||||
highlight_start: inlay_snapshot.to_inlay_offset(range.start),
|
||||
highlight_end: inlay_snapshot.to_inlay_offset(range.end),
|
||||
})
|
||||
.map(DocumentRange::Inlay)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
highlight_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
buffer_snapshot.anchor_before(range.start)
|
||||
..buffer_snapshot.anchor_after(range.end)
|
||||
})
|
||||
.map(DocumentRange::Text)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
highlights.insert(
|
||||
text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
|
||||
log::info!("highlighting text ranges {text_highlight_ranges:?}");
|
||||
text_highlights.insert(
|
||||
Some(TypeId::of::<()>()),
|
||||
Arc::new((HighlightStyle::default(), highlight_ranges)),
|
||||
Arc::new((
|
||||
HighlightStyle::default(),
|
||||
text_highlight_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
buffer_snapshot.anchor_before(range.start)
|
||||
..buffer_snapshot.anchor_after(range.end)
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
);
|
||||
|
||||
let mut inlay_highlights = InlayHighlights::default();
|
||||
if !inlays.is_empty() {
|
||||
let inlay_highlight_count = rng.gen_range(0..inlays.len());
|
||||
let mut inlay_indices = BTreeSet::default();
|
||||
while inlay_indices.len() < inlay_highlight_count {
|
||||
inlay_indices.insert(rng.gen_range(0..inlays.len()));
|
||||
}
|
||||
let new_highlights = inlay_indices
|
||||
.into_iter()
|
||||
.filter_map(|i| {
|
||||
let (_, inlay) = &inlays[i];
|
||||
let inlay_text_len = inlay.text.len();
|
||||
match inlay_text_len {
|
||||
0 => None,
|
||||
1 => Some(InlayHighlight {
|
||||
inlay: inlay.id,
|
||||
inlay_position: inlay.position,
|
||||
range: 0..1,
|
||||
}),
|
||||
n => {
|
||||
let inlay_text = inlay.text.to_string();
|
||||
let mut highlight_end = rng.gen_range(1..n);
|
||||
let mut highlight_start = rng.gen_range(0..highlight_end);
|
||||
while !inlay_text.is_char_boundary(highlight_end) {
|
||||
highlight_end += 1;
|
||||
}
|
||||
while !inlay_text.is_char_boundary(highlight_start) {
|
||||
highlight_start -= 1;
|
||||
}
|
||||
Some(InlayHighlight {
|
||||
inlay: inlay.id,
|
||||
inlay_position: inlay.position,
|
||||
range: highlight_start..highlight_end,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.map(|highlight| (highlight.inlay, (HighlightStyle::default(), highlight)))
|
||||
.collect();
|
||||
log::info!("highlighting inlay ranges {new_highlights:?}");
|
||||
inlay_highlights.insert(TypeId::of::<()>(), new_highlights);
|
||||
}
|
||||
|
||||
for _ in 0..5 {
|
||||
let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
|
||||
end = expected_text.clip_offset(end, Bias::Right);
|
||||
let mut start = rng.gen_range(0..=end);
|
||||
start = expected_text.clip_offset(start, Bias::Right);
|
||||
|
||||
let range = InlayOffset(start)..InlayOffset(end);
|
||||
log::info!("calling inlay_snapshot.chunks({range:?})");
|
||||
let actual_text = inlay_snapshot
|
||||
.chunks(
|
||||
InlayOffset(start)..InlayOffset(end),
|
||||
range,
|
||||
false,
|
||||
Some(&highlights),
|
||||
None,
|
||||
None,
|
||||
Highlights {
|
||||
text_highlights: Some(&text_highlights),
|
||||
inlay_highlights: Some(&inlay_highlights),
|
||||
..Highlights::default()
|
||||
},
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect::<String>();
|
||||
|
@ -1,9 +1,8 @@
|
||||
use super::{
|
||||
fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
|
||||
TextHighlights,
|
||||
Highlights,
|
||||
};
|
||||
use crate::MultiBufferSnapshot;
|
||||
use gpui::fonts::HighlightStyle;
|
||||
use language::{Chunk, Point};
|
||||
use std::{cmp, mem, num::NonZeroU32, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
@ -68,9 +67,7 @@ impl TabMap {
|
||||
'outer: for chunk in old_snapshot.fold_snapshot.chunks(
|
||||
fold_edit.old.end..old_end_row_successor_offset,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Highlights::default(),
|
||||
) {
|
||||
for (ix, _) in chunk.text.match_indices('\t') {
|
||||
let offset_from_edit = offset_from_edit + (ix as u32);
|
||||
@ -183,7 +180,7 @@ impl TabSnapshot {
|
||||
self.max_point()
|
||||
};
|
||||
for c in self
|
||||
.chunks(range.start..line_end, false, None, None, None)
|
||||
.chunks(range.start..line_end, false, Highlights::default())
|
||||
.flat_map(|chunk| chunk.text.chars())
|
||||
{
|
||||
if c == '\n' {
|
||||
@ -200,9 +197,7 @@ impl TabSnapshot {
|
||||
.chunks(
|
||||
TabPoint::new(range.end.row(), 0)..range.end,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Highlights::default(),
|
||||
)
|
||||
.flat_map(|chunk| chunk.text.chars())
|
||||
{
|
||||
@ -223,9 +218,7 @@ impl TabSnapshot {
|
||||
&'a self,
|
||||
range: Range<TabPoint>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
highlights: Highlights<'a>,
|
||||
) -> TabChunks<'a> {
|
||||
let (input_start, expanded_char_column, to_next_stop) =
|
||||
self.to_fold_point(range.start, Bias::Left);
|
||||
@ -245,9 +238,7 @@ impl TabSnapshot {
|
||||
fold_chunks: self.fold_snapshot.chunks(
|
||||
input_start..input_end,
|
||||
language_aware,
|
||||
text_highlights,
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
highlights,
|
||||
),
|
||||
input_column,
|
||||
column: expanded_char_column,
|
||||
@ -270,9 +261,13 @@ impl TabSnapshot {
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect()
|
||||
self.chunks(
|
||||
TabPoint::zero()..self.max_point(),
|
||||
false,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> TabPoint {
|
||||
@ -597,9 +592,7 @@ mod tests {
|
||||
.chunks(
|
||||
TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|c| c.text)
|
||||
.collect::<String>(),
|
||||
@ -674,7 +667,8 @@ mod tests {
|
||||
let mut chunks = Vec::new();
|
||||
let mut was_tab = false;
|
||||
let mut text = String::new();
|
||||
for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) {
|
||||
for chunk in snapshot.chunks(start..snapshot.max_point(), false, Highlights::default())
|
||||
{
|
||||
if chunk.is_tab != was_tab {
|
||||
if !text.is_empty() {
|
||||
chunks.push((mem::take(&mut text), was_tab));
|
||||
@ -743,7 +737,7 @@ mod tests {
|
||||
let expected_summary = TextSummary::from(expected_text.as_str());
|
||||
assert_eq!(
|
||||
tabs_snapshot
|
||||
.chunks(start..end, false, None, None, None)
|
||||
.chunks(start..end, false, Highlights::default())
|
||||
.map(|c| c.text)
|
||||
.collect::<String>(),
|
||||
expected_text,
|
||||
|
@ -1,13 +1,11 @@
|
||||
use super::{
|
||||
fold_map::FoldBufferRows,
|
||||
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
|
||||
TextHighlights,
|
||||
Highlights,
|
||||
};
|
||||
use crate::MultiBufferSnapshot;
|
||||
use gpui::{
|
||||
fonts::{FontId, HighlightStyle},
|
||||
text_layout::LineWrapper,
|
||||
AppContext, Entity, ModelContext, ModelHandle, Task,
|
||||
fonts::FontId, text_layout::LineWrapper, AppContext, Entity, ModelContext, ModelHandle, Task,
|
||||
};
|
||||
use language::{Chunk, Point};
|
||||
use lazy_static::lazy_static;
|
||||
@ -444,9 +442,7 @@ impl WrapSnapshot {
|
||||
let mut chunks = new_tab_snapshot.chunks(
|
||||
TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Highlights::default(),
|
||||
);
|
||||
let mut edit_transforms = Vec::<Transform>::new();
|
||||
for _ in edit.new_rows.start..edit.new_rows.end {
|
||||
@ -575,9 +571,7 @@ impl WrapSnapshot {
|
||||
&'a self,
|
||||
rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
highlights: Highlights<'a>,
|
||||
) -> WrapChunks<'a> {
|
||||
let output_start = WrapPoint::new(rows.start, 0);
|
||||
let output_end = WrapPoint::new(rows.end, 0);
|
||||
@ -594,9 +588,7 @@ impl WrapSnapshot {
|
||||
input_chunks: self.tab_snapshot.chunks(
|
||||
input_start..input_end,
|
||||
language_aware,
|
||||
text_highlights,
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
highlights,
|
||||
),
|
||||
input_chunk: Default::default(),
|
||||
output_position: output_start,
|
||||
@ -1323,9 +1315,7 @@ mod tests {
|
||||
self.chunks(
|
||||
wrap_row..self.max_point().row() + 1,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Highlights::default(),
|
||||
)
|
||||
.map(|h| h.text)
|
||||
}
|
||||
@ -1350,7 +1340,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let actual_text = self
|
||||
.chunks(start_row..end_row, true, None, None, None)
|
||||
.chunks(start_row..end_row, true, Highlights::default())
|
||||
.map(|c| c.text)
|
||||
.collect::<String>();
|
||||
assert_eq!(
|
||||
|
@ -66,7 +66,7 @@ use language::{
|
||||
TransactionId,
|
||||
};
|
||||
use link_go_to_definition::{
|
||||
hide_link_definition, show_link_definition, DocumentRange, GoToDefinitionLink, InlayRange,
|
||||
hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight,
|
||||
LinkGoToDefinitionState,
|
||||
};
|
||||
use log::error;
|
||||
@ -99,6 +99,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
pub use sum_tree::Bias;
|
||||
use sum_tree::TreeMap;
|
||||
use text::Rope;
|
||||
use theme::{DiagnosticStyle, Theme, ThemeSettings};
|
||||
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||
@ -548,7 +549,8 @@ type CompletionId = usize;
|
||||
type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
|
||||
type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
|
||||
|
||||
type BackgroundHighlight = (fn(&Theme) -> Color, Vec<DocumentRange>);
|
||||
type BackgroundHighlight = (fn(&Theme) -> Color, Vec<Range<Anchor>>);
|
||||
type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec<InlayHighlight>);
|
||||
|
||||
pub struct Editor {
|
||||
handle: WeakViewHandle<Self>,
|
||||
@ -580,6 +582,7 @@ pub struct Editor {
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
|
||||
inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
context_menu: Option<ContextMenu>,
|
||||
mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
|
||||
@ -1523,6 +1526,7 @@ impl Editor {
|
||||
placeholder_text: None,
|
||||
highlighted_rows: None,
|
||||
background_highlights: Default::default(),
|
||||
inlay_background_highlights: Default::default(),
|
||||
nav_history: None,
|
||||
context_menu: None,
|
||||
mouse_context_menu: cx
|
||||
@ -1734,6 +1738,10 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_only(&self) -> bool {
|
||||
self.read_only
|
||||
}
|
||||
|
||||
pub fn set_read_only(&mut self, read_only: bool) {
|
||||
self.read_only = read_only;
|
||||
}
|
||||
@ -2285,14 +2293,18 @@ impl Editor {
|
||||
// bracket of any of this language's bracket pairs.
|
||||
let mut bracket_pair = None;
|
||||
let mut is_bracket_pair_start = false;
|
||||
for (pair, enabled) in scope.brackets() {
|
||||
if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
|
||||
bracket_pair = Some(pair.clone());
|
||||
is_bracket_pair_start = true;
|
||||
break;
|
||||
} else if pair.end.as_str() == text.as_ref() {
|
||||
bracket_pair = Some(pair.clone());
|
||||
break;
|
||||
if !text.is_empty() {
|
||||
// `text` can be empty when an user is using IME (e.g. Chinese Wubi Simplified)
|
||||
// and they are removing the character that triggered IME popup.
|
||||
for (pair, enabled) in scope.brackets() {
|
||||
if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
|
||||
bracket_pair = Some(pair.clone());
|
||||
is_bracket_pair_start = true;
|
||||
break;
|
||||
} else if pair.end.as_str() == text.as_ref() {
|
||||
bracket_pair = Some(pair.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5121,9 +5133,6 @@ impl Editor {
|
||||
self.unmark_text(cx);
|
||||
self.refresh_copilot_suggestions(true, cx);
|
||||
cx.emit(Event::Edited);
|
||||
cx.emit(Event::TransactionUndone {
|
||||
transaction_id: tx_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -7065,16 +7074,8 @@ impl Editor {
|
||||
} else {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let buffer = this.buffer.read(cx).snapshot(cx);
|
||||
let display_snapshot = this
|
||||
.display_map
|
||||
.update(cx, |display_map, cx| display_map.snapshot(cx));
|
||||
let mut buffer_highlights = this
|
||||
.document_highlights_for_position(
|
||||
selection.head(),
|
||||
&buffer,
|
||||
&display_snapshot,
|
||||
)
|
||||
.filter_map(|highlight| highlight.as_text_range())
|
||||
.document_highlights_for_position(selection.head(), &buffer)
|
||||
.filter(|highlight| {
|
||||
highlight.start.excerpt_id() == selection.head().excerpt_id()
|
||||
&& highlight.end.excerpt_id() == selection.head().excerpt_id()
|
||||
@ -7129,15 +7130,11 @@ impl Editor {
|
||||
let ranges = this
|
||||
.clear_background_highlights::<DocumentHighlightWrite>(cx)
|
||||
.into_iter()
|
||||
.flat_map(|(_, ranges)| {
|
||||
ranges.into_iter().filter_map(|range| range.as_text_range())
|
||||
})
|
||||
.flat_map(|(_, ranges)| ranges.into_iter())
|
||||
.chain(
|
||||
this.clear_background_highlights::<DocumentHighlightRead>(cx)
|
||||
.into_iter()
|
||||
.flat_map(|(_, ranges)| {
|
||||
ranges.into_iter().filter_map(|range| range.as_text_range())
|
||||
}),
|
||||
.flat_map(|(_, ranges)| ranges.into_iter()),
|
||||
)
|
||||
.collect();
|
||||
|
||||
@ -7238,7 +7235,7 @@ impl Editor {
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
);
|
||||
self.clear_text_highlights::<Rename>(cx);
|
||||
self.clear_highlights::<Rename>(cx);
|
||||
self.show_local_selections = true;
|
||||
|
||||
if moving_cursor {
|
||||
@ -7815,29 +7812,20 @@ impl Editor {
|
||||
color_fetcher: fn(&Theme) -> Color,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.background_highlights.insert(
|
||||
TypeId::of::<T>(),
|
||||
(
|
||||
color_fetcher,
|
||||
ranges.into_iter().map(DocumentRange::Text).collect(),
|
||||
),
|
||||
);
|
||||
self.background_highlights
|
||||
.insert(TypeId::of::<T>(), (color_fetcher, ranges));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn highlight_inlay_background<T: 'static>(
|
||||
&mut self,
|
||||
ranges: Vec<InlayRange>,
|
||||
ranges: Vec<InlayHighlight>,
|
||||
color_fetcher: fn(&Theme) -> Color,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.background_highlights.insert(
|
||||
TypeId::of::<T>(),
|
||||
(
|
||||
color_fetcher,
|
||||
ranges.into_iter().map(DocumentRange::Inlay).collect(),
|
||||
),
|
||||
);
|
||||
// TODO: no actual highlights happen for inlays currently, find a way to do that
|
||||
self.inlay_background_highlights
|
||||
.insert(Some(TypeId::of::<T>()), (color_fetcher, ranges));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@ -7845,15 +7833,18 @@ impl Editor {
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<BackgroundHighlight> {
|
||||
let highlights = self.background_highlights.remove(&TypeId::of::<T>());
|
||||
if highlights.is_some() {
|
||||
let text_highlights = self.background_highlights.remove(&TypeId::of::<T>());
|
||||
let inlay_highlights = self
|
||||
.inlay_background_highlights
|
||||
.remove(&Some(TypeId::of::<T>()));
|
||||
if text_highlights.is_some() || inlay_highlights.is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
highlights
|
||||
text_highlights
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn all_background_highlights(
|
||||
pub fn all_text_background_highlights(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<(Range<DisplayPoint>, Color)> {
|
||||
@ -7869,8 +7860,7 @@ impl Editor {
|
||||
&'a self,
|
||||
position: Anchor,
|
||||
buffer: &'a MultiBufferSnapshot,
|
||||
display_snapshot: &'a DisplaySnapshot,
|
||||
) -> impl 'a + Iterator<Item = &DocumentRange> {
|
||||
) -> impl 'a + Iterator<Item = &Range<Anchor>> {
|
||||
let read_highlights = self
|
||||
.background_highlights
|
||||
.get(&TypeId::of::<DocumentHighlightRead>())
|
||||
@ -7879,16 +7869,14 @@ impl Editor {
|
||||
.background_highlights
|
||||
.get(&TypeId::of::<DocumentHighlightWrite>())
|
||||
.map(|h| &h.1);
|
||||
let left_position = display_snapshot.anchor_to_inlay_offset(position.bias_left(buffer));
|
||||
let right_position = display_snapshot.anchor_to_inlay_offset(position.bias_right(buffer));
|
||||
let left_position = position.bias_left(buffer);
|
||||
let right_position = position.bias_right(buffer);
|
||||
read_highlights
|
||||
.into_iter()
|
||||
.chain(write_highlights)
|
||||
.flat_map(move |ranges| {
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = document_to_inlay_range(probe, display_snapshot)
|
||||
.end
|
||||
.cmp(&left_position);
|
||||
let cmp = probe.end.cmp(&left_position, buffer);
|
||||
if cmp.is_ge() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
@ -7899,12 +7887,9 @@ impl Editor {
|
||||
};
|
||||
|
||||
let right_position = right_position.clone();
|
||||
ranges[start_ix..].iter().take_while(move |range| {
|
||||
document_to_inlay_range(range, display_snapshot)
|
||||
.start
|
||||
.cmp(&right_position)
|
||||
.is_le()
|
||||
})
|
||||
ranges[start_ix..]
|
||||
.iter()
|
||||
.take_while(move |range| range.start.cmp(&right_position, buffer).is_le())
|
||||
})
|
||||
}
|
||||
|
||||
@ -7914,15 +7899,13 @@ impl Editor {
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
theme: &Theme,
|
||||
) -> Vec<(Range<DisplayPoint>, Color)> {
|
||||
let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start)
|
||||
..display_snapshot.anchor_to_inlay_offset(search_range.end);
|
||||
let mut results = Vec::new();
|
||||
for (color_fetcher, ranges) in self.background_highlights.values() {
|
||||
let color = color_fetcher(theme);
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = document_to_inlay_range(probe, display_snapshot)
|
||||
let cmp = probe
|
||||
.end
|
||||
.cmp(&search_range.start);
|
||||
.cmp(&search_range.start, &display_snapshot.buffer_snapshot);
|
||||
if cmp.is_gt() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
@ -7932,13 +7915,16 @@ impl Editor {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
for range in &ranges[start_ix..] {
|
||||
let range = document_to_inlay_range(range, display_snapshot);
|
||||
if range.start.cmp(&search_range.end).is_ge() {
|
||||
if range
|
||||
.start
|
||||
.cmp(&search_range.end, &display_snapshot.buffer_snapshot)
|
||||
.is_ge()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left);
|
||||
let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right);
|
||||
let start = range.start.to_display_point(&display_snapshot);
|
||||
let end = range.end.to_display_point(&display_snapshot);
|
||||
results.push((start..end, color))
|
||||
}
|
||||
}
|
||||
@ -7951,17 +7937,15 @@ impl Editor {
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
count: usize,
|
||||
) -> Vec<RangeInclusive<DisplayPoint>> {
|
||||
let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start)
|
||||
..display_snapshot.anchor_to_inlay_offset(search_range.end);
|
||||
let mut results = Vec::new();
|
||||
let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::<T>()) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = document_to_inlay_range(probe, display_snapshot)
|
||||
let cmp = probe
|
||||
.end
|
||||
.cmp(&search_range.start);
|
||||
.cmp(&search_range.start, &display_snapshot.buffer_snapshot);
|
||||
if cmp.is_gt() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
@ -7984,22 +7968,20 @@ impl Editor {
|
||||
return Vec::new();
|
||||
}
|
||||
for range in &ranges[start_ix..] {
|
||||
let range = document_to_inlay_range(range, display_snapshot);
|
||||
if range.start.cmp(&search_range.end).is_ge() {
|
||||
if range
|
||||
.start
|
||||
.cmp(&search_range.end, &display_snapshot.buffer_snapshot)
|
||||
.is_ge()
|
||||
{
|
||||
break;
|
||||
}
|
||||
let end = display_snapshot
|
||||
.inlay_offset_to_display_point(range.end, Bias::Right)
|
||||
.to_point(display_snapshot);
|
||||
let end = range.end.to_point(&display_snapshot.buffer_snapshot);
|
||||
if let Some(current_row) = &end_row {
|
||||
if end.row == current_row.row {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let start = display_snapshot
|
||||
.inlay_offset_to_display_point(range.start, Bias::Left)
|
||||
.to_point(display_snapshot);
|
||||
|
||||
let start = range.start.to_point(&display_snapshot.buffer_snapshot);
|
||||
if start_row.is_none() {
|
||||
assert_eq!(end_row, None);
|
||||
start_row = Some(start);
|
||||
@ -8038,12 +8020,12 @@ impl Editor {
|
||||
|
||||
pub fn highlight_inlays<T: 'static>(
|
||||
&mut self,
|
||||
ranges: Vec<InlayRange>,
|
||||
highlights: Vec<InlayHighlight>,
|
||||
style: HighlightStyle,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.display_map.update(cx, |map, _| {
|
||||
map.highlight_inlays(TypeId::of::<T>(), ranges, style)
|
||||
map.highlight_inlays(TypeId::of::<T>(), highlights, style)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
@ -8051,15 +8033,15 @@ impl Editor {
|
||||
pub fn text_highlights<'a, T: 'static>(
|
||||
&'a self,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<(HighlightStyle, &'a [DocumentRange])> {
|
||||
) -> Option<(HighlightStyle, &'a [Range<Anchor>])> {
|
||||
self.display_map.read(cx).text_highlights(TypeId::of::<T>())
|
||||
}
|
||||
|
||||
pub fn clear_text_highlights<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let text_highlights = self
|
||||
pub fn clear_highlights<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let cleared = self
|
||||
.display_map
|
||||
.update(cx, |map, _| map.clear_text_highlights(TypeId::of::<T>()));
|
||||
if text_highlights.is_some() {
|
||||
.update(cx, |map, _| map.clear_highlights(TypeId::of::<T>()));
|
||||
if cleared {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@ -8276,7 +8258,6 @@ impl Editor {
|
||||
Some(
|
||||
ranges
|
||||
.iter()
|
||||
.filter_map(|range| range.as_text_range())
|
||||
.map(move |range| {
|
||||
range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot)
|
||||
})
|
||||
@ -8491,19 +8472,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn document_to_inlay_range(
|
||||
range: &DocumentRange,
|
||||
snapshot: &DisplaySnapshot,
|
||||
) -> Range<InlayOffset> {
|
||||
match range {
|
||||
DocumentRange::Text(text_range) => {
|
||||
snapshot.anchor_to_inlay_offset(text_range.start)
|
||||
..snapshot.anchor_to_inlay_offset(text_range.end)
|
||||
}
|
||||
DocumentRange::Inlay(inlay_range) => inlay_range.highlight_start..inlay_range.highlight_end,
|
||||
}
|
||||
}
|
||||
|
||||
fn inlay_hint_settings(
|
||||
location: Anchor,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
@ -8605,9 +8573,6 @@ pub enum Event {
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
},
|
||||
TransactionUndone {
|
||||
transaction_id: TransactionId,
|
||||
},
|
||||
Closed,
|
||||
}
|
||||
|
||||
@ -8717,7 +8682,7 @@ impl View for Editor {
|
||||
|
||||
self.link_go_to_definition_state.task = None;
|
||||
|
||||
self.clear_text_highlights::<LinkGoToDefinitionState>(cx);
|
||||
self.clear_highlights::<LinkGoToDefinitionState>(cx);
|
||||
}
|
||||
|
||||
false
|
||||
@ -8786,12 +8751,11 @@ impl View for Editor {
|
||||
fn marked_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
|
||||
let snapshot = self.buffer.read(cx).read(cx);
|
||||
let range = self.text_highlights::<InputComposition>(cx)?.1.get(0)?;
|
||||
let range = range.as_text_range()?;
|
||||
Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0)
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.clear_text_highlights::<InputComposition>(cx);
|
||||
self.clear_highlights::<InputComposition>(cx);
|
||||
self.ime_transaction.take();
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
display_map::{InlayOffset, ToDisplayPoint},
|
||||
link_go_to_definition::{DocumentRange, InlayRange},
|
||||
link_go_to_definition::{InlayHighlight, RangeInEditor},
|
||||
Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
|
||||
ExcerptId, RangeToAnchorExt,
|
||||
};
|
||||
@ -50,19 +50,18 @@ pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewC
|
||||
|
||||
pub struct InlayHover {
|
||||
pub excerpt: ExcerptId,
|
||||
pub triggered_from: InlayOffset,
|
||||
pub range: InlayRange,
|
||||
pub range: InlayHighlight,
|
||||
pub tooltip: HoverBlock,
|
||||
}
|
||||
|
||||
pub fn find_hovered_hint_part(
|
||||
label_parts: Vec<InlayHintLabelPart>,
|
||||
hint_range: Range<InlayOffset>,
|
||||
hint_start: InlayOffset,
|
||||
hovered_offset: InlayOffset,
|
||||
) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
|
||||
if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end {
|
||||
let mut hovered_character = (hovered_offset - hint_range.start).0;
|
||||
let mut part_start = hint_range.start;
|
||||
if hovered_offset >= hint_start {
|
||||
let mut hovered_character = (hovered_offset - hint_start).0;
|
||||
let mut part_start = hint_start;
|
||||
for part in label_parts {
|
||||
let part_len = part.value.chars().count();
|
||||
if hovered_character > part_len {
|
||||
@ -88,10 +87,8 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
|
||||
};
|
||||
|
||||
if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
|
||||
if let DocumentRange::Inlay(range) = symbol_range {
|
||||
if (range.highlight_start..range.highlight_end)
|
||||
.contains(&inlay_hover.triggered_from)
|
||||
{
|
||||
if let RangeInEditor::Inlay(range) = symbol_range {
|
||||
if range == &inlay_hover.range {
|
||||
// Hover triggered from same location as last time. Don't show again.
|
||||
return;
|
||||
}
|
||||
@ -99,18 +96,6 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
|
||||
hide_hover(editor, cx);
|
||||
}
|
||||
|
||||
let snapshot = editor.snapshot(cx);
|
||||
// Don't request again if the location is the same as the previous request
|
||||
if let Some(triggered_from) = editor.hover_state.triggered_from {
|
||||
if inlay_hover.triggered_from
|
||||
== snapshot
|
||||
.display_snapshot
|
||||
.anchor_to_inlay_offset(triggered_from)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let task = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
cx.background()
|
||||
@ -122,7 +107,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
|
||||
|
||||
let hover_popover = InfoPopover {
|
||||
project: project.clone(),
|
||||
symbol_range: DocumentRange::Inlay(inlay_hover.range),
|
||||
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
|
||||
blocks: vec![inlay_hover.tooltip],
|
||||
language: None,
|
||||
rendered_content: None,
|
||||
@ -326,7 +311,7 @@ fn show_hover(
|
||||
|
||||
Some(InfoPopover {
|
||||
project: project.clone(),
|
||||
symbol_range: DocumentRange::Text(range),
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
blocks: hover_result.contents,
|
||||
language: hover_result.language,
|
||||
rendered_content: None,
|
||||
@ -608,8 +593,8 @@ impl HoverState {
|
||||
self.info_popover
|
||||
.as_ref()
|
||||
.map(|info_popover| match &info_popover.symbol_range {
|
||||
DocumentRange::Text(range) => &range.start,
|
||||
DocumentRange::Inlay(range) => &range.inlay_position,
|
||||
RangeInEditor::Text(range) => &range.start,
|
||||
RangeInEditor::Inlay(range) => &range.inlay_position,
|
||||
})
|
||||
})?;
|
||||
let point = anchor.to_display_point(&snapshot.display_snapshot);
|
||||
@ -635,7 +620,7 @@ impl HoverState {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InfoPopover {
|
||||
pub project: ModelHandle<Project>,
|
||||
symbol_range: DocumentRange,
|
||||
symbol_range: RangeInEditor,
|
||||
pub blocks: Vec<HoverBlock>,
|
||||
language: Option<Arc<Language>>,
|
||||
rendered_content: Option<RenderedInfo>,
|
||||
@ -811,6 +796,7 @@ mod tests {
|
||||
inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
|
||||
link_go_to_definition::update_inlay_link_and_hover_points,
|
||||
test::editor_lsp_test_context::EditorLspTestContext,
|
||||
InlayId,
|
||||
};
|
||||
use collections::BTreeSet;
|
||||
use gpui::fonts::Weight;
|
||||
@ -1477,25 +1463,16 @@ mod tests {
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let hover_state = &editor.hover_state;
|
||||
assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
|
||||
let popover = hover_state.info_popover.as_ref().unwrap();
|
||||
let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
let entire_inlay_start = snapshot.display_point_to_inlay_offset(
|
||||
inlay_range.start.to_display_point(&snapshot),
|
||||
Bias::Left,
|
||||
);
|
||||
|
||||
let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len());
|
||||
assert_eq!(
|
||||
popover.symbol_range,
|
||||
DocumentRange::Inlay(InlayRange {
|
||||
RangeInEditor::Inlay(InlayHighlight {
|
||||
inlay: InlayId::Hint(0),
|
||||
inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
|
||||
highlight_start: expected_new_type_label_start,
|
||||
highlight_end: InlayOffset(
|
||||
expected_new_type_label_start.0 + new_type_label.len()
|
||||
),
|
||||
range: ": ".len()..": ".len() + new_type_label.len(),
|
||||
}),
|
||||
"Popover range should match the new type label part"
|
||||
);
|
||||
@ -1543,23 +1520,17 @@ mod tests {
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let hover_state = &editor.hover_state;
|
||||
assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
|
||||
let popover = hover_state.info_popover.as_ref().unwrap();
|
||||
let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
let entire_inlay_start = snapshot.display_point_to_inlay_offset(
|
||||
inlay_range.start.to_display_point(&snapshot),
|
||||
Bias::Left,
|
||||
);
|
||||
let expected_struct_label_start =
|
||||
InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len());
|
||||
assert_eq!(
|
||||
popover.symbol_range,
|
||||
DocumentRange::Inlay(InlayRange {
|
||||
RangeInEditor::Inlay(InlayHighlight {
|
||||
inlay: InlayId::Hint(0),
|
||||
inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
|
||||
highlight_start: expected_struct_label_start,
|
||||
highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()),
|
||||
range: ": ".len() + new_type_label.len() + "<".len()
|
||||
..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
|
||||
}),
|
||||
"Popover range should match the struct label part"
|
||||
);
|
||||
|
@ -43,7 +43,8 @@ pub struct CachedExcerptHints {
|
||||
version: usize,
|
||||
buffer_version: Global,
|
||||
buffer_id: u64,
|
||||
hints: Vec<(InlayId, InlayHint)>,
|
||||
ordered_hints: Vec<InlayId>,
|
||||
hints_by_id: HashMap<InlayId, InlayHint>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@ -316,7 +317,7 @@ impl InlayHintCache {
|
||||
self.hints.retain(|cached_excerpt, cached_hints| {
|
||||
let retain = excerpts_to_query.contains_key(cached_excerpt);
|
||||
if !retain {
|
||||
invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id));
|
||||
invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied());
|
||||
}
|
||||
retain
|
||||
});
|
||||
@ -384,7 +385,7 @@ impl InlayHintCache {
|
||||
let shown_excerpt_hints_to_remove =
|
||||
shown_hints_to_remove.entry(*excerpt_id).or_default();
|
||||
let excerpt_cached_hints = excerpt_cached_hints.read();
|
||||
let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable();
|
||||
let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
|
||||
shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
|
||||
let Some(buffer) = shown_anchor
|
||||
.buffer_id
|
||||
@ -395,7 +396,8 @@ impl InlayHintCache {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
loop {
|
||||
match excerpt_cache.peek() {
|
||||
Some((cached_hint_id, cached_hint)) => {
|
||||
Some(&cached_hint_id) => {
|
||||
let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
|
||||
if cached_hint_id == shown_hint_id {
|
||||
excerpt_cache.next();
|
||||
return !new_kinds.contains(&cached_hint.kind);
|
||||
@ -428,7 +430,8 @@ impl InlayHintCache {
|
||||
}
|
||||
});
|
||||
|
||||
for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
|
||||
for cached_hint_id in excerpt_cache {
|
||||
let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
|
||||
let cached_hint_kind = maybe_missed_cached_hint.kind;
|
||||
if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
|
||||
to_insert.push(Inlay::hint(
|
||||
@ -463,7 +466,7 @@ impl InlayHintCache {
|
||||
self.update_tasks.remove(&excerpt_to_remove);
|
||||
if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) {
|
||||
let cached_hints = cached_hints.read();
|
||||
to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id));
|
||||
to_remove.extend(cached_hints.ordered_hints.iter().copied());
|
||||
}
|
||||
}
|
||||
if to_remove.is_empty() {
|
||||
@ -489,10 +492,8 @@ impl InlayHintCache {
|
||||
self.hints
|
||||
.get(&excerpt_id)?
|
||||
.read()
|
||||
.hints
|
||||
.iter()
|
||||
.find(|&(id, _)| id == &hint_id)
|
||||
.map(|(_, hint)| hint)
|
||||
.hints_by_id
|
||||
.get(&hint_id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
@ -500,7 +501,13 @@ impl InlayHintCache {
|
||||
let mut hints = Vec::new();
|
||||
for excerpt_hints in self.hints.values() {
|
||||
let excerpt_hints = excerpt_hints.read();
|
||||
hints.extend(excerpt_hints.hints.iter().map(|(_, hint)| hint).cloned());
|
||||
hints.extend(
|
||||
excerpt_hints
|
||||
.ordered_hints
|
||||
.iter()
|
||||
.map(|id| &excerpt_hints.hints_by_id[id])
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
hints
|
||||
}
|
||||
@ -518,12 +525,7 @@ impl InlayHintCache {
|
||||
) {
|
||||
if let Some(excerpt_hints) = self.hints.get(&excerpt_id) {
|
||||
let mut guard = excerpt_hints.write();
|
||||
if let Some(cached_hint) = guard
|
||||
.hints
|
||||
.iter_mut()
|
||||
.find(|(hint_id, _)| hint_id == &id)
|
||||
.map(|(_, hint)| hint)
|
||||
{
|
||||
if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
|
||||
if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state {
|
||||
let hint_to_resolve = cached_hint.clone();
|
||||
let server_id = *server_id;
|
||||
@ -555,12 +557,7 @@ impl InlayHintCache {
|
||||
editor.inlay_hint_cache.hints.get(&excerpt_id)
|
||||
{
|
||||
let mut guard = excerpt_hints.write();
|
||||
if let Some(cached_hint) = guard
|
||||
.hints
|
||||
.iter_mut()
|
||||
.find(|(hint_id, _)| hint_id == &id)
|
||||
.map(|(_, hint)| hint)
|
||||
{
|
||||
if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
|
||||
if cached_hint.resolve_state == ResolveState::Resolving {
|
||||
resolved_hint.resolve_state = ResolveState::Resolved;
|
||||
*cached_hint = resolved_hint;
|
||||
@ -986,12 +983,17 @@ fn calculate_hint_updates(
|
||||
let missing_from_cache = match &cached_excerpt_hints {
|
||||
Some(cached_excerpt_hints) => {
|
||||
let cached_excerpt_hints = cached_excerpt_hints.read();
|
||||
match cached_excerpt_hints.hints.binary_search_by(|probe| {
|
||||
probe.1.position.cmp(&new_hint.position, buffer_snapshot)
|
||||
}) {
|
||||
match cached_excerpt_hints
|
||||
.ordered_hints
|
||||
.binary_search_by(|probe| {
|
||||
cached_excerpt_hints.hints_by_id[probe]
|
||||
.position
|
||||
.cmp(&new_hint.position, buffer_snapshot)
|
||||
}) {
|
||||
Ok(ix) => {
|
||||
let mut missing_from_cache = true;
|
||||
for (cached_inlay_id, cached_hint) in &cached_excerpt_hints.hints[ix..] {
|
||||
for id in &cached_excerpt_hints.ordered_hints[ix..] {
|
||||
let cached_hint = &cached_excerpt_hints.hints_by_id[id];
|
||||
if new_hint
|
||||
.position
|
||||
.cmp(&cached_hint.position, buffer_snapshot)
|
||||
@ -1000,7 +1002,7 @@ fn calculate_hint_updates(
|
||||
break;
|
||||
}
|
||||
if cached_hint == &new_hint {
|
||||
excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
|
||||
excerpt_hints_to_persist.insert(*id, cached_hint.kind);
|
||||
missing_from_cache = false;
|
||||
}
|
||||
}
|
||||
@ -1031,12 +1033,12 @@ fn calculate_hint_updates(
|
||||
let cached_excerpt_hints = cached_excerpt_hints.read();
|
||||
remove_from_cache.extend(
|
||||
cached_excerpt_hints
|
||||
.hints
|
||||
.ordered_hints
|
||||
.iter()
|
||||
.filter(|(cached_inlay_id, _)| {
|
||||
.filter(|cached_inlay_id| {
|
||||
!excerpt_hints_to_persist.contains_key(cached_inlay_id)
|
||||
})
|
||||
.map(|(cached_inlay_id, _)| *cached_inlay_id),
|
||||
.copied(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1080,7 +1082,8 @@ fn apply_hint_update(
|
||||
version: query.cache_version,
|
||||
buffer_version: buffer_snapshot.version().clone(),
|
||||
buffer_id: query.buffer_id,
|
||||
hints: Vec::new(),
|
||||
ordered_hints: Vec::new(),
|
||||
hints_by_id: HashMap::default(),
|
||||
}))
|
||||
});
|
||||
let mut cached_excerpt_hints = cached_excerpt_hints.write();
|
||||
@ -1093,20 +1096,27 @@ fn apply_hint_update(
|
||||
|
||||
let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
|
||||
cached_excerpt_hints
|
||||
.hints
|
||||
.retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
|
||||
.ordered_hints
|
||||
.retain(|hint_id| !new_update.remove_from_cache.contains(hint_id));
|
||||
cached_excerpt_hints
|
||||
.hints_by_id
|
||||
.retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
|
||||
let mut splice = InlaySplice {
|
||||
to_remove: new_update.remove_from_visible,
|
||||
to_insert: Vec::new(),
|
||||
};
|
||||
for new_hint in new_update.add_to_cache {
|
||||
let cached_hints = &mut cached_excerpt_hints.hints;
|
||||
let insert_position = match cached_hints
|
||||
.binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot))
|
||||
{
|
||||
let insert_position = match cached_excerpt_hints
|
||||
.ordered_hints
|
||||
.binary_search_by(|probe| {
|
||||
cached_excerpt_hints.hints_by_id[probe]
|
||||
.position
|
||||
.cmp(&new_hint.position, &buffer_snapshot)
|
||||
}) {
|
||||
Ok(i) => {
|
||||
let mut insert_position = Some(i);
|
||||
for (_, cached_hint) in &cached_hints[i..] {
|
||||
for id in &cached_excerpt_hints.ordered_hints[i..] {
|
||||
let cached_hint = &cached_excerpt_hints.hints_by_id[id];
|
||||
if new_hint
|
||||
.position
|
||||
.cmp(&cached_hint.position, &buffer_snapshot)
|
||||
@ -1137,7 +1147,11 @@ fn apply_hint_update(
|
||||
.to_insert
|
||||
.push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
|
||||
}
|
||||
cached_hints.insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint));
|
||||
let new_id = InlayId::Hint(new_inlay_id);
|
||||
cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
|
||||
cached_excerpt_hints
|
||||
.ordered_hints
|
||||
.insert(insert_position, new_id);
|
||||
cached_inlays_changed = true;
|
||||
}
|
||||
}
|
||||
@ -1157,7 +1171,7 @@ fn apply_hint_update(
|
||||
outdated_excerpt_caches.insert(*excerpt_id);
|
||||
splice
|
||||
.to_remove
|
||||
.extend(excerpt_hints.hints.iter().map(|(id, _)| id));
|
||||
.extend(excerpt_hints.ordered_hints.iter().copied());
|
||||
}
|
||||
}
|
||||
cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
|
||||
@ -3311,8 +3325,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
||||
pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
|
||||
let mut labels = Vec::new();
|
||||
for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
|
||||
for (_, inlay) in &excerpt_hints.read().hints {
|
||||
labels.push(inlay.text());
|
||||
let excerpt_hints = excerpt_hints.read();
|
||||
for id in &excerpt_hints.ordered_hints {
|
||||
labels.push(excerpt_hints.hints_by_id[id].text());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
display_map::{DisplaySnapshot, InlayOffset},
|
||||
display_map::DisplaySnapshot,
|
||||
element::PointForPosition,
|
||||
hover_popover::{self, InlayHover},
|
||||
Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase,
|
||||
Anchor, DisplayPoint, Editor, EditorSnapshot, InlayId, SelectPhase,
|
||||
};
|
||||
use gpui::{Task, ViewContext};
|
||||
use language::{Bias, ToOffset};
|
||||
@ -17,44 +17,19 @@ use util::TryFutureExt;
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LinkGoToDefinitionState {
|
||||
pub last_trigger_point: Option<TriggerPoint>,
|
||||
pub symbol_range: Option<DocumentRange>,
|
||||
pub symbol_range: Option<RangeInEditor>,
|
||||
pub kind: Option<LinkDefinitionKind>,
|
||||
pub definitions: Vec<GoToDefinitionLink>,
|
||||
pub task: Option<Task<Option<()>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GoToDefinitionTrigger {
|
||||
Text(DisplayPoint),
|
||||
InlayHint(InlayRange, lsp::Location, LanguageServerId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GoToDefinitionLink {
|
||||
Text(LocationLink),
|
||||
InlayHint(lsp::Location, LanguageServerId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct InlayRange {
|
||||
pub inlay_position: Anchor,
|
||||
pub highlight_start: InlayOffset,
|
||||
pub highlight_end: InlayOffset,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TriggerPoint {
|
||||
Text(Anchor),
|
||||
InlayHint(InlayRange, lsp::Location, LanguageServerId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DocumentRange {
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum RangeInEditor {
|
||||
Text(Range<Anchor>),
|
||||
Inlay(InlayRange),
|
||||
Inlay(InlayHighlight),
|
||||
}
|
||||
|
||||
impl DocumentRange {
|
||||
impl RangeInEditor {
|
||||
pub fn as_text_range(&self) -> Option<Range<Anchor>> {
|
||||
match self {
|
||||
Self::Text(range) => Some(range.clone()),
|
||||
@ -64,28 +39,47 @@ impl DocumentRange {
|
||||
|
||||
fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
|
||||
match (self, trigger_point) {
|
||||
(DocumentRange::Text(range), TriggerPoint::Text(point)) => {
|
||||
(Self::Text(range), TriggerPoint::Text(point)) => {
|
||||
let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
|
||||
point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge()
|
||||
}
|
||||
(DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _, _)) => {
|
||||
range.highlight_start.cmp(&point.highlight_end).is_le()
|
||||
&& range.highlight_end.cmp(&point.highlight_end).is_ge()
|
||||
(Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => {
|
||||
highlight.inlay == point.inlay
|
||||
&& highlight.range.contains(&point.range.start)
|
||||
&& highlight.range.contains(&point.range.end)
|
||||
}
|
||||
(DocumentRange::Inlay(_), TriggerPoint::Text(_))
|
||||
| (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
|
||||
(Self::Inlay(_), TriggerPoint::Text(_))
|
||||
| (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TriggerPoint {
|
||||
fn anchor(&self) -> &Anchor {
|
||||
match self {
|
||||
TriggerPoint::Text(anchor) => anchor,
|
||||
TriggerPoint::InlayHint(range, _, _) => &range.inlay_position,
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum GoToDefinitionTrigger {
|
||||
Text(DisplayPoint),
|
||||
InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GoToDefinitionLink {
|
||||
Text(LocationLink),
|
||||
InlayHint(lsp::Location, LanguageServerId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InlayHighlight {
|
||||
pub inlay: InlayId,
|
||||
pub inlay_position: Anchor,
|
||||
pub range: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TriggerPoint {
|
||||
Text(Anchor),
|
||||
InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
|
||||
}
|
||||
|
||||
impl TriggerPoint {
|
||||
pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind {
|
||||
match self {
|
||||
TriggerPoint::Text(_) => {
|
||||
@ -98,6 +92,13 @@ impl TriggerPoint {
|
||||
TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type,
|
||||
}
|
||||
}
|
||||
|
||||
fn anchor(&self) -> &Anchor {
|
||||
match self {
|
||||
TriggerPoint::Text(anchor) => anchor,
|
||||
TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_go_to_definition_link(
|
||||
@ -135,11 +136,7 @@ pub fn update_go_to_definition_link(
|
||||
}
|
||||
}
|
||||
(TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => {
|
||||
if range_a
|
||||
.inlay_position
|
||||
.cmp(&range_b.inlay_position, &snapshot.buffer_snapshot)
|
||||
.is_eq()
|
||||
{
|
||||
if range_a == range_b {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -173,10 +170,6 @@ pub fn update_inlay_link_and_hover_points(
|
||||
shift_held: bool,
|
||||
cx: &mut ViewContext<'_, '_, Editor>,
|
||||
) {
|
||||
let hint_start_offset =
|
||||
snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left);
|
||||
let hint_end_offset =
|
||||
snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right);
|
||||
let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
|
||||
Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
|
||||
} else {
|
||||
@ -224,15 +217,14 @@ pub fn update_inlay_link_and_hover_points(
|
||||
}
|
||||
}
|
||||
ResolveState::Resolved => {
|
||||
let mut actual_hint_start = hint_start_offset;
|
||||
let mut actual_hint_end = hint_end_offset;
|
||||
let mut extra_shift_left = 0;
|
||||
let mut extra_shift_right = 0;
|
||||
if cached_hint.padding_left {
|
||||
actual_hint_start.0 += 1;
|
||||
actual_hint_end.0 += 1;
|
||||
extra_shift_left += 1;
|
||||
extra_shift_right += 1;
|
||||
}
|
||||
if cached_hint.padding_right {
|
||||
actual_hint_start.0 += 1;
|
||||
actual_hint_end.0 += 1;
|
||||
extra_shift_right += 1;
|
||||
}
|
||||
match cached_hint.label {
|
||||
project::InlayHintLabel::String(_) => {
|
||||
@ -253,11 +245,11 @@ pub fn update_inlay_link_and_hover_points(
|
||||
}
|
||||
}
|
||||
},
|
||||
triggered_from: hovered_offset,
|
||||
range: InlayRange {
|
||||
range: InlayHighlight {
|
||||
inlay: hovered_hint.id,
|
||||
inlay_position: hovered_hint.position,
|
||||
highlight_start: actual_hint_start,
|
||||
highlight_end: actual_hint_end,
|
||||
range: extra_shift_left
|
||||
..hovered_hint.text.len() + extra_shift_right,
|
||||
},
|
||||
},
|
||||
cx,
|
||||
@ -266,13 +258,24 @@ pub fn update_inlay_link_and_hover_points(
|
||||
}
|
||||
}
|
||||
project::InlayHintLabel::LabelParts(label_parts) => {
|
||||
let hint_start =
|
||||
snapshot.anchor_to_inlay_offset(hovered_hint.position);
|
||||
if let Some((hovered_hint_part, part_range)) =
|
||||
hover_popover::find_hovered_hint_part(
|
||||
label_parts,
|
||||
actual_hint_start..actual_hint_end,
|
||||
hint_start,
|
||||
hovered_offset,
|
||||
)
|
||||
{
|
||||
let highlight_start =
|
||||
(part_range.start - hint_start).0 + extra_shift_left;
|
||||
let highlight_end =
|
||||
(part_range.end - hint_start).0 + extra_shift_right;
|
||||
let highlight = InlayHighlight {
|
||||
inlay: hovered_hint.id,
|
||||
inlay_position: hovered_hint.position,
|
||||
range: highlight_start..highlight_end,
|
||||
};
|
||||
if let Some(tooltip) = hovered_hint_part.tooltip {
|
||||
hover_popover::hover_at_inlay(
|
||||
editor,
|
||||
@ -292,12 +295,7 @@ pub fn update_inlay_link_and_hover_points(
|
||||
kind: content.kind,
|
||||
},
|
||||
},
|
||||
triggered_from: hovered_offset,
|
||||
range: InlayRange {
|
||||
inlay_position: hovered_hint.position,
|
||||
highlight_start: part_range.start,
|
||||
highlight_end: part_range.end,
|
||||
},
|
||||
range: highlight.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@ -310,11 +308,7 @@ pub fn update_inlay_link_and_hover_points(
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
Some(GoToDefinitionTrigger::InlayHint(
|
||||
InlayRange {
|
||||
inlay_position: hovered_hint.position,
|
||||
highlight_start: part_range.start,
|
||||
highlight_end: part_range.end,
|
||||
},
|
||||
highlight,
|
||||
location,
|
||||
language_server_id,
|
||||
)),
|
||||
@ -425,7 +419,7 @@ pub fn show_link_definition(
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
|
||||
DocumentRange::Text(start..end)
|
||||
RangeInEditor::Text(start..end)
|
||||
})
|
||||
}),
|
||||
definition_result
|
||||
@ -435,8 +429,8 @@ pub fn show_link_definition(
|
||||
)
|
||||
})
|
||||
}
|
||||
TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => Some((
|
||||
Some(DocumentRange::Inlay(*trigger_source)),
|
||||
TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
|
||||
Some(RangeInEditor::Inlay(highlight.clone())),
|
||||
vec![GoToDefinitionLink::InlayHint(
|
||||
lsp_location.clone(),
|
||||
*server_id,
|
||||
@ -446,7 +440,7 @@ pub fn show_link_definition(
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
// Clear any existing highlights
|
||||
this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
|
||||
this.clear_highlights::<LinkGoToDefinitionState>(cx);
|
||||
this.link_go_to_definition_state.kind = Some(definition_kind);
|
||||
this.link_go_to_definition_state.symbol_range = result
|
||||
.as_ref()
|
||||
@ -498,26 +492,26 @@ pub fn show_link_definition(
|
||||
// If no symbol range returned from language server, use the surrounding word.
|
||||
let (offset_range, _) =
|
||||
snapshot.surrounding_word(*trigger_anchor);
|
||||
DocumentRange::Text(
|
||||
RangeInEditor::Text(
|
||||
snapshot.anchor_before(offset_range.start)
|
||||
..snapshot.anchor_after(offset_range.end),
|
||||
)
|
||||
}
|
||||
TriggerPoint::InlayHint(inlay_coordinates, _, _) => {
|
||||
DocumentRange::Inlay(*inlay_coordinates)
|
||||
TriggerPoint::InlayHint(highlight, _, _) => {
|
||||
RangeInEditor::Inlay(highlight.clone())
|
||||
}
|
||||
});
|
||||
|
||||
match highlight_range {
|
||||
DocumentRange::Text(text_range) => this
|
||||
RangeInEditor::Text(text_range) => this
|
||||
.highlight_text::<LinkGoToDefinitionState>(
|
||||
vec![text_range],
|
||||
style,
|
||||
cx,
|
||||
),
|
||||
DocumentRange::Inlay(inlay_coordinates) => this
|
||||
RangeInEditor::Inlay(highlight) => this
|
||||
.highlight_inlays::<LinkGoToDefinitionState>(
|
||||
vec![inlay_coordinates],
|
||||
vec![highlight],
|
||||
style,
|
||||
cx,
|
||||
),
|
||||
@ -547,7 +541,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||
|
||||
editor.link_go_to_definition_state.task = None;
|
||||
|
||||
editor.clear_text_highlights::<LinkGoToDefinitionState>(cx);
|
||||
editor.clear_highlights::<LinkGoToDefinitionState>(cx);
|
||||
}
|
||||
|
||||
pub fn go_to_fetched_definition(
|
||||
@ -1199,30 +1193,19 @@ mod tests {
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let actual_ranges = snapshot
|
||||
.highlight_ranges::<LinkGoToDefinitionState>()
|
||||
.map(|ranges| ranges.as_ref().clone().1)
|
||||
.unwrap_or_default()
|
||||
let actual_highlights = snapshot
|
||||
.inlay_highlights::<LinkGoToDefinitionState>()
|
||||
.into_iter()
|
||||
.map(|range| match range {
|
||||
DocumentRange::Text(range) => {
|
||||
panic!("Unexpected regular text selection range {range:?}")
|
||||
}
|
||||
DocumentRange::Inlay(inlay_range) => inlay_range,
|
||||
})
|
||||
.flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
let expected_highlight_start = snapshot.display_point_to_inlay_offset(
|
||||
inlay_range.start.to_display_point(&snapshot),
|
||||
Bias::Left,
|
||||
);
|
||||
let expected_ranges = vec![InlayRange {
|
||||
let expected_highlight = InlayHighlight {
|
||||
inlay: InlayId::Hint(0),
|
||||
inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
|
||||
highlight_start: expected_highlight_start,
|
||||
highlight_end: InlayOffset(expected_highlight_start.0 + hint_label.len()),
|
||||
}];
|
||||
assert_set_eq!(actual_ranges, expected_ranges);
|
||||
range: 0..hint_label.len(),
|
||||
};
|
||||
assert_set_eq!(actual_highlights, vec![&expected_highlight]);
|
||||
});
|
||||
|
||||
// Unpress cmd causes highlight to go away
|
||||
@ -1242,17 +1225,9 @@ mod tests {
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let actual_ranges = snapshot
|
||||
.highlight_ranges::<LinkGoToDefinitionState>()
|
||||
.text_highlight_ranges::<LinkGoToDefinitionState>()
|
||||
.map(|ranges| ranges.as_ref().clone().1)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|range| match range {
|
||||
DocumentRange::Text(range) => {
|
||||
panic!("Unexpected regular text selection range {range:?}")
|
||||
}
|
||||
DocumentRange::Inlay(inlay_range) => inlay_range,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.unwrap_or_default();
|
||||
|
||||
assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
|
||||
});
|
||||
|
@ -70,6 +70,9 @@ pub enum Event {
|
||||
Edited {
|
||||
sigleton_buffer_edited: bool,
|
||||
},
|
||||
TransactionUndone {
|
||||
transaction_id: TransactionId,
|
||||
},
|
||||
Reloaded,
|
||||
DiffBaseChanged,
|
||||
LanguageChanged,
|
||||
@ -771,30 +774,36 @@ impl MultiBuffer {
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
|
||||
let mut transaction_id = None;
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
return buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
}
|
||||
transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
} else {
|
||||
while let Some(transaction) = self.history.pop_undo() {
|
||||
let mut undone = false;
|
||||
for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions {
|
||||
if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) {
|
||||
undone |= buffer.update(cx, |buffer, cx| {
|
||||
let undo_to = *buffer_transaction_id;
|
||||
if let Some(entry) = buffer.peek_undo_stack() {
|
||||
*buffer_transaction_id = entry.transaction_id();
|
||||
}
|
||||
buffer.undo_to_transaction(undo_to, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(transaction) = self.history.pop_undo() {
|
||||
let mut undone = false;
|
||||
for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions {
|
||||
if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) {
|
||||
undone |= buffer.update(cx, |buffer, cx| {
|
||||
let undo_to = *buffer_transaction_id;
|
||||
if let Some(entry) = buffer.peek_undo_stack() {
|
||||
*buffer_transaction_id = entry.transaction_id();
|
||||
}
|
||||
buffer.undo_to_transaction(undo_to, cx)
|
||||
});
|
||||
if undone {
|
||||
transaction_id = Some(transaction.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if undone {
|
||||
return Some(transaction.id);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
if let Some(transaction_id) = transaction_id {
|
||||
cx.emit(Event::TransactionUndone { transaction_id });
|
||||
}
|
||||
|
||||
transaction_id
|
||||
}
|
||||
|
||||
pub fn redo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
|
||||
|
@ -225,7 +225,6 @@ impl<'a> EditorTestContext<'a> {
|
||||
.map(|h| h.1.clone())
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|range| range.as_text_range())
|
||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||
.collect()
|
||||
});
|
||||
@ -237,11 +236,10 @@ impl<'a> EditorTestContext<'a> {
|
||||
let expected_ranges = self.ranges(marked_text);
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let actual_ranges: Vec<Range<usize>> = snapshot
|
||||
.highlight_ranges::<Tag>()
|
||||
.text_highlight_ranges::<Tag>()
|
||||
.map(|ranges| ranges.as_ref().clone().1)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|range| range.as_text_range())
|
||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||
.collect();
|
||||
assert_set_eq!(actual_ranges, expected_ranges);
|
||||
|
@ -13,7 +13,7 @@ use anyhow::{anyhow, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
channel::{mpsc, oneshot},
|
||||
future::{BoxFuture, Shared},
|
||||
FutureExt, TryFutureExt as _,
|
||||
};
|
||||
@ -48,9 +48,6 @@ use unicase::UniCase;
|
||||
use util::{http::HttpClient, paths::PathExt};
|
||||
use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use futures::channel::mpsc;
|
||||
|
||||
pub use buffer::Operation;
|
||||
pub use buffer::*;
|
||||
pub use diagnostic_set::DiagnosticEntry;
|
||||
@ -64,6 +61,27 @@ pub fn init(cx: &mut AppContext) {
|
||||
language_settings::init(cx);
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct LspBinaryStatusSender {
|
||||
txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(Arc<Language>, LanguageServerBinaryStatus)>>>>,
|
||||
}
|
||||
|
||||
impl LspBinaryStatusSender {
|
||||
fn subscribe(&self) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.txs.lock().push(tx);
|
||||
rx
|
||||
}
|
||||
|
||||
fn send(&self, language: Arc<Language>, status: LanguageServerBinaryStatus) {
|
||||
let mut txs = self.txs.lock();
|
||||
txs.retain(|tx| {
|
||||
tx.unbounded_send((language.clone(), status.clone()))
|
||||
.is_ok()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
|
||||
}
|
||||
@ -594,14 +612,13 @@ struct AvailableLanguage {
|
||||
pub struct LanguageRegistry {
|
||||
state: RwLock<LanguageRegistryState>,
|
||||
language_server_download_dir: Option<Arc<Path>>,
|
||||
lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
login_shell_env_loaded: Shared<Task<()>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
lsp_binary_paths: Mutex<
|
||||
HashMap<LanguageServerName, Shared<Task<Result<LanguageServerBinary, Arc<anyhow::Error>>>>>,
|
||||
>,
|
||||
executor: Option<Arc<Background>>,
|
||||
lsp_binary_status_tx: LspBinaryStatusSender,
|
||||
}
|
||||
|
||||
struct LanguageRegistryState {
|
||||
@ -624,7 +641,6 @@ pub struct PendingLanguageServer {
|
||||
|
||||
impl LanguageRegistry {
|
||||
pub fn new(login_shell_env_loaded: Task<()>) -> Self {
|
||||
let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
|
||||
Self {
|
||||
state: RwLock::new(LanguageRegistryState {
|
||||
next_language_server_id: 0,
|
||||
@ -638,11 +654,10 @@ impl LanguageRegistry {
|
||||
reload_count: 0,
|
||||
}),
|
||||
language_server_download_dir: None,
|
||||
lsp_binary_statuses_tx,
|
||||
lsp_binary_statuses_rx,
|
||||
login_shell_env_loaded: login_shell_env_loaded.shared(),
|
||||
lsp_binary_paths: Default::default(),
|
||||
executor: None,
|
||||
lsp_binary_status_tx: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -918,8 +933,8 @@ impl LanguageRegistry {
|
||||
let container_dir: Arc<Path> = Arc::from(download_dir.join(adapter.name.0.as_ref()));
|
||||
let root_path = root_path.clone();
|
||||
let adapter = adapter.clone();
|
||||
let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
|
||||
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
|
||||
let lsp_binary_statuses = self.lsp_binary_status_tx.clone();
|
||||
|
||||
let task = {
|
||||
let container_dir = container_dir.clone();
|
||||
@ -976,8 +991,8 @@ impl LanguageRegistry {
|
||||
|
||||
pub fn language_server_binary_statuses(
|
||||
&self,
|
||||
) -> async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)> {
|
||||
self.lsp_binary_statuses_rx.clone()
|
||||
) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
|
||||
self.lsp_binary_status_tx.subscribe()
|
||||
}
|
||||
|
||||
pub fn delete_server_container(
|
||||
@ -1054,7 +1069,7 @@ async fn get_binary(
|
||||
language: Arc<Language>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
container_dir: Arc<Path>,
|
||||
statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
statuses: LspBinaryStatusSender,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
if !container_dir.exists() {
|
||||
@ -1081,19 +1096,15 @@ async fn get_binary(
|
||||
.cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
|
||||
.await
|
||||
{
|
||||
statuses
|
||||
.broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
|
||||
.await?;
|
||||
statuses.send(language.clone(), LanguageServerBinaryStatus::Cached);
|
||||
return Ok(binary);
|
||||
} else {
|
||||
statuses
|
||||
.broadcast((
|
||||
language.clone(),
|
||||
LanguageServerBinaryStatus::Failed {
|
||||
error: format!("{:?}", error),
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
statuses.send(
|
||||
language.clone(),
|
||||
LanguageServerBinaryStatus::Failed {
|
||||
error: format!("{:?}", error),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1105,27 +1116,21 @@ async fn fetch_latest_binary(
|
||||
language: Arc<Language>,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
container_dir: &Path,
|
||||
lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
lsp_binary_statuses_tx: LspBinaryStatusSender,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let container_dir: Arc<Path> = container_dir.into();
|
||||
lsp_binary_statuses_tx
|
||||
.broadcast((
|
||||
language.clone(),
|
||||
LanguageServerBinaryStatus::CheckingForUpdate,
|
||||
))
|
||||
.await?;
|
||||
lsp_binary_statuses_tx.send(
|
||||
language.clone(),
|
||||
LanguageServerBinaryStatus::CheckingForUpdate,
|
||||
);
|
||||
|
||||
let version_info = adapter.fetch_latest_server_version(delegate).await?;
|
||||
lsp_binary_statuses_tx
|
||||
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
|
||||
.await?;
|
||||
lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading);
|
||||
|
||||
let binary = adapter
|
||||
.fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
|
||||
.await?;
|
||||
lsp_binary_statuses_tx
|
||||
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
|
||||
.await?;
|
||||
lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloaded);
|
||||
|
||||
Ok(binary)
|
||||
}
|
||||
|
@ -912,7 +912,6 @@ impl Project {
|
||||
self.user_store.clone()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn opened_buffers(&self, cx: &AppContext) -> Vec<ModelHandle<Buffer>> {
|
||||
self.opened_buffers
|
||||
.values()
|
||||
|
@ -56,6 +56,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(BufferSearchBar::replace_all_on_pane);
|
||||
cx.add_action(BufferSearchBar::replace_next_on_pane);
|
||||
cx.add_action(BufferSearchBar::toggle_replace);
|
||||
cx.add_action(BufferSearchBar::toggle_replace_on_a_pane);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
|
||||
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
|
||||
}
|
||||
@ -101,6 +102,21 @@ impl View for BufferSearchBar {
|
||||
"BufferSearchBar"
|
||||
}
|
||||
|
||||
fn update_keymap_context(
|
||||
&self,
|
||||
keymap: &mut gpui::keymap_matcher::KeymapContext,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
Self::reset_to_default_keymap_context(keymap);
|
||||
let in_replace = self
|
||||
.replacement_editor
|
||||
.read_with(cx, |_, cx| cx.is_self_focused())
|
||||
.unwrap_or(false);
|
||||
if in_replace {
|
||||
keymap.add_identifier("in_replace");
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.query_editor);
|
||||
@ -868,9 +884,25 @@ impl BufferSearchBar {
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
fn toggle_replace(&mut self, _: &ToggleReplace, _: &mut ViewContext<Self>) {
|
||||
fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
|
||||
if let Some(_) = &self.active_searchable_item {
|
||||
self.replace_is_active = !self.replace_is_active;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext<Pane>) {
|
||||
let mut should_propagate = true;
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |bar, cx| {
|
||||
if let Some(_) = &bar.active_searchable_item {
|
||||
should_propagate = false;
|
||||
bar.replace_is_active = !bar.replace_is_active;
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
if should_propagate {
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
|
||||
@ -918,12 +950,16 @@ impl BufferSearchBar {
|
||||
fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext<Pane>) {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |bar, cx| bar.replace_next(action, cx));
|
||||
return;
|
||||
}
|
||||
cx.propagate_action();
|
||||
}
|
||||
fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext<Pane>) {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |bar, cx| bar.replace_all(action, cx));
|
||||
return;
|
||||
}
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
|
||||
@ -976,7 +1012,7 @@ mod tests {
|
||||
.unwrap();
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_background_highlights(cx),
|
||||
editor.all_text_background_highlights(cx),
|
||||
&[
|
||||
(
|
||||
DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
|
||||
@ -997,7 +1033,7 @@ mod tests {
|
||||
editor.next_notification(cx).await;
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_background_highlights(cx),
|
||||
editor.all_text_background_highlights(cx),
|
||||
&[(
|
||||
DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
|
||||
Color::red(),
|
||||
@ -1013,7 +1049,7 @@ mod tests {
|
||||
.unwrap();
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_background_highlights(cx),
|
||||
editor.all_text_background_highlights(cx),
|
||||
&[
|
||||
(
|
||||
DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
|
||||
@ -1054,7 +1090,7 @@ mod tests {
|
||||
editor.next_notification(cx).await;
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_background_highlights(cx),
|
||||
editor.all_text_background_highlights(cx),
|
||||
&[
|
||||
(
|
||||
DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
|
||||
@ -1265,7 +1301,7 @@ mod tests {
|
||||
.unwrap();
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_background_highlights(cx),
|
||||
editor.all_text_background_highlights(cx),
|
||||
&[(
|
||||
DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
|
||||
Color::red(),
|
||||
@ -1292,7 +1328,7 @@ mod tests {
|
||||
editor.next_notification(cx).await;
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.all_background_highlights(cx),
|
||||
editor.all_text_background_highlights(cx),
|
||||
&[(
|
||||
DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
|
||||
Color::red(),
|
||||
|
@ -701,8 +701,9 @@ impl ProjectSearchView {
|
||||
}));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
semantic_state.maintain_rate_limit = None;
|
||||
}
|
||||
semantic_state.maintain_rate_limit = None;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1724,7 +1725,7 @@ pub mod tests {
|
||||
assert_eq!(
|
||||
search_view
|
||||
.results_editor
|
||||
.update(cx, |editor, cx| editor.all_background_highlights(cx)),
|
||||
.update(cx, |editor, cx| editor.all_text_background_highlights(cx)),
|
||||
&[
|
||||
(
|
||||
DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35),
|
||||
|
@ -110,7 +110,7 @@ fn toggle_replace_button<V: View>(
|
||||
button_style: ToggleIconButtonStyle,
|
||||
) -> AnyElement<V> {
|
||||
Button::dynamic_action(Box::new(ToggleReplace))
|
||||
.with_tooltip("Toggle replace", tooltip_style)
|
||||
.with_tooltip("Toggle Replace", tooltip_style)
|
||||
.with_contents(theme::components::svg::Svg::new("icons/replace.svg"))
|
||||
.toggleable(active)
|
||||
.with_style(button_style)
|
||||
|
@ -23,6 +23,7 @@ settings = { path = "../settings" }
|
||||
anyhow.workspace = true
|
||||
postage.workspace = true
|
||||
futures.workspace = true
|
||||
ordered-float.workspace = true
|
||||
smol.workspace = true
|
||||
rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
isahc.workspace = true
|
||||
|
@ -7,12 +7,13 @@ use anyhow::{anyhow, Context, Result};
|
||||
use collections::HashMap;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::executor;
|
||||
use ordered_float::OrderedFloat;
|
||||
use project::{search::PathMatcher, Fs};
|
||||
use rpc::proto::Timestamp;
|
||||
use rusqlite::params;
|
||||
use rusqlite::types::Value;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
cmp::Reverse,
|
||||
future::Future,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
@ -190,6 +191,10 @@ impl VectorDatabase {
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
db.execute(
|
||||
"CREATE INDEX spans_digest ON spans (digest)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
log::trace!("vector database initialized with updated schema.");
|
||||
Ok(())
|
||||
@ -274,6 +279,39 @@ impl VectorDatabase {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn embeddings_for_digests(
|
||||
&self,
|
||||
digests: Vec<SpanDigest>,
|
||||
) -> impl Future<Output = Result<HashMap<SpanDigest, Embedding>>> {
|
||||
self.transact(move |db| {
|
||||
let mut query = db.prepare(
|
||||
"
|
||||
SELECT digest, embedding
|
||||
FROM spans
|
||||
WHERE digest IN rarray(?)
|
||||
",
|
||||
)?;
|
||||
let mut embeddings_by_digest = HashMap::default();
|
||||
let digests = Rc::new(
|
||||
digests
|
||||
.into_iter()
|
||||
.map(|p| Value::Blob(p.0.to_vec()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
let rows = query.query_map(params![digests], |row| {
|
||||
Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?))
|
||||
})?;
|
||||
|
||||
for row in rows {
|
||||
if let Ok(row) = row {
|
||||
embeddings_by_digest.insert(row.0, row.1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(embeddings_by_digest)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn embeddings_for_files(
|
||||
&self,
|
||||
worktree_id_file_paths: HashMap<i64, Vec<Arc<Path>>>,
|
||||
@ -370,16 +408,16 @@ impl VectorDatabase {
|
||||
query_embedding: &Embedding,
|
||||
limit: usize,
|
||||
file_ids: &[i64],
|
||||
) -> impl Future<Output = Result<Vec<(i64, f32)>>> {
|
||||
) -> impl Future<Output = Result<Vec<(i64, OrderedFloat<f32>)>>> {
|
||||
let query_embedding = query_embedding.clone();
|
||||
let file_ids = file_ids.to_vec();
|
||||
self.transact(move |db| {
|
||||
let mut results = Vec::<(i64, f32)>::with_capacity(limit + 1);
|
||||
let mut results = Vec::<(i64, OrderedFloat<f32>)>::with_capacity(limit + 1);
|
||||
Self::for_each_span(db, &file_ids, |id, embedding| {
|
||||
let similarity = embedding.similarity(&query_embedding);
|
||||
let ix = match results.binary_search_by(|(_, s)| {
|
||||
similarity.partial_cmp(&s).unwrap_or(Ordering::Equal)
|
||||
}) {
|
||||
let ix = match results
|
||||
.binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s))
|
||||
{
|
||||
Ok(ix) => ix,
|
||||
Err(ix) => ix,
|
||||
};
|
||||
|
@ -7,6 +7,7 @@ use isahc::http::StatusCode;
|
||||
use isahc::prelude::Configurable;
|
||||
use isahc::{AsyncBody, Response};
|
||||
use lazy_static::lazy_static;
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::Mutex;
|
||||
use parse_duration::parse;
|
||||
use postage::watch;
|
||||
@ -35,7 +36,7 @@ impl From<Vec<f32>> for Embedding {
|
||||
}
|
||||
|
||||
impl Embedding {
|
||||
pub fn similarity(&self, other: &Self) -> f32 {
|
||||
pub fn similarity(&self, other: &Self) -> OrderedFloat<f32> {
|
||||
let len = self.0.len();
|
||||
assert_eq!(len, other.0.len());
|
||||
|
||||
@ -58,7 +59,7 @@ impl Embedding {
|
||||
1,
|
||||
);
|
||||
}
|
||||
result
|
||||
OrderedFloat(result)
|
||||
}
|
||||
}
|
||||
|
||||
@ -379,13 +380,13 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn round_to_decimals(n: f32, decimal_places: i32) -> f32 {
|
||||
fn round_to_decimals(n: OrderedFloat<f32>, decimal_places: i32) -> f32 {
|
||||
let factor = (10.0 as f32).powi(decimal_places);
|
||||
(n * factor).round() / factor
|
||||
}
|
||||
|
||||
fn reference_dot(a: &[f32], b: &[f32]) -> f32 {
|
||||
a.iter().zip(b.iter()).map(|(a, b)| a * b).sum()
|
||||
fn reference_dot(a: &[f32], b: &[f32]) -> OrderedFloat<f32> {
|
||||
OrderedFloat(a.iter().zip(b.iter()).map(|(a, b)| a * b).sum())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use rusqlite::{
|
||||
};
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Reverse},
|
||||
collections::HashSet,
|
||||
ops::Range,
|
||||
@ -16,7 +17,7 @@ use std::{
|
||||
use tree_sitter::{Parser, QueryCursor};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
pub struct SpanDigest([u8; 20]);
|
||||
pub struct SpanDigest(pub [u8; 20]);
|
||||
|
||||
impl FromSql for SpanDigest {
|
||||
fn column_result(value: ValueRef) -> FromSqlResult<Self> {
|
||||
@ -94,12 +95,15 @@ impl CodeContextRetriever {
|
||||
|
||||
fn parse_entire_file(
|
||||
&self,
|
||||
relative_path: &Path,
|
||||
relative_path: Option<&Path>,
|
||||
language_name: Arc<str>,
|
||||
content: &str,
|
||||
) -> Result<Vec<Span>> {
|
||||
let document_span = ENTIRE_FILE_TEMPLATE
|
||||
.replace("<path>", relative_path.to_string_lossy().as_ref())
|
||||
.replace(
|
||||
"<path>",
|
||||
&relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
|
||||
)
|
||||
.replace("<language>", language_name.as_ref())
|
||||
.replace("<item>", &content);
|
||||
let digest = SpanDigest::from(document_span.as_str());
|
||||
@ -114,9 +118,16 @@ impl CodeContextRetriever {
|
||||
}])
|
||||
}
|
||||
|
||||
fn parse_markdown_file(&self, relative_path: &Path, content: &str) -> Result<Vec<Span>> {
|
||||
fn parse_markdown_file(
|
||||
&self,
|
||||
relative_path: Option<&Path>,
|
||||
content: &str,
|
||||
) -> Result<Vec<Span>> {
|
||||
let document_span = MARKDOWN_CONTEXT_TEMPLATE
|
||||
.replace("<path>", relative_path.to_string_lossy().as_ref())
|
||||
.replace(
|
||||
"<path>",
|
||||
&relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
|
||||
)
|
||||
.replace("<item>", &content);
|
||||
let digest = SpanDigest::from(document_span.as_str());
|
||||
let (document_span, token_count) = self.embedding_provider.truncate(&document_span);
|
||||
@ -188,7 +199,7 @@ impl CodeContextRetriever {
|
||||
|
||||
pub fn parse_file_with_template(
|
||||
&mut self,
|
||||
relative_path: &Path,
|
||||
relative_path: Option<&Path>,
|
||||
content: &str,
|
||||
language: Arc<Language>,
|
||||
) -> Result<Vec<Span>> {
|
||||
@ -196,14 +207,17 @@ impl CodeContextRetriever {
|
||||
|
||||
if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) {
|
||||
return self.parse_entire_file(relative_path, language_name, &content);
|
||||
} else if language_name.as_ref() == "Markdown" {
|
||||
} else if ["Markdown", "Plain Text"].contains(&language_name.as_ref()) {
|
||||
return self.parse_markdown_file(relative_path, &content);
|
||||
}
|
||||
|
||||
let mut spans = self.parse_file(content, language)?;
|
||||
for span in &mut spans {
|
||||
let document_content = CODE_CONTEXT_TEMPLATE
|
||||
.replace("<path>", relative_path.to_string_lossy().as_ref())
|
||||
.replace(
|
||||
"<path>",
|
||||
&relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
|
||||
)
|
||||
.replace("<language>", language_name.as_ref())
|
||||
.replace("item", &span.content);
|
||||
|
||||
|
@ -16,14 +16,16 @@ use embedding_queue::{EmbeddingQueue, FileToEmbed};
|
||||
use futures::{future, FutureExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
||||
use language::{Anchor, Bias, Buffer, Language, LanguageRegistry};
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::Mutex;
|
||||
use parsing::{CodeContextRetriever, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES};
|
||||
use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES};
|
||||
use postage::watch;
|
||||
use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, Worktree, WorktreeId};
|
||||
use smol::channel;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
cmp::Reverse,
|
||||
future::Future,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Weak},
|
||||
@ -37,7 +39,7 @@ use util::{
|
||||
};
|
||||
use workspace::WorkspaceCreated;
|
||||
|
||||
const SEMANTIC_INDEX_VERSION: usize = 10;
|
||||
const SEMANTIC_INDEX_VERSION: usize = 11;
|
||||
const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60);
|
||||
const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
|
||||
@ -262,9 +264,11 @@ pub struct PendingFile {
|
||||
job_handle: JobHandle,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SearchResult {
|
||||
pub buffer: ModelHandle<Buffer>,
|
||||
pub range: Range<Anchor>,
|
||||
pub similarity: OrderedFloat<f32>,
|
||||
}
|
||||
|
||||
impl SemanticIndex {
|
||||
@ -402,7 +406,7 @@ impl SemanticIndex {
|
||||
|
||||
if let Some(content) = fs.load(&pending_file.absolute_path).await.log_err() {
|
||||
if let Some(mut spans) = retriever
|
||||
.parse_file_with_template(&pending_file.relative_path, &content, language)
|
||||
.parse_file_with_template(Some(&pending_file.relative_path), &content, language)
|
||||
.log_err()
|
||||
{
|
||||
log::trace!(
|
||||
@ -422,7 +426,7 @@ impl SemanticIndex {
|
||||
path: pending_file.relative_path,
|
||||
mtime: pending_file.modified_time,
|
||||
job_handle: pending_file.job_handle,
|
||||
spans: spans,
|
||||
spans,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -687,39 +691,71 @@ impl SemanticIndex {
|
||||
pub fn search_project(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
phrase: String,
|
||||
query: String,
|
||||
limit: usize,
|
||||
includes: Vec<PathMatcher>,
|
||||
excludes: Vec<PathMatcher>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<SearchResult>>> {
|
||||
if query.is_empty() {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
let index = self.index_project(project.clone(), cx);
|
||||
let embedding_provider = self.embedding_provider.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let query = embedding_provider
|
||||
.embed_batch(vec![query])
|
||||
.await?
|
||||
.pop()
|
||||
.ok_or_else(|| anyhow!("could not embed query"))?;
|
||||
index.await?;
|
||||
|
||||
let search_start = Instant::now();
|
||||
let modified_buffer_results = this.update(&mut cx, |this, cx| {
|
||||
this.search_modified_buffers(&project, query.clone(), limit, &excludes, cx)
|
||||
});
|
||||
let file_results = this.update(&mut cx, |this, cx| {
|
||||
this.search_files(project, query, limit, includes, excludes, cx)
|
||||
});
|
||||
let (modified_buffer_results, file_results) =
|
||||
futures::join!(modified_buffer_results, file_results);
|
||||
|
||||
// Weave together the results from modified buffers and files.
|
||||
let mut results = Vec::new();
|
||||
let mut modified_buffers = HashSet::default();
|
||||
for result in modified_buffer_results.log_err().unwrap_or_default() {
|
||||
modified_buffers.insert(result.buffer.clone());
|
||||
results.push(result);
|
||||
}
|
||||
for result in file_results.log_err().unwrap_or_default() {
|
||||
if !modified_buffers.contains(&result.buffer) {
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
results.sort_by_key(|result| Reverse(result.similarity));
|
||||
results.truncate(limit);
|
||||
log::trace!("Semantic search took {:?}", search_start.elapsed());
|
||||
Ok(results)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search_files(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
query: Embedding,
|
||||
limit: usize,
|
||||
includes: Vec<PathMatcher>,
|
||||
excludes: Vec<PathMatcher>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<SearchResult>>> {
|
||||
let db_path = self.db.path().clone();
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
index.await?;
|
||||
|
||||
let t0 = Instant::now();
|
||||
let database =
|
||||
VectorDatabase::new(fs.clone(), db_path.clone(), cx.background()).await?;
|
||||
|
||||
if phrase.len() == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let phrase_embedding = embedding_provider
|
||||
.embed_batch(vec![phrase])
|
||||
.await?
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
log::trace!(
|
||||
"Embedding search phrase took: {:?} milliseconds",
|
||||
t0.elapsed().as_millis()
|
||||
);
|
||||
|
||||
let worktree_db_ids = this.read_with(&cx, |this, _| {
|
||||
let project_state = this
|
||||
.projects
|
||||
@ -738,6 +774,7 @@ impl SemanticIndex {
|
||||
.collect::<Vec<i64>>();
|
||||
anyhow::Ok(worktree_db_ids)
|
||||
})?;
|
||||
|
||||
let file_ids = database
|
||||
.retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes)
|
||||
.await?;
|
||||
@ -756,26 +793,26 @@ impl SemanticIndex {
|
||||
let limit = limit.clone();
|
||||
let fs = fs.clone();
|
||||
let db_path = db_path.clone();
|
||||
let phrase_embedding = phrase_embedding.clone();
|
||||
let query = query.clone();
|
||||
if let Some(db) = VectorDatabase::new(fs, db_path.clone(), cx.background())
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
batch_results.push(async move {
|
||||
db.top_k_search(&phrase_embedding, limit, batch.as_slice())
|
||||
.await
|
||||
db.top_k_search(&query, limit, batch.as_slice()).await
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let batch_results = futures::future::join_all(batch_results).await;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for batch_result in batch_results {
|
||||
if batch_result.is_ok() {
|
||||
for (id, similarity) in batch_result.unwrap() {
|
||||
let ix = match results.binary_search_by(|(_, s)| {
|
||||
similarity.partial_cmp(&s).unwrap_or(Ordering::Equal)
|
||||
}) {
|
||||
let ix = match results
|
||||
.binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s))
|
||||
{
|
||||
Ok(ix) => ix,
|
||||
Err(ix) => ix,
|
||||
};
|
||||
@ -785,7 +822,11 @@ impl SemanticIndex {
|
||||
}
|
||||
}
|
||||
|
||||
let ids = results.into_iter().map(|(id, _)| id).collect::<Vec<i64>>();
|
||||
let ids = results.iter().map(|(id, _)| *id).collect::<Vec<i64>>();
|
||||
let scores = results
|
||||
.into_iter()
|
||||
.map(|(_, score)| score)
|
||||
.collect::<Vec<_>>();
|
||||
let spans = database.spans_for_ids(ids.as_slice()).await?;
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
@ -810,24 +851,106 @@ impl SemanticIndex {
|
||||
|
||||
let buffers = futures::future::join_all(tasks).await;
|
||||
|
||||
log::trace!(
|
||||
"Semantic Searching took: {:?} milliseconds in total",
|
||||
t0.elapsed().as_millis()
|
||||
);
|
||||
|
||||
Ok(buffers
|
||||
.into_iter()
|
||||
.zip(ranges)
|
||||
.filter_map(|(buffer, range)| {
|
||||
.zip(scores)
|
||||
.filter_map(|((buffer, range), similarity)| {
|
||||
let buffer = buffer.log_err()?;
|
||||
let range = buffer.read_with(&cx, |buffer, _| {
|
||||
let start = buffer.clip_offset(range.start, Bias::Left);
|
||||
let end = buffer.clip_offset(range.end, Bias::Right);
|
||||
buffer.anchor_before(start)..buffer.anchor_after(end)
|
||||
});
|
||||
Some(SearchResult { buffer, range })
|
||||
Some(SearchResult {
|
||||
buffer,
|
||||
range,
|
||||
similarity,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn search_modified_buffers(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
query: Embedding,
|
||||
limit: usize,
|
||||
excludes: &[PathMatcher],
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<SearchResult>>> {
|
||||
let modified_buffers = project
|
||||
.read(cx)
|
||||
.opened_buffers(cx)
|
||||
.into_iter()
|
||||
.filter_map(|buffer_handle| {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let snapshot = buffer.snapshot();
|
||||
let excluded = snapshot.resolve_file_path(cx, false).map_or(false, |path| {
|
||||
excludes.iter().any(|matcher| matcher.is_match(&path))
|
||||
});
|
||||
if buffer.is_dirty() && !excluded {
|
||||
Some((buffer_handle, snapshot))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let embedding_provider = self.embedding_provider.clone();
|
||||
let fs = self.fs.clone();
|
||||
let db_path = self.db.path().clone();
|
||||
let background = cx.background().clone();
|
||||
cx.background().spawn(async move {
|
||||
let db = VectorDatabase::new(fs, db_path.clone(), background).await?;
|
||||
let mut results = Vec::<SearchResult>::new();
|
||||
|
||||
let mut retriever = CodeContextRetriever::new(embedding_provider.clone());
|
||||
for (buffer, snapshot) in modified_buffers {
|
||||
let language = snapshot
|
||||
.language_at(0)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| language::PLAIN_TEXT.clone());
|
||||
let mut spans = retriever
|
||||
.parse_file_with_template(None, &snapshot.text(), language)
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
if Self::embed_spans(&mut spans, embedding_provider.as_ref(), &db)
|
||||
.await
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
for span in spans {
|
||||
let similarity = span.embedding.unwrap().similarity(&query);
|
||||
let ix = match results
|
||||
.binary_search_by_key(&Reverse(similarity), |result| {
|
||||
Reverse(result.similarity)
|
||||
}) {
|
||||
Ok(ix) => ix,
|
||||
Err(ix) => ix,
|
||||
};
|
||||
|
||||
let range = {
|
||||
let start = snapshot.clip_offset(span.range.start, Bias::Left);
|
||||
let end = snapshot.clip_offset(span.range.end, Bias::Right);
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end)
|
||||
};
|
||||
|
||||
results.insert(
|
||||
ix,
|
||||
SearchResult {
|
||||
buffer: buffer.clone(),
|
||||
range,
|
||||
similarity,
|
||||
},
|
||||
);
|
||||
results.truncate(limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
})
|
||||
}
|
||||
|
||||
@ -1009,6 +1132,63 @@ impl SemanticIndex {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn embed_spans(
|
||||
spans: &mut [Span],
|
||||
embedding_provider: &dyn EmbeddingProvider,
|
||||
db: &VectorDatabase,
|
||||
) -> Result<()> {
|
||||
let mut batch = Vec::new();
|
||||
let mut batch_tokens = 0;
|
||||
let mut embeddings = Vec::new();
|
||||
|
||||
let digests = spans
|
||||
.iter()
|
||||
.map(|span| span.digest.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let embeddings_for_digests = db
|
||||
.embeddings_for_digests(digests)
|
||||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
|
||||
for span in &*spans {
|
||||
if embeddings_for_digests.contains_key(&span.digest) {
|
||||
continue;
|
||||
};
|
||||
|
||||
if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() {
|
||||
let batch_embeddings = embedding_provider
|
||||
.embed_batch(mem::take(&mut batch))
|
||||
.await?;
|
||||
embeddings.extend(batch_embeddings);
|
||||
batch_tokens = 0;
|
||||
}
|
||||
|
||||
batch_tokens += span.token_count;
|
||||
batch.push(span.content.clone());
|
||||
}
|
||||
|
||||
if !batch.is_empty() {
|
||||
let batch_embeddings = embedding_provider
|
||||
.embed_batch(mem::take(&mut batch))
|
||||
.await?;
|
||||
|
||||
embeddings.extend(batch_embeddings);
|
||||
}
|
||||
|
||||
let mut embeddings = embeddings.into_iter();
|
||||
for span in spans {
|
||||
let embedding = if let Some(embedding) = embeddings_for_digests.get(&span.digest) {
|
||||
Some(embedding.clone())
|
||||
} else {
|
||||
embeddings.next()
|
||||
};
|
||||
let embedding = embedding.ok_or_else(|| anyhow!("failed to embed spans"))?;
|
||||
span.embedding = Some(embedding);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for SemanticIndex {
|
||||
|
@ -34,7 +34,9 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
|
||||
fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
|
||||
editor.window().update(cx, |cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.clear_operator(cx);
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.recorded_actions.clear();
|
||||
if let Some(previous_editor) = vim.active_editor.clone() {
|
||||
if previous_editor == editor.clone() {
|
||||
vim.active_editor = None;
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{state::Mode, Vim};
|
||||
use crate::{normal::repeat, state::Mode, Vim};
|
||||
use editor::{scroll::autoscroll::Autoscroll, Bias};
|
||||
use gpui::{actions, AppContext, ViewContext};
|
||||
use gpui::{actions, Action, AppContext, ViewContext};
|
||||
use language::SelectionGoal;
|
||||
use workspace::Workspace;
|
||||
|
||||
@ -10,24 +10,41 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(normal_before);
|
||||
}
|
||||
|
||||
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||
let should_repeat = Vim::update(cx, |vim, cx| {
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
vim.stop_recording_immediately(action.boxed_clone());
|
||||
if count <= 1 || vim.workspace_state.replaying {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
})
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if should_repeat {
|
||||
repeat::repeat(cx, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::executor::Deterministic;
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||
@ -40,4 +57,78 @@ mod test {
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.assert_editor_state("Tesˇt");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_with_counts(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["5", "i", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("----ˇ-hello\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["5", "a", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("h----ˇ-ello\n").await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["4", "shift-i", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("---ˇ-h-----ello\n").await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["3", "shift-a", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("----h-----ello--ˇ-\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "o", "o", "i", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("hello\noi\noi\noˇi\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "shift-o", "o", "i", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("oi\noi\noˇi\nhello\n").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_with_repeat(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["3", "i", "-", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("--ˇ-hello\n").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("----ˇ--hello\n").await;
|
||||
cx.simulate_shared_keystrokes(["2", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("-----ˇ---hello\n").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes(["2", "o", "k", "k", "escape"])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkˇk\n").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkk\nkk\nkˇk\n").await;
|
||||
cx.simulate_shared_keystrokes(["1", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("hello\nkk\nkk\nkk\nkk\nkˇk\n").await;
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,8 @@ pub enum Motion {
|
||||
FindForward { before: bool, char: char },
|
||||
FindBackward { after: bool, char: char },
|
||||
NextLineStart,
|
||||
StartOfLineDownward,
|
||||
EndOfLineDownward,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
@ -117,6 +119,8 @@ actions!(
|
||||
EndOfDocument,
|
||||
Matching,
|
||||
NextLineStart,
|
||||
StartOfLineDownward,
|
||||
EndOfLineDownward,
|
||||
]
|
||||
);
|
||||
impl_actions!(
|
||||
@ -207,6 +211,12 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
|
||||
);
|
||||
cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
|
||||
cx.add_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
|
||||
motion(Motion::StartOfLineDownward, cx)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
|
||||
motion(Motion::EndOfLineDownward, cx)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
|
||||
repeat_motion(action.backwards, cx)
|
||||
})
|
||||
@ -219,11 +229,11 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
|
||||
}
|
||||
|
||||
let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
|
||||
let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
|
||||
let operator = Vim::read(cx).active_operator();
|
||||
match Vim::read(cx).state().mode {
|
||||
Mode::Normal => normal_motion(motion, operator, times, cx),
|
||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx),
|
||||
Mode::Normal => normal_motion(motion, operator, count, cx),
|
||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
|
||||
Mode::Insert => {
|
||||
// Shouldn't execute a motion in insert mode. Ignoring
|
||||
}
|
||||
@ -272,6 +282,7 @@ impl Motion {
|
||||
| EndOfDocument
|
||||
| CurrentLine
|
||||
| NextLineStart
|
||||
| StartOfLineDownward
|
||||
| StartOfParagraph
|
||||
| EndOfParagraph => true,
|
||||
EndOfLine { .. }
|
||||
@ -282,6 +293,7 @@ impl Motion {
|
||||
| Backspace
|
||||
| Right
|
||||
| StartOfLine { .. }
|
||||
| EndOfLineDownward
|
||||
| NextWordStart { .. }
|
||||
| PreviousWordStart { .. }
|
||||
| FirstNonWhitespace { .. }
|
||||
@ -305,6 +317,8 @@ impl Motion {
|
||||
| StartOfLine { .. }
|
||||
| StartOfParagraph
|
||||
| EndOfParagraph
|
||||
| StartOfLineDownward
|
||||
| EndOfLineDownward
|
||||
| NextWordStart { .. }
|
||||
| PreviousWordStart { .. }
|
||||
| FirstNonWhitespace { .. }
|
||||
@ -322,6 +336,7 @@ impl Motion {
|
||||
| EndOfDocument
|
||||
| CurrentLine
|
||||
| EndOfLine { .. }
|
||||
| EndOfLineDownward
|
||||
| NextWordEnd { .. }
|
||||
| Matching
|
||||
| FindForward { .. }
|
||||
@ -330,6 +345,7 @@ impl Motion {
|
||||
| Backspace
|
||||
| Right
|
||||
| StartOfLine { .. }
|
||||
| StartOfLineDownward
|
||||
| StartOfParagraph
|
||||
| EndOfParagraph
|
||||
| NextWordStart { .. }
|
||||
@ -396,7 +412,7 @@ impl Motion {
|
||||
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
CurrentLine => (end_of_line(map, false, point), SelectionGoal::None),
|
||||
CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
|
||||
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
||||
EndOfDocument => (
|
||||
end_of_document(map, point, maybe_times),
|
||||
@ -412,6 +428,8 @@ impl Motion {
|
||||
SelectionGoal::None,
|
||||
),
|
||||
NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
|
||||
StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
|
||||
EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
|
||||
};
|
||||
|
||||
(new_point != point || infallible).then_some((new_point, goal))
|
||||
@ -849,6 +867,13 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
|
||||
first_non_whitespace(map, false, correct_line)
|
||||
}
|
||||
|
||||
fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
if times > 1 {
|
||||
point = down(map, point, SelectionGoal::None, times - 1).0;
|
||||
}
|
||||
end_of_line(map, false, point)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
mod test {
|
||||
|
@ -2,7 +2,7 @@ mod case;
|
||||
mod change;
|
||||
mod delete;
|
||||
mod paste;
|
||||
mod repeat;
|
||||
pub(crate) mod repeat;
|
||||
mod scroll;
|
||||
mod search;
|
||||
pub mod substitute;
|
||||
@ -68,21 +68,21 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
let times = vim.take_count(cx);
|
||||
delete_motion(vim, Motion::Left, times, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
let times = vim.take_count(cx);
|
||||
delete_motion(vim, Motion::Right, times, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
let times = vim.take_count(cx);
|
||||
change_motion(
|
||||
vim,
|
||||
Motion::EndOfLine {
|
||||
@ -96,7 +96,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
let times = vim.take_count(cx);
|
||||
delete_motion(
|
||||
vim,
|
||||
Motion::EndOfLine {
|
||||
@ -110,7 +110,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let mut times = vim.pop_number_operator(cx).unwrap_or(1);
|
||||
let mut times = vim.take_count(cx).unwrap_or(1);
|
||||
if vim.state().mode.is_visual() {
|
||||
times = 1;
|
||||
} else if times > 1 {
|
||||
@ -356,7 +356,7 @@ mod test {
|
||||
|
||||
use crate::{
|
||||
state::Mode::{self},
|
||||
test::{ExemptionFeatures, NeovimBackedTestContext},
|
||||
test::NeovimBackedTestContext,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
@ -762,20 +762,22 @@ mod test {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dd(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
|
||||
cx.assert("ˇ").await;
|
||||
cx.assert("The ˇquick").await;
|
||||
cx.assert_all(indoc! {"
|
||||
The qˇuick
|
||||
brown ˇfox
|
||||
jumps ˇover"})
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_neovim_compatible("ˇ", ["d", "d"]).await;
|
||||
cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await;
|
||||
for marked_text in cx.each_marked_position(indoc! {"
|
||||
The qˇuick
|
||||
brown ˇfox
|
||||
jumps ˇover"})
|
||||
{
|
||||
cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
|
||||
}
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
ExemptionFeatures::DeletionOnEmptyLine,
|
||||
["d", "d"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim};
|
||||
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
|
||||
let count = vim.take_count(cx).unwrap_or(1) as u32;
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let mut ranges = Vec::new();
|
||||
let mut cursor_positions = Vec::new();
|
||||
|
@ -121,7 +121,7 @@ fn expand_changed_word_selection(
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
||||
@ -239,150 +239,178 @@ mod test {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_0(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "0"]);
|
||||
cx.assert(indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
brown fox"},
|
||||
["c", "0"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"})
|
||||
.await;
|
||||
brown fox"},
|
||||
["c", "0"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_k(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "k"]);
|
||||
cx.assert(indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown ˇfox
|
||||
jumps over"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
jumps over"},
|
||||
["c", "k"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"})
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
jumps ˇover"},
|
||||
["c", "k"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["c", "k"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
ˇ
|
||||
brown fox
|
||||
jumps over"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["c", "k"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_j(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "j"]);
|
||||
cx.assert(indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown ˇfox
|
||||
jumps over"})
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
jumps over"},
|
||||
["c", "j"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["c", "j"],
|
||||
)
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"})
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
jumps over"},
|
||||
["c", "j"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
ˇ"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["c", "j"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["c", "shift-g"]);
|
||||
cx.assert(indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
the lazy"},
|
||||
["c", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"})
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
the lazy"},
|
||||
["c", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["c", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
ˇ"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["c", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_gg(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["c", "g", "g"]);
|
||||
cx.assert(indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
the lazy"},
|
||||
["c", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"})
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
the lˇazy"},
|
||||
["c", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["c", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
ˇ
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["c", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@ -427,27 +455,17 @@ mod test {
|
||||
async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.add_initial_state_exemptions(
|
||||
indoc! {"
|
||||
ˇThe quick brown
|
||||
|
||||
fox jumps-over
|
||||
the lazy dog
|
||||
"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
);
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.assert_binding_matches_all(
|
||||
["c", &count.to_string(), "b"],
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await;
|
||||
for marked_text in cx.each_marked_position(indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"})
|
||||
{
|
||||
cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"])
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -278,37 +278,41 @@ mod test {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["d", "shift-g"]);
|
||||
cx.assert(indoc! {"
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
the lazy"},
|
||||
["d", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"})
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
the lazy"},
|
||||
["d", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["d", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
ˇ"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["d", "shift-g"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@ -318,34 +322,40 @@ mod test {
|
||||
let mut cx = NeovimBackedTestContext::new(cx)
|
||||
.await
|
||||
.binding(["d", "g", "g"]);
|
||||
cx.assert(indoc! {"
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"})
|
||||
.await;
|
||||
cx.assert(indoc! {"
|
||||
the lazy"},
|
||||
["d", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"})
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
the lˇazy"},
|
||||
["d", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["d", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
cx.assert_exempted(
|
||||
cx.assert_neovim_compatible(
|
||||
indoc! {"
|
||||
ˇ
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
["d", "g", "g"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@ -387,4 +397,40 @@ mod test {
|
||||
assert_eq!(cx.active_operator(), None);
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["d", "2", "d"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["2", "d", "d"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the moon,
|
||||
a star, and
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["2", "d", "2", "d"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
the ˇlazy dog"})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
use crate::{
|
||||
insert::NormalBefore,
|
||||
motion::Motion,
|
||||
state::{Mode, RecordedSelection, ReplayableAction},
|
||||
visual::visual_motion,
|
||||
Vim,
|
||||
};
|
||||
use gpui::{actions, Action, AppContext};
|
||||
use gpui::{actions, Action, AppContext, WindowContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(vim, [Repeat, EndRepeat,]);
|
||||
@ -17,138 +18,187 @@ fn should_replay(action: &Box<dyn Action>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => {
|
||||
if super::InsertBefore.id() == action.id()
|
||||
|| super::InsertAfter.id() == action.id()
|
||||
|| super::InsertFirstNonWhitespace.id() == action.id()
|
||||
|| super::InsertEndOfLine.id() == action.id()
|
||||
{
|
||||
Some(super::InsertBefore.boxed_clone())
|
||||
} else if super::InsertLineAbove.id() == action.id()
|
||||
|| super::InsertLineBelow.id() == action.id()
|
||||
{
|
||||
Some(super::InsertLineBelow.boxed_clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ReplayableAction::Insertion { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.workspace_state.replaying = false;
|
||||
vim.update_active_editor(cx, |editor, _| {
|
||||
editor.show_local_selections = true;
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, false, cx)
|
||||
});
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
|
||||
let Some((actions, editor, selection)) = Vim::update(cx, |vim, cx| {
|
||||
let actions = vim.workspace_state.recorded_actions.clone();
|
||||
let Some(editor) = vim.active_editor.clone() else {
|
||||
return None;
|
||||
};
|
||||
let count = vim.pop_number_operator(cx);
|
||||
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
|
||||
}
|
||||
|
||||
vim.workspace_state.replaying = true;
|
||||
|
||||
let selection = vim.workspace_state.recorded_selection.clone();
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::Visual, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualLine { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualBlock { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualBlock, false, cx)
|
||||
}
|
||||
RecordedSelection::None => {
|
||||
if let Some(count) = count {
|
||||
vim.workspace_state.recorded_count = Some(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(editor) = editor.upgrade(cx) {
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.show_local_selections = false;
|
||||
})
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((actions, editor, selection))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { cols } => {
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
}
|
||||
}
|
||||
RecordedSelection::Visual { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
visual_motion(
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualBlock { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx);
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualLine { rows } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
RecordedSelection::None => {}
|
||||
pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
|
||||
let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| {
|
||||
let actions = vim.workspace_state.recorded_actions.clone();
|
||||
if actions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let window = cx.window();
|
||||
cx.app_context()
|
||||
.spawn(move |mut cx| async move {
|
||||
for action in actions {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => {
|
||||
if should_replay(&action) {
|
||||
window
|
||||
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
ReplayableAction::Insertion {
|
||||
text,
|
||||
utf16_range_to_replace,
|
||||
} => editor.update(&mut cx, |editor, cx| {
|
||||
editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
|
||||
}),
|
||||
}?
|
||||
let Some(editor) = vim.active_editor.clone() else {
|
||||
return None;
|
||||
};
|
||||
let count = vim.take_count(cx);
|
||||
|
||||
let selection = vim.workspace_state.recorded_selection.clone();
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::Visual, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualLine { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualBlock { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualBlock, false, cx)
|
||||
}
|
||||
RecordedSelection::None => {
|
||||
if let Some(count) = count {
|
||||
vim.workspace_state.recorded_count = Some(count);
|
||||
}
|
||||
window
|
||||
.dispatch_action(editor.id(), &EndRepeat, &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some((actions, editor, selection))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { cols } => {
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
}
|
||||
}
|
||||
RecordedSelection::Visual { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
visual_motion(
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualBlock { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx);
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualLine { rows } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
RecordedSelection::None => {}
|
||||
}
|
||||
|
||||
// insert internally uses repeat to handle counts
|
||||
// vim doesn't treat 3a1 as though you literally repeated a1
|
||||
// 3 times, instead it inserts the content thrice at the insert position.
|
||||
if let Some(to_repeat) = repeatable_insert(&actions[0]) {
|
||||
if let Some(ReplayableAction::Action(action)) = actions.last() {
|
||||
if action.id() == NormalBefore.id() {
|
||||
actions.pop();
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_actions = actions.clone();
|
||||
actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
|
||||
|
||||
let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1);
|
||||
|
||||
// if we came from insert mode we're just doing repititions 2 onwards.
|
||||
if from_insert_mode {
|
||||
count -= 1;
|
||||
new_actions[0] = actions[0].clone();
|
||||
}
|
||||
|
||||
for _ in 1..count {
|
||||
new_actions.append(actions.clone().as_mut());
|
||||
}
|
||||
new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
|
||||
actions = new_actions;
|
||||
}
|
||||
|
||||
Vim::update(cx, |vim, _| vim.workspace_state.replaying = true);
|
||||
let window = cx.window();
|
||||
cx.app_context()
|
||||
.spawn(move |mut cx| async move {
|
||||
editor.update(&mut cx, |editor, _| {
|
||||
editor.show_local_selections = false;
|
||||
})?;
|
||||
for action in actions {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => {
|
||||
if should_replay(&action) {
|
||||
window
|
||||
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
ReplayableAction::Insertion {
|
||||
text,
|
||||
utf16_range_to_replace,
|
||||
} => editor.update(&mut cx, |editor, cx| {
|
||||
editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
|
||||
}),
|
||||
}?
|
||||
}
|
||||
editor.update(&mut cx, |editor, _| {
|
||||
editor.show_local_selections = true;
|
||||
})?;
|
||||
window
|
||||
.dispatch_action(editor.id(), &EndRepeat, &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -203,7 +253,7 @@ mod test {
|
||||
deterministic.run_until_parked();
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.set_shared_state("THE QUICK ˇbrown fox").await;
|
||||
cx.assert_shared_state("THE QUICK ˇbrown fox").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@ -424,4 +474,55 @@ mod test {
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_motion_counts(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {
|
||||
"ˇthe quick brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["3", "d", "3", "l"]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"ˇ brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
" brown
|
||||
ˇ over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "2", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
" brown
|
||||
over
|
||||
ˇe lazy dog"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_record_interrupted(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("ˇhello\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape", "escape"]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("ˇjhello\n", Mode::Normal);
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
|
||||
fn scroll(cx: &mut ViewContext<Workspace>, by: fn(c: Option<f32>) -> ScrollAmount) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let amount = by(vim.pop_number_operator(cx).map(|c| c as f32));
|
||||
let amount = by(vim.take_count(cx).map(|c| c as f32));
|
||||
vim.update_active_editor(cx, |editor, cx| scroll_editor(editor, &amount, cx));
|
||||
})
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
|
||||
Direction::Next
|
||||
};
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let count = vim.pop_number_operator(cx).unwrap_or(1);
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
@ -119,7 +119,7 @@ pub fn move_to_internal(
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let pane = workspace.active_pane().clone();
|
||||
let count = vim.pop_number_operator(cx).unwrap_or(1);
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
let search = search_bar.update(cx, |search_bar, cx| {
|
||||
@ -227,7 +227,7 @@ mod test {
|
||||
deterministic.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
let highlights = editor.all_background_highlights(cx);
|
||||
let highlights = editor.all_text_background_highlights(cx);
|
||||
assert_eq!(3, highlights.len());
|
||||
assert_eq!(
|
||||
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
|
||||
|
@ -11,7 +11,7 @@ pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
let count = vim.pop_number_operator(cx);
|
||||
let count = vim.take_count(cx);
|
||||
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
|
||||
})
|
||||
});
|
||||
@ -22,7 +22,7 @@ pub(crate) fn init(cx: &mut AppContext) {
|
||||
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
|
||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||
}
|
||||
let count = vim.pop_number_operator(cx);
|
||||
let count = vim.take_count(cx);
|
||||
substitute(vim, count, true, cx)
|
||||
})
|
||||
});
|
||||
|
@ -33,7 +33,6 @@ impl Default for Mode {
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||
pub enum Operator {
|
||||
Number(usize),
|
||||
Change,
|
||||
Delete,
|
||||
Yank,
|
||||
@ -47,6 +46,12 @@ pub enum Operator {
|
||||
pub struct EditorState {
|
||||
pub mode: Mode,
|
||||
pub last_mode: Mode,
|
||||
|
||||
/// pre_count is the number before an operator is specified (3 in 3d2d)
|
||||
pub pre_count: Option<usize>,
|
||||
/// post_count is the number after an operator is specified (2 in 3d2d)
|
||||
pub post_count: Option<usize>,
|
||||
|
||||
pub operator_stack: Vec<Operator>,
|
||||
}
|
||||
|
||||
@ -158,6 +163,10 @@ impl EditorState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_operator(&self) -> Option<Operator> {
|
||||
self.operator_stack.last().copied()
|
||||
}
|
||||
|
||||
pub fn keymap_context_layer(&self) -> KeymapContext {
|
||||
let mut context = KeymapContext::default();
|
||||
context.add_identifier("VimEnabled");
|
||||
@ -174,7 +183,13 @@ impl EditorState {
|
||||
context.add_identifier("VimControl");
|
||||
}
|
||||
|
||||
let active_operator = self.operator_stack.last();
|
||||
if self.active_operator().is_none() && self.pre_count.is_some()
|
||||
|| self.active_operator().is_some() && self.post_count.is_some()
|
||||
{
|
||||
context.add_identifier("VimCount");
|
||||
}
|
||||
|
||||
let active_operator = self.active_operator();
|
||||
|
||||
if let Some(active_operator) = active_operator {
|
||||
for context_flag in active_operator.context_flags().into_iter() {
|
||||
@ -194,7 +209,6 @@ impl EditorState {
|
||||
impl Operator {
|
||||
pub fn id(&self) -> &'static str {
|
||||
match self {
|
||||
Operator::Number(_) => "n",
|
||||
Operator::Object { around: false } => "i",
|
||||
Operator::Object { around: true } => "a",
|
||||
Operator::Change => "c",
|
||||
|
@ -190,7 +190,7 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
|
||||
search_bar.next_notification(&cx).await;
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
let highlights = editor.all_background_highlights(cx);
|
||||
let highlights = editor.all_text_background_highlights(cx);
|
||||
assert_eq!(3, highlights.len());
|
||||
assert_eq!(
|
||||
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
|
||||
@ -574,3 +574,47 @@ async fn test_folds(cx: &mut gpui::TestAppContext) {
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_clear_counts(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["4", "escape", "3", "d", "l"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick brown
|
||||
fox juˇ over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_zero(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["0"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
ˇThe quick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(["1", "0", "l"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
The quick ˇbrown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
}
|
||||
|
@ -13,20 +13,13 @@ use util::test::{generate_marked_text, marked_text_offsets};
|
||||
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
|
||||
use crate::state::Mode;
|
||||
|
||||
pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[
|
||||
ExemptionFeatures::DeletionOnEmptyLine,
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
];
|
||||
pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
|
||||
|
||||
/// Enum representing features we have tests for but which don't work, yet. Used
|
||||
/// to add exemptions and automatically
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum ExemptionFeatures {
|
||||
// MOTIONS
|
||||
// Deletions on empty lines miss some newlines
|
||||
DeletionOnEmptyLine,
|
||||
// When a motion fails, it should should not apply linewise operations
|
||||
OperatorAbortsOnFailedMotion,
|
||||
// When an operator completes at the end of the file, an extra newline is left
|
||||
OperatorLastNewlineRemains,
|
||||
// Deleting a word on an empty line doesn't remove the newline
|
||||
@ -68,6 +61,8 @@ pub struct NeovimBackedTestContext<'a> {
|
||||
|
||||
last_set_state: Option<String>,
|
||||
recent_keystrokes: Vec<String>,
|
||||
|
||||
is_dirty: bool,
|
||||
}
|
||||
|
||||
impl<'a> NeovimBackedTestContext<'a> {
|
||||
@ -81,6 +76,7 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||
|
||||
last_set_state: None,
|
||||
recent_keystrokes: Default::default(),
|
||||
is_dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,6 +124,7 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||
self.last_set_state = Some(marked_text.to_string());
|
||||
self.recent_keystrokes = Vec::new();
|
||||
self.neovim.set_state(marked_text).await;
|
||||
self.is_dirty = true;
|
||||
context_handle
|
||||
}
|
||||
|
||||
@ -153,6 +150,7 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||
}
|
||||
|
||||
pub async fn assert_shared_state(&mut self, marked_text: &str) {
|
||||
self.is_dirty = false;
|
||||
let marked_text = marked_text.replace("•", " ");
|
||||
let neovim = self.neovim_state().await;
|
||||
let editor = self.editor_state();
|
||||
@ -258,6 +256,7 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||
}
|
||||
|
||||
pub async fn assert_state_matches(&mut self) {
|
||||
self.is_dirty = false;
|
||||
let neovim = self.neovim_state().await;
|
||||
let editor = self.editor_state();
|
||||
let initial_state = self
|
||||
@ -383,6 +382,17 @@ impl<'a> DerefMut for NeovimBackedTestContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// a common mistake in tests is to call set_shared_state when
|
||||
// you mean asswert_shared_state. This notices that and lets
|
||||
// you know.
|
||||
impl<'a> Drop for NeovimBackedTestContext<'a> {
|
||||
fn drop(&mut self) {
|
||||
if self.is_dirty {
|
||||
panic!("Test context was dropped after set_shared_state before assert_shared_state")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
@ -15,8 +15,8 @@ use anyhow::Result;
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use editor::{movement, Editor, EditorMode, Event};
|
||||
use gpui::{
|
||||
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
|
||||
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action,
|
||||
AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use language::{CursorShape, Point, Selection, SelectionGoal};
|
||||
pub use mode_indicator::ModeIndicator;
|
||||
@ -40,9 +40,12 @@ pub struct SwitchMode(pub Mode);
|
||||
pub struct PushOperator(pub Operator);
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
struct Number(u8);
|
||||
struct Number(usize);
|
||||
|
||||
actions!(vim, [Tab, Enter]);
|
||||
actions!(
|
||||
vim,
|
||||
[Tab, Enter, Object, InnerObject, FindForward, FindBackward]
|
||||
);
|
||||
impl_actions!(vim, [Number, SwitchMode, PushOperator]);
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@ -70,7 +73,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
},
|
||||
);
|
||||
cx.add_action(|_: &mut Workspace, n: &Number, cx: _| {
|
||||
Vim::update(cx, |vim, cx| vim.push_number(n, cx));
|
||||
Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx));
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
|
||||
@ -225,23 +228,12 @@ impl Vim {
|
||||
let editor = self.active_editor.clone()?.upgrade(cx)?;
|
||||
Some(editor.update(cx, update))
|
||||
}
|
||||
// ~, shift-j, x, shift-x, p
|
||||
// shift-c, shift-d, shift-i, i, a, o, shift-o, s
|
||||
// c, d
|
||||
// r
|
||||
|
||||
// TODO: shift-j?
|
||||
//
|
||||
pub fn start_recording(&mut self, cx: &mut WindowContext) {
|
||||
if !self.workspace_state.replaying {
|
||||
self.workspace_state.recording = true;
|
||||
self.workspace_state.recorded_actions = Default::default();
|
||||
self.workspace_state.recorded_count =
|
||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||
Some(number)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.workspace_state.recorded_count = None;
|
||||
|
||||
let selections = self
|
||||
.active_editor
|
||||
@ -286,6 +278,16 @@ impl Vim {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_recording_immediately(&mut self, action: Box<dyn Action>) {
|
||||
if self.workspace_state.recording {
|
||||
self.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Action(action.boxed_clone()));
|
||||
self.workspace_state.recording = false;
|
||||
self.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_current_action(&mut self, cx: &mut WindowContext) {
|
||||
self.start_recording(cx);
|
||||
self.stop_recording();
|
||||
@ -300,6 +302,9 @@ impl Vim {
|
||||
state.mode = mode;
|
||||
state.operator_stack.clear();
|
||||
});
|
||||
if mode != Mode::Insert {
|
||||
self.take_count(cx);
|
||||
}
|
||||
|
||||
cx.emit_global(VimEvent::ModeChanged { mode });
|
||||
|
||||
@ -352,6 +357,39 @@ impl Vim {
|
||||
});
|
||||
}
|
||||
|
||||
fn push_count_digit(&mut self, number: usize, cx: &mut WindowContext) {
|
||||
if self.active_operator().is_some() {
|
||||
self.update_state(|state| {
|
||||
state.post_count = Some(state.post_count.unwrap_or(0) * 10 + number)
|
||||
})
|
||||
} else {
|
||||
self.update_state(|state| {
|
||||
state.pre_count = Some(state.pre_count.unwrap_or(0) * 10 + number)
|
||||
})
|
||||
}
|
||||
// update the keymap so that 0 works
|
||||
self.sync_vim_settings(cx)
|
||||
}
|
||||
|
||||
fn take_count(&mut self, cx: &mut WindowContext) -> Option<usize> {
|
||||
if self.workspace_state.replaying {
|
||||
return self.workspace_state.recorded_count;
|
||||
}
|
||||
|
||||
let count = if self.state().post_count == None && self.state().pre_count == None {
|
||||
return None;
|
||||
} else {
|
||||
Some(self.update_state(|state| {
|
||||
state.post_count.take().unwrap_or(1) * state.pre_count.take().unwrap_or(1)
|
||||
}))
|
||||
};
|
||||
if self.workspace_state.recording {
|
||||
self.workspace_state.recorded_count = count;
|
||||
}
|
||||
self.sync_vim_settings(cx);
|
||||
count
|
||||
}
|
||||
|
||||
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
|
||||
if matches!(
|
||||
operator,
|
||||
@ -363,15 +401,6 @@ impl Vim {
|
||||
self.sync_vim_settings(cx);
|
||||
}
|
||||
|
||||
fn push_number(&mut self, Number(number): &Number, cx: &mut WindowContext) {
|
||||
if let Some(Operator::Number(current_number)) = self.active_operator() {
|
||||
self.pop_operator(cx);
|
||||
self.push_operator(Operator::Number(current_number * 10 + *number as usize), cx);
|
||||
} else {
|
||||
self.push_operator(Operator::Number(*number as usize), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_pop_operator(&mut self) -> Option<Operator> {
|
||||
self.update_state(|state| state.operator_stack.pop())
|
||||
}
|
||||
@ -382,22 +411,8 @@ impl Vim {
|
||||
self.sync_vim_settings(cx);
|
||||
popped_operator
|
||||
}
|
||||
|
||||
fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
|
||||
if self.workspace_state.replaying {
|
||||
if let Some(number) = self.workspace_state.recorded_count {
|
||||
return Some(number);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||
self.pop_operator(cx);
|
||||
return Some(number);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn clear_operator(&mut self, cx: &mut WindowContext) {
|
||||
self.take_count(cx);
|
||||
self.update_state(|state| state.operator_stack.clear());
|
||||
self.sync_vim_settings(cx);
|
||||
}
|
||||
|
7
crates/vim/test_data/test_clear_counts.json
Normal file
7
crates/vim/test_data/test_clear_counts.json
Normal file
@ -0,0 +1,7 @@
|
||||
{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
|
||||
{"Key":"4"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"3"}
|
||||
{"Key":"d"}
|
||||
{"Key":"l"}
|
||||
{"Get":{"state":"The quick brown\nfox juˇ over\nthe lazy dog","mode":"Normal"}}
|
16
crates/vim/test_data/test_delete_with_counts.json
Normal file
16
crates/vim/test_data/test_delete_with_counts.json
Normal file
@ -0,0 +1,16 @@
|
||||
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"2"}
|
||||
{"Key":"d"}
|
||||
{"Get":{"state":"the ˇlazy dog","mode":"Normal"}}
|
||||
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"d"}
|
||||
{"Key":"d"}
|
||||
{"Get":{"state":"the ˇlazy dog","mode":"Normal"}}
|
||||
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe moon,\na star, and\nthe lazy dog"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"d"}
|
||||
{"Key":"2"}
|
||||
{"Key":"d"}
|
||||
{"Get":{"state":"the ˇlazy dog","mode":"Normal"}}
|
@ -35,4 +35,4 @@
|
||||
{"Key":"."}
|
||||
{"Put":{"state":"THE QUIˇck brown fox"}}
|
||||
{"Key":"."}
|
||||
{"Put":{"state":"THE QUICK ˇbrown fox"}}
|
||||
{"Get":{"state":"THE QUICK ˇbrown fox","mode":"Normal"}}
|
||||
|
36
crates/vim/test_data/test_insert_with_counts.json
Normal file
36
crates/vim/test_data/test_insert_with_counts.json
Normal file
@ -0,0 +1,36 @@
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"5"}
|
||||
{"Key":"i"}
|
||||
{"Key":"-"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"----ˇ-hello\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"5"}
|
||||
{"Key":"a"}
|
||||
{"Key":"-"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"h----ˇ-ello\n","mode":"Normal"}}
|
||||
{"Key":"4"}
|
||||
{"Key":"shift-i"}
|
||||
{"Key":"-"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"---ˇ-h-----ello\n","mode":"Normal"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-a"}
|
||||
{"Key":"-"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"----h-----ello--ˇ-\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"o"}
|
||||
{"Key":"o"}
|
||||
{"Key":"i"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"hello\noi\noi\noˇi\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-o"}
|
||||
{"Key":"o"}
|
||||
{"Key":"i"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"oi\noi\noˇi\nhello\n","mode":"Normal"}}
|
23
crates/vim/test_data/test_insert_with_repeat.json
Normal file
23
crates/vim/test_data/test_insert_with_repeat.json
Normal file
@ -0,0 +1,23 @@
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"i"}
|
||||
{"Key":"-"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"--ˇ-hello\n","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"----ˇ--hello\n","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"-----ˇ---hello\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇhello\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"o"}
|
||||
{"Key":"k"}
|
||||
{"Key":"k"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"hello\nkk\nkˇk\n","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"hello\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"hello\nkk\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}}
|
13
crates/vim/test_data/test_repeat_motion_counts.json
Normal file
13
crates/vim/test_data/test_repeat_motion_counts.json
Normal file
@ -0,0 +1,13 @@
|
||||
{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"d"}
|
||||
{"Key":"3"}
|
||||
{"Key":"l"}
|
||||
{"Get":{"state":"ˇ brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||
{"Key":"j"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":" brown\nˇ over\nthe lazy dog","mode":"Normal"}}
|
||||
{"Key":"j"}
|
||||
{"Key":"2"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":" brown\n over\nˇe lazy dog","mode":"Normal"}}
|
7
crates/vim/test_data/test_zero.json
Normal file
7
crates/vim/test_data/test_zero.json
Normal file
@ -0,0 +1,7 @@
|
||||
{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
|
||||
{"Key":"0"}
|
||||
{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"0"}
|
||||
{"Key":"l"}
|
||||
{"Get":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.104.0"
|
||||
version = "0.105.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
@ -13,10 +13,11 @@ version=$2
|
||||
export_vars_for_environment ${environment}
|
||||
image_id=$(image_id_for_version ${version})
|
||||
|
||||
export ZED_DO_CERTIFICATE_ID=$(doctl compute certificate list --format ID --no-header)
|
||||
export ZED_KUBE_NAMESPACE=${environment}
|
||||
export ZED_IMAGE_ID=${image_id}
|
||||
|
||||
target_zed_kube_cluster
|
||||
envsubst < crates/collab/k8s/manifest.template.yml | kubectl apply -f -
|
||||
|
||||
echo "deployed collab v${version} to ${environment}"
|
||||
echo "deployed collab v${version} to ${environment}"
|
||||
|
@ -36,6 +36,7 @@ export default function search(): any {
|
||||
left: 10,
|
||||
right: 4,
|
||||
},
|
||||
margin: { right: SEARCH_ROW_SPACING }
|
||||
}
|
||||
|
||||
const include_exclude_editor = {
|
||||
@ -201,7 +202,6 @@ export default function search(): any {
|
||||
},
|
||||
option_button_group: {
|
||||
padding: {
|
||||
left: SEARCH_ROW_SPACING,
|
||||
right: SEARCH_ROW_SPACING,
|
||||
},
|
||||
},
|
||||
@ -375,7 +375,11 @@ export default function search(): any {
|
||||
search_bar_row_height: 34,
|
||||
search_row_spacing: 8,
|
||||
option_button_height: 22,
|
||||
modes_container: {},
|
||||
modes_container: {
|
||||
padding: {
|
||||
right: SEARCH_ROW_SPACING,
|
||||
}
|
||||
},
|
||||
replace_icon: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "disabled"),
|
||||
|
Loading…
Reference in New Issue
Block a user