Compare commits

..

8 Commits

Author SHA1 Message Date
Max Brunsfeld
ba3ce6e18d zed 0.94.2 2023-07-10 11:16:08 -07:00
Kirill Bulatov
bd716f758d Restart LSP server on corresponding initialization_options change (#2690)
Inlay hints depend on LSP server settings, but servers do not update the
initialization options and query hints with old settings.

Generally, we cannot know whether a certain option can be changed
without server restart, which the name of the options implies too, so be
on the safe side and restart the server.
Hints will update automatically after the server either sends a /refresh
request or reports its work progress end after startup.

Release Notes:

- Fixed LSP server not restarting after `initialization_options`
settings changes
2023-07-06 23:33:40 +03:00
Kirill Bulatov
df882e260a Clip find preceding boundary (#2689)
Fixes inability to do `alt + left arrow` when an inlay with `Bias::Left`
is right to the left of the caret.

Release Notes:

- N/A
2023-07-06 17:25:06 +03:00
Kirill Bulatov
c28af10a7c Show inlay hints on startup for every language server with work events (#2686)
Closes https://linear.app/zed-industries/issue/Z-2537/inlay-hint-issues

Language servers such as typescript-language-servers report a single
work event, ending right after server's startup.

Other servers might send more similar event, also during startup. The
rest of the events are diagnostic-related and we filter them out.

React on such events with /refresh-like hint update, that will check
only the visible part of the editor for hints and might be replaced by
other /refresh requests, if needed.

Release Notes:

- N/A
2023-07-06 16:23:26 +03:00
Antonio Scandurra
cc2a803103 zed 0.94.1 2023-07-06 10:51:09 +02:00
Antonio Scandurra
5509c8c9ff Fix panic when saved conversations directory changes (#2685)
Fixes
https://linear.app/zed-industries/issue/Z-2542/deleting-assistant-conversations-with-zed-open-can-cause-a-crash

We were updating the view's state but missed a `notify`, which caused
the `UniformList` responsible for rendering the saved conversations to
panic when some files were deleted.

Release Notes:

- Fixed a crash that could happen when deleting a saved assistant
conversation from the filesystem.
2023-07-06 10:50:14 +02:00
Kirill Bulatov
5121702e6c Use less padding for typescript parameter hints (#2684)
Part of https://linear.app/zed-industries/issue/Z-2537/inlay-hint-issues

Release Notes:

- N/A
2023-07-06 11:26:46 +03:00
Joseph T. Lyons
3930f3bf14 v0.94.x preview 2023-07-05 14:17:22 -04:00
87 changed files with 1114 additions and 4215 deletions

View File

@@ -16,4 +16,8 @@ jobs:
Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
```md
# Changelog
${{ github.event.release.body }}
```

1413
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -63,9 +63,7 @@ members = [
"crates/theme",
"crates/theme_selector",
"crates/util",
"crates/vector_store",
"crates/vim",
"crates/vcs_menu",
"crates/workspace",
"crates/welcome",
"crates/xtask",
@@ -83,8 +81,7 @@ env_logger = { version = "0.9" }
futures = { version = "0.3" }
globset = { version = "0.4" }
indoc = "1"
# We explicitly disable a http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
isahc = "1.7.2"
lazy_static = { version = "1.4.0" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = { version = "2.1.1" }

View File

@@ -23,16 +23,15 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
git clone https://github.com/zed-industries/zed.dev
```
* Return to Zed project directory and Initialize submodules
* Initialize submodules
```
cd zed
git submodule update --init --recursive
```
* Set up a local `zed` database and seed it with some initial users:
[Create a personal GitHub token](https://github.com/settings/tokens/new) to run `script/bootstrap` once successfully: the token needs to have an access to private repositories for the script to work (`repo` OAuth scope).
Create a personal GitHub token to run `script/bootstrap` once successfully: the token needs to have an access to private repositories for the script to work (`repo` OAuth scope).
Then delete that token.
```

View File

@@ -1,4 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 1.5H13.5M13.5 1.5V5.5M13.5 1.5C12.1332 2.86683 10.3668 4.63317 9 6" stroke="white" stroke-linecap="round"/>
<path d="M1.5 9.5V13.5M1.5 13.5L6 9M1.5 13.5H5.5" stroke="white" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 315 B

View File

@@ -1,4 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 6L9 6M9 6L9 2M9 6C10.3668 4.63316 12.1332 2.86683 13.5 1.5" stroke="white" stroke-linecap="round"/>
<path d="M6 13L6 9M6 9L1.5 13.5M6 9L2 9" stroke="white" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 297 B

View File

@@ -39,7 +39,6 @@
"cmd-shift-n": "workspace::NewWindow",
"cmd-o": "workspace::Open",
"alt-cmd-o": "projects::OpenRecent",
"alt-cmd-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"ctrl-`": "terminal_panel::ToggleFocus",
"shift-escape": "workspace::ToggleZoom"
@@ -405,7 +404,6 @@
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-k cmd-s": "zed::OpenKeymap",
"cmd-t": "project_symbols::Toggle",
"cmd-ctrl-t": "semantic_search::Toggle",
"cmd-p": "file_finder::Toggle",
"cmd-shift-p": "command_palette::Toggle",
"cmd-shift-m": "diagnostics::Deploy",

View File

@@ -2,7 +2,6 @@
{
"bindings": {
"cmd-shift-o": "projects::OpenRecent",
"cmd-shift-b": "branches::OpenRecent",
"cmd-alt-tab": "project_panel::ToggleFocus"
}
},

View File

@@ -35,11 +35,8 @@
"l": "vim::Right",
"right": "vim::Right",
"$": "vim::EndOfLine",
"^": "vim::FirstNonWhitespace",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
"{": "vim::StartOfParagraph",
"}": "vim::EndOfParagraph",
"shift-w": [
"vim::NextWordStart",
{
@@ -95,10 +92,7 @@
],
"ctrl-o": "pane::GoBack",
"ctrl-]": "editor::GoToDefinition",
"escape": [
"vim::SwitchMode",
"Normal"
],
"escape": "editor::Cancel",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
"vim::Number",
@@ -171,6 +165,7 @@
"shift-a": "vim::InsertEndOfLine",
"x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft",
"^": "vim::FirstNonWhitespace",
"o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove",
"~": "vim::ChangeCase",
@@ -310,10 +305,6 @@
"vim::PushOperator",
"Replace"
],
"ctrl-c": [
"vim::SwitchMode",
"Normal"
],
"> >": "editor::Indent",
"< <": "editor::Outdent"
}
@@ -330,10 +321,7 @@
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": [
"vim::SwitchMode",
"Normal"
]
"escape": "editor::Cancel"
}
}
]

View File

@@ -71,17 +71,15 @@
// "never"
"show": "auto",
// Whether to show git diff indicators in the scrollbar.
"git_diff": true,
// Whether to show selections in the scrollbar.
"selections": true
"git_diff": true
},
// Inlay hint related settings
"inlay_hints": {
// Global switch to toggle hints on and off, switched off by default.
"enabled": false,
"enabled": false,
// Toggle certain types of hints on and off, all switched on by default.
"show_type_hints": true,
"show_parameter_hints": true,
"show_parameter_hints": true,
// Corresponds to null/None LSP hint type value.
"show_other_hints": true
},
@@ -291,11 +289,6 @@
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Difference settings for vector_store
"vector_store": {
"enabled": false,
"reindexing_delay_seconds": 600
},
// Different settings for specific languages.
"languages": {
"Plain Text": {

View File

@@ -12,7 +12,6 @@ use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
cmp::Reverse,
ffi::OsStr,
fmt::{self, Display},
path::PathBuf,
sync::Arc,
@@ -81,9 +80,6 @@ impl SavedConversationMetadata {
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();

View File

@@ -2061,8 +2061,6 @@ impl ConversationEditor {
let remaining_tokens = self.conversation.read(cx).remaining_tokens()?;
let remaining_tokens_style = if remaining_tokens <= 0 {
&style.no_remaining_tokens
} else if remaining_tokens <= 500 {
&style.low_remaining_tokens
} else {
&style.remaining_tokens
};

View File

@@ -4,7 +4,7 @@ pub mod room;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
use client::{proto, Client, TypedEnvelope, User, UserStore};
use collections::HashSet;
use futures::{future::Shared, FutureExt};
use postage::watch;
@@ -198,7 +198,6 @@ impl ActiveCall {
let result = invite.await;
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
this.report_call_event("invite", cx);
cx.notify();
});
result
@@ -244,26 +243,21 @@ impl ActiveCall {
};
let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("accept incoming", cx)
});
Ok(())
})
}
pub fn decline_incoming(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
pub fn decline_incoming(&mut self) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
Self::report_call_event_for_room("decline incoming", call.room_id, &self.client, cx);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
@@ -272,7 +266,6 @@ impl ActiveCall {
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
if let Some((room, _)) = self.room.take() {
room.update(cx, |room, cx| room.leave(cx))
} else {
@@ -280,28 +273,12 @@ impl ActiveCall {
}
}
pub fn toggle_screen_sharing(&self, cx: &mut AppContext) {
if let Some(room) = self.room().cloned() {
let toggle_screen_sharing = room.update(cx, |room, cx| {
if room.is_screen_sharing() {
self.report_call_event("disable screen share", cx);
Task::ready(room.unshare_screen(cx))
} else {
self.report_call_event("enable screen share", cx);
room.share_screen(cx)
}
});
toggle_screen_sharing.detach_and_log_err(cx);
}
}
pub fn share_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
@@ -314,7 +291,6 @@ impl ActiveCall {
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
@@ -373,29 +349,7 @@ impl ActiveCall {
self.room.as_ref().map(|(room, _)| room)
}
pub fn client(&self) -> Arc<Client> {
self.client.clone()
}
pub fn pending_invites(&self) -> &HashSet<u64> {
&self.pending_invites
}
fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
if let Some(room) = self.room() {
Self::report_call_event_for_room(operation, room.read(cx).id(), &self.client, cx)
}
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: u64,
client: &Arc<Client>,
cx: &AppContext,
) {
let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let event = ClickhouseEvent::Call { operation, room_id };
telemetry.report_clickhouse_event(event, telemetry_settings);
}
}

View File

@@ -201,7 +201,6 @@ impl Bundle {
self.zed_version_string()
);
}
Self::LocalPath { executable, .. } => {
let executable_parent = executable
.parent()

View File

@@ -70,10 +70,6 @@ pub enum ClickhouseEvent {
suggestion_accepted: bool,
file_extension: Option<String>,
},
Call {
operation: &'static str,
room_id: u64,
},
}
#[cfg(debug_assertions)]

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.16.0"
version = "0.15.0"
publish = false
[[bin]]
@@ -14,6 +14,7 @@ name = "seed"
required-features = ["seed-support"]
[dependencies]
audio = { path = "../audio" }
collections = { path = "../collections" }
live_kit_server = { path = "../live_kit_server" }
rpc = { path = "../rpc" }
@@ -57,7 +58,6 @@ tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
[dev-dependencies]
audio = { path = "../audio" }
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
call = { path = "../call", features = ["test-support"] }

View File

@@ -3517,6 +3517,7 @@ pub use test::*;
mod test {
use super::*;
use gpui::executor::Background;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use sea_orm::ConnectionTrait;
use sqlx::migrate::MigrateDatabase;
@@ -3565,7 +3566,9 @@ mod test {
}
pub fn postgres(background: Arc<Background>) -> Self {
static LOCK: Mutex<()> = Mutex::new(());
lazy_static! {
static ref LOCK: Mutex<()> = Mutex::new(());
}
let _guard = LOCK.lock();
let mut rng = StdRng::from_entropy();

View File

@@ -157,7 +157,7 @@ async fn test_basic_calls(
// User C receives the call, but declines it.
let call_c = incoming_call_c.next().await.unwrap().unwrap();
assert_eq!(call_c.calling_user.github_login, "user_b");
active_call_c.update(cx_c, |call, cx| call.decline_incoming(cx).unwrap());
active_call_c.update(cx_c, |call, _| call.decline_incoming().unwrap());
assert!(incoming_call_c.next().await.unwrap().is_none());
deterministic.run_until_parked();
@@ -1080,7 +1080,7 @@ async fn test_calls_on_multiple_connections(
// User B declines the call on one of the two connections, causing both connections
// to stop ringing.
active_call_b2.update(cx_b2, |call, cx| call.decline_incoming(cx).unwrap());
active_call_b2.update(cx_b2, |call, _| call.decline_incoming().unwrap());
deterministic.run_until_parked();
assert!(incoming_call_b1.next().await.unwrap().is_none());
assert!(incoming_call_b2.next().await.unwrap().is_none());
@@ -5945,7 +5945,7 @@ async fn test_contacts(
[("user_b".to_string(), "online", "busy")]
);
active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
deterministic.run_until_parked();
assert_eq!(
contacts(&client_a, cx_a),

View File

@@ -37,9 +37,9 @@ use util::ResultExt;
lazy_static::lazy_static! {
static ref PLAN_LOAD_PATH: Option<PathBuf> = path_env_var("LOAD_PLAN");
static ref PLAN_SAVE_PATH: Option<PathBuf> = path_env_var("SAVE_PLAN");
static ref LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Default::default();
static ref PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Default::default();
}
static LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Mutex::new(None);
static PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Mutex::new(None);
#[gpui::test(iterations = 100, on_failure = "on_failure")]
async fn test_random_collaboration(
@@ -365,7 +365,7 @@ async fn apply_client_operation(
}
log::info!("{}: declining incoming call", client.username);
active_call.update(cx, |call, cx| call.decline_incoming(cx))?;
active_call.update(cx, |call, _| call.decline_incoming())?;
}
ClientOperation::LeaveCall => {

View File

@@ -39,7 +39,6 @@ recent_projects = {path = "../recent_projects"}
settings = { path = "../settings" }
theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" }
vcs_menu = { path = "../vcs_menu" }
util = { path = "../util" }
workspace = { path = "../workspace" }
zed-actions = {path = "../zed-actions"}

View File

@@ -1,22 +1,15 @@
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, bail};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions,
elements::*,
platform::{CursorStyle, MouseButton},
AppContext, MouseState, Task, ViewContext, ViewHandle,
};
use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::{ops::Not, sync::Arc};
use util::ResultExt;
use workspace::{Toast, Workspace};
actions!(branches, [OpenRecent]);
pub fn init(cx: &mut AppContext) {
Picker::<BranchListDelegate>::init(cx);
cx.add_async_action(toggle);
}
pub type BranchList = Picker<BranchListDelegate>;
pub fn build_branch_list(
@@ -29,60 +22,19 @@ pub fn build_branch_list(
workspace,
selected_index: 0,
last_query: String::default(),
branch_name_trailoff_after: 29,
},
cx,
)
.with_theme(|theme| theme.picker.clone())
}
fn toggle(
_: &mut Workspace,
_: &OpenRecent,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
Some(cx.spawn(|workspace, mut cx| async move {
workspace.update(&mut cx, |workspace, cx| {
workspace.toggle_modal(cx, |_, cx| {
let workspace = cx.handle();
cx.add_view(|cx| {
Picker::new(
BranchListDelegate {
matches: vec![],
workspace,
selected_index: 0,
last_query: String::default(),
/// Modal branch picker has a longer trailoff than a popover one.
branch_name_trailoff_after: 70,
},
cx,
)
.with_theme(|theme| theme.picker.clone())
.with_max_size(800., 1200.)
})
});
})?;
Ok(())
}))
}
pub struct BranchListDelegate {
matches: Vec<StringMatch>,
workspace: ViewHandle<Workspace>,
selected_index: usize,
last_query: String,
/// Max length of branch name before we truncate it and add a trailing `...`.
branch_name_trailoff_after: usize,
}
impl BranchListDelegate {
fn display_error_toast(&self, message: String, cx: &mut ViewContext<BranchList>) {
const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
self.workspace.update(cx, |model, ctx| {
model.show_toast(Toast::new(GIT_CHECKOUT_FAILURE_ID, message), ctx)
});
}
}
impl PickerDelegate for BranchListDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Select branch...".into()
@@ -184,39 +136,40 @@ impl PickerDelegate for BranchListDelegate {
let current_pick = self.selected_index();
let current_pick = self.matches[current_pick].string.clone();
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |this, cx| {
let project = this.delegate().workspace.read(cx).project().read(cx);
let mut cwd = project
.visible_worktrees(cx)
.next()
.ok_or_else(|| anyhow!("There are no visisible worktrees."))?
.read(cx)
.abs_path()
.to_path_buf();
cwd.push(".git");
let status = project
.fs()
.open_repo(&cwd)
.ok_or_else(|| {
anyhow!(
"Could not open repository at path `{}`",
cwd.as_os_str().to_string_lossy()
)
})?
.lock()
.change_branch(&current_pick);
if status.is_err() {
this.delegate().display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
status?;
}
cx.emit(PickerEvent::Dismiss);
picker.update(&mut cx, |this, cx| {
let project = this.delegate().workspace.read(cx).project().read(cx);
let mut cwd = project
.visible_worktrees(cx)
.next()
.ok_or_else(|| anyhow!("There are no visisible worktrees."))?
.read(cx)
.abs_path()
.to_path_buf();
cwd.push(".git");
let status = project
.fs()
.open_repo(&cwd)
.ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?
.lock()
.change_branch(&current_pick);
if status.is_err() {
const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
this.delegate().workspace.update(cx, |model, ctx| {
model.show_toast(
Toast::new(
GIT_CHECKOUT_FAILURE_ID,
format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"),
),
ctx,
)
});
status?;
}
cx.emit(PickerEvent::Dismiss);
Ok::<(), anyhow::Error>(())
})
.log_err();
})
.detach();
Ok::<(), anyhow::Error>(())
}).log_err();
}).detach();
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
@@ -230,15 +183,15 @@ impl PickerDelegate for BranchListDelegate {
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
const DISPLAYED_MATCH_LEN: usize = 29;
let theme = &theme::current(cx);
let hit = &self.matches[ix];
let shortened_branch_name =
util::truncate_and_trailoff(&hit.string, self.branch_name_trailoff_after);
let shortened_branch_name = util::truncate_and_trailoff(&hit.string, DISPLAYED_MATCH_LEN);
let highlights = hit
.positions
.iter()
.copied()
.filter(|index| index < &self.branch_name_trailoff_after)
.filter(|index| index < &DISPLAYED_MATCH_LEN)
.collect();
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
Flex::row()
@@ -282,61 +235,4 @@ impl PickerDelegate for BranchListDelegate {
};
Some(label.into_any())
}
fn render_footer(
&self,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<AnyElement<Picker<Self>>> {
if !self.last_query.is_empty() {
let theme = &theme::current(cx);
let style = theme.picker.footer.clone();
enum BranchCreateButton {}
Some(
Flex::row().with_child(MouseEventHandler::<BranchCreateButton, _>::new(0, cx, |state, _| {
let style = style.style_for(state);
Label::new("Create branch", style.label.clone())
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_down(MouseButton::Left, |_, _, cx| {
cx.spawn(|picker, mut cx| async move {
picker.update(&mut cx, |this, cx| {
let project = this.delegate().workspace.read(cx).project().read(cx);
let current_pick = &this.delegate().last_query;
let mut cwd = project
.visible_worktrees(cx)
.next()
.ok_or_else(|| anyhow!("There are no visisible worktrees."))?
.read(cx)
.abs_path()
.to_path_buf();
cwd.push(".git");
let repo = project
.fs()
.open_repo(&cwd)
.ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?;
let repo = repo
.lock();
let status = repo
.create_branch(&current_pick);
if status.is_err() {
this.delegate().display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
status?;
}
let status = repo.change_branch(&current_pick);
if status.is_err() {
this.delegate().display_error_toast(format!("Failed to chec branch '{current_pick}', check for conflicts or unstashed files"), cx);
status?;
}
cx.emit(PickerEvent::Dismiss);
Ok::<(), anyhow::Error>(())
})
}).detach();
})).aligned().right()
.into_any(),
)
} else {
None
}
}
}

View File

@@ -1,5 +1,8 @@
use crate::{
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
branch_list::{build_branch_list, BranchList},
contact_notification::ContactNotification,
contacts_popover,
face_pile::FacePile,
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
ToggleScreenSharing,
};
@@ -24,7 +27,6 @@ use recent_projects::{build_recent_projects, RecentProjects};
use std::{ops::Range, sync::Arc};
use theme::{AvatarStyle, Theme};
use util::ResultExt;
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
const MAX_PROJECT_NAME_LENGTH: usize = 40;
@@ -35,6 +37,7 @@ actions!(
[
ToggleContactsMenu,
ToggleUserMenu,
ToggleVcsMenu,
ToggleProjectMenu,
SwitchBranch,
ShareProject,
@@ -226,23 +229,15 @@ impl CollabTitlebarItem {
let mut ret = Flex::row().with_child(
Stack::new()
.with_child(
MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, cx| {
MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, _| {
let style = project_style
.in_state(self.project_popover.is_some())
.style_for(mouse_state);
enum RecentProjectsTooltip {}
Label::new(name, style.text.clone())
.contained()
.with_style(style.container)
.aligned()
.left()
.with_tooltip::<RecentProjectsTooltip>(
0,
"Recent projects".into(),
Some(Box::new(recent_projects::OpenRecent)),
theme.tooltip.clone(),
cx,
)
.into_any_named("title-project-name")
})
.with_cursor_style(CursorStyle::PointingHand)
@@ -269,8 +264,7 @@ impl CollabTitlebarItem {
MouseEventHandler::<ToggleVcsMenu, Self>::new(
0,
cx,
|mouse_state, cx| {
enum BranchPopoverTooltip {}
|mouse_state, _| {
let style = git_style
.in_state(self.branch_popover.is_some())
.style_for(mouse_state);
@@ -280,13 +274,6 @@ impl CollabTitlebarItem {
.with_margin_right(item_spacing)
.aligned()
.left()
.with_tooltip::<BranchPopoverTooltip>(
0,
"Recent branches".into(),
Some(Box::new(ToggleVcsMenu)),
theme.tooltip.clone(),
cx,
)
.into_any_named("title-project-branch")
},
)

View File

@@ -1,3 +1,4 @@
mod branch_list;
mod collab_titlebar_item;
mod contact_finder;
mod contact_list;
@@ -28,7 +29,7 @@ actions!(
);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
vcs_menu::init(cx);
branch_list::init(cx);
collab_titlebar_item::init(cx);
contact_list::init(cx);
contact_finder::init(cx);
@@ -44,25 +45,11 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
}
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
let call = ActiveCall::global(cx).read(cx);
if let Some(room) = call.room().cloned() {
let client = call.client();
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let toggle_screen_sharing = room.update(cx, |room, cx| {
if room.is_screen_sharing() {
ActiveCall::report_call_event_for_room(
"disable screen share",
room.id(),
&client,
cx,
);
Task::ready(room.unshare_screen(cx))
} else {
ActiveCall::report_call_event_for_room(
"enable screen share",
room.id(),
&client,
cx,
);
room.share_screen(cx)
}
});

View File

@@ -99,8 +99,8 @@ impl IncomingCallNotification {
})
.detach_and_log_err(cx);
} else {
active_call.update(cx, |active_call, cx| {
active_call.decline_incoming(cx).log_err();
active_call.update(cx, |active_call, _| {
active_call.decline_incoming().log_err();
});
}
}

View File

@@ -369,7 +369,6 @@ mod tests {
editor::init(cx);
workspace::init(app_state.clone(), cx);
init(cx);
Project::init_settings(cx);
app_state
})
}

View File

@@ -41,11 +41,12 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
const DB_FILE_NAME: &'static str = "db.sqlite";
lazy_static::lazy_static! {
pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
}
static DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
/// Open or create a database at the given directory path.
/// This will retry a couple times if there are failures. If opening fails once, the db directory

View File

@@ -5123,7 +5123,7 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
selection.collapse_to(
movement::start_of_paragraph(map, selection.head(), 1),
movement::start_of_paragraph(map, selection.head()),
SelectionGoal::None,
)
});
@@ -5143,7 +5143,7 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
selection.collapse_to(
movement::end_of_paragraph(map, selection.head(), 1),
movement::end_of_paragraph(map, selection.head()),
SelectionGoal::None,
)
});
@@ -5162,10 +5162,7 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
(
movement::start_of_paragraph(map, head, 1),
SelectionGoal::None,
)
(movement::start_of_paragraph(map, head), SelectionGoal::None)
});
})
}
@@ -5182,10 +5179,7 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
(
movement::end_of_paragraph(map, head, 1),
SelectionGoal::None,
)
(movement::end_of_paragraph(map, head), SelectionGoal::None)
});
})
}
@@ -7222,47 +7216,6 @@ impl Editor {
}
results
}
pub fn background_highlights_in_range_for<T: 'static>(
&self,
search_range: Range<Anchor>,
display_snapshot: &DisplaySnapshot,
theme: &Theme,
) -> Vec<(Range<DisplayPoint>, Color)> {
let mut results = Vec::new();
let buffer = &display_snapshot.buffer_snapshot;
let Some((color_fetcher, ranges)) = self.background_highlights
.get(&TypeId::of::<T>()) else {
return vec![];
};
let color = color_fetcher(theme);
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe.end.cmp(&search_range.start, buffer);
if cmp.is_gt() {
Ordering::Greater
} else {
Ordering::Less
}
}) {
Ok(i) | Err(i) => i,
};
for range in &ranges[start_ix..] {
if range.start.cmp(&search_range.end, buffer).is_ge() {
break;
}
let start = range
.start
.to_point(buffer)
.to_display_point(display_snapshot);
let end = range
.end
.to_point(buffer)
.to_display_point(display_snapshot);
results.push((start..end, color))
}
results
}
pub fn highlight_text<T: 'static>(
&mut self,
@@ -7565,7 +7518,7 @@ impl Editor {
fn report_editor_event(
&self,
operation: &'static str,
name: &'static str,
file_extension: Option<String>,
cx: &AppContext,
) {
@@ -7602,7 +7555,7 @@ impl Editor {
let event = ClickhouseEvent::Editor {
file_extension,
vim_mode,
operation,
operation: name,
copilot_enabled,
copilot_enabled_for_language,
};

View File

@@ -15,7 +15,6 @@ pub struct EditorSettings {
pub struct Scrollbar {
pub show: ShowScrollbar,
pub git_diff: bool,
pub selections: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -40,7 +39,6 @@ pub struct EditorSettingsContent {
pub struct ScrollbarContent {
pub show: Option<ShowScrollbar>,
pub git_diff: Option<bool>,
pub selections: Option<bool>,
}
impl Setting for EditorSettings {

View File

@@ -22,10 +22,7 @@ use language::{
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
};
use parking_lot::Mutex;
use project::project_settings::{LspSettings, ProjectSettings};
use project::FakeFs;
use std::sync::atomic;
use std::sync::atomic::AtomicUsize;
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use unindent::Unindent;
use util::{
@@ -1799,7 +1796,7 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
"});
}
// Ensure that comment continuations can be disabled.
update_test_language_settings(cx, |settings| {
update_test_settings(cx, |settings| {
settings.defaults.extend_comment_on_newline = Some(false);
});
let mut cx = EditorTestContext::new(cx).await;
@@ -4549,7 +4546,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
assert!(!cx.read(|cx| editor.is_dirty(cx)));
// Set rust language override and assert overridden tabsize is sent to language server
update_test_language_settings(cx, |settings| {
update_test_settings(cx, |settings| {
settings.languages.insert(
"Rust".into(),
LanguageSettingsContent {
@@ -4663,7 +4660,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
assert!(!cx.read(|cx| editor.is_dirty(cx)));
// Set rust language override and assert overridden tabsize is sent to language server
update_test_language_settings(cx, |settings| {
update_test_settings(cx, |settings| {
settings.languages.insert(
"Rust".into(),
LanguageSettingsContent {
@@ -7087,142 +7084,6 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let language_name: Arc<str> = "Rust".into();
let mut language = Language::new(
LanguageConfig {
name: Arc::clone(&language_name),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let server_restarts = Arc::new(AtomicUsize::new(0));
let closure_restarts = Arc::clone(&server_restarts);
let language_server_name = "test language server";
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: language_server_name,
initialization_options: Some(json!({
"testOptionValue": true
})),
initializer: Some(Box::new(move |fake_server| {
let task_restarts = Arc::clone(&closure_restarts);
fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
task_restarts.fetch_add(1, atomic::Ordering::Release);
futures::future::ready(Ok(()))
});
})),
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/a",
json!({
"main.rs": "fn main() { let a = 5; }",
"other.rs": "// Test file",
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let (_, _workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let _buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/a/main.rs", cx)
})
.await
.unwrap();
let _fake_server = fake_servers.next().await.unwrap();
update_test_language_settings(cx, |language_settings| {
language_settings.languages.insert(
Arc::clone(&language_name),
LanguageSettingsContent {
tab_size: NonZeroU32::new(8),
..Default::default()
},
);
});
cx.foreground().run_until_parked();
assert_eq!(
server_restarts.load(atomic::Ordering::Acquire),
0,
"Should not restart LSP server on an unrelated change"
);
update_test_project_settings(cx, |project_settings| {
project_settings.lsp.insert(
"Some other server name".into(),
LspSettings {
initialization_options: Some(json!({
"some other init value": false
})),
},
);
});
cx.foreground().run_until_parked();
assert_eq!(
server_restarts.load(atomic::Ordering::Acquire),
0,
"Should not restart LSP server on an unrelated LSP settings change"
);
update_test_project_settings(cx, |project_settings| {
project_settings.lsp.insert(
language_server_name.into(),
LspSettings {
initialization_options: Some(json!({
"anotherInitValue": false
})),
},
);
});
cx.foreground().run_until_parked();
assert_eq!(
server_restarts.load(atomic::Ordering::Acquire),
1,
"Should restart LSP server on a related LSP settings change"
);
update_test_project_settings(cx, |project_settings| {
project_settings.lsp.insert(
language_server_name.into(),
LspSettings {
initialization_options: Some(json!({
"anotherInitValue": false
})),
},
);
});
cx.foreground().run_until_parked();
assert_eq!(
server_restarts.load(atomic::Ordering::Acquire),
1,
"Should not restart LSP server on a related LSP settings change that is the same"
);
update_test_project_settings(cx, |project_settings| {
project_settings.lsp.insert(
language_server_name.into(),
LspSettings {
initialization_options: None,
},
);
});
cx.foreground().run_until_parked();
assert_eq!(
server_restarts.load(atomic::Ordering::Acquire),
2,
"Should restart LSP server on another related LSP settings change"
);
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
@@ -7342,7 +7203,7 @@ fn handle_copilot_completion_request(
});
}
pub(crate) fn update_test_language_settings(
pub(crate) fn update_test_settings(
cx: &mut TestAppContext,
f: impl Fn(&mut AllLanguageSettingsContent),
) {
@@ -7353,17 +7214,6 @@ pub(crate) fn update_test_language_settings(
});
}
pub(crate) fn update_test_project_settings(
cx: &mut TestAppContext,
f: impl Fn(&mut ProjectSettings),
) {
cx.update(|cx| {
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.update_user_settings::<ProjectSettings>(cx, f);
});
});
}
pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
cx.foreground().forbid_parking();
@@ -7377,5 +7227,5 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
crate::init(cx);
});
update_test_language_settings(cx, f);
update_test_settings(cx, f);
}

View File

@@ -1008,7 +1008,6 @@ impl EditorElement {
bounds: RectF,
layout: &mut LayoutState,
cx: &mut ViewContext<Editor>,
editor: &Editor,
) {
enum ScrollbarMouseHandlers {}
if layout.mode != EditorMode::Full {
@@ -1051,76 +1050,9 @@ impl EditorElement {
background: style.track.background_color,
..Default::default()
});
let scrollbar_settings = settings::get::<EditorSettings>(cx).scrollbar;
let theme = theme::current(cx);
let scrollbar_theme = &theme.editor.scrollbar;
if layout.is_singleton && scrollbar_settings.selections {
let start_anchor = Anchor::min();
let end_anchor = Anchor::max();
let mut start_row = None;
let mut end_row = None;
let color = scrollbar_theme.selections;
let border = Border {
width: 1.,
color: style.thumb.border.color,
overlay: false,
top: false,
right: true,
bottom: false,
left: true,
};
let mut push_region = |start, end| {
if let (Some(start_display), Some(end_display)) = (start, end) {
let start_y = y_for_row(start_display as f32);
let mut end_y = y_for_row(end_display as f32);
if end_y - start_y < 1. {
end_y = start_y + 1.;
}
let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
scene.push_quad(Quad {
bounds,
background: Some(color),
border,
corner_radius: style.thumb.corner_radius,
})
}
};
for (row, _) in &editor
.background_highlights_in_range_for::<crate::items::BufferSearchHighlights>(
start_anchor..end_anchor,
&layout.position_map.snapshot,
&theme,
)
{
let start_display = row.start;
let end_display = row.end;
if start_row.is_none() {
assert_eq!(end_row, None);
start_row = Some(start_display.row());
end_row = Some(end_display.row());
continue;
}
if let Some(current_end) = end_row.as_mut() {
if start_display.row() > *current_end + 1 {
push_region(start_row, end_row);
start_row = Some(start_display.row());
end_row = Some(end_display.row());
} else {
// Merge two hunks.
*current_end = end_display.row();
}
} else {
unreachable!();
}
}
// We might still have a hunk that was not rendered (if there was a search hit on the last line)
push_region(start_row, end_row);
}
if layout.is_singleton && scrollbar_settings.git_diff {
let diff_style = scrollbar_theme.git.clone();
if layout.is_singleton && settings::get::<EditorSettings>(cx).scrollbar.git_diff {
let diff_style = theme::current(cx).editor.scrollbar.git.clone();
for hunk in layout
.position_map
.snapshot
@@ -2151,9 +2083,6 @@ impl Element<Editor> for EditorElement {
ShowScrollbar::Auto => {
// Git
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
||
// Selections
(is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty())
// Scrollmanager
|| editor.scroll_manager.scrollbars_visible()
}
@@ -2439,7 +2368,7 @@ impl Element<Editor> for EditorElement {
if !layout.blocks.is_empty() {
self.paint_blocks(scene, bounds, visible_bounds, layout, editor, cx);
}
self.paint_scrollbar(scene, bounds, layout, cx, &editor);
self.paint_scrollbar(scene, bounds, layout, cx);
scene.pop_layer();
scene.pop_layer();
@@ -2916,7 +2845,7 @@ mod tests {
use super::*;
use crate::{
display_map::{BlockDisposition, BlockProperties},
editor_tests::{init_test, update_test_language_settings},
editor_tests::{init_test, update_test_settings},
Editor, MultiBuffer,
};
use gpui::TestAppContext;
@@ -3113,7 +3042,7 @@ mod tests {
let resize_step = 10.0;
let mut editor_width = 200.0;
while editor_width <= 1000.0 {
update_test_language_settings(cx, |s| {
update_test_settings(cx, |s| {
s.defaults.tab_size = NonZeroU32::new(tab_size);
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
s.defaults.preferred_line_length = Some(editor_width as u32);

View File

@@ -847,7 +847,7 @@ mod tests {
use text::Point;
use workspace::Workspace;
use crate::editor_tests::update_test_language_settings;
use crate::editor_tests::update_test_settings;
use super::*;
@@ -1476,7 +1476,7 @@ mod tests {
),
] {
edits_made += 1;
update_test_language_settings(cx, |settings| {
update_test_settings(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
@@ -1520,7 +1520,7 @@ mod tests {
edits_made += 1;
let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
update_test_language_settings(cx, |settings| {
update_test_settings(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: false,
show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
@@ -1577,7 +1577,7 @@ mod tests {
let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
edits_made += 1;
update_test_language_settings(cx, |settings| {
update_test_settings(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
@@ -2269,7 +2269,7 @@ unedited (2nd) buffer should have the same hint");
crate::init(cx);
});
update_test_language_settings(cx, f);
update_test_settings(cx, f);
}
async fn prepare_test_objects(

View File

@@ -883,7 +883,7 @@ impl ProjectItem for Editor {
}
}
pub(crate) enum BufferSearchHighlights {}
enum BufferSearchHighlights {}
impl SearchableItem for Editor {
type Match = Range<Anchor>;

View File

@@ -193,11 +193,7 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
})
}
pub fn start_of_paragraph(
map: &DisplaySnapshot,
display_point: DisplayPoint,
mut count: usize,
) -> DisplayPoint {
pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == 0 {
return map.max_point();
@@ -207,11 +203,7 @@ pub fn start_of_paragraph(
for row in (0..point.row + 1).rev() {
let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank {
if count <= 1 {
return Point::new(row, 0).to_display_point(map);
}
count -= 1;
found_non_blank_line = false;
return Point::new(row, 0).to_display_point(map);
}
found_non_blank_line |= !blank;
@@ -220,11 +212,7 @@ pub fn start_of_paragraph(
DisplayPoint::zero()
}
pub fn end_of_paragraph(
map: &DisplaySnapshot,
display_point: DisplayPoint,
mut count: usize,
) -> DisplayPoint {
pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == map.max_buffer_row() {
return DisplayPoint::zero();
@@ -234,11 +222,7 @@ pub fn end_of_paragraph(
for row in point.row..map.max_buffer_row() + 1 {
let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank {
if count <= 1 {
return Point::new(row, 0).to_display_point(map);
}
count -= 1;
found_non_blank_line = false;
return Point::new(row, 0).to_display_point(map);
}
found_non_blank_line |= !blank;

View File

@@ -210,10 +210,6 @@ impl<'a> EditorTestContext<'a> {
self.assert_selections(expected_selections, marked_text.to_string())
}
pub fn editor_state(&mut self) -> String {
generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
}
#[track_caller]
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
@@ -252,8 +248,14 @@ impl<'a> EditorTestContext<'a> {
self.assert_selections(expected_selections, expected_marked_text)
}
fn editor_selections(&self) -> Vec<Range<usize>> {
self.editor
#[track_caller]
fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
expected_marked_text: String,
) {
let actual_selections = self
.editor
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
.into_iter()
.map(|s| {
@@ -263,22 +265,12 @@ impl<'a> EditorTestContext<'a> {
s.start..s.end
}
})
.collect::<Vec<_>>()
}
#[track_caller]
fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
expected_marked_text: String,
) {
let actual_selections = self.editor_selections();
.collect::<Vec<_>>();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
if expected_selections != actual_selections {
panic!(
indoc! {"
{}Editor has unexpected selections.
Expected selections:

View File

@@ -39,9 +39,6 @@ pub trait GitRepository: Send {
fn change_branch(&self, _: &str) -> Result<()> {
Ok(())
}
fn create_branch(&self, _: &str) -> Result<()> {
Ok(())
}
}
impl std::fmt::Debug for dyn GitRepository {
@@ -155,12 +152,6 @@ impl GitRepository for LibGitRepository {
)?;
Ok(())
}
fn create_branch(&self, name: &str) -> Result<()> {
let current_commit = self.head()?.peel_to_commit()?;
self.branch(name, &current_commit, false)?;
Ok(())
}
}
fn read_status(status: git2::Status) -> Option<GitFileStatus> {

View File

@@ -4,7 +4,7 @@ use pathfinder_geometry::vector::vec2f;
use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug)]
pub struct KeyDownEvent {
pub keystroke: Keystroke,
pub is_held: bool,

View File

@@ -232,6 +232,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(canBecomeKeyWindow),
yes as extern "C" fn(&Object, Sel) -> BOOL,
);
decl.add_method(
sel!(sendEvent:),
send_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidResize:),
window_did_resize as extern "C" fn(&Object, Sel, id),
@@ -295,7 +299,7 @@ struct WindowState {
appearance_changed_callback: Option<Box<dyn FnMut()>>,
input_handler: Option<Box<dyn InputHandler>>,
pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
last_key_equivalent: Option<KeyDownEvent>,
performed_key_equivalent: bool,
synthetic_drag_counter: usize,
executor: Rc<executor::Foreground>,
scene_to_render: Option<Scene>,
@@ -517,7 +521,7 @@ impl Window {
appearance_changed_callback: None,
input_handler: None,
pending_key_down: None,
last_key_equivalent: None,
performed_key_equivalent: false,
synthetic_drag_counter: 0,
executor,
scene_to_render: Default::default(),
@@ -961,34 +965,36 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
let window_height = window_state_borrow.content_size().y();
let event = unsafe { Event::from_native(native_event, Some(window_height)) };
if let Some(Event::KeyDown(event)) = event {
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
// If that event isn't handled, it will then dispatch a "key down" event. GPUI
// makes no distinction between these two types of events, so we need to ignore
// the "key down" event if we've already just processed its "key equivalent" version.
if let Some(event) = event {
if key_equivalent {
window_state_borrow.last_key_equivalent = Some(event.clone());
} else if window_state_borrow.last_key_equivalent.take().as_ref() == Some(&event) {
window_state_borrow.performed_key_equivalent = true;
} else if window_state_borrow.performed_key_equivalent {
return NO;
}
let keydown = event.keystroke.clone();
let fn_modifier = keydown.function;
// Ignore events from held-down keys after some of the initially-pressed keys
// were released.
if event.is_held {
if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
return YES;
let function_is_held;
window_state_borrow.pending_key_down = match event {
Event::KeyDown(event) => {
let keydown = event.keystroke.clone();
// Ignore events from held-down keys after some of the initially-pressed keys
// were released.
if event.is_held {
if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
return YES;
}
} else {
window_state_borrow.last_fresh_keydown = Some(keydown);
}
function_is_held = event.keystroke.function;
Some((event, None))
}
} else {
window_state_borrow.last_fresh_keydown = Some(keydown);
}
window_state_borrow.pending_key_down = Some((event, None));
_ => return NO,
};
drop(window_state_borrow);
// Send the event to the input context for IME handling, unless the `fn` modifier is
// being pressed.
if !fn_modifier {
if !function_is_held {
unsafe {
let input_context: id = msg_send![this, inputContext];
let _: BOOL = msg_send![input_context, handleEvent: native_event];
@@ -1137,6 +1143,13 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
}
}
extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];
get_window_state(this).borrow_mut().performed_key_equivalent = false;
}
}
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
window_state.as_ref().borrow().move_traffic_light();

View File

@@ -350,7 +350,6 @@ pub struct LanguageQueries {
pub brackets: Option<Cow<'static, str>>,
pub indents: Option<Cow<'static, str>>,
pub outline: Option<Cow<'static, str>>,
pub embedding: Option<Cow<'static, str>>,
pub injections: Option<Cow<'static, str>>,
pub overrides: Option<Cow<'static, str>>,
}
@@ -428,7 +427,6 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D
#[cfg(any(test, feature = "test-support"))]
pub struct FakeLspAdapter {
pub name: &'static str,
pub initialization_options: Option<Value>,
pub capabilities: lsp::ServerCapabilities,
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
pub disk_based_diagnostics_progress_token: Option<String>,
@@ -491,13 +489,12 @@ pub struct Language {
pub struct Grammar {
id: usize,
pub ts_language: tree_sitter::Language,
pub(crate) ts_language: tree_sitter::Language,
pub(crate) error_query: Query,
pub(crate) highlights_query: Option<Query>,
pub(crate) brackets_config: Option<BracketConfig>,
pub(crate) indents_config: Option<IndentConfig>,
pub outline_config: Option<OutlineConfig>,
pub embedding_config: Option<EmbeddingConfig>,
pub(crate) outline_config: Option<OutlineConfig>,
pub(crate) injection_config: Option<InjectionConfig>,
pub(crate) override_config: Option<OverrideConfig>,
pub(crate) highlight_map: Mutex<HighlightMap>,
@@ -511,21 +508,12 @@ struct IndentConfig {
outdent_capture_ix: Option<u32>,
}
pub struct OutlineConfig {
pub query: Query,
pub item_capture_ix: u32,
pub name_capture_ix: u32,
pub context_capture_ix: Option<u32>,
pub extra_context_capture_ix: Option<u32>,
}
#[derive(Debug)]
pub struct EmbeddingConfig {
pub query: Query,
pub item_capture_ix: u32,
pub name_capture_ix: u32,
pub context_capture_ix: Option<u32>,
pub extra_context_capture_ix: Option<u32>,
struct OutlineConfig {
query: Query,
item_capture_ix: u32,
name_capture_ix: u32,
context_capture_ix: Option<u32>,
extra_context_capture_ix: Option<u32>,
}
struct InjectionConfig {
@@ -1157,7 +1145,6 @@ impl Language {
highlights_query: None,
brackets_config: None,
outline_config: None,
embedding_config: None,
indents_config: None,
injection_config: None,
override_config: None,
@@ -1194,9 +1181,6 @@ impl Language {
if let Some(query) = queries.outline {
self = self.with_outline_query(query.as_ref())?;
}
if let Some(query) = queries.embedding {
self = self.with_embedding_query(query.as_ref())?;
}
if let Some(query) = queries.injections {
self = self.with_injection_query(query.as_ref())?;
}
@@ -1205,7 +1189,6 @@ impl Language {
}
Ok(self)
}
pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut();
grammar.highlights_query = Some(Query::new(grammar.ts_language, source)?);
@@ -1240,34 +1223,6 @@ impl Language {
Ok(self)
}
pub fn with_embedding_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut();
let query = Query::new(grammar.ts_language, source)?;
let mut item_capture_ix = None;
let mut name_capture_ix = None;
let mut context_capture_ix = None;
let mut extra_context_capture_ix = None;
get_capture_indices(
&query,
&mut [
("item", &mut item_capture_ix),
("name", &mut name_capture_ix),
("context", &mut context_capture_ix),
("context.extra", &mut extra_context_capture_ix),
],
);
if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
grammar.embedding_config = Some(EmbeddingConfig {
query,
item_capture_ix,
name_capture_ix,
context_capture_ix,
extra_context_capture_ix,
});
}
Ok(self)
}
pub fn with_brackets_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut();
let query = Query::new(grammar.ts_language, source)?;
@@ -1682,7 +1637,6 @@ impl Default for FakeLspAdapter {
capabilities: lsp::LanguageServer::full_capabilities(),
initializer: None,
disk_based_diagnostics_progress_token: None,
initialization_options: None,
disk_based_diagnostics_sources: Vec::new(),
}
}
@@ -1732,10 +1686,6 @@ impl LspAdapter for Arc<FakeLspAdapter> {
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
self.disk_based_diagnostics_progress_token.clone()
}
async fn initialization_options(&self) -> Option<Value> {
self.initialization_options.clone()
}
}
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {

View File

@@ -4,6 +4,7 @@ mod syntax_map_tests;
use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
use collections::HashMap;
use futures::FutureExt;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::{
borrow::Cow,
@@ -24,7 +25,9 @@ thread_local! {
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
}
static QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Mutex::new(vec![]);
lazy_static! {
static ref QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Default::default();
}
#[derive(Default)]
pub struct SyntaxMap {

View File

@@ -17,6 +17,7 @@ test-support = [
"async-trait",
"collections/test-support",
"gpui/test-support",
"lazy_static",
"live_kit_server",
"nanoid",
]
@@ -37,6 +38,7 @@ parking_lot.workspace = true
postage.workspace = true
async-trait = { workspace = true, optional = true }
lazy_static = { workspace = true, optional = true }
nanoid = { version ="0.4", optional = true}
[dev-dependencies]
@@ -58,6 +60,7 @@ foreign-types = "0.3"
futures.workspace = true
hmac = "0.12"
jwt = "0.16"
lazy_static.workspace = true
objc = "0.2"
parking_lot.workspace = true
serde.workspace = true

View File

@@ -1,15 +1,18 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use collections::{BTreeMap, HashMap};
use collections::HashMap;
use futures::Stream;
use gpui::executor::Background;
use lazy_static::lazy_static;
use live_kit_server::token;
use media::core_video::CVImageBuffer;
use parking_lot::Mutex;
use postage::watch;
use std::{future::Future, mem, sync::Arc};
static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
lazy_static! {
static ref SERVERS: Mutex<HashMap<String, Arc<TestServer>>> = Default::default();
}
pub struct TestServer {
pub url: String,

View File

@@ -50,7 +50,7 @@ use lsp::{
};
use lsp_command::*;
use postage::watch;
use project_settings::{LspSettings, ProjectSettings};
use project_settings::ProjectSettings;
use rand::prelude::*;
use search::SearchQuery;
use serde::Serialize;
@@ -149,7 +149,6 @@ pub struct Project {
_maintain_workspace_config: Task<()>,
terminals: Terminals,
copilot_enabled: bool,
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
}
struct DelayedDebounced {
@@ -261,7 +260,6 @@ pub enum Event {
ActiveEntryChanged(Option<ProjectEntryId>),
WorktreeAdded,
WorktreeRemoved(WorktreeId),
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
DiskBasedDiagnosticsStarted {
language_server_id: LanguageServerId,
},
@@ -616,7 +614,6 @@ impl Project {
local_handles: Vec::new(),
},
copilot_enabled: Copilot::global(cx).is_some(),
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
}
})
}
@@ -709,7 +706,6 @@ impl Project {
local_handles: Vec::new(),
},
copilot_enabled: Copilot::global(cx).is_some(),
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
};
for worktree in worktrees {
let _ = this.add_worktree(&worktree, cx);
@@ -783,9 +779,7 @@ impl Project {
let mut language_servers_to_stop = Vec::new();
let mut language_servers_to_restart = Vec::new();
let languages = self.languages.to_vec();
let new_lsp_settings = settings::get::<ProjectSettings>(cx).lsp.clone();
let current_lsp_settings = &self.current_lsp_settings;
let project_settings = settings::get::<ProjectSettings>(cx).clone();
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
let language = languages.iter().find_map(|l| {
let adapter = l
@@ -802,25 +796,16 @@ impl Project {
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
} else if let Some(worktree) = worktree {
let server_name = &adapter.name.0;
match (
current_lsp_settings.get(server_name),
new_lsp_settings.get(server_name),
) {
(None, None) => {}
(Some(_), None) | (None, Some(_)) => {
language_servers_to_restart.push((worktree, Arc::clone(language)));
}
(Some(current_lsp_settings), Some(new_lsp_settings)) => {
if current_lsp_settings != new_lsp_settings {
language_servers_to_restart.push((worktree, Arc::clone(language)));
}
}
let new_lsp_settings = project_settings
.lsp
.get(&adapter.name.0)
.and_then(|s| s.initialization_options.as_ref());
if adapter.initialization_options.as_ref() != new_lsp_settings {
language_servers_to_restart.push((worktree, Arc::clone(language)));
}
}
}
}
self.current_lsp_settings = new_lsp_settings;
// Stop all newly-disabled language servers.
for (worktree_id, adapter_name) in language_servers_to_stop {
@@ -5404,10 +5389,6 @@ impl Project {
this.update_local_worktree_buffers(&worktree, changes, cx);
this.update_local_worktree_language_servers(&worktree, changes, cx);
this.update_local_worktree_settings(&worktree, changes, cx);
cx.emit(Event::WorktreeUpdatedEntries(
worktree.read(cx).id(),
changes.clone(),
));
}
worktree::Event::UpdatedGitRepositories(updated_repos) => {
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)

View File

@@ -134,7 +134,7 @@ impl PickerDelegate for RecentProjectsDelegate {
let combined_string = location
.paths()
.iter()
.map(|path| util::paths::compact(&path).to_string_lossy().into_owned())
.map(|path| path.to_string_lossy().to_owned())
.collect::<Vec<_>>()
.join("");
StringMatchCandidate::new(id, combined_string)

View File

@@ -675,9 +675,6 @@ impl ProjectSearchView {
if match_ranges.is_empty() {
self.active_match_index = None;
} else {
self.active_match_index = Some(0);
self.select_match(Direction::Next, cx);
self.update_match_index(cx);
let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
let is_new_search = self.search_id != prev_search_id;
self.results_editor.update(cx, |editor, cx| {

View File

@@ -221,14 +221,6 @@ impl TerminalPanel {
pane::Event::ZoomIn => cx.emit(Event::ZoomIn),
pane::Event::ZoomOut => cx.emit(Event::ZoomOut),
pane::Event::Focus => cx.emit(Event::Focus),
pane::Event::AddItem { item } => {
if let Some(workspace) = self.workspace.upgrade(cx) {
let pane = self.pane.clone();
workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
}
}
_ => {}
}
}

View File

@@ -275,7 +275,7 @@ impl TerminalView {
cx.spawn(|this, mut cx| async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
.ok();
.log_err();
})
.detach();
}
@@ -907,7 +907,6 @@ mod tests {
let params = cx.update(AppState::test);
cx.update(|cx| {
theme::init((), cx);
Project::init_settings(cx);
language::init(cx);
});

View File

@@ -586,7 +586,7 @@ pub struct Picker {
pub no_matches: ContainedLabel,
pub item: Toggleable<Interactive<ContainedLabel>>,
pub header: ContainedLabel,
pub footer: Interactive<ContainedLabel>,
pub footer: ContainedLabel,
}
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
@@ -722,7 +722,6 @@ pub struct Scrollbar {
pub width: f32,
pub min_height_factor: f32,
pub git: GitDiffColors,
pub selections: Color,
}
#[derive(Clone, Deserialize, Default, JsonSchema)]
@@ -1030,7 +1029,6 @@ pub struct AssistantStyle {
pub system_sender: Interactive<ContainedText>,
pub model: Interactive<ContainedText>,
pub remaining_tokens: ContainedText,
pub low_remaining_tokens: ContainedText,
pub no_remaining_tokens: ContainedText,
pub error_icon: Icon,
pub api_key_editor: FieldEditor,

View File

@@ -6,7 +6,6 @@ lazy_static::lazy_static! {
pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory");
pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed");
pub static ref CONVERSATIONS_DIR: PathBuf = HOME.join(".config/zed/conversations");
pub static ref EMBEDDINGS_DIR: PathBuf = HOME.join(".config/zed/embeddings");
pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed");
pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed");
pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");

View File

@@ -1,16 +0,0 @@
[package]
name = "vcs_menu"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
fuzzy = {path = "../fuzzy"}
gpui = {path = "../gpui"}
picker = {path = "../picker"}
util = {path = "../util"}
theme = {path = "../theme"}
workspace = {path = "../workspace"}
anyhow.workspace = true

View File

@@ -1,49 +0,0 @@
[package]
name = "vector_store"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/vector_store.rs"
doctest = false
[dependencies]
gpui = { path = "../gpui" }
language = { path = "../language" }
project = { path = "../project" }
workspace = { path = "../workspace" }
util = { path = "../util" }
picker = { path = "../picker" }
theme = { path = "../theme" }
editor = { path = "../editor" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }
anyhow.workspace = true
futures.workspace = true
smol.workspace = true
rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] }
isahc.workspace = true
log.workspace = true
tree-sitter.workspace = true
lazy_static.workspace = true
serde.workspace = true
serde_json.workspace = true
async-trait.workspace = true
bincode = "1.3.3"
matrixmultiply = "0.3.7"
tiktoken-rs = "0.5.0"
rand.workspace = true
schemars.workspace = true
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"]}
tree-sitter-rust = "*"
rand.workspace = true
unindent.workspace = true
tempdir.workspace = true

View File

@@ -1,31 +0,0 @@
WIP: Sample SQL Queries
/*
create table "files" (
"id" INTEGER PRIMARY KEY,
"path" VARCHAR,
"sha1" VARCHAR,
);
create table symbols (
"file_id" INTEGER REFERENCES("files", "id") ON CASCADE DELETE,
"offset" INTEGER,
"embedding" VECTOR,
);
insert into "files" ("path", "sha1") values ("src/main.rs", "sha1") return id;
insert into symbols (
"file_id",
"start",
"end",
"embedding"
) values (
(id,),
(id,),
(id,),
(id,),
)
*/

View File

@@ -1,325 +0,0 @@
use std::{
cmp::Ordering,
collections::HashMap,
path::{Path, PathBuf},
rc::Rc,
time::SystemTime,
};
use anyhow::{anyhow, Result};
use crate::parsing::ParsedFile;
use crate::VECTOR_STORE_VERSION;
use rpc::proto::Timestamp;
use rusqlite::{
params,
types::{FromSql, FromSqlResult, ValueRef},
};
#[derive(Debug)]
pub struct FileRecord {
pub id: usize,
pub relative_path: String,
pub mtime: Timestamp,
}
#[derive(Debug)]
struct Embedding(pub Vec<f32>);
impl FromSql for Embedding {
fn column_result(value: ValueRef) -> FromSqlResult<Self> {
let bytes = value.as_blob()?;
let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
if embedding.is_err() {
return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
}
return Ok(Embedding(embedding.unwrap()));
}
}
pub struct VectorDatabase {
db: rusqlite::Connection,
}
impl VectorDatabase {
pub fn new(path: String) -> Result<Self> {
let this = Self {
db: rusqlite::Connection::open(path)?,
};
this.initialize_database()?;
Ok(this)
}
fn initialize_database(&self) -> Result<()> {
rusqlite::vtab::array::load_module(&self.db)?;
// This will create the database if it doesnt exist
// Initialize Vector Databasing Tables
self.db.execute(
"CREATE TABLE IF NOT EXISTS worktrees (
id INTEGER PRIMARY KEY AUTOINCREMENT,
absolute_path VARCHAR NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS worktrees_absolute_path ON worktrees (absolute_path);
",
[],
)?;
self.db.execute(
"CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
worktree_id INTEGER NOT NULL,
relative_path VARCHAR NOT NULL,
mtime_seconds INTEGER NOT NULL,
mtime_nanos INTEGER NOT NULL,
vector_store_version INTEGER NOT NULL,
FOREIGN KEY(worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE
)",
[],
)?;
self.db.execute(
"CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_id INTEGER NOT NULL,
offset INTEGER NOT NULL,
name VARCHAR NOT NULL,
embedding BLOB NOT NULL,
FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
)",
[],
)?;
Ok(())
}
pub fn delete_file(&self, worktree_id: i64, delete_path: PathBuf) -> Result<()> {
self.db.execute(
"DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2",
params![worktree_id, delete_path.to_str()],
)?;
Ok(())
}
pub fn insert_file(&self, worktree_id: i64, indexed_file: ParsedFile) -> Result<()> {
// Write to files table, and return generated id.
self.db.execute(
"
DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2;
",
params![worktree_id, indexed_file.path.to_str()],
)?;
let mtime = Timestamp::from(indexed_file.mtime);
self.db.execute(
"
INSERT INTO files
(worktree_id, relative_path, mtime_seconds, mtime_nanos, vector_store_version)
VALUES
(?1, ?2, $3, $4, $5);
",
params![
worktree_id,
indexed_file.path.to_str(),
mtime.seconds,
mtime.nanos,
VECTOR_STORE_VERSION
],
)?;
let file_id = self.db.last_insert_rowid();
// Currently inserting at approximately 3400 documents a second
// I imagine we can speed this up with a bulk insert of some kind.
for document in indexed_file.documents {
let embedding_blob = bincode::serialize(&document.embedding)?;
self.db.execute(
"INSERT INTO documents (file_id, offset, name, embedding) VALUES (?1, ?2, ?3, ?4)",
params![
file_id,
document.offset.to_string(),
document.name,
embedding_blob
],
)?;
}
Ok(())
}
pub fn find_or_create_worktree(&self, worktree_root_path: &Path) -> Result<i64> {
// Check that the absolute path doesnt exist
let mut worktree_query = self
.db
.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?;
let worktree_id = worktree_query
.query_row(params![worktree_root_path.to_string_lossy()], |row| {
Ok(row.get::<_, i64>(0)?)
})
.map_err(|err| anyhow!(err));
if worktree_id.is_ok() {
return worktree_id;
}
// If worktree_id is Err, insert new worktree
self.db.execute(
"
INSERT into worktrees (absolute_path) VALUES (?1)
",
params![worktree_root_path.to_string_lossy()],
)?;
Ok(self.db.last_insert_rowid())
}
pub fn get_file_mtimes(&self, worktree_id: i64) -> Result<HashMap<PathBuf, SystemTime>> {
let mut statement = self.db.prepare(
"
SELECT relative_path, mtime_seconds, mtime_nanos
FROM files
WHERE worktree_id = ?1
ORDER BY relative_path",
)?;
let mut result: HashMap<PathBuf, SystemTime> = HashMap::new();
for row in statement.query_map(params![worktree_id], |row| {
Ok((
row.get::<_, String>(0)?.into(),
Timestamp {
seconds: row.get(1)?,
nanos: row.get(2)?,
}
.into(),
))
})? {
let row = row?;
result.insert(row.0, row.1);
}
Ok(result)
}
pub fn top_k_search(
&self,
worktree_ids: &[i64],
query_embedding: &Vec<f32>,
limit: usize,
) -> Result<Vec<(i64, PathBuf, usize, String)>> {
let mut results = Vec::<(i64, f32)>::with_capacity(limit + 1);
self.for_each_document(&worktree_ids, |id, embedding| {
let similarity = dot(&embedding, &query_embedding);
let ix = match results
.binary_search_by(|(_, s)| similarity.partial_cmp(&s).unwrap_or(Ordering::Equal))
{
Ok(ix) => ix,
Err(ix) => ix,
};
results.insert(ix, (id, similarity));
results.truncate(limit);
})?;
let ids = results.into_iter().map(|(id, _)| id).collect::<Vec<_>>();
self.get_documents_by_ids(&ids)
}
fn for_each_document(
&self,
worktree_ids: &[i64],
mut f: impl FnMut(i64, Vec<f32>),
) -> Result<()> {
let mut query_statement = self.db.prepare(
"
SELECT
documents.id, documents.embedding
FROM
documents, files
WHERE
documents.file_id = files.id AND
files.worktree_id IN rarray(?)
",
)?;
query_statement
.query_map(params![ids_to_sql(worktree_ids)], |row| {
Ok((row.get(0)?, row.get::<_, Embedding>(1)?))
})?
.filter_map(|row| row.ok())
.for_each(|(id, embedding)| f(id, embedding.0));
Ok(())
}
fn get_documents_by_ids(&self, ids: &[i64]) -> Result<Vec<(i64, PathBuf, usize, String)>> {
let mut statement = self.db.prepare(
"
SELECT
documents.id, files.worktree_id, files.relative_path, documents.offset, documents.name
FROM
documents, files
WHERE
documents.file_id = files.id AND
documents.id in rarray(?)
",
)?;
let result_iter = statement.query_map(params![ids_to_sql(ids)], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, i64>(1)?,
row.get::<_, String>(2)?.into(),
row.get(3)?,
row.get(4)?,
))
})?;
let mut values_by_id = HashMap::<i64, (i64, PathBuf, usize, String)>::default();
for row in result_iter {
let (id, worktree_id, path, offset, name) = row?;
values_by_id.insert(id, (worktree_id, path, offset, name));
}
let mut results = Vec::with_capacity(ids.len());
for id in ids {
let value = values_by_id
.remove(id)
.ok_or(anyhow!("missing document id {}", id))?;
results.push(value);
}
Ok(results)
}
}
fn ids_to_sql(ids: &[i64]) -> Rc<Vec<rusqlite::types::Value>> {
Rc::new(
ids.iter()
.copied()
.map(|v| rusqlite::types::Value::from(v))
.collect::<Vec<_>>(),
)
}
pub(crate) fn dot(vec_a: &[f32], vec_b: &[f32]) -> f32 {
let len = vec_a.len();
assert_eq!(len, vec_b.len());
let mut result = 0.0;
unsafe {
matrixmultiply::sgemm(
1,
len,
1,
1.0,
vec_a.as_ptr(),
len as isize,
1,
vec_b.as_ptr(),
1,
len as isize,
0.0,
&mut result as *mut f32,
1,
1,
);
}
result
}

View File

@@ -1,166 +0,0 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::AsyncReadExt;
use gpui::executor::Background;
use gpui::serde_json;
use isahc::http::StatusCode;
use isahc::prelude::Configurable;
use isahc::{AsyncBody, Response};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::env;
use std::sync::Arc;
use std::time::Duration;
use tiktoken_rs::{cl100k_base, CoreBPE};
use util::http::{HttpClient, Request};
lazy_static! {
static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap();
}
#[derive(Clone)]
pub struct OpenAIEmbeddings {
pub client: Arc<dyn HttpClient>,
pub executor: Arc<Background>,
}
#[derive(Serialize)]
struct OpenAIEmbeddingRequest<'a> {
model: &'static str,
input: Vec<&'a str>,
}
#[derive(Deserialize)]
struct OpenAIEmbeddingResponse {
data: Vec<OpenAIEmbedding>,
usage: OpenAIEmbeddingUsage,
}
#[derive(Debug, Deserialize)]
struct OpenAIEmbedding {
embedding: Vec<f32>,
index: usize,
object: String,
}
#[derive(Deserialize)]
struct OpenAIEmbeddingUsage {
prompt_tokens: usize,
total_tokens: usize,
}
#[async_trait]
pub trait EmbeddingProvider: Sync + Send {
async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>>;
}
pub struct DummyEmbeddings {}
#[async_trait]
impl EmbeddingProvider for DummyEmbeddings {
async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> {
// 1024 is the OpenAI Embeddings size for ada models.
// the model we will likely be starting with.
let dummy_vec = vec![0.32 as f32; 1536];
return Ok(vec![dummy_vec; spans.len()]);
}
}
impl OpenAIEmbeddings {
async fn truncate(span: String) -> String {
let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span.as_ref());
if tokens.len() > 8190 {
tokens.truncate(8190);
let result = OPENAI_BPE_TOKENIZER.decode(tokens.clone());
if result.is_ok() {
let transformed = result.unwrap();
// assert_ne!(transformed, span);
return transformed;
}
}
return span.to_string();
}
async fn send_request(&self, api_key: &str, spans: Vec<&str>) -> Result<Response<AsyncBody>> {
let request = Request::post("https://api.openai.com/v1/embeddings")
.redirect_policy(isahc::config::RedirectPolicy::Follow)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(
serde_json::to_string(&OpenAIEmbeddingRequest {
input: spans.clone(),
model: "text-embedding-ada-002",
})
.unwrap()
.into(),
)?;
Ok(self.client.send(request).await?)
}
}
#[async_trait]
impl EmbeddingProvider for OpenAIEmbeddings {
async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> {
const BACKOFF_SECONDS: [usize; 3] = [65, 180, 360];
const MAX_RETRIES: usize = 3;
let api_key = OPENAI_API_KEY
.as_ref()
.ok_or_else(|| anyhow!("no api key"))?;
let mut request_number = 0;
let mut response: Response<AsyncBody>;
let mut spans: Vec<String> = spans.iter().map(|x| x.to_string()).collect();
while request_number < MAX_RETRIES {
response = self
.send_request(api_key, spans.iter().map(|x| &**x).collect())
.await?;
request_number += 1;
if request_number + 1 == MAX_RETRIES && response.status() != StatusCode::OK {
return Err(anyhow!(
"openai max retries, error: {:?}",
&response.status()
));
}
match response.status() {
StatusCode::TOO_MANY_REQUESTS => {
let delay = Duration::from_secs(BACKOFF_SECONDS[request_number - 1] as u64);
self.executor.timer(delay).await;
}
StatusCode::BAD_REQUEST => {
log::info!("BAD REQUEST: {:?}", &response.status());
// Don't worry about delaying bad request, as we can assume
// we haven't been rate limited yet.
for span in spans.iter_mut() {
*span = Self::truncate(span.to_string()).await;
}
}
StatusCode::OK => {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
let response: OpenAIEmbeddingResponse = serde_json::from_str(&body)?;
log::info!(
"openai embedding completed. tokens: {:?}",
response.usage.total_tokens
);
return Ok(response
.data
.into_iter()
.map(|embedding| embedding.embedding)
.collect());
}
_ => {
return Err(anyhow!("openai embedding failed {}", response.status()));
}
}
}
Err(anyhow!("openai embedding failed"))
}
}

View File

@@ -1,172 +0,0 @@
use crate::{SearchResult, VectorStore};
use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{
actions, elements::*, AnyElement, AppContext, ModelHandle, MouseState, Task, ViewContext,
WeakViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use project::{Project, ProjectPath};
use std::{collections::HashMap, sync::Arc, time::Duration};
use util::ResultExt;
use workspace::Workspace;
const MIN_QUERY_LEN: usize = 5;
const EMBEDDING_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(500);
actions!(semantic_search, [Toggle]);
pub type SemanticSearch = Picker<SemanticSearchDelegate>;
pub struct SemanticSearchDelegate {
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>,
vector_store: ModelHandle<VectorStore>,
selected_match_index: usize,
matches: Vec<SearchResult>,
history: HashMap<String, Vec<SearchResult>>,
}
impl SemanticSearchDelegate {
// This is currently searching on every keystroke,
// This is wildly overkill, and has the potential to get expensive
// We will need to update this to throttle searching
pub fn new(
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>,
vector_store: ModelHandle<VectorStore>,
) -> Self {
Self {
workspace,
project,
vector_store,
selected_match_index: 0,
matches: vec![],
history: HashMap::new(),
}
}
}
impl PickerDelegate for SemanticSearchDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search repository in natural language...".into()
}
fn confirm(&mut self, cx: &mut ViewContext<SemanticSearch>) {
if let Some(search_result) = self.matches.get(self.selected_match_index) {
// Open Buffer
let search_result = search_result.clone();
let buffer = self.project.update(cx, |project, cx| {
project.open_buffer(
ProjectPath {
worktree_id: search_result.worktree_id,
path: search_result.file_path.clone().into(),
},
cx,
)
});
let workspace = self.workspace.clone();
let position = search_result.clone().offset;
cx.spawn(|_, mut cx| async move {
let buffer = buffer.await?;
workspace.update(&mut cx, |workspace, cx| {
let editor = workspace.open_project_item::<Editor>(buffer, cx);
editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([position..position])
});
});
})?;
Ok::<_, anyhow::Error>(())
})
.detach_and_log_err(cx);
cx.emit(PickerEvent::Dismiss);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<SemanticSearch>) {}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_match_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<SemanticSearch>) {
self.selected_match_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<SemanticSearch>) -> Task<()> {
log::info!("Searching for {:?}...", query);
if query.len() < MIN_QUERY_LEN {
log::info!("Query below minimum length");
return Task::ready(());
}
let vector_store = self.vector_store.clone();
let project = self.project.clone();
cx.spawn(|this, mut cx| async move {
cx.background().timer(EMBEDDING_DEBOUNCE_INTERVAL).await;
let retrieved_cached = this.update(&mut cx, |this, _| {
let delegate = this.delegate_mut();
if delegate.history.contains_key(&query) {
let historic_results = delegate.history.get(&query).unwrap().to_owned();
delegate.matches = historic_results.clone();
true
} else {
false
}
});
if let Some(retrieved) = retrieved_cached.log_err() {
if !retrieved {
let task = vector_store.update(&mut cx, |store, cx| {
store.search(project.clone(), query.to_string(), 10, cx)
});
if let Some(results) = task.await.log_err() {
log::info!("Not queried previously, searching...");
this.update(&mut cx, |this, _| {
let delegate = this.delegate_mut();
delegate.matches = results.clone();
delegate.history.insert(query, results);
})
.ok();
}
} else {
log::info!("Already queried, retrieved directly from cached history");
}
}
})
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut MouseState,
selected: bool,
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let style = &theme.picker.item;
let current_style = style.in_state(selected).style_for(mouse_state);
let search_result = &self.matches[ix];
let path = search_result.file_path.to_string_lossy();
let name = search_result.name.clone();
Flex::column()
.with_child(Text::new(name, current_style.label.text.clone()).with_soft_wrap(false))
.with_child(Label::new(
path.to_string(),
style.inactive_state().default.label.clone(),
))
.contained()
.with_style(current_style.container)
.into_any()
}
}

View File

@@ -1,118 +0,0 @@
use std::{path::PathBuf, sync::Arc, time::SystemTime};
use anyhow::{anyhow, Ok, Result};
use project::Fs;
use tree_sitter::{Parser, QueryCursor};
use crate::PendingFile;
#[derive(Debug, PartialEq, Clone)]
pub struct Document {
pub offset: usize,
pub name: String,
pub embedding: Vec<f32>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct ParsedFile {
pub path: PathBuf,
pub mtime: SystemTime,
pub documents: Vec<Document>,
}
const CODE_CONTEXT_TEMPLATE: &str =
"The below code snippet is from file '<path>'\n\n```<language>\n<item>\n```";
pub struct CodeContextRetriever {
pub parser: Parser,
pub cursor: QueryCursor,
pub fs: Arc<dyn Fs>,
}
impl CodeContextRetriever {
pub async fn parse_file(
&mut self,
pending_file: PendingFile,
) -> Result<(ParsedFile, Vec<String>)> {
let grammar = pending_file
.language
.grammar()
.ok_or_else(|| anyhow!("no grammar for language"))?;
let embedding_config = grammar
.embedding_config
.as_ref()
.ok_or_else(|| anyhow!("no embedding queries"))?;
let content = self.fs.load(&pending_file.absolute_path).await?;
self.parser.set_language(grammar.ts_language).unwrap();
let tree = self
.parser
.parse(&content, None)
.ok_or_else(|| anyhow!("parsing failed"))?;
let mut documents = Vec::new();
let mut context_spans = Vec::new();
// Iterate through query matches
for mat in self.cursor.matches(
&embedding_config.query,
tree.root_node(),
content.as_bytes(),
) {
// log::info!("-----MATCH-----");
let mut name: Vec<&str> = vec![];
let mut item: Option<&str> = None;
let mut offset: Option<usize> = None;
for capture in mat.captures {
if capture.index == embedding_config.item_capture_ix {
offset = Some(capture.node.byte_range().start);
item = content.get(capture.node.byte_range());
} else if capture.index == embedding_config.name_capture_ix {
if let Some(name_content) = content.get(capture.node.byte_range()) {
name.push(name_content);
}
}
if let Some(context_capture_ix) = embedding_config.context_capture_ix {
if capture.index == context_capture_ix {
if let Some(context) = content.get(capture.node.byte_range()) {
name.push(context);
}
}
}
}
if item.is_some() && offset.is_some() && name.len() > 0 {
let context_span = CODE_CONTEXT_TEMPLATE
.replace("<path>", pending_file.relative_path.to_str().unwrap())
.replace("<language>", &pending_file.language.name().to_lowercase())
.replace("<item>", item.unwrap());
let mut truncated_span = context_span.clone();
truncated_span.truncate(100);
// log::info!("Name: {:?}", name);
// log::info!("Span: {:?}", truncated_span);
context_spans.push(context_span);
documents.push(Document {
name: name.join(" "),
offset: offset.unwrap(),
embedding: Vec::new(),
})
}
}
return Ok((
ParsedFile {
path: pending_file.relative_path,
mtime: pending_file.modified_time,
documents,
},
context_spans,
));
}
}

View File

@@ -1,770 +0,0 @@
mod db;
mod embedding;
mod modal;
mod parsing;
mod vector_store_settings;
#[cfg(test)]
mod vector_store_tests;
use crate::vector_store_settings::VectorStoreSettings;
use anyhow::{anyhow, Result};
use db::VectorDatabase;
use embedding::{EmbeddingProvider, OpenAIEmbeddings};
use futures::{channel::oneshot, Future};
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, ViewContext,
WeakModelHandle,
};
use language::{Language, LanguageRegistry};
use modal::{SemanticSearch, SemanticSearchDelegate, Toggle};
use parsing::{CodeContextRetriever, ParsedFile};
use project::{Fs, PathChange, Project, ProjectEntryId, WorktreeId};
use smol::channel;
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
time::{Duration, Instant, SystemTime},
};
use tree_sitter::{Parser, QueryCursor};
use util::{
channel::{ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME},
http::HttpClient,
paths::EMBEDDINGS_DIR,
ResultExt,
};
use workspace::{Workspace, WorkspaceCreated};
const VECTOR_STORE_VERSION: usize = 0;
const EMBEDDINGS_BATCH_SIZE: usize = 150;
pub fn init(
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
language_registry: Arc<LanguageRegistry>,
cx: &mut AppContext,
) {
settings::register::<VectorStoreSettings>(cx);
let db_file_path = EMBEDDINGS_DIR
.join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
.join("embeddings_db");
SemanticSearch::init(cx);
cx.add_action(
|workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>| {
if cx.has_global::<ModelHandle<VectorStore>>() {
let vector_store = cx.global::<ModelHandle<VectorStore>>().clone();
workspace.toggle_modal(cx, |workspace, cx| {
let project = workspace.project().clone();
let workspace = cx.weak_handle();
cx.add_view(|cx| {
SemanticSearch::new(
SemanticSearchDelegate::new(workspace, project, vector_store),
cx,
)
})
});
}
},
);
if *RELEASE_CHANNEL == ReleaseChannel::Stable
|| !settings::get::<VectorStoreSettings>(cx).enabled
{
return;
}
cx.spawn(move |mut cx| async move {
let vector_store = VectorStore::new(
fs,
db_file_path,
// Arc::new(embedding::DummyEmbeddings {}),
Arc::new(OpenAIEmbeddings {
client: http_client,
executor: cx.background(),
}),
language_registry,
cx.clone(),
)
.await?;
cx.update(|cx| {
cx.set_global(vector_store.clone());
cx.subscribe_global::<WorkspaceCreated, _>({
let vector_store = vector_store.clone();
move |event, cx| {
let workspace = &event.0;
if let Some(workspace) = workspace.upgrade(cx) {
let project = workspace.read(cx).project().clone();
if project.read(cx).is_local() {
vector_store.update(cx, |store, cx| {
store.add_project(project, cx).detach();
});
}
}
}
})
.detach();
});
anyhow::Ok(())
})
.detach();
}
pub struct VectorStore {
fs: Arc<dyn Fs>,
database_url: Arc<PathBuf>,
embedding_provider: Arc<dyn EmbeddingProvider>,
language_registry: Arc<LanguageRegistry>,
db_update_tx: channel::Sender<DbOperation>,
parsing_files_tx: channel::Sender<PendingFile>,
_db_update_task: Task<()>,
_embed_batch_task: Task<()>,
_batch_files_task: Task<()>,
_parsing_files_tasks: Vec<Task<()>>,
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
}
struct ProjectState {
worktree_db_ids: Vec<(WorktreeId, i64)>,
pending_files: HashMap<PathBuf, (PendingFile, SystemTime)>,
_subscription: gpui::Subscription,
}
impl ProjectState {
fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option<i64> {
self.worktree_db_ids
.iter()
.find_map(|(worktree_id, db_id)| {
if *worktree_id == id {
Some(*db_id)
} else {
None
}
})
}
fn worktree_id_for_db_id(&self, id: i64) -> Option<WorktreeId> {
self.worktree_db_ids
.iter()
.find_map(|(worktree_id, db_id)| {
if *db_id == id {
Some(*worktree_id)
} else {
None
}
})
}
fn update_pending_files(&mut self, pending_file: PendingFile, indexing_time: SystemTime) {
// If Pending File Already Exists, Replace it with the new one
// but keep the old indexing time
if let Some(old_file) = self
.pending_files
.remove(&pending_file.relative_path.clone())
{
self.pending_files.insert(
pending_file.relative_path.clone(),
(pending_file, old_file.1),
);
} else {
self.pending_files.insert(
pending_file.relative_path.clone(),
(pending_file, indexing_time),
);
};
}
fn get_outstanding_files(&mut self) -> Vec<PendingFile> {
let mut outstanding_files = vec![];
let mut remove_keys = vec![];
for key in self.pending_files.keys().into_iter() {
if let Some(pending_details) = self.pending_files.get(key) {
let (pending_file, index_time) = pending_details;
if index_time <= &SystemTime::now() {
outstanding_files.push(pending_file.clone());
remove_keys.push(key.clone());
}
}
}
for key in remove_keys.iter() {
self.pending_files.remove(key);
}
return outstanding_files;
}
}
#[derive(Clone, Debug)]
pub struct PendingFile {
worktree_db_id: i64,
relative_path: PathBuf,
absolute_path: PathBuf,
language: Arc<Language>,
modified_time: SystemTime,
}
#[derive(Debug, Clone)]
pub struct SearchResult {
pub worktree_id: WorktreeId,
pub name: String,
pub offset: usize,
pub file_path: PathBuf,
}
enum DbOperation {
InsertFile {
worktree_id: i64,
indexed_file: ParsedFile,
},
Delete {
worktree_id: i64,
path: PathBuf,
},
FindOrCreateWorktree {
path: PathBuf,
sender: oneshot::Sender<Result<i64>>,
},
FileMTimes {
worktree_id: i64,
sender: oneshot::Sender<Result<HashMap<PathBuf, SystemTime>>>,
},
}
enum EmbeddingJob {
Enqueue {
worktree_id: i64,
parsed_file: ParsedFile,
document_spans: Vec<String>,
},
Flush,
}
impl VectorStore {
async fn new(
fs: Arc<dyn Fs>,
database_url: PathBuf,
embedding_provider: Arc<dyn EmbeddingProvider>,
language_registry: Arc<LanguageRegistry>,
mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> {
let database_url = Arc::new(database_url);
let db = cx
.background()
.spawn({
let fs = fs.clone();
let database_url = database_url.clone();
async move {
if let Some(db_directory) = database_url.parent() {
fs.create_dir(db_directory).await.log_err();
}
let db = VectorDatabase::new(database_url.to_string_lossy().to_string())?;
anyhow::Ok(db)
}
})
.await?;
Ok(cx.add_model(|cx| {
// paths_tx -> embeddings_tx -> db_update_tx
//db_update_tx/rx: Updating Database
let (db_update_tx, db_update_rx) = channel::unbounded();
let _db_update_task = cx.background().spawn(async move {
while let Ok(job) = db_update_rx.recv().await {
match job {
DbOperation::InsertFile {
worktree_id,
indexed_file,
} => {
db.insert_file(worktree_id, indexed_file).log_err();
}
DbOperation::Delete { worktree_id, path } => {
db.delete_file(worktree_id, path).log_err();
}
DbOperation::FindOrCreateWorktree { path, sender } => {
let id = db.find_or_create_worktree(&path);
sender.send(id).ok();
}
DbOperation::FileMTimes {
worktree_id: worktree_db_id,
sender,
} => {
let file_mtimes = db.get_file_mtimes(worktree_db_id);
sender.send(file_mtimes).ok();
}
}
}
});
// embed_tx/rx: Embed Batch and Send to Database
let (embed_batch_tx, embed_batch_rx) =
channel::unbounded::<Vec<(i64, ParsedFile, Vec<String>)>>();
let _embed_batch_task = cx.background().spawn({
let db_update_tx = db_update_tx.clone();
let embedding_provider = embedding_provider.clone();
async move {
while let Ok(mut embeddings_queue) = embed_batch_rx.recv().await {
// Construct Batch
let mut document_spans = vec![];
for (_, _, document_span) in embeddings_queue.iter() {
document_spans.extend(document_span.iter().map(|s| s.as_str()));
}
if let Ok(embeddings) = embedding_provider.embed_batch(document_spans).await
{
let mut i = 0;
let mut j = 0;
for embedding in embeddings.iter() {
while embeddings_queue[i].1.documents.len() == j {
i += 1;
j = 0;
}
embeddings_queue[i].1.documents[j].embedding = embedding.to_owned();
j += 1;
}
for (worktree_id, indexed_file, _) in embeddings_queue.into_iter() {
for document in indexed_file.documents.iter() {
// TODO: Update this so it doesn't panic
assert!(
document.embedding.len() > 0,
"Document Embedding Not Complete"
);
}
db_update_tx
.send(DbOperation::InsertFile {
worktree_id,
indexed_file,
})
.await
.unwrap();
}
}
}
}
});
// batch_tx/rx: Batch Files to Send for Embeddings
let (batch_files_tx, batch_files_rx) = channel::unbounded::<EmbeddingJob>();
let _batch_files_task = cx.background().spawn(async move {
let mut queue_len = 0;
let mut embeddings_queue = vec![];
while let Ok(job) = batch_files_rx.recv().await {
let should_flush = match job {
EmbeddingJob::Enqueue {
document_spans,
worktree_id,
parsed_file,
} => {
queue_len += &document_spans.len();
embeddings_queue.push((worktree_id, parsed_file, document_spans));
queue_len >= EMBEDDINGS_BATCH_SIZE
}
EmbeddingJob::Flush => true,
};
if should_flush {
embed_batch_tx.try_send(embeddings_queue).unwrap();
embeddings_queue = vec![];
queue_len = 0;
}
}
});
// parsing_files_tx/rx: Parsing Files to Embeddable Documents
let (parsing_files_tx, parsing_files_rx) = channel::unbounded::<PendingFile>();
let mut _parsing_files_tasks = Vec::new();
// for _ in 0..cx.background().num_cpus() {
for _ in 0..1 {
let fs = fs.clone();
let parsing_files_rx = parsing_files_rx.clone();
let batch_files_tx = batch_files_tx.clone();
_parsing_files_tasks.push(cx.background().spawn(async move {
let parser = Parser::new();
let cursor = QueryCursor::new();
let mut retriever = CodeContextRetriever { parser, cursor, fs };
while let Ok(pending_file) = parsing_files_rx.recv().await {
if let Some((indexed_file, document_spans)) =
retriever.parse_file(pending_file.clone()).await.log_err()
{
batch_files_tx
.try_send(EmbeddingJob::Enqueue {
worktree_id: pending_file.worktree_db_id,
parsed_file: indexed_file,
document_spans,
})
.unwrap();
}
if parsing_files_rx.len() == 0 {
batch_files_tx.try_send(EmbeddingJob::Flush).unwrap();
}
}
}));
}
Self {
fs,
database_url,
embedding_provider,
language_registry,
db_update_tx,
parsing_files_tx,
_db_update_task,
_embed_batch_task,
_batch_files_task,
_parsing_files_tasks,
projects: HashMap::new(),
}
}))
}
fn find_or_create_worktree(&self, path: PathBuf) -> impl Future<Output = Result<i64>> {
let (tx, rx) = oneshot::channel();
self.db_update_tx
.try_send(DbOperation::FindOrCreateWorktree { path, sender: tx })
.unwrap();
async move { rx.await? }
}
fn get_file_mtimes(
&self,
worktree_id: i64,
) -> impl Future<Output = Result<HashMap<PathBuf, SystemTime>>> {
let (tx, rx) = oneshot::channel();
self.db_update_tx
.try_send(DbOperation::FileMTimes {
worktree_id,
sender: tx,
})
.unwrap();
async move { rx.await? }
}
fn add_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let worktree_scans_complete = project
.read(cx)
.worktrees(cx)
.map(|worktree| {
let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete();
async move {
scan_complete.await;
}
})
.collect::<Vec<_>>();
let worktree_db_ids = project
.read(cx)
.worktrees(cx)
.map(|worktree| {
self.find_or_create_worktree(worktree.read(cx).abs_path().to_path_buf())
})
.collect::<Vec<_>>();
let fs = self.fs.clone();
let language_registry = self.language_registry.clone();
let database_url = self.database_url.clone();
let db_update_tx = self.db_update_tx.clone();
let parsing_files_tx = self.parsing_files_tx.clone();
cx.spawn(|this, mut cx| async move {
futures::future::join_all(worktree_scans_complete).await;
let worktree_db_ids = futures::future::join_all(worktree_db_ids).await;
if let Some(db_directory) = database_url.parent() {
fs.create_dir(db_directory).await.log_err();
}
let worktrees = project.read_with(&cx, |project, cx| {
project
.worktrees(cx)
.map(|worktree| worktree.read(cx).snapshot())
.collect::<Vec<_>>()
});
let mut worktree_file_times = HashMap::new();
let mut db_ids_by_worktree_id = HashMap::new();
for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) {
let db_id = db_id?;
db_ids_by_worktree_id.insert(worktree.id(), db_id);
worktree_file_times.insert(
worktree.id(),
this.read_with(&cx, |this, _| this.get_file_mtimes(db_id))
.await?,
);
}
cx.background()
.spawn({
let db_ids_by_worktree_id = db_ids_by_worktree_id.clone();
let db_update_tx = db_update_tx.clone();
let language_registry = language_registry.clone();
let parsing_files_tx = parsing_files_tx.clone();
async move {
let t0 = Instant::now();
for worktree in worktrees.into_iter() {
let mut file_mtimes =
worktree_file_times.remove(&worktree.id()).unwrap();
for file in worktree.files(false, 0) {
let absolute_path = worktree.absolutize(&file.path);
if let Ok(language) = language_registry
.language_for_file(&absolute_path, None)
.await
{
if language
.grammar()
.and_then(|grammar| grammar.embedding_config.as_ref())
.is_none()
{
continue;
}
let path_buf = file.path.to_path_buf();
let stored_mtime = file_mtimes.remove(&file.path.to_path_buf());
let already_stored = stored_mtime
.map_or(false, |existing_mtime| {
existing_mtime == file.mtime
});
if !already_stored {
parsing_files_tx
.try_send(PendingFile {
worktree_db_id: db_ids_by_worktree_id
[&worktree.id()],
relative_path: path_buf,
absolute_path,
language,
modified_time: file.mtime,
})
.unwrap();
}
}
}
for file in file_mtimes.keys() {
db_update_tx
.try_send(DbOperation::Delete {
worktree_id: db_ids_by_worktree_id[&worktree.id()],
path: file.to_owned(),
})
.unwrap();
}
}
log::info!(
"Parsing Worktree Completed in {:?}",
t0.elapsed().as_millis()
);
}
})
.detach();
// let mut pending_files: Vec<(PathBuf, ((i64, PathBuf, Arc<Language>, SystemTime), SystemTime))> = vec![];
this.update(&mut cx, |this, cx| {
// The below is managing for updated on save
// Currently each time a file is saved, this code is run, and for all the files that were changed, if the current time is
// greater than the previous embedded time by the REINDEXING_DELAY variable, we will send the file off to be indexed.
let _subscription = cx.subscribe(&project, |this, project, event, cx| {
if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event {
this.project_entries_changed(project, changes.clone(), cx, worktree_id);
}
});
this.projects.insert(
project.downgrade(),
ProjectState {
pending_files: HashMap::new(),
worktree_db_ids: db_ids_by_worktree_id.into_iter().collect(),
_subscription,
},
);
});
anyhow::Ok(())
})
}
pub fn search(
&mut self,
project: ModelHandle<Project>,
phrase: String,
limit: usize,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<SearchResult>>> {
let project_state = if let Some(state) = self.projects.get(&project.downgrade()) {
state
} else {
return Task::ready(Err(anyhow!("project not added")));
};
let worktree_db_ids = project
.read(cx)
.worktrees(cx)
.filter_map(|worktree| {
let worktree_id = worktree.read(cx).id();
project_state.db_id_for_worktree_id(worktree_id)
})
.collect::<Vec<_>>();
let embedding_provider = self.embedding_provider.clone();
let database_url = self.database_url.clone();
cx.spawn(|this, cx| async move {
let documents = cx
.background()
.spawn(async move {
let database = VectorDatabase::new(database_url.to_string_lossy().into())?;
let phrase_embedding = embedding_provider
.embed_batch(vec![&phrase])
.await?
.into_iter()
.next()
.unwrap();
database.top_k_search(&worktree_db_ids, &phrase_embedding, limit)
})
.await?;
this.read_with(&cx, |this, _| {
let project_state = if let Some(state) = this.projects.get(&project.downgrade()) {
state
} else {
return Err(anyhow!("project not added"));
};
Ok(documents
.into_iter()
.filter_map(|(worktree_db_id, file_path, offset, name)| {
let worktree_id = project_state.worktree_id_for_db_id(worktree_db_id)?;
Some(SearchResult {
worktree_id,
name,
offset,
file_path,
})
})
.collect())
})
})
}
fn project_entries_changed(
&mut self,
project: ModelHandle<Project>,
changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
cx: &mut ModelContext<'_, VectorStore>,
worktree_id: &WorktreeId,
) -> Option<()> {
let reindexing_delay = settings::get::<VectorStoreSettings>(cx).reindexing_delay_seconds;
let worktree = project
.read(cx)
.worktree_for_id(worktree_id.clone(), cx)?
.read(cx)
.snapshot();
let worktree_db_id = self
.projects
.get(&project.downgrade())?
.db_id_for_worktree_id(worktree.id())?;
let file_mtimes = self.get_file_mtimes(worktree_db_id);
let language_registry = self.language_registry.clone();
cx.spawn(|this, mut cx| async move {
let file_mtimes = file_mtimes.await.log_err()?;
for change in changes.into_iter() {
let change_path = change.0.clone();
let absolute_path = worktree.absolutize(&change_path);
// Skip if git ignored or symlink
if let Some(entry) = worktree.entry_for_id(change.1) {
if entry.is_ignored || entry.is_symlink || entry.is_external {
continue;
}
}
match change.2 {
PathChange::Removed => this.update(&mut cx, |this, _| {
this.db_update_tx
.try_send(DbOperation::Delete {
worktree_id: worktree_db_id,
path: absolute_path,
})
.unwrap();
}),
_ => {
if let Ok(language) = language_registry
.language_for_file(&change_path.to_path_buf(), None)
.await
{
if language
.grammar()
.and_then(|grammar| grammar.embedding_config.as_ref())
.is_none()
{
continue;
}
let modified_time =
change_path.metadata().log_err()?.modified().log_err()?;
let existing_time = file_mtimes.get(&change_path.to_path_buf());
let already_stored = existing_time
.map_or(false, |existing_time| &modified_time != existing_time);
if !already_stored {
this.update(&mut cx, |this, _| {
let reindex_time = modified_time
+ Duration::from_secs(reindexing_delay as u64);
let project_state =
this.projects.get_mut(&project.downgrade())?;
project_state.update_pending_files(
PendingFile {
relative_path: change_path.to_path_buf(),
absolute_path,
modified_time,
worktree_db_id,
language: language.clone(),
},
reindex_time,
);
for file in project_state.get_outstanding_files() {
this.parsing_files_tx.try_send(file).unwrap();
}
Some(())
});
}
}
}
}
}
Some(())
})
.detach();
Some(())
}
}
impl Entity for VectorStore {
type Event = ();
}

View File

@@ -1,30 +0,0 @@
use anyhow;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Setting;
#[derive(Deserialize, Debug)]
pub struct VectorStoreSettings {
pub enabled: bool,
pub reindexing_delay_seconds: usize,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct VectorStoreSettingsContent {
pub enabled: Option<bool>,
pub reindexing_delay_seconds: Option<usize>,
}
impl Setting for VectorStoreSettings {
const KEY: Option<&'static str> = Some("vector_store");
type FileContent = VectorStoreSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

View File

@@ -1,161 +0,0 @@
use crate::{
db::dot, embedding::EmbeddingProvider, vector_store_settings::VectorStoreSettings, VectorStore,
};
use anyhow::Result;
use async_trait::async_trait;
use gpui::{Task, TestAppContext};
use language::{Language, LanguageConfig, LanguageRegistry};
use project::{project_settings::ProjectSettings, FakeFs, Project};
use rand::{rngs::StdRng, Rng};
use serde_json::json;
use settings::SettingsStore;
use std::sync::Arc;
use unindent::Unindent;
#[gpui::test]
async fn test_vector_store(cx: &mut TestAppContext) {
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
settings::register::<VectorStoreSettings>(cx);
settings::register::<ProjectSettings>(cx);
});
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/the-root",
json!({
"src": {
"file1.rs": "
fn aaa() {
println!(\"aaaa!\");
}
fn zzzzzzzzz() {
println!(\"SLEEPING\");
}
".unindent(),
"file2.rs": "
fn bbb() {
println!(\"bbbb!\");
}
".unindent(),
}
}),
)
.await;
let languages = Arc::new(LanguageRegistry::new(Task::ready(())));
let rust_language = Arc::new(
Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".into()],
..Default::default()
},
Some(tree_sitter_rust::language()),
)
.with_embedding_query(
r#"
(function_item
name: (identifier) @name
body: (block)) @item
"#,
)
.unwrap(),
);
languages.add(rust_language);
let db_dir = tempdir::TempDir::new("vector-store").unwrap();
let db_path = db_dir.path().join("db.sqlite");
let store = VectorStore::new(
fs.clone(),
db_path,
Arc::new(FakeEmbeddingProvider),
languages,
cx.to_async(),
)
.await
.unwrap();
let project = Project::test(fs, ["/the-root".as_ref()], cx).await;
let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});
store
.update(cx, |store, cx| store.add_project(project.clone(), cx))
.await
.unwrap();
cx.foreground().run_until_parked();
let search_results = store
.update(cx, |store, cx| {
store.search(project.clone(), "aaaa".to_string(), 5, cx)
})
.await
.unwrap();
assert_eq!(search_results[0].offset, 0);
assert_eq!(search_results[0].name, "aaa");
assert_eq!(search_results[0].worktree_id, worktree_id);
}
#[gpui::test]
fn test_dot_product(mut rng: StdRng) {
assert_eq!(dot(&[1., 0., 0., 0., 0.], &[0., 1., 0., 0., 0.]), 0.);
assert_eq!(dot(&[2., 0., 0., 0., 0.], &[3., 1., 0., 0., 0.]), 6.);
for _ in 0..100 {
let size = 1536;
let mut a = vec![0.; size];
let mut b = vec![0.; size];
for (a, b) in a.iter_mut().zip(b.iter_mut()) {
*a = rng.gen();
*b = rng.gen();
}
assert_eq!(
round_to_decimals(dot(&a, &b), 1),
round_to_decimals(reference_dot(&a, &b), 1)
);
}
fn round_to_decimals(n: f32, decimal_places: i32) -> f32 {
let factor = (10.0 as f32).powi(decimal_places);
(n * factor).round() / factor
}
fn reference_dot(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(a, b)| a * b).sum()
}
}
struct FakeEmbeddingProvider;
#[async_trait]
impl EmbeddingProvider for FakeEmbeddingProvider {
async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> {
Ok(spans
.iter()
.map(|span| {
let mut result = vec![1.0; 26];
for letter in span.chars() {
let letter = letter.to_ascii_lowercase();
if letter as u32 >= 'a' as u32 {
let ix = (letter as u32) - ('a' as u32);
if ix < 26 {
result[ix as usize] += 1.0;
}
}
}
let norm = result.iter().map(|x| x * x).sum::<f32>().sqrt();
for x in &mut result {
*x /= norm;
}
result
})
.collect())
}
}

View File

@@ -36,6 +36,7 @@ workspace = { path = "../workspace" }
[dev-dependencies]
indoc.workspace = true
parking_lot.workspace = true
lazy_static.workspace = true
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }

View File

@@ -31,8 +31,6 @@ pub enum Motion {
CurrentLine,
StartOfLine,
EndOfLine,
StartOfParagraph,
EndOfParagraph,
StartOfDocument,
EndOfDocument,
Matching,
@@ -74,8 +72,6 @@ actions!(
StartOfLine,
EndOfLine,
CurrentLine,
StartOfParagraph,
EndOfParagraph,
StartOfDocument,
EndOfDocument,
Matching,
@@ -96,12 +92,6 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
motion(Motion::StartOfParagraph, cx)
});
cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
motion(Motion::EndOfParagraph, cx)
});
cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
motion(Motion::StartOfDocument, cx)
});
@@ -152,8 +142,7 @@ impl Motion {
pub fn linewise(&self) -> bool {
use Motion::*;
match self {
Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
| StartOfParagraph | EndOfParagraph => true,
Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true,
EndOfLine
| NextWordEnd { .. }
| Matching
@@ -183,8 +172,6 @@ impl Motion {
| Backspace
| Right
| StartOfLine
| StartOfParagraph
| EndOfParagraph
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace
@@ -210,8 +197,6 @@ impl Motion {
| Backspace
| Right
| StartOfLine
| StartOfParagraph
| EndOfParagraph
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace
@@ -250,14 +235,6 @@ impl Motion {
FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
StartOfLine => (start_of_line(map, point), SelectionGoal::None),
EndOfLine => (end_of_line(map, point), SelectionGoal::None),
StartOfParagraph => (
movement::start_of_paragraph(map, point, times),
SelectionGoal::None,
),
EndOfParagraph => (
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
SelectionGoal::None,
),
CurrentLine => (end_of_line(map, point), SelectionGoal::None),
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => (
@@ -525,13 +502,10 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
if line_end == point {
line_end = map.max_point().to_point(map);
}
line_end.column = line_end.column.saturating_sub(1);
let line_range = map.prev_line_boundary(point).0..line_end;
let visible_line_range =
line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
let ranges = map
.buffer_snapshot
.bracket_ranges(visible_line_range.clone());
let ranges = map.buffer_snapshot.bracket_ranges(line_range.clone());
if let Some(ranges) = ranges {
let line_range = line_range.start.to_offset(&map.buffer_snapshot)
..line_range.end.to_offset(&map.buffer_snapshot);
@@ -616,131 +590,3 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
let new_row = (point.row() + times as u32).min(map.max_buffer_row());
map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
}
#[cfg(test)]
mod test {
use crate::test::NeovimBackedTestContext;
use indoc::indoc;
#[gpui::test]
async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
let initial_state = indoc! {r"ˇabc
def
paragraph
the second
third and
final"};
// goes down once
cx.set_shared_state(initial_state).await;
cx.simulate_shared_keystrokes(["}"]).await;
cx.assert_shared_state(indoc! {r"abc
def
ˇ
paragraph
the second
third and
final"})
.await;
// goes up once
cx.simulate_shared_keystrokes(["{"]).await;
cx.assert_shared_state(initial_state).await;
// goes down twice
cx.simulate_shared_keystrokes(["2", "}"]).await;
cx.assert_shared_state(indoc! {r"abc
def
paragraph
the second
ˇ
third and
final"})
.await;
// goes down over multiple blanks
cx.simulate_shared_keystrokes(["}"]).await;
cx.assert_shared_state(indoc! {r"abc
def
paragraph
the second
third and
finaˇl"})
.await;
// goes up twice
cx.simulate_shared_keystrokes(["2", "{"]).await;
cx.assert_shared_state(indoc! {r"abc
def
ˇ
paragraph
the second
third and
final"})
.await
}
#[gpui::test]
async fn test_matching(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {r"func ˇ(a string) {
do(something(with<Types>.and_arrays[0, 2]))
}"})
.await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state(indoc! {r"func (a stringˇ) {
do(something(with<Types>.and_arrays[0, 2]))
}"})
.await;
// test it works on the last character of the line
cx.set_shared_state(indoc! {r"func (a string) ˇ{
do(something(with<Types>.and_arrays[0, 2]))
}"})
.await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state(indoc! {r"func (a string) {
do(something(with<Types>.and_arrays[0, 2]))
ˇ}"})
.await;
// test it works on immediate nesting
cx.set_shared_state("ˇ{()}").await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state("{()ˇ}").await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state("ˇ{()}").await;
// test it works on immediate nesting inside braces
cx.set_shared_state("{\n ˇ{()}\n}").await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state("{\n {()ˇ}\n}").await;
// test it jumps to the next paren on a line
cx.set_shared_state("func ˇboop() {\n}").await;
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state("func boop(ˇ) {\n}").await;
}
}

View File

@@ -1,51 +1,29 @@
use editor::scroll::autoscroll::Autoscroll;
use gpui::ViewContext;
use language::{Bias, Point};
use language::Point;
use workspace::Workspace;
use crate::{normal::ChangeCase, state::Mode, Vim};
use crate::{motion::Motion, normal::ChangeCase, Vim};
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
let count = vim.pop_number_operator(cx);
vim.update_active_editor(cx, |editor, cx| {
let mut ranges = Vec::new();
let mut cursor_positions = Vec::new();
let snapshot = editor.buffer().read(cx).snapshot(cx);
for selection in editor.selections.all::<Point>(cx) {
match vim.state.mode {
Mode::Visual { line: true } => {
let start = Point::new(selection.start.row, 0);
let end =
Point::new(selection.end.row, snapshot.line_len(selection.end.row));
ranges.push(start..end);
cursor_positions.push(start..start);
}
Mode::Visual { line: false } => {
ranges.push(selection.start..selection.end);
cursor_positions.push(selection.start..selection.start);
}
Mode::Insert | Mode::Normal => {
let start = selection.start;
let mut end = start;
for _ in 0..count {
end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
}
ranges.push(start..end);
if end.column == snapshot.line_len(end.row) {
end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
}
cursor_positions.push(end..end)
}
}
}
editor.set_clip_at_line_ends(false, cx);
editor.transact(cx, |editor, cx| {
for range in ranges.into_iter().rev() {
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
if selection.start == selection.end {
Motion::Right.expand_selection(map, selection, count, true);
}
})
});
let selections = editor.selections.all::<Point>(cx);
for selection in selections.into_iter().rev() {
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor.buffer().update(cx, |buffer, cx| {
let range = selection.start..selection.end;
let text = snapshot
.text_for_range(range.start..range.end)
.text_for_range(selection.start..selection.end)
.flat_map(|s| s.chars())
.flat_map(|c| {
if c.is_lowercase() {
@@ -59,46 +37,28 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
buffer.edit([(range, text)], None, cx)
})
}
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(cursor_positions)
})
});
editor.set_clip_at_line_ends(true, cx);
});
vim.switch_mode(Mode::Normal, true, cx)
})
}
#[cfg(test)]
mod test {
use crate::{state::Mode, test::NeovimBackedTestContext};
use crate::{state::Mode, test::VimTestContext};
use indoc::indoc;
#[gpui::test]
async fn test_change_case(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("ˇabC\n").await;
cx.simulate_shared_keystrokes(["~"]).await;
cx.assert_shared_state("AˇbC\n").await;
cx.simulate_shared_keystrokes(["2", "~"]).await;
cx.assert_shared_state("ABˇc\n").await;
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(indoc! {"ˇabC\n"}, Mode::Normal);
cx.simulate_keystrokes(["~"]);
cx.assert_editor_state("AˇbC\n");
cx.simulate_keystrokes(["2", "~"]);
cx.assert_editor_state("ABcˇ\n");
// works in visual mode
cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
cx.simulate_shared_keystrokes(["~"]).await;
cx.assert_shared_state("a😀CˇDé1*F\n").await;
// works with multibyte characters
cx.simulate_shared_keystrokes(["~"]).await;
cx.set_shared_state("aˇC😀é1*F\n").await;
cx.simulate_shared_keystrokes(["4", "~"]).await;
cx.assert_shared_state("ac😀É1ˇ*F\n").await;
// works with line selections
cx.set_shared_state("abˇC\n").await;
cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
cx.assert_shared_state("ˇABc\n").await;
// works with multiple cursors (zed only)
cx.set_state("aˇßcdˇe\n", Mode::Normal);
cx.simulate_keystroke("~");
cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
cx.set_state(indoc! {"a😀C«dÉ1*fˇ»\n"}, Mode::Normal);
cx.simulate_keystrokes(["~"]);
cx.assert_editor_state("a😀CDé1*Fˇ\n");
}
}

View File

@@ -4,7 +4,6 @@ mod neovim_connection;
mod vim_binding_test_context;
mod vim_test_context;
use command_palette::CommandPalette;
pub use neovim_backed_binding_test_context::*;
pub use neovim_backed_test_context::*;
pub use vim_binding_test_context::*;
@@ -140,16 +139,3 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
cx.simulate_keystrokes(["shift-v", "down", ">", ">"]);
cx.assert_editor_state("aa\n b«b\n cˇ»c");
}
#[gpui::test]
async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state("aˇbc\n", Mode::Normal);
cx.simulate_keystrokes(["i", "cmd-shift-p"]);
assert!(cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
cx.simulate_keystroke("escape");
assert!(!cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
cx.assert_state("aˇbc\n", Mode::Insert);
}

View File

@@ -1,10 +1,9 @@
use indoc::indoc;
use std::ops::{Deref, DerefMut, Range};
use std::ops::{Deref, DerefMut};
use collections::{HashMap, HashSet};
use gpui::ContextHandle;
use language::OffsetRangeExt;
use util::test::{generate_marked_text, marked_text_offsets};
use util::test::marked_text_offsets;
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
use crate::state::Mode;
@@ -113,43 +112,6 @@ impl<'a> NeovimBackedTestContext<'a> {
context_handle
}
pub async fn assert_shared_state(&mut self, marked_text: &str) {
let neovim = self.neovim_state().await;
if neovim != marked_text {
panic!(
indoc! {"Test is incorrect (currently expected != neovim state)
# currently expected:
{}
# neovim state:
{}
# zed state:
{}"},
marked_text,
neovim,
self.editor_state(),
)
}
self.assert_editor_state(marked_text)
}
pub async fn neovim_state(&mut self) -> String {
generate_marked_text(
self.neovim.text().await.as_str(),
&vec![self.neovim_selection().await],
true,
)
}
async fn neovim_selection(&mut self) -> Range<usize> {
let mut neovim_selection = self.neovim.selection().await;
// Zed selections adjust themselves to make the end point visually make sense
if neovim_selection.start > neovim_selection.end {
neovim_selection.start.column += 1;
}
neovim_selection.to_offset(&self.buffer_snapshot())
}
pub async fn assert_state_matches(&mut self) {
assert_eq!(
self.neovim.text().await,
@@ -158,8 +120,13 @@ impl<'a> NeovimBackedTestContext<'a> {
self.assertion_context()
);
let selections = vec![self.neovim_selection().await];
self.assert_editor_selections(selections);
let mut neovim_selection = self.neovim.selection().await;
// Zed selections adjust themselves to make the end point visually make sense
if neovim_selection.start > neovim_selection.end {
neovim_selection.start.column += 1;
}
let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
self.assert_editor_selections(vec![neovim_selection]);
if let Some(neovim_mode) = self.neovim.mode().await {
assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);

View File

@@ -11,6 +11,8 @@ use gpui::keymap_matcher::Keystroke;
use language::Point;
#[cfg(feature = "neovim")]
use lazy_static::lazy_static;
#[cfg(feature = "neovim")]
use nvim_rs::{
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
@@ -30,7 +32,9 @@ use collections::VecDeque;
// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock
// to ensure we are only constructing one neovim connection at a time.
#[cfg(feature = "neovim")]
static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
lazy_static! {
static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum NeovimData {
@@ -167,25 +171,15 @@ impl NeovimConnection {
.await
.expect("Could not get neovim window");
if !selection.is_empty() {
panic!("Setting neovim state with non empty selection not yet supported");
}
let cursor = selection.start;
nvim_window
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
.await
.expect("Could not set nvim cursor position");
if !selection.is_empty() {
self.nvim
.input("v")
.await
.expect("could not enter visual mode");
let cursor = selection.end;
nvim_window
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
.await
.expect("Could not set nvim cursor position");
}
if let Some(NeovimData::Get { mode, state }) = self.data.back() {
if *mode == Some(Mode::Normal) && *state == marked_text {
return;

View File

@@ -21,14 +21,12 @@ impl<'a> VimTestContext<'a> {
cx.update(|cx| {
search::init(cx);
crate::init(cx);
command_palette::init(cx);
});
cx.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
});
settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
});

View File

@@ -12,7 +12,7 @@ mod visual;
use anyhow::Result;
use collections::CommandPaletteFilter;
use editor::{Bias, Editor, EditorMode, Event};
use editor::{Bias, Cancel, Editor, EditorMode, Event};
use gpui::{
actions, impl_actions, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle,
WindowContext,
@@ -64,6 +64,22 @@ pub fn init(cx: &mut AppContext) {
Vim::update(cx, |vim, cx| vim.push_number(n, cx));
});
// Editor Actions
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
// If we are in aren't in normal mode or have an active operator, swap to normal mode
// Otherwise forward cancel on to the editor
let vim = Vim::read(cx);
if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
WindowContext::defer(cx, |cx| {
Vim::update(cx, |state, cx| {
state.switch_mode(Mode::Normal, false, cx);
});
});
} else {
cx.propagate_action();
}
});
cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
Vim::active_editor_input_ignored(" ".into(), cx)
});
@@ -93,7 +109,10 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| {
if let Some(handled_by) = handled_by {
// Keystroke is handled by the vim system, so continue forward
if handled_by.namespace() == "vim" {
// Also short circuit if it is the special cancel action
if handled_by.namespace() == "vim"
|| (handled_by.namespace() == "editor" && handled_by.name() == "Cancel")
{
return true;
}
}

View File

@@ -1,18 +0,0 @@
{"Put":{"state":"ˇabC\n"}}
{"Key":"~"}
{"Get":{"state":"AˇbC\n","mode":"Normal"}}
{"Key":"2"}
{"Key":"~"}
{"Get":{"state":"ABˇc\n","mode":"Normal"}}
{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}}
{"Key":"~"}
{"Get":{"state":"a😀CˇDé1*F\n","mode":"Normal"}}
{"Key":"~"}
{"Put":{"state":"aˇC😀é1*F\n"}}
{"Key":"4"}
{"Key":"~"}
{"Get":{"state":"ac😀É1ˇ*F\n","mode":"Normal"}}
{"Put":{"state":"abˇC\n"}}
{"Key":"shift-v"}
{"Key":"~"}
{"Get":{"state":"ˇABc\n","mode":"Normal"}}

View File

@@ -1,17 +0,0 @@
{"Put":{"state":"func ˇ(a string) {\n do(something(with<Types>.and_arrays[0, 2]))\n}"}}
{"Key":"%"}
{"Get":{"state":"func (a stringˇ) {\n do(something(with<Types>.and_arrays[0, 2]))\n}","mode":"Normal"}}
{"Put":{"state":"func (a string) ˇ{\ndo(something(with<Types>.and_arrays[0, 2]))\n}"}}
{"Key":"%"}
{"Get":{"state":"func (a string) {\ndo(something(with<Types>.and_arrays[0, 2]))\nˇ}","mode":"Normal"}}
{"Put":{"state":"ˇ{()}"}}
{"Key":"%"}
{"Get":{"state":"{()ˇ}","mode":"Normal"}}
{"Key":"%"}
{"Get":{"state":"ˇ{()}","mode":"Normal"}}
{"Put":{"state":"{\n ˇ{()}\n}"}}
{"Key":"%"}
{"Get":{"state":"{\n {()ˇ}\n}","mode":"Normal"}}
{"Put":{"state":"func ˇboop() {\n}"}}
{"Key":"%"}
{"Get":{"state":"func boop(ˇ) {\n}","mode":"Normal"}}

View File

@@ -1,13 +0,0 @@
{"Put":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal"}}
{"Key":"}"}
{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
{"Key":"{"}
{"Get":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
{"Key":"2"}
{"Key":"}"}
{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\nˇ\n\n\nthird and\nfinal","mode":"Normal"}}
{"Key":"}"}
{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinaˇl","mode":"Normal"}}
{"Key":"2"}
{"Key":"{"}
{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}

View File

@@ -27,7 +27,7 @@ use std::{
};
use theme::Theme;
#[derive(Eq, PartialEq, Hash, Debug)]
#[derive(Eq, PartialEq, Hash)]
pub enum ItemEvent {
CloseItem,
UpdateTab,

View File

@@ -2316,7 +2316,6 @@ mod tests {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
crate::init_settings(cx);
Project::init_settings(cx);
});
}

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.95.1"
version = "0.94.2"
publish = false
[lib]
@@ -64,7 +64,6 @@ terminal_view = { path = "../terminal_view" }
theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" }
util = { path = "../util" }
vector_store = { path = "../vector_store" }
vim = { path = "../vim" }
workspace = { path = "../workspace" }
welcome = { path = "../welcome" }

View File

@@ -170,7 +170,6 @@ fn load_queries(name: &str) -> LanguageQueries {
brackets: load_query(name, "/brackets"),
indents: load_query(name, "/indents"),
outline: load_query(name, "/outline"),
embedding: load_query(name, "/embedding"),
injections: load_query(name, "/injections"),
overrides: load_query(name, "/overrides"),
}

View File

@@ -1,56 +0,0 @@
; (internal_module
; "namespace" @context
; name: (_) @name) @item
(enum_declaration
"enum" @context
name: (_) @name) @item
(function_declaration
"async"? @context
"function" @context
name: (_) @name) @item
(interface_declaration
"interface" @context
name: (_) @name) @item
; (program
; (export_statement
; (lexical_declaration
; ["let" "const"] @context
; (variable_declarator
; name: (_) @name) @item)))
(program
(lexical_declaration
["let" "const"] @context
(variable_declarator
name: (_) @name) @item))
(class_declaration
"class" @context
name: (_) @name) @item
(method_definition
[
"get"
"set"
"async"
"*"
"readonly"
"static"
(override_modifier)
(accessibility_modifier)
]* @context
name: (_) @name) @item
; (public_field_definition
; [
; "declare"
; "readonly"
; "abstract"
; "static"
; (accessibility_modifier)
; ]* @context
; name: (_) @name) @item

View File

@@ -1,9 +0,0 @@
(class_definition
"class" @context
name: (identifier) @name
) @item
(function_definition
"async"? @context
"def" @context
name: (_) @name) @item

View File

@@ -1,36 +0,0 @@
(struct_item
(visibility_modifier)? @context
"struct" @context
name: (_) @name) @item
(enum_item
(visibility_modifier)? @context
"enum" @context
name: (_) @name) @item
(impl_item
"impl" @context
trait: (_)? @name
"for"? @context
type: (_) @name) @item
(trait_item
(visibility_modifier)? @context
"trait" @context
name: (_) @name) @item
(function_item
(visibility_modifier)? @context
(function_modifiers)? @context
"fn" @context
name: (_) @name) @item
(function_signature_item
(visibility_modifier)? @context
(function_modifiers)? @context
"fn" @context
name: (_) @name) @item
(macro_definition
. "macro_rules!" @context
name: (_) @name) @item

View File

@@ -1,35 +0,0 @@
(enum_declaration
"enum" @context
name: (_) @name) @item
(function_declaration
"async"? @context
"function" @context
name: (_) @name) @item
(interface_declaration
"interface" @context
name: (_) @name) @item
(program
(lexical_declaration
["let" "const"] @context
(variable_declarator
name: (_) @name) @item))
(class_declaration
"class" @context
name: (_) @name) @item
(method_definition
[
"get"
"set"
"async"
"*"
"readonly"
"static"
(override_modifier)
(accessibility_modifier)
]* @context
name: (_) @name) @item

View File

@@ -1,59 +0,0 @@
; (internal_module
; "namespace" @context
; name: (_) @name) @item
(enum_declaration
"enum" @context
name: (_) @name) @item
; (type_alias_declaration
; "type" @context
; name: (_) @name) @item
(function_declaration
"async"? @context
"function" @context
name: (_) @name) @item
(interface_declaration
"interface" @context
name: (_) @name) @item
; (export_statement
; (lexical_declaration
; ["let" "const"] @context
; (variable_declarator
; name: (_) @name) @item))
(program
(lexical_declaration
["let" "const"] @context
(variable_declarator
name: (_) @name) @item))
(class_declaration
"class" @context
name: (_) @name) @item
(method_definition
[
"get"
"set"
"async"
"*"
"readonly"
"static"
(override_modifier)
(accessibility_modifier)
]* @context
name: (_) @name) @item
; (public_field_definition
; [
; "declare"
; "readonly"
; "abstract"
; "static"
; (accessibility_modifier)
; ]* @context
; name: (_) @name) @item

View File

@@ -57,9 +57,8 @@ use staff_mode::StaffMode;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
use zed::{
assets::Assets,
build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
only_instance::{ensure_only_instance, IsOnlyInstance},
assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace,
languages, menus,
};
fn main() {
@@ -67,10 +66,6 @@ fn main() {
init_paths();
init_logger();
if ensure_only_instance() != IsOnlyInstance::Yes {
return;
}
log::info!("========== starting zed ==========");
let mut app = gpui::App::new(Assets).unwrap();
@@ -157,7 +152,6 @@ fn main() {
project_panel::init(cx);
diagnostics::init(cx);
search::init(cx);
vector_store::init(fs.clone(), http.clone(), languages.clone(), cx);
vim::init(cx);
terminal_view::init(cx);
copilot::init(http.clone(), node_runtime, cx);

View File

@@ -1,103 +0,0 @@
use std::{
io::{Read, Write},
net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream},
thread,
time::Duration,
};
use util::channel::ReleaseChannel;
const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
const CONNECT_TIMEOUT: Duration = Duration::from_millis(10);
const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35);
const SEND_TIMEOUT: Duration = Duration::from_millis(20);
fn address() -> SocketAddr {
let port = match *util::channel::RELEASE_CHANNEL {
ReleaseChannel::Dev => 43737,
ReleaseChannel::Preview => 43738,
ReleaseChannel::Stable => 43739,
};
SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port))
}
fn instance_handshake() -> &'static str {
match *util::channel::RELEASE_CHANNEL {
ReleaseChannel::Dev => "Zed Editor Dev Instance Running",
ReleaseChannel::Preview => "Zed Editor Preview Instance Running",
ReleaseChannel::Stable => "Zed Editor Stable Instance Running",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsOnlyInstance {
Yes,
No,
}
pub fn ensure_only_instance() -> IsOnlyInstance {
if *db::ZED_STATELESS {
return IsOnlyInstance::Yes;
}
if check_got_handshake() {
return IsOnlyInstance::No;
}
let listener = match TcpListener::bind(address()) {
Ok(listener) => listener,
Err(err) => {
log::warn!("Error binding to single instance port: {err}");
if check_got_handshake() {
return IsOnlyInstance::No;
}
// Avoid failing to start when some other application by chance already has
// a claim on the port. This is sub-par as any other instance that gets launched
// will be unable to communicate with this instance and will duplicate
log::warn!("Backup handshake request failed, continuing without handshake");
return IsOnlyInstance::Yes;
}
};
thread::spawn(move || {
for stream in listener.incoming() {
let mut stream = match stream {
Ok(stream) => stream,
Err(_) => return,
};
_ = stream.set_nodelay(true);
_ = stream.set_read_timeout(Some(SEND_TIMEOUT));
_ = stream.write_all(instance_handshake().as_bytes());
}
});
IsOnlyInstance::Yes
}
fn check_got_handshake() -> bool {
match TcpStream::connect_timeout(&address(), CONNECT_TIMEOUT) {
Ok(mut stream) => {
let mut buf = vec![0u8; instance_handshake().len()];
stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap();
if let Err(err) = stream.read_exact(&mut buf) {
log::warn!("Connected to single instance port but failed to read: {err}");
return false;
}
if buf == instance_handshake().as_bytes() {
log::info!("Got instance handshake");
return true;
}
log::warn!("Got wrong instance handshake value");
false
}
Err(_) => false,
}
}

View File

@@ -1,7 +1,6 @@
pub mod assets;
pub mod languages;
pub mod menus;
pub mod only_instance;
#[cfg(any(test, feature = "test-support"))]
pub mod test;

View File

@@ -1,55 +0,0 @@
import { Theme, StyleSets } from "../common"
import { interactive } from "../element"
import { InteractiveState } from "../element/interactive"
import { background, foreground } from "../style_tree/components"
interface TabBarButtonOptions {
icon: string
color?: StyleSets
}
type TabBarButtonProps = TabBarButtonOptions & {
state?: Partial<Record<InteractiveState, Partial<TabBarButtonOptions>>>
}
export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) {
const button_spacing = 8
return (
interactive({
base: {
icon: {
color: foreground(theme.middle, color),
asset: icon,
dimensions: {
width: 15,
height: 15,
},
},
container: {
corner_radius: 4,
padding: {
top: 4, bottom: 4, left: 4, right: 4
},
margin: {
left: button_spacing / 2,
right: button_spacing / 2,
},
},
},
state: {
hovered: {
container: {
background: background(theme.middle, color, "hovered"),
}
},
clicked: {
container: {
background: background(theme.middle, color, "pressed"),
}
},
},
})
)
}

View File

@@ -1,133 +1,233 @@
import { text, border, background, foreground, TextStyle } from "./components"
import { Interactive, interactive } from "../element"
import { tab_bar_button } from "../component/tab_bar_button"
import { StyleSets, useTheme } from "../theme"
type RoleCycleButton = TextStyle & {
background?: string
}
// TODO: Replace these with zed types
type RemainingTokens = TextStyle & {
background: string,
margin: { top: number, right: number },
padding: {
right: number,
left: number,
top: number,
bottom: number,
},
corner_radius: number,
}
import { text, border, background, foreground } from "./components"
import { interactive } from "../element"
import { useTheme } from "../theme"
export default function assistant(): any {
const theme = useTheme()
const interactive_role = (color: StyleSets): Interactive<RoleCycleButton> => {
return (
interactive({
base: {
...text(theme.highest, "sans", color, { size: "sm" }),
},
state: {
hovered: {
...text(theme.highest, "sans", color, { size: "sm" }),
background: background(theme.highest, color, "hovered"),
},
clicked: {
...text(theme.highest, "sans", color, { size: "sm" }),
background: background(theme.highest, color, "pressed"),
}
},
})
)
}
const tokens_remaining = (color: StyleSets): RemainingTokens => {
return (
{
...text(theme.highest, "mono", color, { size: "xs" }),
background: background(theme.highest, "on", "default"),
margin: { top: 12, right: 20 },
padding: { right: 4, left: 4, top: 1, bottom: 1 },
corner_radius: 6,
}
)
}
return {
container: {
background: background(theme.highest),
padding: { left: 12 },
},
message_header: {
margin: { bottom: 4, top: 4 },
margin: { bottom: 6, top: 6 },
background: background(theme.highest),
},
hamburger_button: tab_bar_button(theme, {
icon: "icons/hamburger_15.svg",
hamburger_button: interactive({
base: {
icon: {
color: foreground(theme.highest, "variant"),
asset: "icons/hamburger_15.svg",
dimensions: {
width: 15,
height: 15,
},
},
container: {
padding: { left: 12, right: 8.5 },
},
},
state: {
hovered: {
icon: {
color: foreground(theme.highest, "hovered"),
},
},
},
}),
split_button: tab_bar_button(theme, {
icon: "icons/split_message_15.svg",
split_button: interactive({
base: {
icon: {
color: foreground(theme.highest, "variant"),
asset: "icons/split_message_15.svg",
dimensions: {
width: 15,
height: 15,
},
},
container: {
padding: { left: 8.5, right: 8.5 },
},
},
state: {
hovered: {
icon: {
color: foreground(theme.highest, "hovered"),
},
},
},
}),
quote_button: tab_bar_button(theme, {
icon: "icons/radix/quote.svg",
quote_button: interactive({
base: {
icon: {
color: foreground(theme.highest, "variant"),
asset: "icons/quote_15.svg",
dimensions: {
width: 15,
height: 15,
},
},
container: {
padding: { left: 8.5, right: 8.5 },
},
},
state: {
hovered: {
icon: {
color: foreground(theme.highest, "hovered"),
},
},
},
}),
assist_button: tab_bar_button(theme, {
icon: "icons/radix/magic-wand.svg",
assist_button: interactive({
base: {
icon: {
color: foreground(theme.highest, "variant"),
asset: "icons/assist_15.svg",
dimensions: {
width: 15,
height: 15,
},
},
container: {
padding: { left: 8.5, right: 8.5 },
},
},
state: {
hovered: {
icon: {
color: foreground(theme.highest, "hovered"),
},
},
},
}),
zoom_in_button: tab_bar_button(theme, {
icon: "icons/radix/maximize.svg",
zoom_in_button: interactive({
base: {
icon: {
color: foreground(theme.highest, "variant"),
asset: "icons/maximize_8.svg",
dimensions: {
width: 12,
height: 12,
},
},
container: {
padding: { left: 10, right: 10 },
},
},
state: {
hovered: {
icon: {
color: foreground(theme.highest, "hovered"),
},
},
},
}),
zoom_out_button: tab_bar_button(theme, {
icon: "icons/radix/minimize.svg",
zoom_out_button: interactive({
base: {
icon: {
color: foreground(theme.highest, "variant"),
asset: "icons/minimize_8.svg",
dimensions: {
width: 12,
height: 12,
},
},
container: {
padding: { left: 10, right: 10 },
},
},
state: {
hovered: {
icon: {
color: foreground(theme.highest, "hovered"),
},
},
},
}),
plus_button: tab_bar_button(theme, {
icon: "icons/radix/plus.svg",
plus_button: interactive({
base: {
icon: {
color: foreground(theme.highest, "variant"),
asset: "icons/plus_12.svg",
dimensions: {
width: 12,
height: 12,
},
},
container: {
padding: { left: 10, right: 10 },
},
},
state: {
hovered: {
icon: {
color: foreground(theme.highest, "hovered"),
},
},
},
}),
title: {
...text(theme.highest, "sans", "default", { size: "xs" }),
...text(theme.highest, "sans", "default", { size: "sm" }),
},
saved_conversation: {
container: interactive({
base: {
background: background(theme.middle),
background: background(theme.highest, "on"),
padding: { top: 4, bottom: 4 },
border: border(theme.middle, "default", { top: true, overlay: true }),
},
state: {
hovered: {
background: background(theme.middle, "hovered"),
background: background(theme.highest, "on", "hovered"),
},
clicked: {
background: background(theme.middle, "pressed"),
}
},
}),
saved_at: {
margin: { left: 8 },
...text(theme.highest, "sans", "variant", { size: "xs" }),
...text(theme.highest, "sans", "default", { size: "xs" }),
},
title: {
margin: { left: 12 },
margin: { left: 16 },
...text(theme.highest, "sans", "default", {
size: "sm",
weight: "bold",
}),
},
},
user_sender: interactive_role("base"),
assistant_sender: interactive_role("accent"),
system_sender: interactive_role("warning"),
user_sender: {
default: {
...text(theme.highest, "sans", "default", {
size: "sm",
weight: "bold",
}),
},
},
assistant_sender: {
default: {
...text(theme.highest, "sans", "accent", {
size: "sm",
weight: "bold",
}),
},
},
system_sender: {
default: {
...text(theme.highest, "sans", "variant", {
size: "sm",
weight: "bold",
}),
},
},
sent_at: {
margin: { top: 2, left: 8 },
...text(theme.highest, "sans", "variant", { size: "2xs" }),
...text(theme.highest, "sans", "default", { size: "2xs" }),
},
model: interactive({
base: {
background: background(theme.highest),
margin: { left: 12, right: 4, top: 12 },
padding: { right: 4, left: 4, top: 1, bottom: 1 },
background: background(theme.highest, "on"),
margin: { left: 12, right: 12, top: 12 },
padding: 4,
corner_radius: 4,
...text(theme.highest, "sans", "default", { size: "xs" }),
},
@@ -138,9 +238,20 @@ export default function assistant(): any {
},
},
}),
remaining_tokens: tokens_remaining("positive"),
low_remaining_tokens: tokens_remaining("warning"),
no_remaining_tokens: tokens_remaining("negative"),
remaining_tokens: {
background: background(theme.highest, "on"),
margin: { top: 12, right: 24 },
padding: 4,
corner_radius: 4,
...text(theme.highest, "sans", "positive", { size: "xs" }),
},
no_remaining_tokens: {
background: background(theme.highest, "on"),
margin: { top: 12, right: 24 },
padding: 4,
corner_radius: 4,
...text(theme.highest, "sans", "negative", { size: "xs" }),
},
error_icon: {
margin: { left: 8 },
color: foreground(theme.highest, "negative"),
@@ -148,7 +259,7 @@ export default function assistant(): any {
},
api_key_editor: {
background: background(theme.highest, "on"),
corner_radius: 4,
corner_radius: 6,
text: text(theme.highest, "mono", "on"),
placeholder_text: text(theme.highest, "mono", "on", "disabled", {
size: "xs",

View File

@@ -304,7 +304,6 @@ export default function editor(): any {
? with_opacity(theme.ramps.green(0.5).hex(), 0.8)
: with_opacity(theme.ramps.green(0.4).hex(), 0.8),
},
selections: foreground(layer, "accent")
},
composition_mark: {
underline: {

View File

@@ -119,40 +119,14 @@ export default function picker(): any {
right: 8,
},
},
footer: interactive({
base: {
text: text(theme.lowest, "sans", "base", { size: "xs" }),
padding: {
bottom: 4,
left: 12,
right: 12,
top: 4,
},
margin: {
top: 1,
left: 4,
right: 4,
},
corner_radius: 8,
background: with_opacity(
background(theme.lowest, "active"),
0.5
),
footer: {
text: text(theme.lowest, "sans", "variant", { size: "xs" }),
margin: {
top: 1,
left: 8,
right: 8,
},
state: {
hovered: {
background: with_opacity(
background(theme.lowest, "hovered"),
0.5
),
},
clicked: {
background: with_opacity(
background(theme.lowest, "pressed"),
0.5
),
},
}
}),
}
}
}

View File

@@ -84,7 +84,7 @@ function user_menu() {
base: {
corner_radius: 6,
height: button_height,
width: 20,
width: online ? 37 : 24,
padding: {
top: 2,
bottom: 2,
@@ -153,7 +153,6 @@ function user_menu() {
},
}
}
return {
user_menu_button_online: build_button({ online: true }),
user_menu_button_offline: build_button({ online: false }),

View File

@@ -12,17 +12,8 @@ export interface Theme {
name: string
is_light: boolean
/**
* App background, other elements that should sit directly on top of the background.
*/
lowest: Layer
/**
* Panels, tabs, other UI surfaces that sit on top of the background.
*/
middle: Layer
/**
* Editors like code buffers, conversation editors, etc.
*/
highest: Layer
ramps: RampSet