Compare commits
34 Commits
fix-deseri
...
diagnostic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3000f6ea22 | ||
|
|
377e24b798 | ||
|
|
a0c0f1ebcd | ||
|
|
70ce06cb95 | ||
|
|
9a5b97db00 | ||
|
|
0b75afd322 | ||
|
|
4fd698a093 | ||
|
|
b50846205c | ||
|
|
a574036efd | ||
|
|
89641acf2f | ||
|
|
611bf2d905 | ||
|
|
f476a8bc2a | ||
|
|
d3d0d01571 | ||
|
|
29d29f5a90 | ||
|
|
9824e40878 | ||
|
|
1ad8d6ab1c | ||
|
|
8745719687 | ||
|
|
c7c19609b3 | ||
|
|
428c143fbb | ||
|
|
f3460d440c | ||
|
|
071270fe88 | ||
|
|
a59dd7d06d | ||
|
|
868284876d | ||
|
|
6bbe9a2253 | ||
|
|
7a05db6d3d | ||
|
|
3587e9726b | ||
|
|
a96782cc6b | ||
|
|
0289c312c9 | ||
|
|
63a8095879 | ||
|
|
1768c0d996 | ||
|
|
27e9c68988 | ||
|
|
ad2ddf1200 | ||
|
|
d6e271c956 | ||
|
|
da29e33f50 |
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -88,9 +88,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alacritty_terminal"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6d1ea4484c8676f295307a4892d478c70ac8da1dbd8c7c10830a504b7f1022f"
|
||||
version = "0.24.1-dev"
|
||||
source = "git+https://github.com/alacritty/alacritty?rev=cacdb5bb3b72bad2c729227537979d95af75978f#cacdb5bb3b72bad2c729227537979d95af75978f"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"bitflags 2.4.2",
|
||||
@@ -107,7 +106,7 @@ dependencies = [
|
||||
"signal-hook",
|
||||
"unicode-width",
|
||||
"vte",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13152,7 +13151,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.139.0"
|
||||
version = "0.140.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"ib": "storage",
|
||||
"ico": "image",
|
||||
"ini": "settings",
|
||||
"inl": "cpp",
|
||||
"j2k": "image",
|
||||
"java": "java",
|
||||
"jfif": "image",
|
||||
|
||||
1
assets/icons/search_selection.svg
Normal file
1
assets/icons/search_selection.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>
|
||||
|
After Width: | Height: | Size: 299 B |
1
assets/icons/sparkle.svg
Normal file
1
assets/icons/sparkle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkle"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 481 B |
3
assets/icons/sparkle_filled.svg
Normal file
3
assets/icons/sparkle_filled.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.937 15.5C9.84772 15.1539 9.66734 14.8381 9.41462 14.5854C9.1619 14.3327 8.84607 14.1523 8.5 14.063L2.365 12.481C2.26033 12.4513 2.16821 12.3883 2.10261 12.3014C2.03702 12.2146 2.00153 12.1088 2.00153 12C2.00153 11.8912 2.03702 11.7854 2.10261 11.6986C2.16821 11.6118 2.26033 11.5487 2.365 11.519L8.5 9.93601C8.84595 9.84681 9.16169 9.66658 9.4144 9.41404C9.66711 9.16151 9.84757 8.84589 9.937 8.50001L11.519 2.36501C11.5484 2.25992 11.6114 2.16735 11.6983 2.1014C11.7853 2.03545 11.8914 1.99976 12.0005 1.99976C12.1096 1.99976 12.2157 2.03545 12.3027 2.1014C12.3896 2.16735 12.4526 2.25992 12.482 2.36501L14.063 8.50001C14.1523 8.84608 14.3327 9.1619 14.5854 9.41462C14.8381 9.66734 15.1539 9.84773 15.5 9.93701L21.635 11.518C21.7405 11.5471 21.8335 11.61 21.8998 11.6971C21.9661 11.7841 22.0021 11.8906 22.0021 12C22.0021 12.1094 21.9661 12.2159 21.8998 12.3029C21.8335 12.39 21.7405 12.4529 21.635 12.482L15.5 14.063C15.1539 14.1523 14.8381 14.3327 14.5854 14.5854C14.3327 14.8381 14.1523 15.1539 14.063 15.5L12.481 21.635C12.4516 21.7401 12.3886 21.8327 12.3017 21.8986C12.2147 21.9646 12.1086 22.0003 11.9995 22.0003C11.8904 22.0003 11.7843 21.9646 11.6973 21.8986C11.6104 21.8327 11.5474 21.7401 11.518 21.635L9.937 15.5Z" fill="black" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -204,18 +204,6 @@
|
||||
"alt-m": "assistant::ToggleModelSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
@@ -232,7 +220,8 @@
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"ctrl-h": "search::ToggleReplace"
|
||||
"ctrl-h": "search::ToggleReplace",
|
||||
"ctrl-l": "search::ToggleSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -296,6 +285,7 @@
|
||||
"ctrl-alt-g": "search::SelectNextMatch",
|
||||
"ctrl-alt-shift-g": "search::SelectPrevMatch",
|
||||
"ctrl-alt-shift-h": "search::ToggleReplace",
|
||||
"ctrl-alt-shift-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-w": "search::ToggleWholeWord",
|
||||
@@ -554,6 +544,18 @@
|
||||
"ctrl-enter": "assistant::InlineAssist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
|
||||
@@ -176,6 +176,12 @@
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-alt-l": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"selection_search_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-e": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
@@ -222,7 +228,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"cmd-enter": "assistant::Assist",
|
||||
"cmd-s": "workspace::Save",
|
||||
@@ -250,7 +256,8 @@
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"cmd-f": "search::FocusSearch",
|
||||
"cmd-alt-f": "search::ToggleReplace"
|
||||
"cmd-alt-f": "search::ToggleReplace",
|
||||
"cmd-alt-l": "search::ToggleSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -316,6 +323,7 @@
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"cmd-alt-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||
"alt-cmd-w": "search::ToggleWholeWord",
|
||||
|
||||
@@ -131,14 +131,7 @@
|
||||
// The default number of lines to expand excerpts in the multibuffer by.
|
||||
"expand_excerpt_lines": 3,
|
||||
// Globs to match against file paths to determine if a file is private.
|
||||
"private_files": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/secrets.yml"
|
||||
],
|
||||
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||
"use_on_type_format": true,
|
||||
@@ -164,6 +157,12 @@
|
||||
// "none"
|
||||
// 3. Draw all invisible symbols:
|
||||
// "all"
|
||||
// 4. Draw whitespaces at boundaries only:
|
||||
// "boundaries"
|
||||
// For a whitespace to be on a boundary, any of the following conditions need to be met:
|
||||
// - It is a tab
|
||||
// - It is adjacent to an edge (start or end)
|
||||
// - It is adjacent to a whitespace (left or right)
|
||||
"show_whitespaces": "selection",
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
@@ -453,7 +452,8 @@
|
||||
// Send anonymized usage data like what languages you're using Zed with.
|
||||
"metrics": true
|
||||
},
|
||||
// Automatically update Zed
|
||||
// Automatically update Zed. This setting may be ignored on Linux if
|
||||
// installed through a package manager.
|
||||
"auto_update": true,
|
||||
// Diagnostics configuration.
|
||||
"diagnostics": {
|
||||
@@ -697,7 +697,7 @@
|
||||
}
|
||||
},
|
||||
"JavaScript": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", ".."],
|
||||
"language_servers": ["typescript-language-server", "!vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -740,7 +740,7 @@
|
||||
}
|
||||
},
|
||||
"TSX": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", ".."],
|
||||
"language_servers": ["typescript-language-server", "!vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -751,7 +751,7 @@
|
||||
}
|
||||
},
|
||||
"TypeScript": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", ".."],
|
||||
"language_servers": ["typescript-language-server", "!vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod codegen;
|
||||
mod completion_provider;
|
||||
mod context_store;
|
||||
mod inline_assistant;
|
||||
mod model_selector;
|
||||
mod prompt_library;
|
||||
mod prompts;
|
||||
mod saved_conversation;
|
||||
mod search;
|
||||
mod slash_command;
|
||||
mod streaming_diff;
|
||||
@@ -17,9 +17,10 @@ use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
pub(crate) use context_store::*;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
pub(crate) use inline_assistant::*;
|
||||
pub(crate) use model_selector::*;
|
||||
pub(crate) use saved_conversation::*;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -31,6 +32,7 @@ use std::{
|
||||
fmt::{self, Display},
|
||||
sync::Arc,
|
||||
};
|
||||
pub(crate) use streaming_diff::*;
|
||||
use util::paths::EMBEDDINGS_DIR;
|
||||
|
||||
actions!(
|
||||
@@ -273,10 +275,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
.detach();
|
||||
|
||||
prompt_library::init(cx);
|
||||
completion_provider::init(client, cx);
|
||||
completion_provider::init(client.clone(), cx);
|
||||
assistant_slash_command::init(cx);
|
||||
register_slash_commands(cx);
|
||||
assistant_panel::init(cx);
|
||||
inline_assistant::init(client.telemetry().clone(), cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Assistant::NAMESPACE);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,704 +0,0 @@
|
||||
use crate::{
|
||||
streaming_diff::{Hunk, StreamingDiff},
|
||||
CompletionProvider, LanguageModelRequest,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{EventEmitter, Model, ModelContext, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use std::{cmp, future, ops::Range, sync::Arc, time::Instant};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
Finished,
|
||||
Undone,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CodegenKind {
|
||||
Transform { range: Range<Anchor> },
|
||||
Generate { position: Anchor },
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
buffer: Model<MultiBuffer>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
kind: CodegenKind,
|
||||
last_equal_ranges: Vec<Range<Anchor>>,
|
||||
transaction_id: Option<TransactionId>,
|
||||
error: Option<anyhow::Error>,
|
||||
generation: Task<()>,
|
||||
idle: bool,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Codegen {}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
kind: CodegenKind,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
Self {
|
||||
buffer: buffer.clone(),
|
||||
snapshot,
|
||||
kind,
|
||||
last_equal_ranges: Default::default(),
|
||||
transaction_id: Default::default(),
|
||||
error: Default::default(),
|
||||
idle: true,
|
||||
generation: Task::ready(()),
|
||||
telemetry,
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
_buffer: Model<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: LanguageModelRequest, 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(MultiBufferRow(selection_start.row)));
|
||||
|
||||
let model_telemetry_id = prompt.model.telemetry_id();
|
||||
let response = CompletionProvider::global(cx).complete(prompt);
|
||||
let telemetry = self.telemetry.clone();
|
||||
self.generation = cx.spawn(|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: Task<anyhow::Result<()>> =
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = strip_invalid_spans_from_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 {
|
||||
if response_latency.is_none() {
|
||||
response_latency = Some(request_start.elapsed());
|
||||
}
|
||||
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(())
|
||||
};
|
||||
|
||||
let result = diff.await;
|
||||
|
||||
let error_message =
|
||||
result.as_ref().err().map(|error| error.to_string());
|
||||
if let Some(telemetry) = telemetry {
|
||||
telemetry.report_assistant_event(
|
||||
None,
|
||||
telemetry_events::AssistantKind::Inline,
|
||||
model_telemetry_id,
|
||||
response_latency,
|
||||
error_message,
|
||||
);
|
||||
}
|
||||
|
||||
result?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
while let Some(hunks) = hunks_rx.next().await {
|
||||
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;
|
||||
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();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
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_invalid_spans_from_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_markdown_codeblock = false;
|
||||
let mut includes_start_or_end_span = 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 buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
|
||||
includes_start_or_end_span = true;
|
||||
|
||||
buffer = buffer
|
||||
.strip_prefix("<|S|>")
|
||||
.or_else(|| buffer.strip_prefix("<|S|"))
|
||||
.unwrap_or(&buffer)
|
||||
.to_string();
|
||||
} else if buffer.ends_with("|E|>") {
|
||||
includes_start_or_end_span = true;
|
||||
} else if buffer.starts_with("<|")
|
||||
|| buffer.starts_with("<|S")
|
||||
|| buffer.starts_with("<|S|")
|
||||
|| buffer.ends_with('|')
|
||||
|| buffer.ends_with("|E")
|
||||
|| buffer.ends_with("|E|")
|
||||
{
|
||||
return future::ready(None);
|
||||
}
|
||||
|
||||
if first_line {
|
||||
if buffer.is_empty() || buffer == "`" || buffer == "``" {
|
||||
return future::ready(None);
|
||||
} else if buffer.starts_with("```") {
|
||||
starts_with_markdown_codeblock = true;
|
||||
if let Some(newline_ix) = buffer.find('\n') {
|
||||
buffer.replace_range(..newline_ix + 1, "");
|
||||
first_line = false;
|
||||
} else {
|
||||
return future::ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut text = buffer.to_string();
|
||||
if starts_with_markdown_codeblock {
|
||||
text = text
|
||||
.strip_suffix("\n```\n")
|
||||
.or_else(|| text.strip_suffix("\n```"))
|
||||
.or_else(|| text.strip_suffix("\n``"))
|
||||
.or_else(|| text.strip_suffix("\n`"))
|
||||
.or_else(|| text.strip_suffix('\n'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
}
|
||||
|
||||
if includes_start_or_end_span {
|
||||
text = text
|
||||
.strip_suffix("|E|>")
|
||||
.or_else(|| text.strip_suffix("E|>"))
|
||||
.or_else(|| text.strip_prefix("|>"))
|
||||
.or_else(|| text.strip_prefix('>'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
};
|
||||
|
||||
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 std::sync::Arc;
|
||||
|
||||
use crate::FakeCompletionProvider;
|
||||
|
||||
use super::*;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
Point,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DummyCompletionRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
let x = 0;
|
||||
for _ in 0..10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_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, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(buffer.clone(), CodegenKind::Transform { range }, None, cx)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, 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.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.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,
|
||||
) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
le
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_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 codegen = cx.new_model(|cx| {
|
||||
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, 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.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.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,
|
||||
) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = concat!(
|
||||
"fn main() {\n",
|
||||
" \n",
|
||||
"}\n" //
|
||||
);
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_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 codegen = cx.new_model(|cx| {
|
||||
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, 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.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.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_invalid_spans_from_codeblock() {
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_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_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"``\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
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 rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
196
crates/assistant/src/context_store.rs
Normal file
196
crates/assistant/src/context_store.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Model, ModelContext, Task};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
|
||||
use ui::Context;
|
||||
use util::{paths::CONTEXTS_DIR, ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedContext {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
impl SavedContext {
|
||||
pub const VERSION: &'static str = "0.2.0";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedContextV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SavedContextMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
pub struct ContextStore {
|
||||
contexts_metadata: Vec<SavedContextMetadata>,
|
||||
fs: Arc<dyn Fs>,
|
||||
_watch_updates: Task<Option<()>>,
|
||||
}
|
||||
|
||||
impl ContextStore {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(&CONTEXTS_DIR, CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
|
||||
contexts_metadata: Vec::new(),
|
||||
fs,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
}),
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
Ok(this)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedContext>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let saved_context = fs.load(&path).await?;
|
||||
let saved_context_json = serde_json::from_str::<serde_json::Value>(&saved_context)?;
|
||||
match saved_context_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
SavedContext::VERSION => {
|
||||
Ok(serde_json::from_value::<SavedContext>(saved_context_json)?)
|
||||
}
|
||||
"0.1.0" => {
|
||||
let saved_context =
|
||||
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
|
||||
Ok(SavedContext {
|
||||
id: saved_context.id,
|
||||
zed: saved_context.zed,
|
||||
version: saved_context.version,
|
||||
text: saved_context.text,
|
||||
messages: saved_context.messages,
|
||||
message_metadata: saved_context.message_metadata,
|
||||
summary: saved_context.summary,
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved context")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
|
||||
let metadata = self.contexts_metadata.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
if query.is_empty() {
|
||||
metadata
|
||||
} else {
|
||||
let candidates = metadata
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| metadata[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
fs.create_dir(&CONTEXTS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONTEXTS_DIR).await?;
|
||||
let mut contexts = Vec::<SavedContextMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
// This is used to filter out contexts saved by the new assistant.
|
||||
if !re.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(title) = re.replace(file_name, "").lines().next() {
|
||||
contexts.push(SavedContextMetadata {
|
||||
title: title.to_string(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.contexts_metadata = contexts;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
1536
crates/assistant/src/inline_assistant.rs
Normal file
1536
crates/assistant/src/inline_assistant.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,43 +1,44 @@
|
||||
use crate::{
|
||||
slash_command::SlashCommandLine, CompletionProvider, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
slash_command::SlashCommandCompletionProvider, AssistantPanel, CompletionProvider,
|
||||
InlineAssist, InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use editor::{actions::Tab, Editor, EditorEvent};
|
||||
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorEvent};
|
||||
use futures::{
|
||||
future::{self, BoxFuture, Shared},
|
||||
FutureExt,
|
||||
};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
actions, point, size, AnyElement, AppContext, BackgroundExecutor, Bounds, DevicePixels,
|
||||
EventEmitter, Global, Model, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
|
||||
View, WindowBounds, WindowHandle, WindowOptions,
|
||||
actions, percentage, point, size, Animation, AnimationExt, AnyElement, AppContext,
|
||||
BackgroundExecutor, Bounds, DevicePixels, EventEmitter, Global, PromptLevel, ReadGlobal,
|
||||
Subscription, Task, TitlebarOptions, Transformation, UpdateGlobal, View, WindowBounds,
|
||||
WindowHandle, WindowOptions,
|
||||
};
|
||||
use heed::{types::SerdeBincode, Database, RoTxn};
|
||||
use language::{
|
||||
language_settings::SoftWrap, Buffer, Documentation, LanguageRegistry, LanguageServerId, Point,
|
||||
ToPoint as _,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
|
||||
use parking_lot::RwLock;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use rope::Rope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
future::Future,
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
div, prelude::*, IconButtonShape, ListHeader, ListItem, ListItemSpacing, ListSubHeader,
|
||||
ParentElement, Render, SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
|
||||
};
|
||||
use util::{paths::PROMPTS_DIR, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(
|
||||
prompt_library,
|
||||
@@ -127,7 +128,7 @@ struct PromptPickerDelegate {
|
||||
}
|
||||
|
||||
enum PromptPickerEvent {
|
||||
Selected { prompt_id: PromptId },
|
||||
Selected { prompt_id: Option<PromptId> },
|
||||
Confirmed { prompt_id: PromptId },
|
||||
Deleted { prompt_id: PromptId },
|
||||
ToggledDefault { prompt_id: PromptId },
|
||||
@@ -166,11 +167,14 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
if let Some(PromptPickerEntry::Prompt(prompt)) = self.entries.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Selected {
|
||||
prompt_id: prompt.id,
|
||||
});
|
||||
}
|
||||
let prompt_id = if let Some(PromptPickerEntry::Prompt(prompt)) =
|
||||
self.entries.get(self.selected_index)
|
||||
{
|
||||
Some(prompt.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
cx.emit(PromptPickerEvent::Selected { prompt_id });
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
@@ -248,7 +252,11 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
let element = match prompt {
|
||||
PromptPickerEntry::DefaultPromptsHeader => ListHeader::new("Default Prompts")
|
||||
.inset(true)
|
||||
.start_slot(Icon::new(IconName::ZedAssistant))
|
||||
.start_slot(
|
||||
Icon::new(IconName::Sparkle)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.selected(selected)
|
||||
.into_any_element(),
|
||||
PromptPickerEntry::DefaultPromptsEmpty => {
|
||||
@@ -259,7 +267,11 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
}
|
||||
PromptPickerEntry::AllPromptsHeader => ListHeader::new("All Prompts")
|
||||
.inset(true)
|
||||
.start_slot(Icon::new(IconName::Library))
|
||||
.start_slot(
|
||||
Icon::new(IconName::Library)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.selected(selected)
|
||||
.into_any_element(),
|
||||
PromptPickerEntry::AllPromptsEmpty => ListSubHeader::new("No prompts")
|
||||
@@ -273,14 +285,15 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(Label::new(
|
||||
.child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
|
||||
prompt.title.clone().unwrap_or("Untitled".into()),
|
||||
))
|
||||
)))
|
||||
.end_hover_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
@@ -288,30 +301,24 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"toggle-default-prompt",
|
||||
if default {
|
||||
IconName::ZedAssistantFilled
|
||||
} else {
|
||||
IconName::ZedAssistant
|
||||
},
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(
|
||||
move |_, _, cx| {
|
||||
IconButton::new("toggle-default-prompt", IconName::Sparkle)
|
||||
.selected(default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if default { Color::Accent } else { Color::Muted })
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
},
|
||||
)),
|
||||
})),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
@@ -319,6 +326,18 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
};
|
||||
Some(element)
|
||||
}
|
||||
|
||||
fn render_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Picker<Self>>) -> Div {
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.mx_2()
|
||||
.child(editor.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptLibrary {
|
||||
@@ -357,7 +376,11 @@ impl PromptLibrary {
|
||||
) {
|
||||
match event {
|
||||
PromptPickerEvent::Selected { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, false, cx);
|
||||
if let Some(prompt_id) = *prompt_id {
|
||||
self.load_prompt(prompt_id, false, cx);
|
||||
} else {
|
||||
self.focus_picker(&Default::default(), cx);
|
||||
}
|
||||
}
|
||||
PromptPickerEvent::Confirmed { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, true, cx);
|
||||
@@ -482,6 +505,7 @@ impl PromptLibrary {
|
||||
self.set_active_prompt(Some(prompt_id), cx);
|
||||
} else {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let commands = SlashCommandRegistry::global(cx);
|
||||
let prompt = self.store.load(prompt_id);
|
||||
self.pending_load = cx.spawn(|this, mut cx| async move {
|
||||
let prompt = prompt.await;
|
||||
@@ -500,8 +524,10 @@ impl PromptLibrary {
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor
|
||||
.set_completion_provider(Box::new(SlashCommandCompletionProvider));
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.set_completion_provider(Box::new(
|
||||
SlashCommandCompletionProvider::new(commands, None, None),
|
||||
));
|
||||
if focus {
|
||||
editor.focus(cx);
|
||||
}
|
||||
@@ -604,6 +630,49 @@ impl PromptLibrary {
|
||||
self.picker.update(cx, |picker, cx| picker.focus(cx));
|
||||
}
|
||||
|
||||
pub fn inline_assist(&mut self, _: &InlineAssist, cx: &mut ViewContext<Self>) {
|
||||
let Some(active_prompt_id) = self.active_prompt_id else {
|
||||
cx.propagate();
|
||||
return;
|
||||
};
|
||||
|
||||
let prompt_editor = &self.prompt_editors[&active_prompt_id].editor;
|
||||
let provider = CompletionProvider::global(cx);
|
||||
if provider.is_authenticated() {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&prompt_editor, None, false, cx)
|
||||
})
|
||||
} else {
|
||||
for window in cx.windows() {
|
||||
if let Some(workspace) = window.downcast::<Workspace>() {
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
cx.activate_window();
|
||||
workspace.focus_panel::<AssistantPanel>(cx)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
if panel.is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_last_inline_assist(
|
||||
&mut self,
|
||||
_: &editor::actions::Cancel,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let canceled = InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.cancel_last_inline_assist(cx)
|
||||
});
|
||||
if !canceled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_event(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
@@ -695,14 +764,13 @@ impl PromptLibrary {
|
||||
.child(
|
||||
h_flex()
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.h(TitleBar::height(cx))
|
||||
.w_full()
|
||||
.flex_none()
|
||||
.justify_end()
|
||||
.child(
|
||||
IconButton::new("new-prompt", IconName::Plus)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
|
||||
.on_click(|_, cx| {
|
||||
@@ -724,19 +792,30 @@ impl PromptLibrary {
|
||||
.flex_none()
|
||||
.min_w_64()
|
||||
.children(self.active_prompt_id.and_then(|prompt_id| {
|
||||
let buffer_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
|
||||
let prompt_metadata = self.store.metadata(prompt_id)?;
|
||||
let prompt_editor = &self.prompt_editors[&prompt_id];
|
||||
let focus_handle = prompt_editor.editor.focus_handle(cx);
|
||||
let current_model = CompletionProvider::global(cx).model();
|
||||
let token_count = prompt_editor.token_count.map(|count| count.to_string());
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.id("prompt-editor-inner")
|
||||
.size_full()
|
||||
.items_start()
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.focus(&focus_handle);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.on_action(cx.listener(Self::focus_picker))
|
||||
.on_action(cx.listener(Self::inline_assist))
|
||||
.on_action(cx.listener(Self::cancel_last_inline_assist))
|
||||
.flex_grow()
|
||||
.h_full()
|
||||
.pt(Spacing::Large.rems(cx))
|
||||
.pl(Spacing::Large.rems(cx))
|
||||
.pt(Spacing::XXLarge.rems(cx))
|
||||
.pl(Spacing::XXLarge.rems(cx))
|
||||
.child(prompt_editor.editor.clone()),
|
||||
)
|
||||
.child(
|
||||
@@ -744,49 +823,92 @@ impl PromptLibrary {
|
||||
.w_12()
|
||||
.py(Spacing::Large.rems(cx))
|
||||
.justify_start()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"toggle-default-prompt",
|
||||
if prompt_metadata.default {
|
||||
IconName::ZedAssistantFilled
|
||||
} else {
|
||||
IconName::ZedAssistant
|
||||
},
|
||||
)
|
||||
.size(ButtonSize::Large)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
if prompt_metadata.default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
&ToggleDefaultPrompt,
|
||||
cx,
|
||||
.items_end()
|
||||
.gap_1()
|
||||
.child(h_flex().h_8().font_family(buffer_font).when_some_else(
|
||||
token_count,
|
||||
|tokens_ready, token_count| {
|
||||
tokens_ready.pr_3().justify_end().child(
|
||||
// This isn't actually a button, it just let's us easily add
|
||||
// a tooltip to the token count.
|
||||
Button::new("token_count", token_count.clone())
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!("{} tokens", token_count,),
|
||||
None,
|
||||
format!(
|
||||
"Model: {}",
|
||||
current_model.display_name()
|
||||
),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(ToggleDefaultPrompt));
|
||||
}),
|
||||
},
|
||||
|tokens_loading| {
|
||||
tokens_loading.w_12().justify_center().child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(4)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(
|
||||
percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
))
|
||||
.child(
|
||||
h_flex().justify_center().w_12().h_8().child(
|
||||
IconButton::new("toggle-default-prompt", IconName::Sparkle)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.selected(prompt_metadata.default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if prompt_metadata.default {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if prompt_metadata.default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(ToggleDefaultPrompt));
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action("Delete Prompt", &DeletePrompt, cx)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(DeletePrompt));
|
||||
}),
|
||||
)
|
||||
.children(prompt_editor.token_count.map(|token_count| {
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.child(Label::new(token_count.to_string()))
|
||||
})),
|
||||
h_flex().justify_center().w_12().h_8().child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.size(ButtonSize::Large)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Prompt",
|
||||
&DeletePrompt,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(DeletePrompt));
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}))
|
||||
@@ -795,6 +917,14 @@ impl PromptLibrary {
|
||||
|
||||
impl Render for PromptLibrary {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let (ui_font, ui_font_size) = {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
(theme_settings.ui_font.clone(), theme_settings.ui_font_size)
|
||||
};
|
||||
|
||||
let theme = cx.theme().clone();
|
||||
cx.set_rem_size(ui_font_size);
|
||||
|
||||
h_flex()
|
||||
.id("prompt-manager")
|
||||
.key_context("PromptLibrary")
|
||||
@@ -805,6 +935,8 @@ impl Render for PromptLibrary {
|
||||
}))
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.font(ui_font)
|
||||
.text_color(theme.colors().text)
|
||||
.child(self.render_prompt_list(cx))
|
||||
.child(self.render_active_prompt(cx))
|
||||
}
|
||||
@@ -1092,123 +1224,3 @@ fn title_from_body(body: impl IntoIterator<Item = char>) -> Option<SharedString>
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct SlashCommandCompletionProvider;
|
||||
|
||||
impl editor::CompletionProvider for SlashCommandCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let Some((command_name, name_range)) = buffer.update(cx, |buffer, _cx| {
|
||||
let position = buffer_position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
let line = lines.next()?;
|
||||
let call = SlashCommandLine::parse(line)?;
|
||||
|
||||
if call.argument.is_some() {
|
||||
// Don't autocomplete arguments.
|
||||
None
|
||||
} else {
|
||||
let name = line[call.name.clone()].to_string();
|
||||
let name_range_start = Point::new(position.row, call.name.start as u32);
|
||||
let name_range_end = Point::new(position.row, call.name.end as u32);
|
||||
let name_range =
|
||||
buffer.anchor_after(name_range_start)..buffer.anchor_after(name_range_end);
|
||||
Some((name, name_range))
|
||||
}
|
||||
}) else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
let commands = SlashCommandRegistry::global(cx);
|
||||
let candidates = commands
|
||||
.command_names()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, def)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: def.to_string(),
|
||||
char_bag: def.as_ref().into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let command_name = command_name.to_string();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let matches = match_strings(
|
||||
&candidates,
|
||||
&command_name,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
cx.update(|cx| {
|
||||
matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| {
|
||||
let command = commands.command(&mat.string)?;
|
||||
let mut new_text = mat.string.clone();
|
||||
let requires_argument = command.requires_argument();
|
||||
if requires_argument {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
Some(project::Completion {
|
||||
old_range: name_range.clone(),
|
||||
documentation: Some(Documentation::SingleLine(command.description())),
|
||||
new_text,
|
||||
label: command.label(cx),
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: false,
|
||||
confirm: None,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
_: Model<Buffer>,
|
||||
_: Vec<usize>,
|
||||
_: Arc<RwLock<Box<[project::Completion]>>>,
|
||||
_: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<bool>> {
|
||||
Task::ready(Ok(true))
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
_: Model<Buffer>,
|
||||
_: project::Completion,
|
||||
_: bool,
|
||||
_: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: language::Anchor,
|
||||
_text: &str,
|
||||
_trigger_in_words: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
let buffer = buffer.read(cx);
|
||||
let position = position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
if let Some(line) = lines.next() {
|
||||
SlashCommandLine::parse(line).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedConversation {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
pub const VERSION: &'static str = "0.2.0";
|
||||
|
||||
pub async fn load(path: &Path, fs: &dyn Fs) -> Result<Self> {
|
||||
let saved_conversation = fs.load(path).await?;
|
||||
let saved_conversation_json =
|
||||
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
|
||||
match saved_conversation_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
Self::VERSION => Ok(serde_json::from_value::<Self>(saved_conversation_json)?),
|
||||
"0.1.0" => {
|
||||
let saved_conversation =
|
||||
serde_json::from_value::<SavedConversationV0_1_0>(saved_conversation_json)?;
|
||||
Ok(Self {
|
||||
id: saved_conversation.id,
|
||||
zed: saved_conversation.zed,
|
||||
version: saved_conversation.version,
|
||||
text: saved_conversation.text,
|
||||
messages: saved_conversation.messages,
|
||||
message_metadata: saved_conversation.message_metadata,
|
||||
summary: saved_conversation.summary,
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"unrecognized saved conversation version: {}",
|
||||
version
|
||||
)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved conversation")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversationV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
}
|
||||
|
||||
pub struct SavedConversationMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
// This is used to filter out conversations saved by the new assistant.
|
||||
if !re.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::assistant_panel::ConversationEditor;
|
||||
use crate::assistant_panel::ContextEditor;
|
||||
use anyhow::Result;
|
||||
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
|
||||
use editor::{CompletionProvider, Editor};
|
||||
@@ -27,10 +27,10 @@ pub mod search_command;
|
||||
pub mod tabs_command;
|
||||
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
editor: WeakView<ConversationEditor>,
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandLine {
|
||||
@@ -42,9 +42,9 @@ pub(crate) struct SlashCommandLine {
|
||||
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
editor: WeakView<ConversationEditor>,
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
workspace: WeakView<Workspace>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
@@ -98,6 +98,30 @@ impl SlashCommandCompletionProvider {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
let confirm = editor.clone().zip(workspace.clone()).and_then(
|
||||
|(editor, workspace)| {
|
||||
(!requires_argument).then(|| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
None,
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}) as Arc<_>
|
||||
})
|
||||
},
|
||||
);
|
||||
Some(project::Completion {
|
||||
old_range: name_range.clone(),
|
||||
documentation: Some(Documentation::SingleLine(command.description())),
|
||||
@@ -106,26 +130,7 @@ impl SlashCommandCompletionProvider {
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: requires_argument,
|
||||
confirm: (!requires_argument).then(|| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
None,
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}) as Arc<_>
|
||||
}),
|
||||
confirm,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -160,34 +165,42 @@ impl SlashCommandCompletionProvider {
|
||||
Ok(completions
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|arg| project::Completion {
|
||||
old_range: argument_range.clone(),
|
||||
label: CodeLabel::plain(arg.clone(), None),
|
||||
new_text: arg.clone(),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: false,
|
||||
confirm: Some(Arc::new({
|
||||
let command_name = command_name.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
move |cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
Some(&arg),
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})),
|
||||
.map(|command_argument| {
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
Arc::new({
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
let command_argument = command_argument.clone();
|
||||
move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
Some(&command_argument),
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}) as Arc<_>
|
||||
});
|
||||
project::Completion {
|
||||
old_range: argument_range.clone(),
|
||||
label: CodeLabel::plain(command_argument.clone(), None),
|
||||
new_text: command_argument.clone(),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: false,
|
||||
confirm,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
|
||||
@@ -34,7 +34,7 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
|
||||
@@ -62,7 +62,7 @@ impl SlashCommand for FetchSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
|
||||
@@ -101,10 +101,10 @@ impl SlashCommand for FileSlashCommand {
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
|
||||
@@ -31,7 +31,7 @@ impl SlashCommand for PromptSlashCommand {
|
||||
&self,
|
||||
query: String,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
|
||||
@@ -119,7 +119,7 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
|
||||
@@ -47,7 +47,7 @@ impl SlashCommand for SearchSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
|
||||
@@ -32,7 +32,7 @@ impl SlashCommand for TabsSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<std::sync::atomic::AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
|
||||
@@ -25,7 +25,7 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
&self,
|
||||
query: String,
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>>;
|
||||
fn requires_argument(&self) -> bool;
|
||||
|
||||
@@ -23,7 +23,10 @@ use smol::{fs::File, process::Command};
|
||||
use http::{HttpClient, HttpClientWithUrl};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use std::{
|
||||
env::consts::{ARCH, OS},
|
||||
env::{
|
||||
self,
|
||||
consts::{ARCH, OS},
|
||||
},
|
||||
ffi::OsString,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
@@ -138,20 +141,24 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
let auto_updater = cx.new_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client);
|
||||
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
if option_env!("ZED_UPDATE_EXPLANATION").is_none()
|
||||
&& env::var("ZED_UPDATE_EXPLANATION").is_err()
|
||||
{
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
}
|
||||
} else {
|
||||
update_subscription.take();
|
||||
}
|
||||
} else {
|
||||
update_subscription.take();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
updater
|
||||
});
|
||||
@@ -159,6 +166,26 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Zed was installed via a package manager.",
|
||||
Some(message),
|
||||
&["Ok"],
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(message) = env::var("ZED_UPDATE_EXPLANATION").ok() {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Zed was installed via a package manager.",
|
||||
Some(&message),
|
||||
&["Ok"],
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
} else {
|
||||
@@ -342,16 +369,6 @@ impl AutoUpdater {
|
||||
}
|
||||
|
||||
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
||||
// Skip auto-update for flatpaks
|
||||
#[cfg(target_os = "linux")]
|
||||
if matches!(std::env::var("ZED_IS_FLATPAK_INSTALL"), Ok(_)) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (client, current_version) = this.read_with(&cx, |this, _| {
|
||||
(this.http_client.clone(), this.current_version)
|
||||
})?;
|
||||
@@ -509,7 +526,7 @@ async fn install_release_linux(
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
|
||||
let home_dir = PathBuf::from(std::env::var("HOME").context("no HOME env var set")?);
|
||||
let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
|
||||
|
||||
let extracted = temp_dir.path().join("zed");
|
||||
fs::create_dir_all(&extracted)
|
||||
|
||||
@@ -267,7 +267,7 @@ impl Room {
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(room),
|
||||
Err(error) => Err(anyhow!("room creation failed: {:?}", error)),
|
||||
Err(error) => Err(error.context("room creation failed")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -314,7 +314,7 @@ mod flatpak {
|
||||
if let Some(flatpak_dir) = get_flatpak_dir() {
|
||||
let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
|
||||
args.append(&mut get_xdg_env_args());
|
||||
args.push("--env=ZED_IS_FLATPAK_INSTALL=1".into());
|
||||
args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
|
||||
args.push(
|
||||
format!(
|
||||
"--env={EXTRA_LIB_ENV_NAME}={}",
|
||||
@@ -347,7 +347,7 @@ mod flatpak {
|
||||
{
|
||||
if args.zed.is_none() {
|
||||
args.zed = Some("/app/libexec/zed-editor".into());
|
||||
env::set_var("ZED_IS_FLATPAK_INSTALL", "1");
|
||||
env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed");
|
||||
}
|
||||
}
|
||||
args
|
||||
|
||||
@@ -69,7 +69,6 @@ struct TestPlan<T: RandomizedTest> {
|
||||
pub struct UserTestPlan {
|
||||
pub user_id: UserId,
|
||||
pub username: String,
|
||||
pub allow_client_reconnection: bool,
|
||||
pub allow_client_disconnection: bool,
|
||||
next_root_id: usize,
|
||||
operation_ix: usize,
|
||||
@@ -237,7 +236,6 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
next_root_id: 0,
|
||||
operation_ix: 0,
|
||||
allow_client_disconnection,
|
||||
allow_client_reconnection,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -129,10 +129,10 @@ where
|
||||
impl Clamp for RGBAColor {
|
||||
fn clamp(self) -> Self {
|
||||
RGBAColor {
|
||||
r: self.r.min(1.0).max(0.0),
|
||||
g: self.g.min(1.0).max(0.0),
|
||||
b: self.b.min(1.0).max(0.0),
|
||||
a: self.a.min(1.0).max(0.0),
|
||||
r: self.r.clamp(0., 1.),
|
||||
g: self.g.clamp(0., 1.),
|
||||
b: self.b.clamp(0., 1.),
|
||||
a: self.a.clamp(0., 1.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +289,7 @@ gpui::actions!(
|
||||
ToggleLineNumbers,
|
||||
ToggleIndentGuides,
|
||||
ToggleSoftWrap,
|
||||
ToggleTabBar,
|
||||
Transpose,
|
||||
Undo,
|
||||
UndoSelection,
|
||||
|
||||
@@ -277,8 +277,55 @@ impl DisplayMap {
|
||||
block_map.insert(blocks)
|
||||
}
|
||||
|
||||
pub fn replace_blocks(&mut self, styles: HashMap<BlockId, RenderBlock>) {
|
||||
self.block_map.replace(styles);
|
||||
pub fn replace_blocks(
|
||||
&mut self,
|
||||
heights_and_renderers: HashMap<BlockId, (Option<u8>, RenderBlock)>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
//
|
||||
// Note: previous implementation of `replace_blocks` simply called
|
||||
// `self.block_map.replace(styles)` which just modified the render by replacing
|
||||
// the `RenderBlock` with the new one.
|
||||
//
|
||||
// ```rust
|
||||
// for block in &self.blocks {
|
||||
// if let Some(render) = renderers.remove(&block.id) {
|
||||
// *block.render.lock() = render;
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// If height changes however, we need to update the tree. There's a performance
|
||||
// cost to this, so we'll split the replace blocks into handling the old behavior
|
||||
// directly and the new behavior separately.
|
||||
//
|
||||
//
|
||||
let mut only_renderers = HashMap::<BlockId, RenderBlock>::default();
|
||||
let mut full_replace = HashMap::<BlockId, (u8, RenderBlock)>::default();
|
||||
for (id, (height, render)) in heights_and_renderers {
|
||||
if let Some(height) = height {
|
||||
full_replace.insert(id, (height, render));
|
||||
} else {
|
||||
only_renderers.insert(id, render);
|
||||
}
|
||||
}
|
||||
self.block_map.replace_renderers(only_renderers);
|
||||
|
||||
if full_replace.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
let mut block_map = self.block_map.write(snapshot, edits);
|
||||
block_map.replace(full_replace);
|
||||
}
|
||||
|
||||
pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
|
||||
|
||||
@@ -467,8 +467,8 @@ impl BlockMap {
|
||||
*transforms = new_transforms;
|
||||
}
|
||||
|
||||
pub fn replace(&mut self, mut renderers: HashMap<BlockId, RenderBlock>) {
|
||||
for block in &self.blocks {
|
||||
pub fn replace_renderers(&mut self, mut renderers: HashMap<BlockId, RenderBlock>) {
|
||||
for block in &mut self.blocks {
|
||||
if let Some(render) = renderers.remove(&block.id) {
|
||||
*block.render.lock() = render;
|
||||
}
|
||||
@@ -659,6 +659,48 @@ impl<'a> BlockMapWriter<'a> {
|
||||
ids
|
||||
}
|
||||
|
||||
pub fn replace(&mut self, mut heights_and_renderers: HashMap<BlockId, (u8, RenderBlock)>) {
|
||||
let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
|
||||
let buffer = wrap_snapshot.buffer_snapshot();
|
||||
let mut edits = Patch::default();
|
||||
let mut last_block_buffer_row = None;
|
||||
|
||||
for block in &mut self.0.blocks {
|
||||
if let Some((new_height, render)) = heights_and_renderers.remove(&block.id) {
|
||||
if block.height != new_height {
|
||||
let new_block = Block {
|
||||
id: block.id,
|
||||
position: block.position,
|
||||
height: new_height,
|
||||
style: block.style,
|
||||
render: Mutex::new(render),
|
||||
disposition: block.disposition,
|
||||
};
|
||||
*block = Arc::new(new_block);
|
||||
|
||||
let buffer_row = block.position.to_point(buffer).row;
|
||||
if last_block_buffer_row != Some(buffer_row) {
|
||||
last_block_buffer_row = Some(buffer_row);
|
||||
let wrap_row = wrap_snapshot
|
||||
.make_wrap_point(Point::new(buffer_row, 0), Bias::Left)
|
||||
.row();
|
||||
let start_row =
|
||||
wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0));
|
||||
let end_row = wrap_snapshot
|
||||
.next_row_boundary(WrapPoint::new(wrap_row, 0))
|
||||
.unwrap_or(wrap_snapshot.max_point().row() + 1);
|
||||
edits.push(Edit {
|
||||
old: start_row..end_row,
|
||||
new: start_row..end_row,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.0.sync(wrap_snapshot, edits);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, block_ids: HashSet<BlockId>) {
|
||||
let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
|
||||
let buffer = wrap_snapshot.buffer_snapshot();
|
||||
@@ -1305,6 +1347,111 @@ mod tests {
|
||||
assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_with_heights(cx: &mut gpui::TestAppContext) {
|
||||
let _update = cx.update(|cx| init_test(cx));
|
||||
|
||||
let text = "aaa\nbbb\nccc\nddd";
|
||||
|
||||
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
|
||||
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
|
||||
let _subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
|
||||
let (_wrap_map, wraps_snapshot) =
|
||||
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
|
||||
|
||||
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
let block_ids = writer.insert(vec![
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 0)),
|
||||
height: 1,
|
||||
disposition: BlockDisposition::Above,
|
||||
render: Box::new(|_| div().into_any()),
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 2)),
|
||||
height: 2,
|
||||
disposition: BlockDisposition::Above,
|
||||
render: Box::new(|_| div().into_any()),
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(3, 3)),
|
||||
height: 3,
|
||||
disposition: BlockDisposition::Below,
|
||||
render: Box::new(|_| div().into_any()),
|
||||
},
|
||||
]);
|
||||
|
||||
{
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
|
||||
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
|
||||
let mut hash_map = HashMap::default();
|
||||
let render: RenderBlock = Box::new(|_| div().into_any());
|
||||
hash_map.insert(block_ids[0], (2_u8, render));
|
||||
block_map_writer.replace(hash_map);
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
|
||||
{
|
||||
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
|
||||
let mut hash_map = HashMap::default();
|
||||
let render: RenderBlock = Box::new(|_| div().into_any());
|
||||
hash_map.insert(block_ids[0], (1_u8, render));
|
||||
block_map_writer.replace(hash_map);
|
||||
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
|
||||
{
|
||||
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
|
||||
let mut hash_map = HashMap::default();
|
||||
let render: RenderBlock = Box::new(|_| div().into_any());
|
||||
hash_map.insert(block_ids[0], (0_u8, render));
|
||||
block_map_writer.replace(hash_map);
|
||||
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
|
||||
{
|
||||
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
|
||||
let mut hash_map = HashMap::default();
|
||||
let render: RenderBlock = Box::new(|_| div().into_any());
|
||||
hash_map.insert(block_ids[0], (3_u8, render));
|
||||
block_map_writer.replace(hash_map);
|
||||
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
|
||||
{
|
||||
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
|
||||
let mut hash_map = HashMap::default();
|
||||
let render: RenderBlock = Box::new(|_| div().into_any());
|
||||
hash_map.insert(block_ids[0], (3_u8, render));
|
||||
block_map_writer.replace(hash_map);
|
||||
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
// Same height as before, should remain the same
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| init_test(cx));
|
||||
|
||||
@@ -53,8 +53,7 @@ use convert_case::{Case, Casing};
|
||||
use debounced_delay::DebouncedDelay;
|
||||
use display_map::*;
|
||||
pub use display_map::{DisplayPoint, FoldPlaceholder};
|
||||
use editor_settings::CurrentLineHighlight;
|
||||
pub use editor_settings::EditorSettings;
|
||||
pub use editor_settings::{CurrentLineHighlight, EditorSettings};
|
||||
use element::LineWithInvisibles;
|
||||
pub use element::{
|
||||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
||||
@@ -112,7 +111,7 @@ use rpc::{proto::*, ErrorExt};
|
||||
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
|
||||
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use smallvec::SmallVec;
|
||||
use snippet::Snippet;
|
||||
use std::ops::Not as _;
|
||||
@@ -144,7 +143,7 @@ use workspace::notifications::{DetachAndPromptErr, NotificationId};
|
||||
use workspace::{
|
||||
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
use workspace::{OpenInTerminal, OpenTerminal, Toast};
|
||||
use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
|
||||
|
||||
use crate::hover_links::find_url;
|
||||
|
||||
@@ -480,7 +479,7 @@ pub struct Editor {
|
||||
pending_rename: Option<RenameState>,
|
||||
searchable: bool,
|
||||
cursor_shape: CursorShape,
|
||||
current_line_highlight: CurrentLineHighlight,
|
||||
current_line_highlight: Option<CurrentLineHighlight>,
|
||||
collapse_matches: bool,
|
||||
autoindent_mode: Option<AutoindentMode>,
|
||||
workspace: Option<(WeakView<Workspace>, Option<WorkspaceId>)>,
|
||||
@@ -523,6 +522,7 @@ pub struct Editor {
|
||||
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
|
||||
tasks_update_task: Option<Task<()>>,
|
||||
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -1768,7 +1768,7 @@ impl Editor {
|
||||
pending_rename: Default::default(),
|
||||
searchable: true,
|
||||
cursor_shape: Default::default(),
|
||||
current_line_highlight: EditorSettings::get_global(cx).current_line_highlight,
|
||||
current_line_highlight: None,
|
||||
autoindent_mode: Some(AutoindentMode::EachLine),
|
||||
collapse_matches: false,
|
||||
workspace: None,
|
||||
@@ -1825,6 +1825,7 @@ impl Editor {
|
||||
}),
|
||||
],
|
||||
tasks_update_task: None,
|
||||
previous_search_ranges: None,
|
||||
};
|
||||
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
@@ -1992,7 +1993,9 @@ impl Editor {
|
||||
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
|
||||
placeholder_text: self.placeholder_text.clone(),
|
||||
is_focused: self.focus_handle.is_focused(cx),
|
||||
current_line_highlight: self.current_line_highlight,
|
||||
current_line_highlight: self
|
||||
.current_line_highlight
|
||||
.unwrap_or_else(|| EditorSettings::get_global(cx).current_line_highlight),
|
||||
gutter_hovered: self.gutter_hovered,
|
||||
}
|
||||
}
|
||||
@@ -2082,7 +2085,10 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_current_line_highlight(&mut self, current_line_highlight: CurrentLineHighlight) {
|
||||
pub fn set_current_line_highlight(
|
||||
&mut self,
|
||||
current_line_highlight: Option<CurrentLineHighlight>,
|
||||
) {
|
||||
self.current_line_highlight = current_line_highlight;
|
||||
}
|
||||
|
||||
@@ -2813,6 +2819,9 @@ impl Editor {
|
||||
}
|
||||
|
||||
if let Some(bracket_pair) = bracket_pair {
|
||||
let autoclose = self.use_autoclose
|
||||
&& snapshot.settings_at(selection.start, cx).use_autoclose;
|
||||
|
||||
if selection.is_empty() {
|
||||
if is_bracket_pair_start {
|
||||
let prefix_len = bracket_pair.start.len() - text.len();
|
||||
@@ -2833,8 +2842,6 @@ impl Editor {
|
||||
),
|
||||
&bracket_pair.start[..prefix_len],
|
||||
));
|
||||
let autoclose = self.use_autoclose
|
||||
&& snapshot.settings_at(selection.start, cx).use_autoclose;
|
||||
if autoclose
|
||||
&& following_text_allows_autoclose
|
||||
&& preceding_text_matches_prefix
|
||||
@@ -2887,7 +2894,10 @@ impl Editor {
|
||||
}
|
||||
// If an opening bracket is 1 character long and is typed while
|
||||
// text is selected, then surround that text with the bracket pair.
|
||||
else if is_bracket_pair_start && bracket_pair.start.chars().count() == 1 {
|
||||
else if autoclose
|
||||
&& is_bracket_pair_start
|
||||
&& bracket_pair.start.chars().count() == 1
|
||||
{
|
||||
edits.push((selection.start..selection.start, text.clone()));
|
||||
edits.push((
|
||||
selection.end..selection.end,
|
||||
@@ -3010,12 +3020,7 @@ impl Editor {
|
||||
s.select(new_selections)
|
||||
});
|
||||
|
||||
if brace_inserted {
|
||||
// If we inserted a brace while composing text (i.e. typing `"` on a
|
||||
// Brazilian keyboard), exit the composing state because most likely
|
||||
// the user wanted to surround the selection.
|
||||
this.unmark_text(cx);
|
||||
} else if EditorSettings::get_global(cx).use_on_type_format {
|
||||
if !brace_inserted && EditorSettings::get_global(cx).use_on_type_format {
|
||||
if let Some(on_type_format_task) =
|
||||
this.trigger_on_type_formatting(text.to_string(), cx)
|
||||
{
|
||||
@@ -9263,11 +9268,15 @@ impl Editor {
|
||||
for (block_id, diagnostic) in &active_diagnostics.blocks {
|
||||
new_styles.insert(
|
||||
*block_id,
|
||||
diagnostic_block_renderer(diagnostic.clone(), is_valid),
|
||||
(
|
||||
None,
|
||||
diagnostic_block_renderer(diagnostic.clone(), is_valid),
|
||||
),
|
||||
);
|
||||
}
|
||||
self.display_map
|
||||
.update(cx, |display_map, _| display_map.replace_blocks(new_styles));
|
||||
self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.replace_blocks(new_styles, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9624,12 +9633,12 @@ impl Editor {
|
||||
|
||||
pub fn replace_blocks(
|
||||
&mut self,
|
||||
blocks: HashMap<BlockId, RenderBlock>,
|
||||
blocks: HashMap<BlockId, (Option<u8>, RenderBlock)>,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.display_map
|
||||
.update(cx, |display_map, _| display_map.replace_blocks(blocks));
|
||||
.update(cx, |display_map, cx| display_map.replace_blocks(blocks, cx));
|
||||
if let Some(autoscroll) = autoscroll {
|
||||
self.request_autoscroll(autoscroll, cx);
|
||||
}
|
||||
@@ -9790,6 +9799,17 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_tab_bar(&mut self, _: &ToggleTabBar, cx: &mut ViewContext<Self>) {
|
||||
let Some(workspace) = self.workspace() else {
|
||||
return;
|
||||
};
|
||||
let fs = workspace.read(cx).app_state().fs.clone();
|
||||
let current_show = TabBarSettings::get_global(cx).show;
|
||||
update_settings_file::<TabBarSettings>(fs, cx, move |setting| {
|
||||
setting.show = Some(!current_show);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_indent_guides(&mut self, _: &ToggleIndentGuides, cx: &mut ViewContext<Self>) {
|
||||
let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| {
|
||||
self.buffer
|
||||
@@ -10256,6 +10276,27 @@ impl Editor {
|
||||
self.background_highlights_in_range(start..end, &snapshot, theme)
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn search_background_highlights(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<Range<Point>> {
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
|
||||
let highlights = self
|
||||
.background_highlights
|
||||
.get(&TypeId::of::<items::BufferSearchHighlights>());
|
||||
|
||||
if let Some((_color, ranges)) = highlights {
|
||||
ranges
|
||||
.iter()
|
||||
.map(|range| range.start.to_point(&snapshot)..range.end.to_point(&snapshot))
|
||||
.collect_vec()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
fn document_highlights_for_position<'a>(
|
||||
&'a self,
|
||||
position: Anchor,
|
||||
@@ -10604,7 +10645,6 @@ impl Editor {
|
||||
let editor_settings = EditorSettings::get_global(cx);
|
||||
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
|
||||
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
|
||||
self.current_line_highlight = editor_settings.current_line_highlight;
|
||||
|
||||
if self.mode == EditorMode::Full {
|
||||
let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled();
|
||||
|
||||
@@ -318,6 +318,7 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::open_excerpts);
|
||||
register_action(view, cx, Editor::open_excerpts_in_split);
|
||||
register_action(view, cx, Editor::toggle_soft_wrap);
|
||||
register_action(view, cx, Editor::toggle_tab_bar);
|
||||
register_action(view, cx, Editor::toggle_line_numbers);
|
||||
register_action(view, cx, Editor::toggle_indent_guides);
|
||||
register_action(view, cx, Editor::toggle_inlay_hints);
|
||||
@@ -1220,34 +1221,41 @@ impl EditorElement {
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
|
||||
let git_gutter_setting = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.git_gutter
|
||||
.unwrap_or_default();
|
||||
buffer_snapshot
|
||||
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
|
||||
.map(|hunk| diff_hunk_to_display(&hunk, snapshot))
|
||||
.dedup()
|
||||
.map(|hunk| {
|
||||
let hitbox = if let DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} = &hunk
|
||||
{
|
||||
let was_expanded = expanded_hunk_display_rows
|
||||
.get(&display_row_range.start)
|
||||
.map(|expanded_end_row| expanded_end_row == &display_row_range.end)
|
||||
.unwrap_or(false);
|
||||
if was_expanded {
|
||||
None
|
||||
.map(|hunk| match git_gutter_setting {
|
||||
GitGutterSetting::TrackedFiles => {
|
||||
let hitbox = if let DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} = &hunk
|
||||
{
|
||||
let was_expanded = expanded_hunk_display_rows
|
||||
.get(&display_row_range.start)
|
||||
.map(|expanded_end_row| expanded_end_row == &display_row_range.end)
|
||||
.unwrap_or(false);
|
||||
if was_expanded {
|
||||
None
|
||||
} else {
|
||||
let hunk_bounds = Self::diff_hunk_bounds(
|
||||
&snapshot,
|
||||
line_height,
|
||||
gutter_hitbox.bounds,
|
||||
&hunk,
|
||||
);
|
||||
Some(cx.insert_hitbox(hunk_bounds, true))
|
||||
}
|
||||
} else {
|
||||
let hunk_bounds = Self::diff_hunk_bounds(
|
||||
&snapshot,
|
||||
line_height,
|
||||
gutter_hitbox.bounds,
|
||||
&hunk,
|
||||
);
|
||||
Some(cx.insert_hitbox(hunk_bounds, true))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(hunk, hitbox)
|
||||
None
|
||||
};
|
||||
(hunk, hitbox)
|
||||
}
|
||||
GitGutterSetting::Hide => (hunk, None),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -4064,6 +4072,7 @@ impl LineWithInvisibles {
|
||||
if non_whitespace_added || !inside_wrapped_string {
|
||||
invisibles.push(Invisible::Tab {
|
||||
line_start_offset: line.len(),
|
||||
line_end_offset: line.len() + line_chunk.len(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -4179,16 +4188,15 @@ impl LineWithInvisibles {
|
||||
whitespace_setting: ShowWhitespaceSetting,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let allowed_invisibles_regions = match whitespace_setting {
|
||||
ShowWhitespaceSetting::None => return,
|
||||
ShowWhitespaceSetting::Selection => Some(selection_ranges),
|
||||
ShowWhitespaceSetting::All => None,
|
||||
};
|
||||
|
||||
for invisible in &self.invisibles {
|
||||
let (&token_offset, invisible_symbol) = match invisible {
|
||||
Invisible::Tab { line_start_offset } => (line_start_offset, &layout.tab_invisible),
|
||||
Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible),
|
||||
let extract_whitespace_info = |invisible: &Invisible| {
|
||||
let (token_offset, token_end_offset, invisible_symbol) = match invisible {
|
||||
Invisible::Tab {
|
||||
line_start_offset,
|
||||
line_end_offset,
|
||||
} => (*line_start_offset, *line_end_offset, &layout.tab_invisible),
|
||||
Invisible::Whitespace { line_offset } => {
|
||||
(*line_offset, line_offset + 1, &layout.space_invisible)
|
||||
}
|
||||
};
|
||||
|
||||
let x_offset = self.x_for_index(token_offset);
|
||||
@@ -4200,17 +4208,73 @@ impl LineWithInvisibles {
|
||||
line_y,
|
||||
);
|
||||
|
||||
if let Some(allowed_regions) = allowed_invisibles_regions {
|
||||
let invisible_point = DisplayPoint::new(row, token_offset as u32);
|
||||
if !allowed_regions
|
||||
(
|
||||
[token_offset, token_end_offset],
|
||||
Box::new(move |cx: &mut WindowContext| {
|
||||
invisible_symbol.paint(origin, line_height, cx).log_err();
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
let invisible_iter = self.invisibles.iter().map(extract_whitespace_info);
|
||||
match whitespace_setting {
|
||||
ShowWhitespaceSetting::None => return,
|
||||
ShowWhitespaceSetting::All => invisible_iter.for_each(|(_, paint)| paint(cx)),
|
||||
ShowWhitespaceSetting::Selection => invisible_iter.for_each(|([start, _], paint)| {
|
||||
let invisible_point = DisplayPoint::new(row, start as u32);
|
||||
if !selection_ranges
|
||||
.iter()
|
||||
.any(|region| region.start <= invisible_point && invisible_point < region.end)
|
||||
{
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
paint(cx);
|
||||
}),
|
||||
|
||||
// For a whitespace to be on a boundary, any of the following conditions need to be met:
|
||||
// - It is a tab
|
||||
// - It is adjacent to an edge (start or end)
|
||||
// - It is adjacent to a whitespace (left or right)
|
||||
ShowWhitespaceSetting::Boundary => {
|
||||
// We'll need to keep track of the last invisible we've seen and then check if we are adjacent to it for some of
|
||||
// the above cases.
|
||||
// Note: We zip in the original `invisibles` to check for tab equality
|
||||
let mut last_seen: Option<(bool, usize, Box<dyn Fn(&mut WindowContext)>)> = None;
|
||||
for (([start, end], paint), invisible) in
|
||||
invisible_iter.zip_eq(self.invisibles.iter())
|
||||
{
|
||||
let should_render = match (&last_seen, invisible) {
|
||||
(_, Invisible::Tab { .. }) => true,
|
||||
(Some((_, last_end, _)), _) => *last_end == start,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if should_render || start == 0 || end == self.len {
|
||||
paint(cx);
|
||||
|
||||
// Since we are scanning from the left, we will skip over the first available whitespace that is part
|
||||
// of a boundary between non-whitespace segments, so we correct by manually redrawing it if needed.
|
||||
if let Some((should_render_last, last_end, paint_last)) = last_seen {
|
||||
// Note that we need to make sure that the last one is actually adjacent
|
||||
if !should_render_last && last_end == start {
|
||||
paint_last(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manually render anything within a selection
|
||||
let invisible_point = DisplayPoint::new(row, start as u32);
|
||||
if selection_ranges.iter().any(|region| {
|
||||
region.start <= invisible_point && invisible_point < region.end
|
||||
}) {
|
||||
paint(cx);
|
||||
}
|
||||
|
||||
last_seen = Some((should_render, end, paint));
|
||||
}
|
||||
}
|
||||
invisible_symbol.paint(origin, line_height, cx).log_err();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn x_for_index(&self, index: usize) -> Pixels {
|
||||
@@ -4300,8 +4364,18 @@ impl LineWithInvisibles {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Invisible {
|
||||
Tab { line_start_offset: usize },
|
||||
Whitespace { line_offset: usize },
|
||||
/// A tab character
|
||||
///
|
||||
/// A tab character is internally represented by spaces (configured by the user's tab width)
|
||||
/// aligned to the nearest column, so it's necessary to store the start and end offset for
|
||||
/// adjacency checks.
|
||||
Tab {
|
||||
line_start_offset: usize,
|
||||
line_end_offset: usize,
|
||||
},
|
||||
Whitespace {
|
||||
line_offset: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl EditorElement {
|
||||
@@ -5846,15 +5920,18 @@ mod tests {
|
||||
let expected_invisibles = vec![
|
||||
Invisible::Tab {
|
||||
line_start_offset: 0,
|
||||
line_end_offset: TAB_SIZE as usize,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: TAB_SIZE as usize,
|
||||
},
|
||||
Invisible::Tab {
|
||||
line_start_offset: TAB_SIZE as usize + 1,
|
||||
line_end_offset: TAB_SIZE as usize * 2,
|
||||
},
|
||||
Invisible::Tab {
|
||||
line_start_offset: TAB_SIZE as usize * 2 + 1,
|
||||
line_end_offset: TAB_SIZE as usize * 3,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: TAB_SIZE as usize * 3 + 1,
|
||||
@@ -5908,10 +5985,11 @@ mod tests {
|
||||
#[gpui::test]
|
||||
fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
|
||||
let tab_size = 4;
|
||||
let input_text = "a\tbcd ".repeat(9);
|
||||
let input_text = "a\tbcd ".repeat(9);
|
||||
let repeated_invisibles = [
|
||||
Invisible::Tab {
|
||||
line_start_offset: 1,
|
||||
line_end_offset: tab_size as usize,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 3,
|
||||
@@ -5922,6 +6000,12 @@ mod tests {
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 5,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 6,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 7,
|
||||
},
|
||||
];
|
||||
let expected_invisibles = std::iter::once(repeated_invisibles)
|
||||
.cycle()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
hover_popover::{self, InlayHover},
|
||||
scroll::ScrollAmount,
|
||||
Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, InlayId,
|
||||
PointForPosition, SelectPhase,
|
||||
};
|
||||
@@ -38,7 +39,11 @@ impl RangeInEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
|
||||
pub fn point_within_range(
|
||||
&self,
|
||||
trigger_point: &TriggerPoint,
|
||||
snapshot: &EditorSnapshot,
|
||||
) -> bool {
|
||||
match (self, trigger_point) {
|
||||
(Self::Text(range), TriggerPoint::Text(point)) => {
|
||||
let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
|
||||
@@ -169,6 +174,21 @@ impl Editor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn scroll_hover(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) -> bool {
|
||||
let selection = self.selections.newest_anchor().head();
|
||||
let snapshot = self.snapshot(cx);
|
||||
|
||||
let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
|
||||
popover
|
||||
.symbol_range
|
||||
.point_within_range(&TriggerPoint::Text(selection), &snapshot)
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
popover.scroll(amount, cx);
|
||||
true
|
||||
}
|
||||
|
||||
fn cmd_click_reveal_task(
|
||||
&mut self,
|
||||
point: PointForPosition,
|
||||
@@ -302,7 +322,6 @@ pub fn update_inlay_link_and_hover_points(
|
||||
hover_popover::hover_at_inlay(
|
||||
editor,
|
||||
InlayHover {
|
||||
excerpt: excerpt_id,
|
||||
tooltip: match tooltip {
|
||||
InlayHintTooltip::String(text) => HoverBlock {
|
||||
text,
|
||||
@@ -350,7 +369,6 @@ pub fn update_inlay_link_and_hover_points(
|
||||
hover_popover::hover_at_inlay(
|
||||
editor,
|
||||
InlayHover {
|
||||
excerpt: excerpt_id,
|
||||
tooltip: match tooltip {
|
||||
InlayHintLabelPartTooltip::String(text) => {
|
||||
HoverBlock {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use crate::{
|
||||
display_map::{InlayOffset, ToDisplayPoint},
|
||||
hover_links::{InlayHighlight, RangeInEditor},
|
||||
scroll::ScrollAmount,
|
||||
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
|
||||
EditorStyle, ExcerptId, Hover, RangeToAnchorExt,
|
||||
EditorStyle, Hover, RangeToAnchorExt,
|
||||
};
|
||||
use futures::{stream::FuturesUnordered, FutureExt};
|
||||
use gpui::{
|
||||
div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task,
|
||||
ViewContext, WeakView,
|
||||
ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled,
|
||||
Task, ViewContext, WeakView,
|
||||
};
|
||||
use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
|
||||
|
||||
@@ -48,7 +49,6 @@ pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContex
|
||||
}
|
||||
|
||||
pub struct InlayHover {
|
||||
pub excerpt: ExcerptId,
|
||||
pub range: InlayHighlight,
|
||||
pub tooltip: HoverBlock,
|
||||
}
|
||||
@@ -118,6 +118,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
|
||||
let hover_popover = InfoPopover {
|
||||
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -317,6 +318,7 @@ fn show_hover(
|
||||
InfoPopover {
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -423,7 +425,7 @@ async fn parse_blocks(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct HoverState {
|
||||
pub info_popovers: Vec<InfoPopover>,
|
||||
pub diagnostic_popover: Option<DiagnosticPopover>,
|
||||
@@ -487,10 +489,11 @@ impl HoverState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct InfoPopover {
|
||||
symbol_range: RangeInEditor,
|
||||
parsed_content: ParsedMarkdown,
|
||||
pub symbol_range: RangeInEditor,
|
||||
pub parsed_content: ParsedMarkdown,
|
||||
pub scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
impl InfoPopover {
|
||||
@@ -504,23 +507,33 @@ impl InfoPopover {
|
||||
div()
|
||||
.id("info_popover")
|
||||
.elevation_2(cx)
|
||||
.p_2()
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
// Prevent a mouse down/move on the popover from being propagated to the editor,
|
||||
// because that would dismiss the popover.
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.child(crate::render_parsed_markdown(
|
||||
.child(div().p_2().child(crate::render_parsed_markdown(
|
||||
"content",
|
||||
&self.parsed_content,
|
||||
style,
|
||||
workspace,
|
||||
cx,
|
||||
))
|
||||
)))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
|
||||
let mut current = self.scroll_handle.offset();
|
||||
current.y -= amount.pixels(
|
||||
cx.line_height(),
|
||||
self.scroll_handle.bounds().size.height - px(16.),
|
||||
) / 2.0;
|
||||
cx.notify();
|
||||
self.scroll_handle.set_offset(current);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -10,7 +10,7 @@ use language::Buffer;
|
||||
use multi_buffer::{
|
||||
Anchor, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint,
|
||||
};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::SettingsStore;
|
||||
use text::{BufferId, Point};
|
||||
use ui::{
|
||||
div, ActiveTheme, Context as _, IntoElement, ParentElement, Styled, ViewContext, VisualContext,
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
editor_settings::CurrentLineHighlight,
|
||||
git::{diff_hunk_to_display, DisplayDiffHunk},
|
||||
hunk_status, hunks_for_selections, BlockDisposition, BlockId, BlockProperties, BlockStyle,
|
||||
DiffRowHighlight, Editor, EditorSettings, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt,
|
||||
DiffRowHighlight, Editor, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt,
|
||||
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
|
||||
};
|
||||
|
||||
@@ -591,7 +591,7 @@ fn editor_with_deleted_text(
|
||||
let subscription_editor = parent_editor.clone();
|
||||
editor._subscriptions.extend([
|
||||
cx.on_blur(&editor.focus_handle, |editor, cx| {
|
||||
editor.set_current_line_highlight(CurrentLineHighlight::None);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.try_cancel();
|
||||
});
|
||||
@@ -602,14 +602,14 @@ fn editor_with_deleted_text(
|
||||
{
|
||||
parent_editor.read(cx).current_line_highlight
|
||||
} else {
|
||||
EditorSettings::get_global(cx).current_line_highlight
|
||||
None
|
||||
};
|
||||
editor.set_current_line_highlight(restored_highlight);
|
||||
cx.notify();
|
||||
}),
|
||||
cx.observe_global::<SettingsStore>(|editor, cx| {
|
||||
if !editor.is_focused(cx) {
|
||||
editor.set_current_line_highlight(CurrentLineHighlight::None);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -13,8 +13,7 @@ use gpui::{
|
||||
VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
|
||||
Point, SelectionGoal,
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
|
||||
};
|
||||
use multi_buffer::AnchorRangeExt;
|
||||
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
||||
@@ -1008,6 +1007,25 @@ impl SearchableItem for Editor {
|
||||
self.has_background_highlights::<SearchWithinRange>()
|
||||
}
|
||||
|
||||
fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
|
||||
if self.has_filtered_search_ranges() {
|
||||
self.previous_search_ranges = self
|
||||
.clear_background_highlights::<SearchWithinRange>(cx)
|
||||
.map(|(_, ranges)| ranges)
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let ranges = self.selections.disjoint_anchor_ranges();
|
||||
if ranges.iter().any(|range| range.start != range.end) {
|
||||
self.set_search_within_ranges(&ranges, cx);
|
||||
} else if let Some(previous_search_ranges) = self.previous_search_ranges.take() {
|
||||
self.set_search_within_ranges(&previous_search_ranges, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
|
||||
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
|
||||
let snapshot = &self.snapshot(cx).buffer_snapshot;
|
||||
@@ -1016,9 +1034,14 @@ impl SearchableItem for Editor {
|
||||
match setting {
|
||||
SeedQuerySetting::Never => String::new(),
|
||||
SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => {
|
||||
snapshot
|
||||
let text: String = snapshot
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect()
|
||||
.collect();
|
||||
if text.contains('\n') {
|
||||
String::new()
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
SeedQuerySetting::Selection => String::new(),
|
||||
SeedQuerySetting::Always => {
|
||||
@@ -1135,58 +1158,64 @@ impl SearchableItem for Editor {
|
||||
let search_within_ranges = self
|
||||
.background_highlights
|
||||
.get(&TypeId::of::<SearchWithinRange>())
|
||||
.map(|(_color, ranges)| {
|
||||
ranges
|
||||
.iter()
|
||||
.map(|range| range.to_offset(&buffer))
|
||||
.collect::<Vec<_>>()
|
||||
.map_or(vec![], |(_color, ranges)| {
|
||||
ranges.iter().map(|range| range.clone()).collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
|
||||
if let Some(search_within_ranges) = search_within_ranges {
|
||||
for range in search_within_ranges {
|
||||
let offset = range.start;
|
||||
ranges.extend(
|
||||
query
|
||||
.search(excerpt_buffer, Some(range))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
buffer.anchor_after(range.start + offset)
|
||||
..buffer.anchor_before(range.end + offset)
|
||||
}),
|
||||
);
|
||||
}
|
||||
let search_within_ranges = if search_within_ranges.is_empty() {
|
||||
vec![None]
|
||||
} else {
|
||||
ranges.extend(query.search(excerpt_buffer, None).await.into_iter().map(
|
||||
|range| buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||
));
|
||||
search_within_ranges
|
||||
.into_iter()
|
||||
.map(|range| Some(range.to_offset(&buffer)))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
for range in search_within_ranges {
|
||||
let buffer = &buffer;
|
||||
ranges.extend(
|
||||
query
|
||||
.search(excerpt_buffer, range.clone())
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|matched_range| {
|
||||
let offset = range.clone().map(|r| r.start).unwrap_or(0);
|
||||
buffer.anchor_after(matched_range.start + offset)
|
||||
..buffer.anchor_before(matched_range.end + offset)
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
|
||||
if let Some(next_excerpt) = excerpt.next {
|
||||
let excerpt_range =
|
||||
next_excerpt.range.context.to_offset(&next_excerpt.buffer);
|
||||
ranges.extend(
|
||||
query
|
||||
.search(&next_excerpt.buffer, Some(excerpt_range.clone()))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let start = next_excerpt
|
||||
.buffer
|
||||
.anchor_after(excerpt_range.start + range.start);
|
||||
let end = next_excerpt
|
||||
.buffer
|
||||
.anchor_before(excerpt_range.start + range.end);
|
||||
buffer.anchor_in_excerpt(next_excerpt.id, start).unwrap()
|
||||
..buffer.anchor_in_excerpt(next_excerpt.id, end).unwrap()
|
||||
}),
|
||||
);
|
||||
}
|
||||
let search_within_ranges = if search_within_ranges.is_empty() {
|
||||
vec![buffer.anchor_before(0)..buffer.anchor_after(buffer.len())]
|
||||
} else {
|
||||
search_within_ranges
|
||||
};
|
||||
|
||||
for (excerpt_id, search_buffer, search_range) in
|
||||
buffer.excerpts_in_ranges(search_within_ranges)
|
||||
{
|
||||
ranges.extend(
|
||||
query
|
||||
.search(&search_buffer, Some(search_range.clone()))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|match_range| {
|
||||
let start = search_buffer
|
||||
.anchor_after(search_range.start + match_range.start);
|
||||
let end = search_buffer
|
||||
.anchor_before(search_range.start + match_range.end);
|
||||
buffer.anchor_in_excerpt(excerpt_id, start).unwrap()
|
||||
..buffer.anchor_in_excerpt(excerpt_id, end).unwrap()
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ranges
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::Editor;
|
||||
use serde::Deserialize;
|
||||
use ui::{px, Pixels};
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub enum ScrollAmount {
|
||||
// Scroll N lines (positive is towards the end of the document)
|
||||
Line(f32),
|
||||
@@ -25,4 +26,11 @@ impl ScrollAmount {
|
||||
.unwrap_or(0.),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pixels(&self, line_height: Pixels, height: Pixels) -> Pixels {
|
||||
match self {
|
||||
ScrollAmount::Line(x) => px(line_height.0 * x),
|
||||
ScrollAmount::Page(x) => px(height.0 * x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +273,13 @@ impl SelectionsCollection {
|
||||
self.all(cx).last().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn disjoint_anchor_ranges(&self) -> Vec<Range<Anchor>> {
|
||||
self.disjoint_anchors()
|
||||
.iter()
|
||||
.map(|s| s.start..s.end)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
|
||||
&self,
|
||||
|
||||
@@ -39,7 +39,7 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
|
||||
@@ -2437,7 +2437,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
struct ScrollHandleState {
|
||||
offset: Rc<RefCell<Point<Pixels>>>,
|
||||
bounds: Bounds<Pixels>,
|
||||
@@ -2449,7 +2449,7 @@ struct ScrollHandleState {
|
||||
/// A handle to the scrollable aspects of an element.
|
||||
/// Used for accessing scroll state, like the current scroll offset,
|
||||
/// and for mutating the scroll state, like scrolling to a specific child.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScrollHandle(Rc<RefCell<ScrollHandleState>>);
|
||||
|
||||
impl Default for ScrollHandle {
|
||||
@@ -2526,6 +2526,14 @@ impl ScrollHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the offset explicitly. The offset is the distance from the top left of the
|
||||
/// parent container to the top left of the first child.
|
||||
/// As you scroll further down the offset becomes more negative.
|
||||
pub fn set_offset(&self, mut position: Point<Pixels>) {
|
||||
let state = self.0.borrow();
|
||||
*state.offset.borrow_mut() = position;
|
||||
}
|
||||
|
||||
/// Get the logical scroll top, based on a child index and a pixel offset.
|
||||
pub fn logical_scroll_top(&self) -> (usize, Pixels) {
|
||||
let ix = self.top_item();
|
||||
|
||||
@@ -201,7 +201,7 @@ mod sys {
|
||||
|
||||
#[link(name = "CoreFoundation", kind = "framework")]
|
||||
#[link(name = "CoreVideo", kind = "framework")]
|
||||
#[allow(improper_ctypes)]
|
||||
#[allow(improper_ctypes, unknown_lints, clippy::duplicated_attributes)]
|
||||
extern "C" {
|
||||
pub fn CVDisplayLinkCreateWithActiveCGDisplays(
|
||||
display_link_out: *mut *mut CVDisplayLink,
|
||||
|
||||
@@ -310,8 +310,8 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
|
||||
decl.register()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Clone)]
|
||||
enum ImeInput {
|
||||
InsertText(String, Option<Range<usize>>),
|
||||
SetMarkedText(String, Option<Range<usize>>, Option<Range<usize>>),
|
||||
@@ -340,7 +340,7 @@ struct MacWindowState {
|
||||
traffic_light_position: Option<Point<Pixels>>,
|
||||
previous_modifiers_changed_event: Option<PlatformInput>,
|
||||
// State tracking what the IME did after the last request
|
||||
last_ime_action: Option<ImeInput>,
|
||||
last_ime_inputs: Option<SmallVec<[(String, Option<Range<usize>>); 1]>>,
|
||||
previous_keydown_inserted_text: Option<String>,
|
||||
external_files_dragged: bool,
|
||||
// Whether the next left-mouse click is also the focusing click.
|
||||
@@ -636,7 +636,7 @@ impl MacWindow {
|
||||
.as_ref()
|
||||
.and_then(|titlebar| titlebar.traffic_light_position),
|
||||
previous_modifiers_changed_event: None,
|
||||
last_ime_action: None,
|
||||
last_ime_inputs: None,
|
||||
previous_keydown_inserted_text: None,
|
||||
external_files_dragged: false,
|
||||
first_mouse: false,
|
||||
@@ -1195,18 +1195,26 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
|
||||
// - The IME consumes characters like 'j' and 'k', which makes paging through `less` in
|
||||
// the terminal behave incorrectly by default. This behavior should be patched by our
|
||||
// IME integration
|
||||
// - `alt-t` should open the tasks menu
|
||||
// - In vim mode, this keybinding should work:
|
||||
// ```
|
||||
// {
|
||||
// "context": "Editor && vim_mode == insert",
|
||||
// "bindings": {"j j": "vim::NormalBefore"}
|
||||
// }
|
||||
// ```
|
||||
// and typing 'j k' in insert mode with this keybinding should insert the two characters
|
||||
// Brazilian layout:
|
||||
// - `" space` should create an unmarked quote
|
||||
// - `" backspace` should delete the marked quote
|
||||
// - `" up` should insert a quote, unmark it, and move up one line
|
||||
// - `" cmd-down` should insert a quote, unmark it, and move to the end of the file
|
||||
// - NOTE: The current implementation does not move the selection to the end of the file
|
||||
// - `cmd-ctrl-space` and clicking on an emoji should type it
|
||||
// Czech (QWERTY) layout:
|
||||
// - in vim mode `option-4` should go to end of line (same as $)
|
||||
// Japanese (Romaji) layout:
|
||||
// - Triggering the IME composer (e.g. via typing 'a i' and then the left key), and then selecting
|
||||
// results of different length (e.g. kana -> kanji -> emoji -> back to kanji via the up and down keys)
|
||||
// should maintain the composing state in the editor
|
||||
// - type `a i left down up enter enter` should create an unmarked text "愛"
|
||||
extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let mut lock = window_state.as_ref().lock();
|
||||
@@ -1236,12 +1244,12 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
} else {
|
||||
lock.last_fresh_keydown = Some(keydown.clone());
|
||||
}
|
||||
lock.last_ime_inputs = Some(Default::default());
|
||||
drop(lock);
|
||||
|
||||
// Send the event to the input context for IME handling, unless the `fn` modifier is
|
||||
// being pressed. This will call back into other functions like `insert_text`, etc.
|
||||
// Note that the IME expects it's actions to be applied immediately, and buffering them
|
||||
// can break pre-edit
|
||||
// being pressed.
|
||||
// this will call back into `insert_text`, etc.
|
||||
if !fn_modifier {
|
||||
unsafe {
|
||||
let input_context: id = msg_send![this, inputContext];
|
||||
@@ -1252,27 +1260,36 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
let mut handled = false;
|
||||
let mut lock = window_state.lock();
|
||||
let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take();
|
||||
let mut last_ime = lock.last_ime_action.take();
|
||||
let mut last_inserts = lock.last_ime_inputs.take().unwrap();
|
||||
|
||||
let mut callback = lock.event_callback.take();
|
||||
drop(lock);
|
||||
|
||||
let last_insert = last_inserts.pop();
|
||||
// on a brazilian keyboard typing `"` and then hitting `up` will cause two IME
|
||||
// events, one to unmark the quote, and one to send the up arrow.
|
||||
for (text, range) in last_inserts {
|
||||
send_to_input_handler(this, ImeInput::InsertText(text, range));
|
||||
}
|
||||
|
||||
let is_composing =
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.is_some();
|
||||
|
||||
if let Some(ime) = last_ime {
|
||||
if let ImeInput::InsertText(text, _) = &ime {
|
||||
if !is_composing {
|
||||
window_state.lock().previous_keydown_inserted_text = Some(text.clone());
|
||||
if let Some(callback) = callback.as_mut() {
|
||||
event.keystroke.ime_key = Some(text.clone());
|
||||
let _ = callback(PlatformInput::KeyDown(event));
|
||||
}
|
||||
if let Some((text, range)) = last_insert {
|
||||
if !is_composing {
|
||||
window_state.lock().previous_keydown_inserted_text = Some(text.clone());
|
||||
if let Some(callback) = callback.as_mut() {
|
||||
event.keystroke.ime_key = Some(text.clone());
|
||||
handled = !callback(PlatformInput::KeyDown(event)).propagate;
|
||||
}
|
||||
}
|
||||
|
||||
handled = true;
|
||||
if !handled {
|
||||
handled = true;
|
||||
send_to_input_handler(this, ImeInput::InsertText(text, range));
|
||||
}
|
||||
} else if !is_composing {
|
||||
let is_held = event.is_held;
|
||||
|
||||
@@ -1653,21 +1670,24 @@ extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
|
||||
}
|
||||
|
||||
extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL {
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.is_some() as BOOL
|
||||
let has_marked_text_result =
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten();
|
||||
|
||||
has_marked_text_result.is_some() as BOOL
|
||||
}
|
||||
|
||||
extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.map_or(NSRange::invalid(), |range| range.into())
|
||||
let marked_range_result =
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten();
|
||||
|
||||
marked_range_result.map_or(NSRange::invalid(), |range| range.into())
|
||||
}
|
||||
|
||||
extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
|
||||
with_input_handler(this, |input_handler| input_handler.selected_text_range())
|
||||
.flatten()
|
||||
.map_or(NSRange::invalid(), |range| range.into())
|
||||
let selected_range_result =
|
||||
with_input_handler(this, |input_handler| input_handler.selected_text_range()).flatten();
|
||||
|
||||
selected_range_result.map_or(NSRange::invalid(), |range| range.into())
|
||||
}
|
||||
|
||||
extern "C" fn first_rect_for_character_range(
|
||||
@@ -1760,7 +1780,7 @@ extern "C" fn attributed_substring_for_proposed_range(
|
||||
return None;
|
||||
}
|
||||
|
||||
let selected_text = input_handler.text_for_range(range)?;
|
||||
let selected_text = input_handler.text_for_range(range.clone())?;
|
||||
unsafe {
|
||||
let string: id = msg_send![class!(NSAttributedString), alloc];
|
||||
let string: id = msg_send![string, initWithString: ns_string(&selected_text)];
|
||||
@@ -1926,17 +1946,25 @@ fn send_to_input_handler(window: &Object, ime: ImeInput) {
|
||||
let window_state = get_window_state(window);
|
||||
let mut lock = window_state.lock();
|
||||
|
||||
lock.last_ime_action = Some(ime.clone());
|
||||
if let Some(mut input_handler) = lock.input_handler.take() {
|
||||
drop(lock);
|
||||
match ime {
|
||||
match ime.clone() {
|
||||
ImeInput::InsertText(text, range) => {
|
||||
if let Some(ime_input) = lock.last_ime_inputs.as_mut() {
|
||||
ime_input.push((text, range));
|
||||
lock.input_handler = Some(input_handler);
|
||||
return;
|
||||
}
|
||||
drop(lock);
|
||||
input_handler.replace_text_in_range(range, &text)
|
||||
}
|
||||
ImeInput::SetMarkedText(text, range, marked_range) => {
|
||||
drop(lock);
|
||||
input_handler.replace_and_mark_text_in_range(range, &text, marked_range)
|
||||
}
|
||||
ImeInput::UnmarkText => input_handler.unmark_text(),
|
||||
ImeInput::UnmarkText => {
|
||||
drop(lock);
|
||||
input_handler.unmark_text()
|
||||
}
|
||||
}
|
||||
window_state.lock().input_handler = Some(input_handler);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,9 @@ impl TaffyLayoutEngine {
|
||||
let parent_id = self
|
||||
.taffy
|
||||
// This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId.
|
||||
.new_with_children(taffy_style, unsafe { std::mem::transmute(children) })
|
||||
.new_with_children(taffy_style, unsafe {
|
||||
std::mem::transmute::<&[LayoutId], &[taffy::NodeId]>(children)
|
||||
})
|
||||
.expect(EXPECT_MESSAGE)
|
||||
.into();
|
||||
self.children_to_parents
|
||||
|
||||
@@ -1036,6 +1036,37 @@ impl<'a> WindowContext<'a> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Subscribe to events emitted by a model or view.
|
||||
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
||||
/// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
|
||||
pub fn observe<E, T>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
mut on_notify: impl FnMut(E, &mut WindowContext<'_>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
E: Entity<T>,
|
||||
{
|
||||
let entity_id = entity.entity_id();
|
||||
let entity = entity.downgrade();
|
||||
let window_handle = self.window.handle;
|
||||
self.app.new_observer(
|
||||
entity_id,
|
||||
Box::new(move |cx| {
|
||||
window_handle
|
||||
.update(cx, |_, cx| {
|
||||
if let Some(handle) = E::upgrade_from(&entity) {
|
||||
on_notify(handle, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Subscribe to events emitted by a model or view.
|
||||
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
||||
/// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
|
||||
|
||||
@@ -16,7 +16,9 @@ use html5ever::tendril::TendrilSink;
|
||||
use html5ever::tree_builder::TreeBuilderOpts;
|
||||
use markup5ever_rcdom::RcDom;
|
||||
|
||||
use crate::markdown::{HeadingHandler, ListHandler, ParagraphHandler, StyledTextHandler};
|
||||
use crate::markdown::{
|
||||
HeadingHandler, ListHandler, ParagraphHandler, StyledTextHandler, TableHandler,
|
||||
};
|
||||
use crate::markdown_writer::{HandleTag, MarkdownWriter};
|
||||
|
||||
/// Converts the provided HTML to Markdown.
|
||||
@@ -27,11 +29,11 @@ pub fn convert_html_to_markdown(html: impl Read) -> Result<String> {
|
||||
Box::new(ParagraphHandler),
|
||||
Box::new(HeadingHandler),
|
||||
Box::new(ListHandler),
|
||||
Box::new(TableHandler::new()),
|
||||
Box::new(StyledTextHandler),
|
||||
Box::new(structure::rustdoc::RustdocChromeRemover),
|
||||
Box::new(structure::rustdoc::RustdocHeadingHandler),
|
||||
Box::new(structure::rustdoc::RustdocCodeHandler),
|
||||
Box::new(structure::rustdoc::RustdocTableHandler::new()),
|
||||
Box::new(structure::rustdoc::RustdocItemHandler),
|
||||
];
|
||||
|
||||
@@ -51,11 +53,11 @@ pub fn convert_rustdoc_to_markdown(html: impl Read) -> Result<String> {
|
||||
Box::new(ParagraphHandler),
|
||||
Box::new(HeadingHandler),
|
||||
Box::new(ListHandler),
|
||||
Box::new(TableHandler::new()),
|
||||
Box::new(StyledTextHandler),
|
||||
Box::new(structure::rustdoc::RustdocChromeRemover),
|
||||
Box::new(structure::rustdoc::RustdocHeadingHandler),
|
||||
Box::new(structure::rustdoc::RustdocCodeHandler),
|
||||
Box::new(structure::rustdoc::RustdocTableHandler::new()),
|
||||
Box::new(structure::rustdoc::RustdocItemHandler),
|
||||
];
|
||||
|
||||
|
||||
@@ -101,6 +101,87 @@ impl HandleTag for ListHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TableHandler {
|
||||
/// The number of columns in the current `<table>`.
|
||||
current_table_columns: usize,
|
||||
is_first_th: bool,
|
||||
is_first_td: bool,
|
||||
}
|
||||
|
||||
impl TableHandler {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_table_columns: 0,
|
||||
is_first_th: true,
|
||||
is_first_td: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HandleTag for TableHandler {
|
||||
fn should_handle(&self, tag: &str) -> bool {
|
||||
match tag {
|
||||
"table" | "thead" | "tbody" | "tr" | "th" | "td" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tag_start(
|
||||
&mut self,
|
||||
tag: &HtmlElement,
|
||||
writer: &mut MarkdownWriter,
|
||||
) -> StartTagOutcome {
|
||||
match tag.tag.as_str() {
|
||||
"thead" => writer.push_blank_line(),
|
||||
"tr" => writer.push_newline(),
|
||||
"th" => {
|
||||
self.current_table_columns += 1;
|
||||
if self.is_first_th {
|
||||
self.is_first_th = false;
|
||||
} else {
|
||||
writer.push_str(" ");
|
||||
}
|
||||
writer.push_str("| ");
|
||||
}
|
||||
"td" => {
|
||||
if self.is_first_td {
|
||||
self.is_first_td = false;
|
||||
} else {
|
||||
writer.push_str(" ");
|
||||
}
|
||||
writer.push_str("| ");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
|
||||
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
|
||||
match tag.tag.as_str() {
|
||||
"thead" => {
|
||||
writer.push_newline();
|
||||
for ix in 0..self.current_table_columns {
|
||||
if ix > 0 {
|
||||
writer.push_str(" ");
|
||||
}
|
||||
writer.push_str("| ---");
|
||||
}
|
||||
writer.push_str(" |");
|
||||
self.is_first_th = true;
|
||||
}
|
||||
"tr" => {
|
||||
writer.push_str(" |");
|
||||
self.is_first_td = true;
|
||||
}
|
||||
"table" => {
|
||||
self.current_table_columns = 0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StyledTextHandler;
|
||||
|
||||
impl HandleTag for StyledTextHandler {
|
||||
|
||||
@@ -96,87 +96,6 @@ impl HandleTag for RustdocCodeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RustdocTableHandler {
|
||||
/// The number of columns in the current `<table>`.
|
||||
current_table_columns: usize,
|
||||
is_first_th: bool,
|
||||
is_first_td: bool,
|
||||
}
|
||||
|
||||
impl RustdocTableHandler {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_table_columns: 0,
|
||||
is_first_th: true,
|
||||
is_first_td: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HandleTag for RustdocTableHandler {
|
||||
fn should_handle(&self, tag: &str) -> bool {
|
||||
match tag {
|
||||
"table" | "thead" | "tbody" | "tr" | "th" | "td" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tag_start(
|
||||
&mut self,
|
||||
tag: &HtmlElement,
|
||||
writer: &mut MarkdownWriter,
|
||||
) -> StartTagOutcome {
|
||||
match tag.tag.as_str() {
|
||||
"thead" => writer.push_blank_line(),
|
||||
"tr" => writer.push_newline(),
|
||||
"th" => {
|
||||
self.current_table_columns += 1;
|
||||
if self.is_first_th {
|
||||
self.is_first_th = false;
|
||||
} else {
|
||||
writer.push_str(" ");
|
||||
}
|
||||
writer.push_str("| ");
|
||||
}
|
||||
"td" => {
|
||||
if self.is_first_td {
|
||||
self.is_first_td = false;
|
||||
} else {
|
||||
writer.push_str(" ");
|
||||
}
|
||||
writer.push_str("| ");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
StartTagOutcome::Continue
|
||||
}
|
||||
|
||||
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
|
||||
match tag.tag.as_str() {
|
||||
"thead" => {
|
||||
writer.push_newline();
|
||||
for ix in 0..self.current_table_columns {
|
||||
if ix > 0 {
|
||||
writer.push_str(" ");
|
||||
}
|
||||
writer.push_str("| ---");
|
||||
}
|
||||
writer.push_str(" |");
|
||||
self.is_first_th = true;
|
||||
}
|
||||
"tr" => {
|
||||
writer.push_str(" |");
|
||||
self.is_first_td = true;
|
||||
}
|
||||
"table" => {
|
||||
self.current_table_columns = 0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const RUSTDOC_ITEM_NAME_CLASS: &str = "item-name";
|
||||
|
||||
pub struct RustdocItemHandler;
|
||||
|
||||
@@ -391,6 +391,13 @@ pub enum ShowWhitespaceSetting {
|
||||
None,
|
||||
/// Draw all invisible symbols.
|
||||
All,
|
||||
/// Draw whitespaces at boundaries only.
|
||||
///
|
||||
/// For a whitespace to be on a boundary, any of the following conditions need to be met:
|
||||
/// - It is a tab
|
||||
/// - It is adjacent to an edge (start or end)
|
||||
/// - It is adjacent to a whitespace (left or right)
|
||||
Boundary,
|
||||
}
|
||||
|
||||
/// Controls which formatter should be used when formatting code.
|
||||
|
||||
@@ -48,7 +48,6 @@ pub struct SyntaxMapMatches<'a> {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SyntaxMapCapture<'a> {
|
||||
pub depth: usize,
|
||||
pub node: Node<'a>,
|
||||
pub index: u32,
|
||||
pub grammar_index: usize,
|
||||
@@ -886,7 +885,9 @@ impl<'a> SyntaxMapCaptures<'a> {
|
||||
|
||||
// TODO - add a Tree-sitter API to remove the need for this.
|
||||
let cursor = unsafe {
|
||||
std::mem::transmute::<_, &'static mut QueryCursor>(query_cursor.deref_mut())
|
||||
std::mem::transmute::<&mut tree_sitter::QueryCursor, &'static mut QueryCursor>(
|
||||
query_cursor.deref_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
cursor.set_byte_range(range.clone());
|
||||
@@ -933,7 +934,6 @@ impl<'a> SyntaxMapCaptures<'a> {
|
||||
let layer = self.layers[..self.active_layer_count].first()?;
|
||||
let capture = layer.next_capture?;
|
||||
Some(SyntaxMapCapture {
|
||||
depth: layer.depth,
|
||||
grammar_index: layer.grammar_index,
|
||||
index: capture.index,
|
||||
node: capture.node,
|
||||
@@ -1004,7 +1004,9 @@ impl<'a> SyntaxMapMatches<'a> {
|
||||
|
||||
// TODO - add a Tree-sitter API to remove the need for this.
|
||||
let cursor = unsafe {
|
||||
std::mem::transmute::<_, &'static mut QueryCursor>(query_cursor.deref_mut())
|
||||
std::mem::transmute::<&mut tree_sitter::QueryCursor, &'static mut QueryCursor>(
|
||||
query_cursor.deref_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
cursor.set_byte_range(range.clone());
|
||||
|
||||
@@ -765,6 +765,7 @@ impl SearchableItem for LspLogView {
|
||||
regex: true,
|
||||
// LSP log is read-only.
|
||||
replacement: false,
|
||||
selection: false,
|
||||
}
|
||||
}
|
||||
fn active_match_index(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name = "C++"
|
||||
grammar = "cpp"
|
||||
path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp"]
|
||||
path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl"]
|
||||
line_comments = ["// "]
|
||||
autoclose_before = ";:.,=}])>"
|
||||
brackets = [
|
||||
|
||||
@@ -29,14 +29,14 @@ pub(super) fn json_task_context() -> ContextProviderWithTasks {
|
||||
ContextProviderWithTasks::new(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: "package script $ZED_CUSTOM_script".to_owned(),
|
||||
command: "npm run".to_owned(),
|
||||
command: "npm --prefix $ZED_DIRNAME run".to_owned(),
|
||||
args: vec![VariableName::Custom("script".into()).template_value()],
|
||||
tags: vec!["package-script".into()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "composer script $ZED_CUSTOM_script".to_owned(),
|
||||
command: "composer".to_owned(),
|
||||
command: "composer -d $ZED_DIRNAME".to_owned(),
|
||||
args: vec![VariableName::Custom("script".into()).template_value()],
|
||||
tags: vec!["composer-script".into()],
|
||||
..TaskTemplate::default()
|
||||
|
||||
159
crates/lsp/src/input_handler.rs
Normal file
159
crates/lsp/src/input_handler.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use std::str;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
|
||||
AsyncBufReadExt, AsyncRead, AsyncReadExt as _,
|
||||
};
|
||||
use gpui::{BackgroundExecutor, Task};
|
||||
use log::warn;
|
||||
use parking_lot::Mutex;
|
||||
use smol::io::BufReader;
|
||||
|
||||
use crate::{
|
||||
AnyNotification, AnyResponse, IoHandler, IoKind, RequestId, ResponseHandler, CONTENT_LEN_HEADER,
|
||||
};
|
||||
|
||||
const HEADER_DELIMITER: &'static [u8; 4] = b"\r\n\r\n";
|
||||
/// Handler for stdout of language server.
|
||||
pub struct LspStdoutHandler {
|
||||
pub(super) loop_handle: Task<Result<()>>,
|
||||
pub(super) notifications_channel: UnboundedReceiver<AnyNotification>,
|
||||
}
|
||||
|
||||
pub(self) async fn read_headers<Stdout>(
|
||||
reader: &mut BufReader<Stdout>,
|
||||
buffer: &mut Vec<u8>,
|
||||
) -> Result<()>
|
||||
where
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
loop {
|
||||
if buffer.len() >= HEADER_DELIMITER.len()
|
||||
&& buffer[(buffer.len() - HEADER_DELIMITER.len())..] == HEADER_DELIMITER[..]
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if reader.read_until(b'\n', buffer).await? == 0 {
|
||||
return Err(anyhow!("cannot read LSP message headers"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LspStdoutHandler {
|
||||
pub fn new<Input>(
|
||||
stdout: Input,
|
||||
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
||||
io_handlers: Arc<Mutex<HashMap<i32, IoHandler>>>,
|
||||
cx: BackgroundExecutor,
|
||||
) -> Self
|
||||
where
|
||||
Input: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
let (tx, notifications_channel) = unbounded();
|
||||
let loop_handle = cx.spawn(Self::handler(stdout, tx, response_handlers, io_handlers));
|
||||
Self {
|
||||
loop_handle,
|
||||
notifications_channel,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handler<Input>(
|
||||
stdout: Input,
|
||||
notifications_sender: UnboundedSender<AnyNotification>,
|
||||
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
||||
io_handlers: Arc<Mutex<HashMap<i32, IoHandler>>>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
Input: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
let mut stdout = BufReader::new(stdout);
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
loop {
|
||||
buffer.clear();
|
||||
|
||||
read_headers(&mut stdout, &mut buffer).await?;
|
||||
|
||||
let headers = std::str::from_utf8(&buffer)?;
|
||||
|
||||
let message_len = headers
|
||||
.split('\n')
|
||||
.find(|line| line.starts_with(CONTENT_LEN_HEADER))
|
||||
.and_then(|line| line.strip_prefix(CONTENT_LEN_HEADER))
|
||||
.ok_or_else(|| anyhow!("invalid LSP message header {headers:?}"))?
|
||||
.trim_end()
|
||||
.parse()?;
|
||||
|
||||
buffer.resize(message_len, 0);
|
||||
stdout.read_exact(&mut buffer).await?;
|
||||
|
||||
if let Ok(message) = str::from_utf8(&buffer) {
|
||||
log::trace!("incoming message: {message}");
|
||||
for handler in io_handlers.lock().values_mut() {
|
||||
handler(IoKind::StdOut, message);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
|
||||
notifications_sender.unbounded_send(msg)?;
|
||||
} else if let Ok(AnyResponse {
|
||||
id, error, result, ..
|
||||
}) = serde_json::from_slice(&buffer)
|
||||
{
|
||||
let mut response_handlers = response_handlers.lock();
|
||||
if let Some(handler) = response_handlers
|
||||
.as_mut()
|
||||
.and_then(|handlers| handlers.remove(&id))
|
||||
{
|
||||
drop(response_handlers);
|
||||
if let Some(error) = error {
|
||||
handler(Err(error));
|
||||
} else if let Some(result) = result {
|
||||
handler(Ok(result.get().into()));
|
||||
} else {
|
||||
handler(Ok("null".into()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"failed to deserialize LSP message:\n{}",
|
||||
std::str::from_utf8(&buffer)?
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_read_headers() {
|
||||
let mut buf = Vec::new();
|
||||
let mut reader = smol::io::BufReader::new(b"Content-Length: 123\r\n\r\n" as &[u8]);
|
||||
read_headers(&mut reader, &mut buf).await.unwrap();
|
||||
assert_eq!(buf, b"Content-Length: 123\r\n\r\n");
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut reader = smol::io::BufReader::new(b"Content-Type: application/vscode-jsonrpc\r\nContent-Length: 1235\r\n\r\n{\"somecontent\":123}" as &[u8]);
|
||||
read_headers(&mut reader, &mut buf).await.unwrap();
|
||||
assert_eq!(
|
||||
buf,
|
||||
b"Content-Type: application/vscode-jsonrpc\r\nContent-Length: 1235\r\n\r\n"
|
||||
);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut reader = smol::io::BufReader::new(b"Content-Length: 1235\r\nContent-Type: application/vscode-jsonrpc\r\n\r\n{\"somecontent\":true}" as &[u8]);
|
||||
read_headers(&mut reader, &mut buf).await.unwrap();
|
||||
assert_eq!(
|
||||
buf,
|
||||
b"Content-Length: 1235\r\nContent-Type: application/vscode-jsonrpc\r\n\r\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use log::warn;
|
||||
mod input_handler;
|
||||
|
||||
pub use lsp_types::request::*;
|
||||
pub use lsp_types::*;
|
||||
|
||||
@@ -12,7 +13,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::{json, value::RawValue, Value};
|
||||
use smol::{
|
||||
channel,
|
||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
process::{self, Child},
|
||||
};
|
||||
|
||||
@@ -25,7 +26,6 @@ use std::{
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
pin::Pin,
|
||||
str::{self, FromStr as _},
|
||||
sync::{
|
||||
atomic::{AtomicI32, Ordering::SeqCst},
|
||||
Arc, Weak,
|
||||
@@ -36,13 +36,13 @@ use std::{
|
||||
use std::{path::Path, process::Stdio};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
const HEADER_DELIMITER: &'static [u8; 4] = b"\r\n\r\n";
|
||||
const JSON_RPC_VERSION: &str = "2.0";
|
||||
const CONTENT_LEN_HEADER: &str = "Content-Length: ";
|
||||
|
||||
const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
|
||||
const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
type NotificationHandler = Box<dyn Send + FnMut(Option<RequestId>, &str, AsyncAppContext)>;
|
||||
type NotificationHandler = Box<dyn Send + FnMut(Option<RequestId>, Value, AsyncAppContext)>;
|
||||
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
|
||||
type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
|
||||
|
||||
@@ -164,13 +164,12 @@ struct Notification<'a, T> {
|
||||
|
||||
/// Language server RPC notification message before it is deserialized into a concrete type.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct AnyNotification<'a> {
|
||||
struct AnyNotification {
|
||||
#[serde(default)]
|
||||
id: Option<RequestId>,
|
||||
#[serde(borrow)]
|
||||
method: &'a str,
|
||||
#[serde(borrow, default)]
|
||||
params: Option<&'a RawValue>,
|
||||
method: String,
|
||||
#[serde(default)]
|
||||
params: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -297,13 +296,7 @@ impl LanguageServer {
|
||||
"Language server with id {} sent unhandled notification {}:\n{}",
|
||||
server_id,
|
||||
notification.method,
|
||||
serde_json::to_string_pretty(
|
||||
¬ification
|
||||
.params
|
||||
.and_then(|params| Value::from_str(params.get()).ok())
|
||||
.unwrap_or(Value::Null)
|
||||
)
|
||||
.unwrap(),
|
||||
serde_json::to_string_pretty(¬ification.params).unwrap(),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -418,79 +411,36 @@ impl LanguageServer {
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
F: FnMut(AnyNotification) + 'static + Send,
|
||||
{
|
||||
let mut stdout = BufReader::new(stdout);
|
||||
use smol::stream::StreamExt;
|
||||
let stdout = BufReader::new(stdout);
|
||||
let _clear_response_handlers = util::defer({
|
||||
let response_handlers = response_handlers.clone();
|
||||
move || {
|
||||
response_handlers.lock().take();
|
||||
}
|
||||
});
|
||||
let mut buffer = Vec::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
let mut input_handler = input_handler::LspStdoutHandler::new(
|
||||
stdout,
|
||||
response_handlers,
|
||||
io_handlers,
|
||||
cx.background_executor().clone(),
|
||||
);
|
||||
|
||||
read_headers(&mut stdout, &mut buffer).await?;
|
||||
|
||||
let headers = std::str::from_utf8(&buffer)?;
|
||||
|
||||
let message_len = headers
|
||||
.split('\n')
|
||||
.find(|line| line.starts_with(CONTENT_LEN_HEADER))
|
||||
.and_then(|line| line.strip_prefix(CONTENT_LEN_HEADER))
|
||||
.ok_or_else(|| anyhow!("invalid LSP message header {headers:?}"))?
|
||||
.trim_end()
|
||||
.parse()?;
|
||||
|
||||
buffer.resize(message_len, 0);
|
||||
stdout.read_exact(&mut buffer).await?;
|
||||
|
||||
if let Ok(message) = str::from_utf8(&buffer) {
|
||||
log::trace!("incoming message: {message}");
|
||||
for handler in io_handlers.lock().values_mut() {
|
||||
handler(IoKind::StdOut, message);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
|
||||
while let Some(msg) = input_handler.notifications_channel.next().await {
|
||||
{
|
||||
let mut notification_handlers = notification_handlers.lock();
|
||||
if let Some(handler) = notification_handlers.get_mut(msg.method) {
|
||||
handler(
|
||||
msg.id,
|
||||
msg.params.map(|params| params.get()).unwrap_or("null"),
|
||||
cx.clone(),
|
||||
);
|
||||
if let Some(handler) = notification_handlers.get_mut(msg.method.as_str()) {
|
||||
handler(msg.id, msg.params.unwrap_or(Value::Null), cx.clone());
|
||||
} else {
|
||||
drop(notification_handlers);
|
||||
on_unhandled_notification(msg);
|
||||
}
|
||||
} else if let Ok(AnyResponse {
|
||||
id, error, result, ..
|
||||
}) = serde_json::from_slice(&buffer)
|
||||
{
|
||||
let mut response_handlers = response_handlers.lock();
|
||||
if let Some(handler) = response_handlers
|
||||
.as_mut()
|
||||
.and_then(|handlers| handlers.remove(&id))
|
||||
{
|
||||
drop(response_handlers);
|
||||
if let Some(error) = error {
|
||||
handler(Err(error));
|
||||
} else if let Some(result) = result {
|
||||
handler(Ok(result.get().into()));
|
||||
} else {
|
||||
handler(Ok("null".into()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"failed to deserialize LSP message:\n{}",
|
||||
std::str::from_utf8(&buffer)?
|
||||
);
|
||||
}
|
||||
|
||||
// Don't starve the main thread when receiving lots of messages at once.
|
||||
// Don't starve the main thread when receiving lots of notifications at once.
|
||||
smol::future::yield_now().await;
|
||||
}
|
||||
input_handler.loop_handle.await
|
||||
}
|
||||
|
||||
async fn handle_stderr<Stderr>(
|
||||
@@ -512,7 +462,7 @@ impl LanguageServer {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Ok(message) = str::from_utf8(&buffer) {
|
||||
if let Ok(message) = std::str::from_utf8(&buffer) {
|
||||
log::trace!("incoming stderr message:{message}");
|
||||
for handler in io_handlers.lock().values_mut() {
|
||||
handler(IoKind::StdErr, message);
|
||||
@@ -850,7 +800,7 @@ impl LanguageServer {
|
||||
let prev_handler = self.notification_handlers.lock().insert(
|
||||
method,
|
||||
Box::new(move |_, params, cx| {
|
||||
if let Some(params) = serde_json::from_str(params).log_err() {
|
||||
if let Some(params) = serde_json::from_value(params).log_err() {
|
||||
f(params, cx);
|
||||
}
|
||||
}),
|
||||
@@ -878,7 +828,7 @@ impl LanguageServer {
|
||||
method,
|
||||
Box::new(move |id, params, cx| {
|
||||
if let Some(id) = id {
|
||||
match serde_json::from_str(params) {
|
||||
match serde_json::from_value(params) {
|
||||
Ok(params) => {
|
||||
let response = f(params, cx.clone());
|
||||
cx.foreground_executor()
|
||||
@@ -910,12 +860,7 @@ impl LanguageServer {
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error deserializing {} request: {:?}, message: {:?}",
|
||||
method,
|
||||
error,
|
||||
params
|
||||
);
|
||||
log::error!("error deserializing {} request: {:?}", method, error);
|
||||
let response = AnyResponse {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
id,
|
||||
@@ -1202,10 +1147,7 @@ impl FakeLanguageServer {
|
||||
notifications_tx
|
||||
.try_send((
|
||||
msg.method.to_string(),
|
||||
msg.params
|
||||
.map(|raw_value| raw_value.get())
|
||||
.unwrap_or("null")
|
||||
.to_string(),
|
||||
msg.params.unwrap_or(Value::Null).to_string(),
|
||||
))
|
||||
.ok();
|
||||
},
|
||||
@@ -1372,30 +1314,11 @@ impl FakeLanguageServer {
|
||||
}
|
||||
}
|
||||
|
||||
pub(self) async fn read_headers<Stdout>(
|
||||
reader: &mut BufReader<Stdout>,
|
||||
buffer: &mut Vec<u8>,
|
||||
) -> Result<()>
|
||||
where
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
loop {
|
||||
if buffer.len() >= HEADER_DELIMITER.len()
|
||||
&& buffer[(buffer.len() - HEADER_DELIMITER.len())..] == HEADER_DELIMITER[..]
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if reader.read_until(b'\n', buffer).await? == 0 {
|
||||
return Err(anyhow!("cannot read LSP message headers"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
@@ -1475,30 +1398,6 @@ mod tests {
|
||||
fake.receive_notification::<notification::Exit>().await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_read_headers() {
|
||||
let mut buf = Vec::new();
|
||||
let mut reader = smol::io::BufReader::new(b"Content-Length: 123\r\n\r\n" as &[u8]);
|
||||
read_headers(&mut reader, &mut buf).await.unwrap();
|
||||
assert_eq!(buf, b"Content-Length: 123\r\n\r\n");
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut reader = smol::io::BufReader::new(b"Content-Type: application/vscode-jsonrpc\r\nContent-Length: 1235\r\n\r\n{\"somecontent\":123}" as &[u8]);
|
||||
read_headers(&mut reader, &mut buf).await.unwrap();
|
||||
assert_eq!(
|
||||
buf,
|
||||
b"Content-Type: application/vscode-jsonrpc\r\nContent-Length: 1235\r\n\r\n"
|
||||
);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut reader = smol::io::BufReader::new(b"Content-Length: 1235\r\nContent-Type: application/vscode-jsonrpc\r\n\r\n{\"somecontent\":true}" as &[u8]);
|
||||
read_headers(&mut reader, &mut buf).await.unwrap();
|
||||
assert_eq!(
|
||||
buf,
|
||||
b"Content-Length: 1235\r\nContent-Type: application/vscode-jsonrpc\r\n\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_deserialize_string_digit_id() {
|
||||
let json = r#"{"jsonrpc":"2.0","id":"2","method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#;
|
||||
|
||||
@@ -3740,6 +3740,62 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns excerpts overlapping the given ranges. If range spans multiple excerpts returns one range for each excerpt
|
||||
pub fn excerpts_in_ranges(
|
||||
&self,
|
||||
ranges: impl IntoIterator<Item = Range<Anchor>>,
|
||||
) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, Range<usize>)> {
|
||||
let mut ranges = ranges.into_iter().map(|range| range.to_offset(self));
|
||||
|
||||
let mut cursor = self.excerpts.cursor::<usize>();
|
||||
let mut next_range = move |cursor: &mut Cursor<Excerpt, usize>| {
|
||||
let range = ranges.next();
|
||||
if let Some(range) = range.as_ref() {
|
||||
cursor.seek_forward(&range.start, Bias::Right, &());
|
||||
}
|
||||
|
||||
range
|
||||
};
|
||||
let mut range = next_range(&mut cursor);
|
||||
|
||||
iter::from_fn(move || {
|
||||
if range.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if range.as_ref().unwrap().is_empty() || *cursor.start() >= range.as_ref().unwrap().end
|
||||
{
|
||||
range = next_range(&mut cursor);
|
||||
if range.is_none() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
cursor.item().map(|excerpt| {
|
||||
let multibuffer_excerpt = MultiBufferExcerpt::new(&excerpt, *cursor.start());
|
||||
|
||||
let multibuffer_excerpt_range = multibuffer_excerpt
|
||||
.map_range_from_buffer(excerpt.range.context.to_offset(&excerpt.buffer));
|
||||
|
||||
let overlap_range = cmp::max(
|
||||
range.as_ref().unwrap().start,
|
||||
multibuffer_excerpt_range.start,
|
||||
)
|
||||
..cmp::min(range.as_ref().unwrap().end, multibuffer_excerpt_range.end);
|
||||
|
||||
let overlap_range = multibuffer_excerpt.map_range_to_buffer(overlap_range);
|
||||
|
||||
if multibuffer_excerpt_range.end <= range.as_ref().unwrap().end {
|
||||
cursor.next(&());
|
||||
} else {
|
||||
range = next_range(&mut cursor);
|
||||
}
|
||||
|
||||
(excerpt.id, &excerpt.buffer, overlap_range)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remote_selections_in_range<'a>(
|
||||
&'a self,
|
||||
range: &'a Range<Anchor>,
|
||||
@@ -6076,4 +6132,415 @@ mod tests {
|
||||
assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_excerpts_in_ranges_no_ranges(cx: &mut AppContext) {
|
||||
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
|
||||
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
|
||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_1.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_2.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||||
|
||||
let mut excerpts = snapshot.excerpts_in_ranges(iter::from_fn(|| None));
|
||||
|
||||
assert!(excerpts.next().is_none());
|
||||
}
|
||||
|
||||
fn validate_excerpts(
|
||||
actual: &Vec<(ExcerptId, BufferId, Range<Anchor>)>,
|
||||
expected: &Vec<(ExcerptId, BufferId, Range<Anchor>)>,
|
||||
) {
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
|
||||
actual
|
||||
.into_iter()
|
||||
.zip(expected)
|
||||
.map(|(actual, expected)| {
|
||||
assert_eq!(actual.0, expected.0);
|
||||
assert_eq!(actual.1, expected.1);
|
||||
assert_eq!(actual.2.start, expected.2.start);
|
||||
assert_eq!(actual.2.end, expected.2.end);
|
||||
})
|
||||
.collect_vec();
|
||||
}
|
||||
|
||||
fn map_range_from_excerpt(
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
excerpt_id: ExcerptId,
|
||||
excerpt_buffer: &BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
) -> Range<Anchor> {
|
||||
snapshot
|
||||
.anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_before(range.start))
|
||||
.unwrap()
|
||||
..snapshot
|
||||
.anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_after(range.end))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn make_expected_excerpt_info(
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
cx: &mut AppContext,
|
||||
excerpt_id: ExcerptId,
|
||||
buffer: &Model<Buffer>,
|
||||
range: Range<usize>,
|
||||
) -> (ExcerptId, BufferId, Range<Anchor>) {
|
||||
(
|
||||
excerpt_id,
|
||||
buffer.read(cx).remote_id(),
|
||||
map_range_from_excerpt(&snapshot, excerpt_id, &buffer.read(cx).snapshot(), range),
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_excerpts_in_ranges_range_inside_the_excerpt(cx: &mut AppContext) {
|
||||
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
|
||||
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
|
||||
let buffer_len = buffer_1.read(cx).len();
|
||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||
let mut expected_excerpt_id = ExcerptId(0);
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
expected_excerpt_id = multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_1.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
)[0];
|
||||
multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_2.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||||
|
||||
let range = snapshot
|
||||
.anchor_in_excerpt(expected_excerpt_id, buffer_1.read(cx).anchor_before(1))
|
||||
.unwrap()
|
||||
..snapshot
|
||||
.anchor_in_excerpt(
|
||||
expected_excerpt_id,
|
||||
buffer_1.read(cx).anchor_after(buffer_len / 2),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let expected_excerpts = vec![make_expected_excerpt_info(
|
||||
&snapshot,
|
||||
cx,
|
||||
expected_excerpt_id,
|
||||
&buffer_1,
|
||||
1..(buffer_len / 2),
|
||||
)];
|
||||
|
||||
let excerpts = snapshot
|
||||
.excerpts_in_ranges(vec![range.clone()].into_iter())
|
||||
.map(|(excerpt_id, buffer, actual_range)| {
|
||||
(
|
||||
excerpt_id,
|
||||
buffer.remote_id(),
|
||||
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
|
||||
)
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
validate_excerpts(&excerpts, &expected_excerpts);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_excerpts_in_ranges_range_crosses_excerpts_boundary(cx: &mut AppContext) {
|
||||
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
|
||||
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
|
||||
let buffer_len = buffer_1.read(cx).len();
|
||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||
let mut excerpt_1_id = ExcerptId(0);
|
||||
let mut excerpt_2_id = ExcerptId(0);
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
excerpt_1_id = multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_1.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
)[0];
|
||||
excerpt_2_id = multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_2.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
)[0];
|
||||
});
|
||||
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
|
||||
let expected_range = snapshot
|
||||
.anchor_in_excerpt(
|
||||
excerpt_1_id,
|
||||
buffer_1.read(cx).anchor_before(buffer_len / 2),
|
||||
)
|
||||
.unwrap()
|
||||
..snapshot
|
||||
.anchor_in_excerpt(excerpt_2_id, buffer_2.read(cx).anchor_after(buffer_len / 2))
|
||||
.unwrap();
|
||||
|
||||
let expected_excerpts = vec![
|
||||
make_expected_excerpt_info(
|
||||
&snapshot,
|
||||
cx,
|
||||
excerpt_1_id,
|
||||
&buffer_1,
|
||||
(buffer_len / 2)..buffer_len,
|
||||
),
|
||||
make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len / 2),
|
||||
];
|
||||
|
||||
let excerpts = snapshot
|
||||
.excerpts_in_ranges(vec![expected_range.clone()].into_iter())
|
||||
.map(|(excerpt_id, buffer, actual_range)| {
|
||||
(
|
||||
excerpt_id,
|
||||
buffer.remote_id(),
|
||||
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
|
||||
)
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
validate_excerpts(&excerpts, &expected_excerpts);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_excerpts_in_ranges_range_encloses_excerpt(cx: &mut AppContext) {
|
||||
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
|
||||
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
|
||||
let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'r'), cx));
|
||||
let buffer_len = buffer_1.read(cx).len();
|
||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||
let mut excerpt_1_id = ExcerptId(0);
|
||||
let mut excerpt_2_id = ExcerptId(0);
|
||||
let mut excerpt_3_id = ExcerptId(0);
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
excerpt_1_id = multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_1.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
)[0];
|
||||
excerpt_2_id = multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_2.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
)[0];
|
||||
excerpt_3_id = multibuffer.push_excerpts(
|
||||
buffer_3.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_3.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
)[0];
|
||||
});
|
||||
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
|
||||
let expected_range = snapshot
|
||||
.anchor_in_excerpt(
|
||||
excerpt_1_id,
|
||||
buffer_1.read(cx).anchor_before(buffer_len / 2),
|
||||
)
|
||||
.unwrap()
|
||||
..snapshot
|
||||
.anchor_in_excerpt(excerpt_3_id, buffer_3.read(cx).anchor_after(buffer_len / 2))
|
||||
.unwrap();
|
||||
|
||||
let expected_excerpts = vec![
|
||||
make_expected_excerpt_info(
|
||||
&snapshot,
|
||||
cx,
|
||||
excerpt_1_id,
|
||||
&buffer_1,
|
||||
(buffer_len / 2)..buffer_len,
|
||||
),
|
||||
make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len),
|
||||
make_expected_excerpt_info(&snapshot, cx, excerpt_3_id, &buffer_3, 0..buffer_len / 2),
|
||||
];
|
||||
|
||||
let excerpts = snapshot
|
||||
.excerpts_in_ranges(vec![expected_range.clone()].into_iter())
|
||||
.map(|(excerpt_id, buffer, actual_range)| {
|
||||
(
|
||||
excerpt_id,
|
||||
buffer.remote_id(),
|
||||
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
|
||||
)
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
validate_excerpts(&excerpts, &expected_excerpts);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_excerpts_in_ranges_multiple_ranges(cx: &mut AppContext) {
|
||||
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
|
||||
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
|
||||
let buffer_len = buffer_1.read(cx).len();
|
||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||
let mut excerpt_1_id = ExcerptId(0);
|
||||
let mut excerpt_2_id = ExcerptId(0);
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
excerpt_1_id = multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_1.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
)[0];
|
||||
excerpt_2_id = multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_2.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
)[0];
|
||||
});
|
||||
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
|
||||
let ranges = vec![
|
||||
1..(buffer_len / 4),
|
||||
(buffer_len / 3)..(buffer_len / 2),
|
||||
(buffer_len / 4 * 3)..(buffer_len),
|
||||
];
|
||||
|
||||
let expected_excerpts = ranges
|
||||
.iter()
|
||||
.map(|range| {
|
||||
make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, range.clone())
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let ranges = ranges.into_iter().map(|range| {
|
||||
map_range_from_excerpt(
|
||||
&snapshot,
|
||||
excerpt_1_id,
|
||||
&buffer_1.read(cx).snapshot(),
|
||||
range,
|
||||
)
|
||||
});
|
||||
|
||||
let excerpts = snapshot
|
||||
.excerpts_in_ranges(ranges)
|
||||
.map(|(excerpt_id, buffer, actual_range)| {
|
||||
(
|
||||
excerpt_id,
|
||||
buffer.remote_id(),
|
||||
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
|
||||
)
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
validate_excerpts(&excerpts, &expected_excerpts);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_excerpts_in_ranges_range_ends_at_excerpt_end(cx: &mut AppContext) {
|
||||
let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
|
||||
let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx));
|
||||
let buffer_len = buffer_1.read(cx).len();
|
||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||
let mut excerpt_1_id = ExcerptId(0);
|
||||
let mut excerpt_2_id = ExcerptId(0);
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
excerpt_1_id = multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_1.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
)[0];
|
||||
excerpt_2_id = multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..buffer_2.read(cx).len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
)[0];
|
||||
});
|
||||
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
|
||||
let ranges = [0..buffer_len, (buffer_len / 3)..(buffer_len / 2)];
|
||||
|
||||
let expected_excerpts = vec![
|
||||
make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, ranges[0].clone()),
|
||||
make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, ranges[1].clone()),
|
||||
];
|
||||
|
||||
let ranges = [
|
||||
map_range_from_excerpt(
|
||||
&snapshot,
|
||||
excerpt_1_id,
|
||||
&buffer_1.read(cx).snapshot(),
|
||||
ranges[0].clone(),
|
||||
),
|
||||
map_range_from_excerpt(
|
||||
&snapshot,
|
||||
excerpt_2_id,
|
||||
&buffer_2.read(cx).snapshot(),
|
||||
ranges[1].clone(),
|
||||
),
|
||||
];
|
||||
|
||||
let excerpts = snapshot
|
||||
.excerpts_in_ranges(ranges.into_iter())
|
||||
.map(|(excerpt_id, buffer, actual_range)| {
|
||||
(
|
||||
excerpt_id,
|
||||
buffer.remote_id(),
|
||||
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
|
||||
)
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
validate_excerpts(&excerpts, &expected_excerpts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,19 @@ pub trait PickerDelegate: Sized + 'static {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_editor(&self, editor: &View<Editor>, _cx: &mut ViewContext<Picker<Self>>) -> Div {
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
.h_9()
|
||||
.px_4()
|
||||
.child(editor.clone()),
|
||||
)
|
||||
.child(Divider::horizontal())
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
@@ -552,16 +565,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||
.on_action(cx.listener(Self::use_selected_query))
|
||||
.on_action(cx.listener(Self::confirm_input))
|
||||
.child(match &self.head {
|
||||
Head::Editor(editor) => v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
.h_9()
|
||||
.px_4()
|
||||
.child(editor.clone()),
|
||||
)
|
||||
.child(Divider::horizontal()),
|
||||
Head::Editor(editor) => self.delegate.render_editor(&editor.clone(), cx),
|
||||
Head::Empty(empty_head) => div().child(empty_head.clone()),
|
||||
})
|
||||
.when(self.delegate.match_count() > 0, |el| {
|
||||
|
||||
@@ -10282,7 +10282,7 @@ impl Project {
|
||||
fn deserialize_symbol(serialized_symbol: proto::Symbol) -> Result<CoreSymbol> {
|
||||
let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
|
||||
let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
|
||||
let kind = unsafe { mem::transmute(serialized_symbol.kind) };
|
||||
let kind = unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
|
||||
let path = ProjectPath {
|
||||
worktree_id,
|
||||
path: PathBuf::from(serialized_symbol.path).into(),
|
||||
@@ -11393,7 +11393,7 @@ fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
|
||||
worktree_id: symbol.path.worktree_id.to_proto(),
|
||||
path: symbol.path.path.to_string_lossy().to_string(),
|
||||
name: symbol.name.clone(),
|
||||
kind: unsafe { mem::transmute(symbol.kind) },
|
||||
kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
|
||||
start: Some(proto::PointUtf16 {
|
||||
row: symbol.range.start.0.row,
|
||||
column: symbol.range.start.0.column,
|
||||
|
||||
@@ -38,6 +38,7 @@ pub struct GitSettings {
|
||||
|
||||
impl GitSettings {
|
||||
pub fn inline_blame_enabled(&self) -> bool {
|
||||
#[allow(unknown_lints, clippy::manual_unwrap_or_default)]
|
||||
match self.inline_blame {
|
||||
Some(InlineBlameSettings { enabled, .. }) => enabled,
|
||||
_ => false,
|
||||
|
||||
@@ -1295,7 +1295,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
|
||||
project
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.collect::<Vec<_>>(),
|
||||
[LanguageServerId(0); 0]
|
||||
[] as [language::LanguageServerId; 0]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ mod registrar;
|
||||
use crate::{
|
||||
search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
|
||||
ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
|
||||
ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleWholeWord,
|
||||
ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
|
||||
};
|
||||
use any_vec::AnyVec;
|
||||
use collections::HashMap;
|
||||
@@ -48,6 +48,8 @@ pub struct Deploy {
|
||||
pub focus: bool,
|
||||
#[serde(default)]
|
||||
pub replace_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub selection_search_enabled: bool,
|
||||
}
|
||||
|
||||
impl_actions!(buffer_search, [Deploy]);
|
||||
@@ -59,6 +61,7 @@ impl Deploy {
|
||||
Self {
|
||||
focus: true,
|
||||
replace_enabled: false,
|
||||
selection_search_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +93,7 @@ pub struct BufferSearchBar {
|
||||
search_history: SearchHistory,
|
||||
search_history_cursor: SearchHistoryCursor,
|
||||
replace_enabled: bool,
|
||||
selection_search_enabled: bool,
|
||||
scroll_handle: ScrollHandle,
|
||||
editor_scroll_handle: ScrollHandle,
|
||||
editor_needed_width: Pixels,
|
||||
@@ -228,7 +232,7 @@ impl Render for BufferSearchBar {
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.children(supported_options.word.then(|| {
|
||||
.children(supported_options.regex.then(|| {
|
||||
self.render_search_option_button(
|
||||
SearchOptions::REGEX,
|
||||
cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
|
||||
@@ -251,6 +255,26 @@ impl Render for BufferSearchBar {
|
||||
.tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
|
||||
)
|
||||
})
|
||||
.when(supported_options.selection, |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"buffer-search-bar-toggle-search-selection-button",
|
||||
IconName::SearchSelection,
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.when(self.selection_search_enabled, |button| {
|
||||
button.style(ButtonStyle::Filled)
|
||||
})
|
||||
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
|
||||
this.toggle_selection(&ToggleSelection, cx);
|
||||
}))
|
||||
.selected(self.selection_search_enabled)
|
||||
.size(ButtonSize::Compact)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action("Toggle search selection", &ToggleSelection, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
@@ -359,6 +383,9 @@ impl Render for BufferSearchBar {
|
||||
.when(self.supported_options().regex, |this| {
|
||||
this.on_action(cx.listener(Self::toggle_regex))
|
||||
})
|
||||
.when(self.supported_options().selection, |this| {
|
||||
this.on_action(cx.listener(Self::toggle_selection))
|
||||
})
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -440,6 +467,11 @@ impl BufferSearchBar {
|
||||
this.toggle_whole_word(action, cx);
|
||||
}
|
||||
}));
|
||||
registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, cx| {
|
||||
if this.supported_options().selection {
|
||||
this.toggle_selection(action, cx);
|
||||
}
|
||||
}));
|
||||
registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| {
|
||||
if this.supported_options().replacement {
|
||||
this.toggle_replace(action, cx);
|
||||
@@ -497,6 +529,7 @@ impl BufferSearchBar {
|
||||
search_history_cursor: Default::default(),
|
||||
active_search: None,
|
||||
replace_enabled: false,
|
||||
selection_search_enabled: false,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
editor_scroll_handle: ScrollHandle::new(),
|
||||
editor_needed_width: px(0.),
|
||||
@@ -516,8 +549,11 @@ impl BufferSearchBar {
|
||||
searchable_item.clear_matches(cx);
|
||||
}
|
||||
}
|
||||
if let Some(active_editor) = self.active_searchable_item.as_ref() {
|
||||
if let Some(active_editor) = self.active_searchable_item.as_mut() {
|
||||
self.selection_search_enabled = false;
|
||||
self.replace_enabled = false;
|
||||
active_editor.search_bar_visibility_changed(false, cx);
|
||||
active_editor.toggle_filtered_search_ranges(false, cx);
|
||||
let handle = active_editor.focus_handle(cx);
|
||||
cx.focus(&handle);
|
||||
}
|
||||
@@ -530,8 +566,12 @@ impl BufferSearchBar {
|
||||
|
||||
pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.show(cx) {
|
||||
if let Some(active_item) = self.active_searchable_item.as_mut() {
|
||||
active_item.toggle_filtered_search_ranges(deploy.selection_search_enabled, cx);
|
||||
}
|
||||
self.search_suggested(cx);
|
||||
self.replace_enabled = deploy.replace_enabled;
|
||||
self.selection_search_enabled = deploy.selection_search_enabled;
|
||||
if deploy.focus {
|
||||
let mut handle = self.query_editor.focus_handle(cx).clone();
|
||||
let mut select_query = true;
|
||||
@@ -539,9 +579,11 @@ impl BufferSearchBar {
|
||||
handle = self.replacement_editor.focus_handle(cx).clone();
|
||||
select_query = false;
|
||||
};
|
||||
|
||||
if select_query {
|
||||
self.select_query(cx);
|
||||
}
|
||||
|
||||
cx.focus(&handle);
|
||||
}
|
||||
return true;
|
||||
@@ -823,6 +865,15 @@ impl BufferSearchBar {
|
||||
self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
|
||||
}
|
||||
|
||||
fn toggle_selection(&mut self, _: &ToggleSelection, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_item) = self.active_searchable_item.as_mut() {
|
||||
self.selection_search_enabled = !self.selection_search_enabled;
|
||||
active_item.toggle_filtered_search_ranges(self.selection_search_enabled, cx);
|
||||
let _ = self.update_matches(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
|
||||
self.toggle_search_option(SearchOptions::REGEX, cx)
|
||||
}
|
||||
@@ -1090,9 +1141,9 @@ mod tests {
|
||||
use std::ops::Range;
|
||||
|
||||
use super::*;
|
||||
use editor::{display_map::DisplayRow, DisplayPoint, Editor};
|
||||
use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer};
|
||||
use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
|
||||
use language::Buffer;
|
||||
use language::{Buffer, Point};
|
||||
use project::Project;
|
||||
use smol::stream::StreamExt as _;
|
||||
use unindent::Unindent as _;
|
||||
@@ -1405,6 +1456,15 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
fn display_points_of(
|
||||
background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
|
||||
) -> Vec<Range<DisplayPoint>> {
|
||||
background_highlights
|
||||
.into_iter()
|
||||
.map(|(range, _)| range)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_option_handling(cx: &mut TestAppContext) {
|
||||
let (editor, search_bar, cx) = init_test(cx);
|
||||
@@ -1417,12 +1477,6 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
|
||||
background_highlights
|
||||
.into_iter()
|
||||
.map(|(range, _)| range)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
display_points_of(editor.all_text_background_highlights(cx)),
|
||||
@@ -2032,15 +2086,156 @@ mod tests {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_globals(cx);
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::local(
|
||||
r#"
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
"#
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let cx = cx.add_empty_window();
|
||||
let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
|
||||
|
||||
let search_bar = cx.new_view(|cx| {
|
||||
let mut search_bar = BufferSearchBar::new(cx);
|
||||
search_bar.set_active_pane_item(Some(&editor), cx);
|
||||
search_bar.show(cx);
|
||||
search_bar
|
||||
});
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
|
||||
})
|
||||
});
|
||||
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
let deploy = Deploy {
|
||||
focus: true,
|
||||
replace_enabled: false,
|
||||
selection_search_enabled: true,
|
||||
};
|
||||
search_bar.deploy(&deploy, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
search_bar
|
||||
.update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.search_background_highlights(cx),
|
||||
&[
|
||||
Point::new(1, 0)..Point::new(1, 3),
|
||||
Point::new(1, 8)..Point::new(1, 11),
|
||||
Point::new(2, 0)..Point::new(2, 3),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_globals(cx);
|
||||
let text = r#"
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
aaa bbb aaa ccc
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let cx = cx.add_empty_window();
|
||||
let editor = cx.new_view(|cx| {
|
||||
let multibuffer = MultiBuffer::build_multi(
|
||||
[
|
||||
(
|
||||
&text,
|
||||
vec![
|
||||
Point::new(0, 0)..Point::new(2, 0),
|
||||
Point::new(4, 0)..Point::new(5, 0),
|
||||
],
|
||||
),
|
||||
(&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
Editor::for_multibuffer(multibuffer, None, false, cx)
|
||||
});
|
||||
|
||||
let search_bar = cx.new_view(|cx| {
|
||||
let mut search_bar = BufferSearchBar::new(cx);
|
||||
search_bar.set_active_pane_item(Some(&editor), cx);
|
||||
search_bar.show(cx);
|
||||
search_bar
|
||||
});
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(vec![
|
||||
Point::new(1, 0)..Point::new(1, 4),
|
||||
Point::new(5, 3)..Point::new(6, 4),
|
||||
])
|
||||
})
|
||||
});
|
||||
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
let deploy = Deploy {
|
||||
focus: true,
|
||||
replace_enabled: false,
|
||||
selection_search_enabled: true,
|
||||
};
|
||||
search_bar.deploy(&deploy, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
search_bar
|
||||
.update(cx, |search_bar, cx| search_bar.search("aaa", None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.search_background_highlights(cx),
|
||||
&[
|
||||
Point::new(1, 0)..Point::new(1, 3),
|
||||
Point::new(5, 8)..Point::new(5, 11),
|
||||
Point::new(6, 0)..Point::new(6, 3),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
|
||||
let (editor, search_bar, cx) = init_test(cx);
|
||||
let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
|
||||
background_highlights
|
||||
.into_iter()
|
||||
.map(|(range, _)| range)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
// Search using valid regexp
|
||||
search_bar
|
||||
.update(cx, |search_bar, cx| {
|
||||
|
||||
@@ -25,6 +25,7 @@ actions!(
|
||||
ToggleIncludeIgnored,
|
||||
ToggleRegex,
|
||||
ToggleReplace,
|
||||
ToggleSelection,
|
||||
SelectNextMatch,
|
||||
SelectPrevMatch,
|
||||
SelectAllMatches,
|
||||
|
||||
@@ -14,7 +14,7 @@ doctest = false
|
||||
|
||||
|
||||
[dependencies]
|
||||
alacritty_terminal = "0.23"
|
||||
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "cacdb5bb3b72bad2c729227537979d95af75978f" }
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
dirs = "4.0.0"
|
||||
|
||||
@@ -553,7 +553,6 @@ impl Element for TerminalElement {
|
||||
global_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
self.interactivity.occlude_mouse();
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |mut style, cx| {
|
||||
|
||||
@@ -972,6 +972,7 @@ impl SearchableItem for TerminalView {
|
||||
word: false,
|
||||
regex: true,
|
||||
replacement: false,
|
||||
selection: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,10 +54,14 @@ pub enum IconDecoration {
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum IconSize {
|
||||
/// 10px
|
||||
Indicator,
|
||||
/// 12px
|
||||
XSmall,
|
||||
/// 14px
|
||||
Small,
|
||||
#[default]
|
||||
/// 16px
|
||||
Medium,
|
||||
}
|
||||
|
||||
@@ -169,12 +173,15 @@ pub enum IconName {
|
||||
Save,
|
||||
Screen,
|
||||
SelectAll,
|
||||
SearchSelection,
|
||||
Server,
|
||||
Settings,
|
||||
Shift,
|
||||
Sliders,
|
||||
Snip,
|
||||
Space,
|
||||
Sparkle,
|
||||
SparkleFilled,
|
||||
Spinner,
|
||||
Split,
|
||||
Star,
|
||||
@@ -293,12 +300,15 @@ impl IconName {
|
||||
IconName::Save => "icons/save.svg",
|
||||
IconName::Screen => "icons/desktop.svg",
|
||||
IconName::SelectAll => "icons/select_all.svg",
|
||||
IconName::SearchSelection => "icons/search_selection.svg",
|
||||
IconName::Server => "icons/server.svg",
|
||||
IconName::Settings => "icons/file_icons/settings.svg",
|
||||
IconName::Shift => "icons/shift.svg",
|
||||
IconName::Sliders => "icons/sliders.svg",
|
||||
IconName::Snip => "icons/snip.svg",
|
||||
IconName::Space => "icons/space.svg",
|
||||
IconName::Sparkle => "icons/sparkle.svg",
|
||||
IconName::SparkleFilled => "icons/sparkle_filled.svg",
|
||||
IconName::Spinner => "icons/spinner.svg",
|
||||
IconName::Split => "icons/split.svg",
|
||||
IconName::Star => "icons/star.svg",
|
||||
|
||||
@@ -21,7 +21,7 @@ lazy_static::lazy_static! {
|
||||
} else {
|
||||
HOME.join(".config").join("zed")
|
||||
};
|
||||
pub static ref CONVERSATIONS_DIR: PathBuf = if cfg!(target_os = "macos") {
|
||||
pub static ref CONTEXTS_DIR: PathBuf = if cfg!(target_os = "macos") {
|
||||
CONFIG_DIR.join("conversations")
|
||||
} else {
|
||||
SUPPORT_DIR.join("conversations")
|
||||
|
||||
@@ -1440,6 +1440,14 @@ pub(crate) fn last_non_whitespace(
|
||||
) -> DisplayPoint {
|
||||
let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
|
||||
let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
|
||||
|
||||
// NOTE: depending on clip_at_line_end we may already be one char back from the end.
|
||||
if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
|
||||
if char_kind(&scope, ch) != CharKind::Whitespace {
|
||||
return end_of_line.to_display_point(map);
|
||||
}
|
||||
}
|
||||
|
||||
for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
|
||||
if ch == '\n' {
|
||||
break;
|
||||
@@ -1935,6 +1943,10 @@ mod test {
|
||||
#[gpui::test]
|
||||
async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state("ˇ one\n two \nthree").await;
|
||||
cx.simulate_shared_keystrokes("g _").await;
|
||||
cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
|
||||
|
||||
cx.set_shared_state("ˇ one \n two \nthree").await;
|
||||
cx.simulate_shared_keystrokes("g _").await;
|
||||
cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
|
||||
|
||||
@@ -69,6 +69,10 @@ fn scroll_editor(
|
||||
let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
|
||||
let old_top_anchor = editor.scroll_manager.anchor().anchor;
|
||||
|
||||
if editor.scroll_hover(amount, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.scroll_screen(amount, cx);
|
||||
if should_move_cursor {
|
||||
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
{"Put":{"state":"ˇ one\n two \nthree"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"_"}
|
||||
{"Get":{"state":" onˇe\n two \nthree","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇ one \n two \nthree"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"_"}
|
||||
|
||||
@@ -551,8 +551,7 @@ where
|
||||
if let Err(err) = self.await {
|
||||
log::error!("{err:?}");
|
||||
if let Ok(prompt) = cx.update(|cx| {
|
||||
let detail = f(&err, cx)
|
||||
.unwrap_or_else(|| format!("{err:?}. Please try again.", err = err));
|
||||
let detail = f(&err, cx).unwrap_or_else(|| format!("{err}. Please try again."));
|
||||
cx.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"])
|
||||
}) {
|
||||
prompt.await.ok();
|
||||
|
||||
@@ -39,8 +39,9 @@ pub struct SearchOptions {
|
||||
pub case: bool,
|
||||
pub word: bool,
|
||||
pub regex: bool,
|
||||
/// Specifies whether the item supports search & replace.
|
||||
/// Specifies whether the supports search & replace.
|
||||
pub replacement: bool,
|
||||
pub selection: bool,
|
||||
}
|
||||
|
||||
pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
|
||||
@@ -52,15 +53,18 @@ pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
|
||||
word: true,
|
||||
regex: true,
|
||||
replacement: true,
|
||||
selection: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn has_filtered_search_ranges(&mut self) -> bool {
|
||||
false
|
||||
Self::supported_options().selection
|
||||
}
|
||||
|
||||
fn toggle_filtered_search_ranges(&mut self, _enabled: bool, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
|
||||
fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>);
|
||||
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
|
||||
@@ -138,6 +142,8 @@ pub trait SearchableItemHandle: ItemHandle {
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<usize>;
|
||||
fn search_bar_visibility_changed(&self, visible: bool, cx: &mut WindowContext);
|
||||
|
||||
fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut WindowContext);
|
||||
}
|
||||
|
||||
impl<T: SearchableItem> SearchableItemHandle for View<T> {
|
||||
@@ -240,6 +246,12 @@ impl<T: SearchableItem> SearchableItemHandle for View<T> {
|
||||
this.search_bar_visibility_changed(visible, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut WindowContext) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.toggle_filtered_search_ranges(enabled, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn SearchableItemHandle>> for AnyView {
|
||||
|
||||
@@ -3969,8 +3969,7 @@ impl Workspace {
|
||||
fn adjust_padding(padding: Option<f32>) -> f32 {
|
||||
padding
|
||||
.unwrap_or(Self::DEFAULT_PADDING)
|
||||
.min(Self::MAX_PADDING)
|
||||
.max(0.0)
|
||||
.clamp(0.0, Self::MAX_PADDING)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.139.0"
|
||||
version = "0.140.0"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -83,6 +83,7 @@ Zed has two main binaries:
|
||||
* If you are going to provide a `.desktop` file you can find a template in `crates/zed/resources/zed.desktop.in`, and use `envsubst` to populate it with the values required.
|
||||
* You will need to ensure that the necessary libraries are installed. You can get the current list by [inspecting the built binary](https://github.com/zed-industries/zed/blob/059a4141b756cf4afac4c977afc488539aec6470/script/bundle-linux#L65-L70) on your system.
|
||||
* For an example of a complete build script, see [script/bundle-linux](https://github.com/zed-industries/zed/blob/main/script/bundle-linux).
|
||||
* You can disable Zed's auto updates and provide instructions for users who try to Update zed manually by building (or running) Zed with the environment variable `ZED_UPDATE_EXPLANATION`. For example: `ZED_UPDATE_EXPLANATION="Please use flatpak to update zed."`.
|
||||
|
||||
### Other things to note
|
||||
|
||||
@@ -92,7 +93,6 @@ However, we realize that many distros have other priorities. We want to work wit
|
||||
|
||||
* Zed is a fast moving early-phase project. We typically release 2-3 builds a week to fix user-reported issues and release major features.
|
||||
* There are a couple of other `zed` binaries that may be present on linux systems ([1](https://openzfs.github.io/openzfs-docs/man/v2.2/8/zed.8.html), [2](https://zed.brimdata.io/docs/commands/zed)).
|
||||
* We automatically install updates to Zed by default (though we do need a way for [package managers to opt out](https://github.com/zed-industries/zed/issues/12588)).
|
||||
* Zed automatically installs the correct version of common developer tools in the same way as rustup/rbenv/pyenv, etc. We understand that this is contentious, [see here](https://github.com/zed-industries/zed/issues/12589).
|
||||
* We allow users to install extensions on their own and from [zed-industries/extensions](https://github.com/zed-industries/extensions). These extensions may install further tooling as needed, such as language servers. In the long run we would like to make this safer, [see here](https://github.com/zed-industries/zed/issues/12358).
|
||||
* Zed connects to a number of online services by default (AI, telemetry, collaboration). AI and our telemetry can be disabled by your users with their own zed settings or by patching our [default settings file](https://github.com/zed-industries/zed/blob/main/assets/settings/default.json).
|
||||
|
||||
@@ -12,7 +12,7 @@ We have a growing collection of pre-defined keymaps in [zed repository's keymaps
|
||||
- TextMate
|
||||
- VSCode (default)
|
||||
|
||||
These keymaps can be set via the `base_keymap` setting in your `keymap.json` file. Additionally, if you'd like to work from a clean slate, you can provide `"None"` to the setting.
|
||||
These keymaps can be set via the `base_keymap` setting in your `settings.json` file. Additionally, if you'd like to work from a clean slate, you can provide `"None"` to the setting.
|
||||
|
||||
## Custom key bindings
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ if [[ -n $apt ]]; then
|
||||
libzstd-dev
|
||||
libvulkan1
|
||||
libgit2-dev
|
||||
make
|
||||
)
|
||||
$maysudo "$apt" install -y "${deps[@]}"
|
||||
exit 0
|
||||
|
||||
@@ -32,7 +32,6 @@ ADDITIONAL_LABELS: set[str] = {
|
||||
}
|
||||
IGNORED_LABELS: set[str] = {
|
||||
"ignore top-ranking issues",
|
||||
"meta",
|
||||
}
|
||||
ISSUES_PER_LABEL: int = 20
|
||||
|
||||
|
||||
Reference in New Issue
Block a user