Compare commits

..

53 Commits

Author SHA1 Message Date
Max Brunsfeld
56dbd2e031 zed 0.68.1 2023-01-06 12:04:43 -08:00
Max Brunsfeld
121249c7c4 Merge pull request #2008 from zed-industries/callback-leaks
Fix callback leaks when subscriptions are added and dropped in the same effect cycle
2023-01-06 12:03:52 -08:00
Mikayla Maki
8d0b6a3e58 v0.68.x preview 2023-01-04 11:45:00 -08:00
Julia
1e18480808 Merge pull request #2005 from zed-industries/tsserver-include-completion-detail
Include Typescript completion item `detail` field in completion label
2023-01-03 16:44:10 -05:00
Julia
93a634991b Include Typescript completion item detail field in completion label 2023-01-03 16:37:35 -05:00
Julia
d0ce7b3516 Merge pull request #2003 from zed-industries/correct-ra-name-key-default-settings
Correct default settings' name key for RA in init options example
2023-01-03 13:51:03 -05:00
Julia
b94c265240 Correct default settings' name key for RA in init options example 2023-01-03 13:50:08 -05:00
Julia
6b62ce2aaa Merge pull request #2001 from zed-industries/dissmis-search-button
Add dismiss buffer search button & fix some faulty icon button styling
2023-01-02 11:21:16 -05:00
Julia
2b1118f597 Add dismiss buffer search button & fix some faulty icon button styling
Co-Authored-By: Nate Butler <nate@zed.dev>
2023-01-01 23:50:46 -05:00
Mikayla Maki
eeb21af841 Merge pull request #2000 from zed-industries/fix-line-seperator
Add other line seperators to regex normalization
2022-12-30 18:24:36 -08:00
Mikayla Maki
a5bccecd48 Add other line seperators to regex normalization 2022-12-30 18:18:02 -08:00
Joseph T. Lyons
0f818f2458 Merge pull request #1996 from zed-industries/add-close-clean-items-command
Add close clean items command
2022-12-29 14:12:04 -05:00
Joseph T. Lyons
7187cc8a4c Merge pull request #1994 from zed-industries/add-close-all-items-command
Add close all items command
2022-12-29 14:11:44 -05:00
Joseph Lyons
2bc36600d4 Rename variable 2022-12-29 13:43:56 -05:00
Joseph Lyons
60f29410ca Add close clean items command 2022-12-29 13:28:52 -05:00
Joseph Lyons
ca3c4566dd Add close all items command 2022-12-29 01:43:49 -05:00
Joseph T. Lyons
b6337f59fd Merge pull request #1992 from zed-industries/add-home-and-end-key-support
Add home and end key support
2022-12-26 00:34:37 -05:00
Joseph Lyons
21a0df406f Add home and end key support 2022-12-26 00:24:26 -05:00
Joseph T. Lyons
04e053a216 Merge pull request #1991 from zed-industries/add-actions-for-requesting-features-and-filing-bug-reports
Add actions for requesting features and filing bug reports
2022-12-22 23:17:44 -05:00
Joseph Lyons
41bff3947c Add actions for requesting features and filing bug reports 2022-12-22 23:04:33 -05:00
Joseph T. Lyons
46152c6249 Merge pull request #1990 from zed-industries/add-memory-to-system-specs
Add memory to system specs
2022-12-22 18:16:50 -05:00
Joseph Lyons
f65fda2fa4 Add memory to system specs 2022-12-22 18:10:49 -05:00
Joseph T. Lyons
96ac650465 Merge pull request #1989 from zed-industries/add-command-to-copy-system-information-to-the-clipboard
add command to copy system information to the clipboard
2022-12-22 14:31:23 -05:00
Joseph Lyons
ea16082a42 Factored data into a SystemSpecs struct
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2022-12-22 14:27:32 -05:00
Joseph Lyons
eeb5b03d63 add command to copy system information to the clipboard 2022-12-22 03:43:04 -05:00
Max Brunsfeld
c8b209306e collab 0.4.2 2022-12-19 11:29:22 -08:00
Max Brunsfeld
61c6c825b5 Merge pull request #1980 from zed-industries/following-panics
Fix panics when following
2022-12-19 11:26:28 -08:00
Julia
6f211292b2 Merge pull request #1984 from zed-industries/format-problematic-db-macros
Format problematic DB macros
2022-12-19 11:17:34 -05:00
Julia
c49573dc11 Format problematic DB macros 2022-12-19 11:11:10 -05:00
Julia
de9c58d216 Merge pull request #1983 from zed-industries/multi-buffer-git-gutter
Multi buffer git gutter
2022-12-19 10:53:42 -05:00
Antonio Scandurra
84a860e54d Merge pull request #1982 from zed-industries/fix-rust-analyzer
Update rust-analyzer's `disk_based_diagnostics_progress_token`
2022-12-19 16:33:01 +01:00
Antonio Scandurra
cb60eb8a57 Update rust-analyzer's disk_based_diagnostics_progress_token 2022-12-19 16:27:25 +01:00
Max Brunsfeld
1e02ebbd11 Replicate pending selections separately from other selections
This fixes a panic that would occur when a leader created
a pending selection that overlapped another selection,
because the follower would attempt to treat that pending
selection as non-pending, which would violate the invariant
that selections are sorted and disjoint.
2022-12-17 14:00:53 -08:00
Max Brunsfeld
8c64514570 Add ZED_STATELESS env var, for suppressing persistence
Use this env var in the start-local-collaboration script to make
the behavior more predictable.
2022-12-17 12:03:51 -08:00
Kay Simmons
6fcb3c9020 Merge pull request #1972 from zed-industries/recent-workspace
Recent Project Picker
2022-12-16 15:51:57 -08:00
Kay Simmons
2c47bd4a97 Clear stale projects if they no longer exist 2022-12-16 15:45:17 -08:00
Antonio Scandurra
a5f624203e collab 0.4.1 2022-12-16 12:02:03 +01:00
Antonio Scandurra
98d1b6ec5a Merge pull request #1975 from zed-industries/screen-share-after-reconnect
Prevent screen-sharing from being lost after a reconnection
2022-12-16 12:00:02 +01:00
Antonio Scandurra
457e1046c8 Bump protocol version 2022-12-16 11:48:14 +01:00
Antonio Scandurra
21ab1bb434 Remove unnecessary PeerId parsing code 2022-12-16 11:45:42 +01:00
Antonio Scandurra
aa44de3d16 Fix test ensuring room is left when disconnected from LiveKit 2022-12-16 10:52:32 +01:00
Max Brunsfeld
ad37034960 Identify LiveKit room participants by user id, not peer id
This way, their participant id can remain the same when they reconnect.
2022-12-15 17:19:32 -08:00
Julia
ebd0c5d000 Handle reversed=true for multi-buffer git-hunks-in-range iteration
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-12-15 18:17:32 -05:00
Julia
f88b413f6a Rewrite multi-buffer aware git hunks in range to be more correct
Less ad-hoc state tracking, rely more on values provided by the
underlying data

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-12-15 17:09:09 -05:00
Julia
0dedc1f3a4 Get tests building again 2022-12-15 00:17:28 -05:00
Kay Simmons
81e3b48f37 Add keybinding 2022-12-14 16:14:16 -08:00
Kay Simmons
6da59311d1 Add open recent project to file menu 2022-12-14 16:02:48 -08:00
Kay Simmons
2bc685281c Add recent project picker 2022-12-14 15:59:50 -08:00
Julia
cf72173282 Clamp end of visual git hunk to requested range 2022-12-13 13:58:50 -05:00
Julia
ecd44e6914 Git diff recalc in project diagnostics 2022-12-13 12:35:58 -05:00
Julia
2cd9987b54 Git diff recalc in project search 2022-12-13 12:35:58 -05:00
Julia
7c3dc1e3dc Cleanup 2022-12-13 12:35:58 -05:00
Julia
00b7c78e33 Initial hacky displaying of git gutter in multi-buffers 2022-12-13 12:35:58 -05:00
57 changed files with 2447 additions and 1741 deletions

64
Cargo.lock generated
View File

@@ -1131,7 +1131,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.4.0"
version = "0.4.2"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -2757,6 +2757,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "human_bytes"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39b528196c838e8b3da8b665e08c30958a6f2ede91d79f2ffcd0d4664b9c64eb"
[[package]]
name = "humantime"
version = "2.1.0"
@@ -3755,6 +3761,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "ntapi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@@ -4424,7 +4439,7 @@ source = "git+https://github.com/zed-industries/wezterm?rev=5cd757e5f2eb039ed0c6
dependencies = [
"libc",
"log",
"ntapi",
"ntapi 0.3.7",
"winapi 0.3.9",
]
@@ -4807,6 +4822,24 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "recent_projects"
version = "0.1.0"
dependencies = [
"db",
"editor",
"fuzzy",
"gpui",
"language",
"ordered-float",
"picker",
"postage",
"settings",
"smol",
"text",
"workspace",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
@@ -6201,6 +6234,21 @@ dependencies = [
"libc",
]
[[package]]
name = "sysinfo"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccb297c0afb439440834b4bcf02c5c9da8ec2e808e70f36b0d8e815ff403bd24"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys",
"libc",
"ntapi 0.4.0",
"once_cell",
"rayon",
"winapi 0.3.9",
]
[[package]]
name = "system-interface"
version = "0.20.0"
@@ -7183,6 +7231,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]]
name = "usvg"
version = "0.14.1"
@@ -8130,7 +8184,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.67.2"
version = "0.68.1"
dependencies = [
"activity_indicator",
"anyhow",
@@ -8162,6 +8216,7 @@ dependencies = [
"fuzzy",
"go_to_line",
"gpui",
"human_bytes",
"ignore",
"image",
"indexmap",
@@ -8181,6 +8236,7 @@ dependencies = [
"project_panel",
"project_symbols",
"rand 0.8.5",
"recent_projects",
"regex",
"rpc",
"rsa",
@@ -8194,6 +8250,7 @@ dependencies = [
"smallvec",
"smol",
"sum_tree",
"sysinfo",
"tempdir",
"terminal_view",
"text",
@@ -8222,6 +8279,7 @@ dependencies = [
"tree-sitter-typescript",
"unindent",
"url",
"urlencoding",
"util",
"vim",
"workspace",

View File

@@ -40,6 +40,7 @@ members = [
"crates/project",
"crates/project_panel",
"crates/project_symbols",
"crates/recent_projects",
"crates/rope",
"crates/rpc",
"crates/search",

View File

@@ -20,8 +20,10 @@
"alt-cmd-left": "pane::ActivatePrevItem",
"alt-cmd-right": "pane::ActivateNextItem",
"cmd-w": "pane::CloseActiveItem",
"cmd-shift-w": "workspace::CloseWindow",
"alt-cmd-t": "pane::CloseInactiveItems",
"cmd-k u": "pane::CloseCleanItems",
"cmd-k cmd-w": "pane::CloseAllItems",
"cmd-shift-w": "workspace::CloseWindow",
"cmd-s": "workspace::Save",
"cmd-shift-s": "workspace::SaveAs",
"cmd-=": "zed::IncreaseBufferFontSize",
@@ -36,6 +38,7 @@
"cmd-n": "workspace::NewFile",
"cmd-shift-n": "workspace::NewWindow",
"cmd-o": "workspace::Open",
"alt-cmd-o": "recent_projects::Toggle",
"ctrl-`": "workspace::NewTerminal"
}
},
@@ -66,9 +69,11 @@
"up": "editor::MoveUp",
"pageup": "editor::PageUp",
"shift-pageup": "editor::MovePageUp",
"home": "editor::MoveToBeginningOfLine",
"down": "editor::MoveDown",
"pagedown": "editor::PageDown",
"shift-pagedown": "editor::MovePageDown",
"end": "editor::MoveToEndOfLine",
"left": "editor::MoveLeft",
"right": "editor::MoveRight",
"ctrl-p": "editor::MoveUp",
@@ -109,6 +114,12 @@
"stop_at_soft_wraps": true
}
],
"shift-home": [
"editor::SelectToBeginningOfLine",
{
"stop_at_soft_wraps": true
}
],
"ctrl-shift-a": [
"editor::SelectToBeginningOfLine",
{
@@ -121,6 +132,12 @@
"stop_at_soft_wraps": true
}
],
"shift-end": [
"editor::SelectToEndOfLine",
{
"stop_at_soft_wraps": true
}
],
"ctrl-shift-e": [
"editor::SelectToEndOfLine",
{

View File

@@ -221,7 +221,7 @@
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust_analyzer": {
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {

View File

@@ -39,6 +39,7 @@ pub struct LocalParticipant {
#[derive(Clone, Debug)]
pub struct RemoteParticipant {
pub user: Arc<User>,
pub peer_id: proto::PeerId,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
pub tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,

View File

@@ -3,7 +3,10 @@ use crate::{
IncomingCall,
};
use anyhow::{anyhow, Result};
use client::{proto, Client, TypedEnvelope, User, UserStore};
use client::{
proto::{self, PeerId},
Client, TypedEnvelope, User, UserStore,
};
use collections::{BTreeMap, HashSet};
use futures::{FutureExt, StreamExt};
use gpui::{
@@ -41,7 +44,7 @@ pub struct Room {
live_kit: Option<LiveKitRoom>,
status: RoomStatus,
local_participant: LocalParticipant,
remote_participants: BTreeMap<proto::PeerId, RemoteParticipant>,
remote_participants: BTreeMap<u64, RemoteParticipant>,
pending_participants: Vec<Arc<User>>,
participant_user_ids: HashSet<u64>,
pending_call_count: usize,
@@ -349,10 +352,16 @@ impl Room {
&self.local_participant
}
pub fn remote_participants(&self) -> &BTreeMap<proto::PeerId, RemoteParticipant> {
pub fn remote_participants(&self) -> &BTreeMap<u64, RemoteParticipant> {
&self.remote_participants
}
pub fn remote_participant_for_peer_id(&self, peer_id: PeerId) -> Option<&RemoteParticipant> {
self.remote_participants
.values()
.find(|p| p.peer_id == peer_id)
}
pub fn pending_participants(&self) -> &[Arc<User>] {
&self.pending_participants
}
@@ -417,15 +426,13 @@ impl Room {
}
if let Some(participants) = remote_participants.log_err() {
let mut participant_peer_ids = HashSet::default();
for (participant, user) in room.participants.into_iter().zip(participants) {
let Some(peer_id) = participant.peer_id else { continue };
this.participant_user_ids.insert(participant.user_id);
participant_peer_ids.insert(peer_id);
let old_projects = this
.remote_participants
.get(&peer_id)
.get(&participant.user_id)
.into_iter()
.flat_map(|existing| &existing.projects)
.map(|project| project.id)
@@ -454,9 +461,11 @@ impl Room {
let location = ParticipantLocation::from_proto(participant.location)
.unwrap_or(ParticipantLocation::External);
if let Some(remote_participant) = this.remote_participants.get_mut(&peer_id)
if let Some(remote_participant) =
this.remote_participants.get_mut(&participant.user_id)
{
remote_participant.projects = participant.projects;
remote_participant.peer_id = peer_id;
if location != remote_participant.location {
remote_participant.location = location;
cx.emit(Event::ParticipantLocationChanged {
@@ -465,9 +474,10 @@ impl Room {
}
} else {
this.remote_participants.insert(
peer_id,
participant.user_id,
RemoteParticipant {
user: user.clone(),
peer_id,
projects: participant.projects,
location,
tracks: Default::default(),
@@ -488,8 +498,8 @@ impl Room {
}
}
this.remote_participants.retain(|peer_id, participant| {
if participant_peer_ids.contains(peer_id) {
this.remote_participants.retain(|user_id, participant| {
if this.participant_user_ids.contains(user_id) {
true
} else {
for project in &participant.projects {
@@ -531,11 +541,11 @@ impl Room {
) -> Result<()> {
match change {
RemoteVideoTrackUpdate::Subscribed(track) => {
let peer_id = track.publisher_id().parse()?;
let user_id = track.publisher_id().parse()?;
let track_id = track.sid().to_string();
let participant = self
.remote_participants
.get_mut(&peer_id)
.get_mut(&user_id)
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
participant.tracks.insert(
track_id.clone(),
@@ -544,21 +554,21 @@ impl Room {
}),
);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: peer_id,
participant_id: participant.peer_id,
});
}
RemoteVideoTrackUpdate::Unsubscribed {
publisher_id,
track_id,
} => {
let peer_id = publisher_id.parse()?;
let user_id = publisher_id.parse()?;
let participant = self
.remote_participants
.get_mut(&peer_id)
.get_mut(&user_id)
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
participant.tracks.remove(&track_id);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: peer_id,
participant_id: participant.peer_id,
});
}
}

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.4.0"
version = "0.4.2"
[[bin]]
name = "collab"

View File

@@ -199,7 +199,7 @@ async fn test_basic_calls(
assert_eq!(participant_id, client_a.peer_id().unwrap());
room_b.read_with(cx_b, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.peer_id().unwrap()]
room.remote_participants()[&client_a.user_id().unwrap()]
.tracks
.len(),
1
@@ -492,7 +492,7 @@ async fn test_client_disconnecting_from_room(
// to automatically leave the room.
server
.test_live_kit_server
.disconnect_client(client_b.peer_id().unwrap().to_string())
.disconnect_client(client_b.user_id().unwrap().to_string())
.await;
deterministic.run_until_parked();
active_call_a.update(cx_a, |call, _| assert!(call.room().is_none()));
@@ -1817,7 +1817,7 @@ async fn test_git_diff_base_change(
buffer_local_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -1837,7 +1837,7 @@ async fn test_git_diff_base_change(
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -1861,7 +1861,7 @@ async fn test_git_diff_base_change(
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -1872,7 +1872,7 @@ async fn test_git_diff_base_change(
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -1915,7 +1915,7 @@ async fn test_git_diff_base_change(
buffer_local_b.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -1935,7 +1935,7 @@ async fn test_git_diff_base_change(
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
@@ -1963,12 +1963,12 @@ async fn test_git_diff_base_change(
"{:?}",
buffer
.snapshot()
.git_diff_hunks_in_range(0..4, false)
.git_diff_hunks_in_row_range(0..4, false)
.collect::<Vec<_>>()
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
@@ -1979,7 +1979,7 @@ async fn test_git_diff_base_change(
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4, false),
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],

View File

@@ -850,7 +850,7 @@ async fn create_room(
.trace_err()
{
if let Some(token) = live_kit
.room_token(&live_kit_room, &session.connection_id.to_string())
.room_token(&live_kit_room, &session.user_id.to_string())
.trace_err()
{
Some(proto::LiveKitConnectionInfo {
@@ -918,7 +918,7 @@ async fn join_room(
let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() {
if let Some(token) = live_kit
.room_token(&room.live_kit_room, &session.connection_id.to_string())
.room_token(&room.live_kit_room, &session.user_id.to_string())
.trace_err()
{
Some(proto::LiveKitConnectionInfo {
@@ -2066,7 +2066,7 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
if let Some(live_kit) = session.live_kit_client.as_ref() {
live_kit
.remove_participant(live_kit_room.clone(), session.connection_id.to_string())
.remove_participant(live_kit_room.clone(), session.user_id.to_string())
.await
.trace_err();

View File

@@ -342,24 +342,27 @@ impl CollabTitlebarItem {
let mut participants = room
.read(cx)
.remote_participants()
.iter()
.map(|(peer_id, collaborator)| (*peer_id, collaborator.clone()))
.values()
.cloned()
.collect::<Vec<_>>();
participants
.sort_by_key(|(peer_id, _)| Some(project.collaborators().get(peer_id)?.replica_id));
participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
participants
.into_iter()
.filter_map(|(peer_id, participant)| {
.filter_map(|participant| {
let project = workspace.read(cx).project().read(cx);
let replica_id = project
.collaborators()
.get(&peer_id)
.get(&participant.peer_id)
.map(|collaborator| collaborator.replica_id);
let user = participant.user.clone();
Some(self.render_avatar(
&user,
replica_id,
Some((peer_id, &user.github_login, participant.location)),
Some((
participant.peer_id,
&user.github_login,
participant.location,
)),
workspace,
theme,
cx,

View File

@@ -73,7 +73,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(peer_id, _)| *peer_id)
.map(|(_, p)| p.peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace

View File

@@ -461,15 +461,13 @@ impl ContactList {
// Populate remote participants.
self.match_candidates.clear();
self.match_candidates
.extend(
room.remote_participants()
.iter()
.map(|(peer_id, participant)| StringMatchCandidate {
id: peer_id.as_u64() as usize,
string: participant.user.github_login.clone(),
char_bag: participant.user.github_login.chars().collect(),
}),
);
.extend(room.remote_participants().iter().map(|(_, participant)| {
StringMatchCandidate {
id: participant.user.id as usize,
string: participant.user.github_login.clone(),
char_bag: participant.user.github_login.chars().collect(),
}
}));
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
@@ -479,8 +477,8 @@ impl ContactList {
executor.clone(),
));
for mat in matches {
let peer_id = PeerId::from_u64(mat.candidate_id as u64);
let participant = &room.remote_participants()[&peer_id];
let user_id = mat.candidate_id as u64;
let participant = &room.remote_participants()[&user_id];
participant_entries.push(ContactEntry::CallParticipant {
user: participant.user.clone(),
is_pending: false,
@@ -496,7 +494,7 @@ impl ContactList {
}
if !participant.tracks.is_empty() {
participant_entries.push(ContactEntry::ParticipantScreen {
peer_id,
peer_id: participant.peer_id,
is_last: true,
});
}

View File

@@ -20,8 +20,8 @@ use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use util::{async_iife, ResultExt};
use util::channel::ReleaseChannel;
use util::{async_iife, ResultExt};
const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
PRAGMA foreign_keys=TRUE;
@@ -39,16 +39,24 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
const DB_FILE_NAME: &'static str = "db.sqlite";
lazy_static::lazy_static! {
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);
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
}
/// 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
/// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created.
/// In either case, static variables are set so that the user can be notified.
pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &ReleaseChannel) -> ThreadSafeConnection<M> {
pub async fn open_db<M: Migrator + 'static>(
db_dir: &Path,
release_channel: &ReleaseChannel,
) -> ThreadSafeConnection<M> {
if *ZED_STATELESS {
return open_fallback_db().await;
}
let release_channel_name = release_channel.dev_name();
let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
@@ -64,11 +72,11 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
//
// Basically: Don't ever push invalid migrations to stable or everyone will have
// a bad time.
// If no db folder, create one at 0-{channel}
create_dir_all(&main_db_dir).context("Could not create db directory")?;
let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
// Optimistically open databases in parallel
if !DB_FILE_OPERATIONS.is_locked() {
// Try building a connection
@@ -76,7 +84,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
return Ok(connection)
};
}
// Take a lock in the failure case so that we move the db once per process instead
// of potentially multiple times from different threads. This shouldn't happen in the
// normal path
@@ -84,12 +92,12 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
if let Some(connection) = open_main_db(&db_path).await {
return Ok(connection)
};
let backup_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System clock is set before the unix timestamp, Zed does not support this region of spacetime")
.as_millis();
// If failed, move 0-{channel} to {current unix timestamp}-{channel}
let backup_db_dir = db_dir.join(Path::new(&format!(
"{}-{}",
@@ -105,7 +113,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
let mut guard = BACKUP_DB_PATH.write();
*guard = Some(backup_db_dir);
}
// Create a new 0-{channel}
create_dir_all(&main_db_dir).context("Should be able to create the database directory")?;
let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
@@ -117,10 +125,10 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
if let Some(connection) = connection {
return connection;
}
// Set another static ref so that we can escalate the notification
ALL_FILE_DB_FAILED.store(true, Ordering::Release);
// If still failed, create an in memory db with a known name
open_fallback_db().await
}
@@ -174,15 +182,15 @@ macro_rules! define_connection {
&self.0
}
}
impl $crate::sqlez::domain::Domain for $t {
fn name() -> &'static str {
stringify!($t)
}
fn migrations() -> &'static [&'static str] {
$migrations
}
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -205,15 +213,15 @@ macro_rules! define_connection {
&self.0
}
}
impl $crate::sqlez::domain::Domain for $t {
fn name() -> &'static str {
stringify!($t)
}
fn migrations() -> &'static [&'static str] {
$migrations
}
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -232,134 +240,157 @@ macro_rules! define_connection {
mod tests {
use std::{fs, thread};
use sqlez::{domain::Domain, connection::Connection};
use sqlez::{connection::Connection, domain::Domain};
use sqlez_macros::sql;
use tempdir::TempDir;
use crate::{open_db, DB_FILE_NAME};
// Test bad migration panics
#[gpui::test]
#[should_panic]
async fn test_bad_migration_panics() {
enum BadDB {}
impl Domain for BadDB {
fn name() -> &'static str {
"db_tests"
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);),
&[
sql!(CREATE TABLE test(value);),
// failure because test already exists
sql!(CREATE TABLE test(value);)]
sql!(CREATE TABLE test(value);),
]
}
}
let tempdir = TempDir::new("DbTests").unwrap();
let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
}
/// Test that DB exists but corrupted (causing recreate)
#[gpui::test]
async fn test_db_corruption() {
enum CorruptedDB {}
impl Domain for CorruptedDB {
fn name() -> &'static str {
"db_tests"
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);)]
}
}
enum GoodDB {}
impl Domain for GoodDB {
fn name() -> &'static str {
"db_tests" //Notice same name
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
}
let tempdir = TempDir::new("DbTests").unwrap();
{
let corrupt_db = open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
let corrupt_db =
open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
assert!(corrupt_db.persistent());
}
let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
assert!(good_db.select_row::<usize>("SELECT * FROM test2").unwrap()().unwrap().is_none());
let mut corrupted_backup_dir = fs::read_dir(
tempdir.path()
).unwrap().find(|entry| {
!entry.as_ref().unwrap().file_name().to_str().unwrap().starts_with("0")
}
).unwrap().unwrap().path();
assert!(
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
.unwrap()
.is_none()
);
let mut corrupted_backup_dir = fs::read_dir(tempdir.path())
.unwrap()
.find(|entry| {
!entry
.as_ref()
.unwrap()
.file_name()
.to_str()
.unwrap()
.starts_with("0")
})
.unwrap()
.unwrap()
.path();
corrupted_backup_dir.push(DB_FILE_NAME);
dbg!(&corrupted_backup_dir);
let backup = Connection::open_file(&corrupted_backup_dir.to_string_lossy());
assert!(backup.select_row::<usize>("SELECT * FROM test").unwrap()().unwrap().is_none());
assert!(backup.select_row::<usize>("SELECT * FROM test").unwrap()()
.unwrap()
.is_none());
}
/// Test that DB exists but corrupted (causing recreate)
#[gpui::test]
async fn test_simultaneous_db_corruption() {
enum CorruptedDB {}
impl Domain for CorruptedDB {
fn name() -> &'static str {
"db_tests"
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);)]
}
}
enum GoodDB {}
impl Domain for GoodDB {
fn name() -> &'static str {
"db_tests" //Notice same name
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
}
let tempdir = TempDir::new("DbTests").unwrap();
{
// Setup the bad database
let corrupt_db = open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
let corrupt_db =
open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
assert!(corrupt_db.persistent());
}
// Try to connect to it a bunch of times at once
let mut guards = vec![];
for _ in 0..10 {
let tmp_path = tempdir.path().to_path_buf();
let guard = thread::spawn(move || {
let good_db = smol::block_on(open_db::<GoodDB>(tmp_path.as_path(), &util::channel::ReleaseChannel::Dev));
assert!(good_db.select_row::<usize>("SELECT * FROM test2").unwrap()().unwrap().is_none());
let good_db = smol::block_on(open_db::<GoodDB>(
tmp_path.as_path(),
&util::channel::ReleaseChannel::Dev,
));
assert!(
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
.unwrap()
.is_none()
);
});
guards.push(guard);
}
for guard in guards.into_iter() {
assert!(guard.join().is_ok());
}
for guard in guards.into_iter() {
assert!(guard.join().is_ok());
}
}
}

View File

@@ -80,7 +80,7 @@ macro_rules! query {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.select::<$return_type>(sql_stmt)?(())
self.select::<$return_type>(sql_stmt)?()
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),
@@ -95,7 +95,7 @@ macro_rules! query {
self.write(|connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.select::<$return_type>(sql_stmt)?(())
connection.select::<$return_type>(sql_stmt)?()
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),

View File

@@ -575,6 +575,15 @@ impl Item for ProjectDiagnosticsEditor {
unreachable!()
}
fn git_diff_recalc(
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.editor
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
}
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
Editor::to_item_events(event)
}

View File

@@ -5453,11 +5453,17 @@ impl Editor {
pub fn set_selections_from_remote(
&mut self,
selections: Vec<Selection<Anchor>>,
pending_selection: Option<Selection<Anchor>>,
cx: &mut ViewContext<Self>,
) {
let old_cursor_position = self.selections.newest_anchor().head();
self.selections.change_with(cx, |s| {
s.select_anchors(selections);
if let Some(pending_selection) = pending_selection {
s.set_pending(pending_selection, SelectMode::Character);
} else {
s.clear_pending();
}
});
self.selections_did_change(false, &old_cursor_position, cx);
}

View File

@@ -130,13 +130,17 @@ impl FollowableItem for Editor {
.ok_or_else(|| anyhow!("invalid selection"))
})
.collect::<Result<Vec<_>>>()?;
let pending_selection = state
.pending_selection
.map(|selection| deserialize_selection(&buffer, selection))
.flatten();
let scroll_top_anchor = state
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
drop(buffer);
if !selections.is_empty() {
editor.set_selections_from_remote(selections, cx);
if !selections.is_empty() || pending_selection.is_some() {
editor.set_selections_from_remote(selections, pending_selection, cx);
}
if let Some(scroll_top_anchor) = scroll_top_anchor {
@@ -216,6 +220,11 @@ impl FollowableItem for Editor {
.iter()
.map(serialize_selection)
.collect(),
pending_selection: self
.selections
.pending_anchor()
.as_ref()
.map(serialize_selection),
}))
}
@@ -269,9 +278,13 @@ impl FollowableItem for Editor {
.selections
.disjoint_anchors()
.iter()
.chain(self.selections.pending_anchor().as_ref())
.map(serialize_selection)
.collect();
update.pending_selection = self
.selections
.pending_anchor()
.as_ref()
.map(serialize_selection);
true
}
_ => false,
@@ -307,6 +320,10 @@ impl FollowableItem for Editor {
.into_iter()
.filter_map(|selection| deserialize_selection(&multibuffer, selection))
.collect::<Vec<_>>();
let pending_selection = message
.pending_selection
.and_then(|selection| deserialize_selection(&multibuffer, selection));
let scroll_top_anchor = message
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
@@ -361,8 +378,8 @@ impl FollowableItem for Editor {
multibuffer.remove_excerpts(removals, cx);
});
if !selections.is_empty() {
this.set_selections_from_remote(selections, cx);
if !selections.is_empty() || pending_selection.is_some() {
this.set_selections_from_remote(selections, pending_selection, cx);
this.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(anchor) = scroll_top_anchor {
this.set_scroll_anchor_remote(ScrollAnchor {

View File

@@ -2710,11 +2710,73 @@ impl MultiBufferSnapshot {
row_range: Range<u32>,
reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
self.as_singleton()
.into_iter()
.flat_map(move |(_, _, buffer)| {
buffer.git_diff_hunks_in_range(row_range.clone(), reversed)
})
let mut cursor = self.excerpts.cursor::<Point>();
if reversed {
cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
if cursor.item().is_none() {
cursor.prev(&());
}
} else {
cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
}
std::iter::from_fn(move || {
let excerpt = cursor.item()?;
let multibuffer_start = *cursor.start();
let multibuffer_end = multibuffer_start + excerpt.text_summary.lines;
if multibuffer_start.row >= row_range.end {
return None;
}
let mut buffer_start = excerpt.range.context.start;
let mut buffer_end = excerpt.range.context.end;
let excerpt_start_point = buffer_start.to_point(&excerpt.buffer);
let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines;
if row_range.start > multibuffer_start.row {
let buffer_start_point =
excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0);
buffer_start = excerpt.buffer.anchor_before(buffer_start_point);
}
if row_range.end < multibuffer_end.row {
let buffer_end_point =
excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0);
buffer_end = excerpt.buffer.anchor_before(buffer_end_point);
}
let buffer_hunks = excerpt
.buffer
.git_diff_hunks_intersecting_range(buffer_start..buffer_end, reversed)
.filter_map(move |hunk| {
let start = multibuffer_start.row
+ hunk
.buffer_range
.start
.saturating_sub(excerpt_start_point.row);
let end = multibuffer_start.row
+ hunk
.buffer_range
.end
.min(excerpt_end_point.row + 1)
.saturating_sub(excerpt_start_point.row);
Some(DiffHunk {
buffer_range: start..end,
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
})
});
if reversed {
cursor.prev(&());
} else {
cursor.next(&());
}
Some(buffer_hunks)
})
.flatten()
}
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
@@ -3546,11 +3608,12 @@ impl ToPointUtf16 for PointUtf16 {
#[cfg(test)]
mod tests {
use super::*;
use gpui::MutableAppContext;
use gpui::{MutableAppContext, TestAppContext};
use language::{Buffer, Rope};
use rand::prelude::*;
use settings::Settings;
use std::{env, rc::Rc};
use unindent::Unindent;
use util::test::sample_text;
@@ -4168,6 +4231,178 @@ mod tests {
);
}
#[gpui::test]
async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
use git::diff::DiffHunkStatus;
// buffer has two modified hunks with two rows each
let buffer_1 = cx.add_model(|cx| {
let mut buffer = Buffer::new(
0,
"
1.zero
1.ONE
1.TWO
1.three
1.FOUR
1.FIVE
1.six
"
.unindent(),
cx,
);
buffer.set_diff_base(
Some(
"
1.zero
1.one
1.two
1.three
1.four
1.five
1.six
"
.unindent(),
),
cx,
);
buffer
});
// buffer has a deletion hunk and an insertion hunk
let buffer_2 = cx.add_model(|cx| {
let mut buffer = Buffer::new(
0,
"
2.zero
2.one
2.two
2.three
2.four
2.five
2.six
"
.unindent(),
cx,
);
buffer.set_diff_base(
Some(
"
2.zero
2.one
2.one-and-a-half
2.two
2.three
2.four
2.six
"
.unindent(),
),
cx,
);
buffer
});
cx.foreground().run_until_parked();
let multibuffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0);
multibuffer.push_excerpts(
buffer_1.clone(),
[
// excerpt ends in the middle of a modified hunk
ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 5),
primary: Default::default(),
},
// excerpt begins in the middle of a modified hunk
ExcerptRange {
context: Point::new(5, 0)..Point::new(6, 5),
primary: Default::default(),
},
],
cx,
);
multibuffer.push_excerpts(
buffer_2.clone(),
[
// excerpt ends at a deletion
ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 5),
primary: Default::default(),
},
// excerpt starts at a deletion
ExcerptRange {
context: Point::new(2, 0)..Point::new(2, 5),
primary: Default::default(),
},
// excerpt fully contains a deletion hunk
ExcerptRange {
context: Point::new(1, 0)..Point::new(2, 5),
primary: Default::default(),
},
// excerpt fully contains an insertion hunk
ExcerptRange {
context: Point::new(4, 0)..Point::new(6, 5),
primary: Default::default(),
},
],
cx,
);
multibuffer
});
let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
assert_eq!(
snapshot.text(),
"
1.zero
1.ONE
1.FIVE
1.six
2.zero
2.one
2.two
2.one
2.two
2.four
2.five
2.six"
.unindent()
);
let expected = [
(DiffHunkStatus::Modified, 1..2),
(DiffHunkStatus::Modified, 2..3),
//TODO: Define better when and where removed hunks show up at range extremities
(DiffHunkStatus::Removed, 6..6),
(DiffHunkStatus::Removed, 8..8),
(DiffHunkStatus::Added, 10..11),
];
assert_eq!(
snapshot
.git_diff_hunks_in_range(0..12, false)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
&expected,
);
assert_eq!(
snapshot
.git_diff_hunks_in_range(0..12, true)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
expected
.iter()
.rev()
.cloned()
.collect::<Vec<_>>()
.as_slice(),
);
}
#[gpui::test(iterations = 100)]
fn test_random_multibuffer(cx: &mut MutableAppContext, mut rng: StdRng) {
let operations = env::var("OPERATIONS")

View File

@@ -254,7 +254,7 @@ impl<'a> EditorTestContext<'a> {
Actual selections:
{}
"},
"},
self.assertion_context(),
expected_marked_text,
actual_marked_text,

View File

@@ -62,11 +62,12 @@ impl View for FileFinder {
impl FileFinder {
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
let path_string = path_match.path.to_string_lossy();
let path = &path_match.path;
let path_string = path.to_string_lossy();
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
let path_positions = path_match.positions.clone();
let file_name = path_match.path.file_name().map_or_else(
let file_name = path.file_name().map_or_else(
|| path_match.path_prefix.to_string(),
|file_name| file_name.to_string_lossy().to_string(),
);
@@ -161,7 +162,7 @@ impl FileFinder {
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
cx.spawn(|this, mut cx| async move {
let matches = fuzzy::match_paths(
let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(),
&query,
false,

View File

@@ -35,7 +35,7 @@ use repository::FakeGitRepositoryState;
use std::sync::Weak;
lazy_static! {
static ref CARRIAGE_RETURNS_REGEX: Regex = Regex::new("\r\n|\r").unwrap();
static ref LINE_SEPERATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
}
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -80,13 +80,13 @@ impl LineEnding {
}
pub fn normalize(text: &mut String) {
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") {
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(text, "\n") {
*text = replaced;
}
}
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") {
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(&text, "\n") {
replaced.into()
} else {
text

View File

@@ -1,794 +1,8 @@
mod char_bag;
use gpui::executor;
use std::{
borrow::Cow,
cmp::{self, Ordering},
path::Path,
sync::atomic::{self, AtomicBool},
sync::Arc,
};
mod matcher;
mod paths;
mod strings;
pub use char_bag::CharBag;
const BASE_DISTANCE_PENALTY: f64 = 0.6;
const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
const MIN_DISTANCE_PENALTY: f64 = 0.2;
pub struct Matcher<'a> {
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
min_score: f64,
match_positions: Vec<usize>,
last_positions: Vec<usize>,
score_matrix: Vec<Option<f64>>,
best_position_matrix: Vec<usize>,
}
trait Match: Ord {
fn score(&self) -> f64;
fn set_positions(&mut self, positions: Vec<usize>);
}
trait MatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool;
fn to_string(&self) -> Cow<'_, str>;
}
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
pub path: &'a Arc<Path>,
pub char_bag: CharBag,
}
#[derive(Clone, Debug)]
pub struct PathMatch {
pub score: f64,
pub positions: Vec<usize>,
pub worktree_id: usize,
pub path: Arc<Path>,
pub path_prefix: Arc<str>,
}
#[derive(Clone, Debug)]
pub struct StringMatchCandidate {
pub id: usize,
pub string: String,
pub char_bag: CharBag,
}
pub trait PathMatchCandidateSet<'a>: Send + Sync {
type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
fn id(&self) -> usize;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn prefix(&self) -> Arc<str>;
fn candidates(&'a self, start: usize) -> Self::Candidates;
}
impl Match for PathMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl<'a> MatchCandidate for PathMatchCandidate<'a> {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.path.to_string_lossy()
}
}
impl StringMatchCandidate {
pub fn new(id: usize, string: String) -> Self {
Self {
id,
char_bag: CharBag::from(string.as_str()),
string,
}
}
}
impl<'a> MatchCandidate for &'a StringMatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.string.as_str().into()
}
}
#[derive(Clone, Debug)]
pub struct StringMatch {
pub candidate_id: usize,
pub score: f64,
pub positions: Vec<usize>,
pub string: String,
}
impl PartialEq for StringMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for StringMatch {}
impl PartialOrd for StringMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StringMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.candidate_id.cmp(&other.candidate_id))
}
}
impl PartialEq for PathMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for PathMatch {}
impl PartialOrd for PathMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PathMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
.then_with(|| Arc::as_ptr(&self.path).cmp(&Arc::as_ptr(&other.path)))
}
}
pub async fn match_strings(
candidates: &[StringMatchCandidate],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<StringMatch> {
if candidates.is_empty() || max_results == 0 {
return Default::default();
}
if query.is_empty() {
return candidates
.iter()
.map(|candidate| StringMatch {
candidate_id: candidate.id,
score: 0.,
positions: Default::default(),
string: candidate.string.clone(),
})
.collect();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(candidates.len());
let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results.min(candidates.len())))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
let cancel_flag = &cancel_flag;
scope.spawn(async move {
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
matcher.match_strings(
&candidates[segment_start..segment_end],
results,
cancel_flag,
);
});
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}
pub async fn match_paths<'a, Set: PathMatchCandidateSet<'a>>(
candidate_sets: &'a [Set],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<PathMatch> {
let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
if path_count == 0 {
return Vec::new();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(path_count);
let segment_size = (path_count + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
scope.spawn(async move {
let segment_start = segment_idx * segment_size;
let segment_end = segment_start + segment_size;
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
let mut tree_start = 0;
for candidate_set in candidate_sets {
let tree_end = tree_start + candidate_set.len();
if tree_start < segment_end && segment_start < tree_end {
let start = cmp::max(tree_start, segment_start) - tree_start;
let end = cmp::min(tree_end, segment_end) - tree_start;
let candidates = candidate_set.candidates(start).take(end - start);
matcher.match_paths(
candidate_set.id(),
candidate_set.prefix(),
candidates,
results,
cancel_flag,
);
}
if tree_end >= segment_end {
break;
}
tree_start = tree_end;
}
})
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}
impl<'a> Matcher<'a> {
pub fn new(
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
) -> Self {
Self {
query,
lowercase_query,
query_char_bag,
min_score: 0.0,
last_positions: vec![0; query.len()],
match_positions: vec![0; query.len()],
score_matrix: Vec::new(),
best_position_matrix: Vec::new(),
smart_case,
max_results,
}
}
pub fn match_strings(
&mut self,
candidates: &[StringMatchCandidate],
results: &mut Vec<StringMatch>,
cancel_flag: &AtomicBool,
) {
self.match_internal(
&[],
&[],
candidates.iter(),
results,
cancel_flag,
|candidate, score| StringMatch {
candidate_id: candidate.id,
score,
positions: Vec::new(),
string: candidate.string.to_string(),
},
)
}
pub fn match_paths<'c: 'a>(
&mut self,
tree_id: usize,
path_prefix: Arc<str>,
path_entries: impl Iterator<Item = PathMatchCandidate<'c>>,
results: &mut Vec<PathMatch>,
cancel_flag: &AtomicBool,
) {
let prefix = path_prefix.chars().collect::<Vec<_>>();
let lowercase_prefix = prefix
.iter()
.map(|c| c.to_ascii_lowercase())
.collect::<Vec<_>>();
self.match_internal(
&prefix,
&lowercase_prefix,
path_entries,
results,
cancel_flag,
|candidate, score| PathMatch {
score,
worktree_id: tree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: path_prefix.clone(),
},
)
}
fn match_internal<C: MatchCandidate, R, F>(
&mut self,
prefix: &[char],
lowercase_prefix: &[char],
candidates: impl Iterator<Item = C>,
results: &mut Vec<R>,
cancel_flag: &AtomicBool,
build_match: F,
) where
R: Match,
F: Fn(&C, f64) -> R,
{
let mut candidate_chars = Vec::new();
let mut lowercase_candidate_chars = Vec::new();
for candidate in candidates {
if !candidate.has_chars(self.query_char_bag) {
continue;
}
if cancel_flag.load(atomic::Ordering::Relaxed) {
break;
}
candidate_chars.clear();
lowercase_candidate_chars.clear();
for c in candidate.to_string().chars() {
candidate_chars.push(c);
lowercase_candidate_chars.push(c.to_ascii_lowercase());
}
if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) {
continue;
}
let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len());
self.score_matrix.clear();
self.score_matrix.resize(matrix_len, None);
self.best_position_matrix.clear();
self.best_position_matrix.resize(matrix_len, 0);
let score = self.score_match(
&candidate_chars,
&lowercase_candidate_chars,
prefix,
lowercase_prefix,
);
if score > 0.0 {
let mut mat = build_match(&candidate, score);
if let Err(i) = results.binary_search_by(|m| mat.cmp(m)) {
if results.len() < self.max_results {
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
} else if i < results.len() {
results.pop();
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
}
if results.len() == self.max_results {
self.min_score = results.last().unwrap().score();
}
}
}
}
}
fn find_last_positions(
&mut self,
lowercase_prefix: &[char],
lowercase_candidate: &[char],
) -> bool {
let mut lowercase_prefix = lowercase_prefix.iter();
let mut lowercase_candidate = lowercase_candidate.iter();
for (i, char) in self.lowercase_query.iter().enumerate().rev() {
if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
self.last_positions[i] = j + lowercase_prefix.len();
} else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
self.last_positions[i] = j;
} else {
return false;
}
}
true
}
fn score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
) -> f64 {
let score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
0,
0,
self.query.len() as f64,
) * self.query.len() as f64;
if score <= 0.0 {
return 0.0;
}
let path_len = prefix.len() + path.len();
let mut cur_start = 0;
let mut byte_ix = 0;
let mut char_ix = 0;
for i in 0..self.query.len() {
let match_char_ix = self.best_position_matrix[i * path_len + cur_start];
while char_ix < match_char_ix {
let ch = prefix
.get(char_ix)
.or_else(|| path.get(char_ix - prefix.len()))
.unwrap();
byte_ix += ch.len_utf8();
char_ix += 1;
}
cur_start = match_char_ix + 1;
self.match_positions[i] = byte_ix;
}
score
}
#[allow(clippy::too_many_arguments)]
fn recursive_score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
query_idx: usize,
path_idx: usize,
cur_score: f64,
) -> f64 {
if query_idx == self.query.len() {
return 1.0;
}
let path_len = prefix.len() + path.len();
if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] {
return memoized;
}
let mut score = 0.0;
let mut best_position = 0;
let query_char = self.lowercase_query[query_idx];
let limit = self.last_positions[query_idx];
let mut last_slash = 0;
for j in path_idx..=limit {
let path_char = if j < prefix.len() {
lowercase_prefix[j]
} else {
path_cased[j - prefix.len()]
};
let is_path_sep = path_char == '/' || path_char == '\\';
if query_idx == 0 && is_path_sep {
last_slash = j;
}
if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
let curr = if j < prefix.len() {
prefix[j]
} else {
path[j - prefix.len()]
};
let mut char_score = 1.0;
if j > path_idx {
let last = if j - 1 < prefix.len() {
prefix[j - 1]
} else {
path[j - 1 - prefix.len()]
};
if last == '/' {
char_score = 0.9;
} else if (last == '-' || last == '_' || last == ' ' || last.is_numeric())
|| (last.is_lowercase() && curr.is_uppercase())
{
char_score = 0.8;
} else if last == '.' {
char_score = 0.7;
} else if query_idx == 0 {
char_score = BASE_DISTANCE_PENALTY;
} else {
char_score = MIN_DISTANCE_PENALTY.max(
BASE_DISTANCE_PENALTY
- (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY,
);
}
}
// Apply a severe penalty if the case doesn't match.
// This will make the exact matches have higher score than the case-insensitive and the
// path insensitive matches.
if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
char_score *= 0.001;
}
let mut multiplier = char_score;
// Scale the score based on how deep within the path we found the match.
if query_idx == 0 {
multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
}
let mut next_score = 1.0;
if self.min_score > 0.0 {
next_score = cur_score * multiplier;
// Scores only decrease. If we can't pass the previous best, bail
if next_score < self.min_score {
// Ensure that score is non-zero so we use it in the memo table.
if score == 0.0 {
score = 1e-18;
}
continue;
}
}
let new_score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
query_idx + 1,
j + 1,
next_score,
) * multiplier;
if new_score > score {
score = new_score;
best_position = j;
// Optimization: can't score better than 1.
if new_score == 1.0 {
break;
}
}
}
}
if best_position != 0 {
self.best_position_matrix[query_idx * path_len + path_idx] = best_position;
}
self.score_matrix[query_idx * path_len + path_idx] = Some(score);
score
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_get_last_positions() {
let mut query: &[char] = &['d', 'c'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(!result);
query = &['c', 'd'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![2, 4]);
query = &['z', '/', 'z', 'f'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
}
#[test]
fn test_match_path_entries() {
let paths = vec![
"",
"a",
"ab",
"abC",
"abcd",
"alphabravocharlie",
"AlphaBravoCharlie",
"thisisatestdir",
"/////ThisIsATestDir",
"/this/is/a/test/dir",
"/test/tiatd",
];
assert_eq!(
match_query("abc", false, &paths),
vec![
("abC", vec![0, 1, 2]),
("abcd", vec![0, 1, 2]),
("AlphaBravoCharlie", vec![0, 5, 10]),
("alphabravocharlie", vec![4, 5, 10]),
]
);
assert_eq!(
match_query("t/i/a/t/d", false, &paths),
vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
);
assert_eq!(
match_query("tiatd", false, &paths),
vec![
("/test/tiatd", vec![6, 7, 8, 9, 10]),
("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
("thisisatestdir", vec![0, 2, 6, 7, 11]),
]
);
}
#[test]
fn test_match_multibyte_path_entries() {
let paths = vec!["aαbβ/cγ", "αβγδ/bcde", "c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", "/d/🆒/h"];
assert_eq!("1".len(), 7);
assert_eq!(
match_query("bcd", false, &paths),
vec![
("αβγδ/bcde", vec![9, 10, 11]),
("aαbβ/cγ", vec![3, 7, 10]),
]
);
assert_eq!(
match_query("cde", false, &paths),
vec![
("αβγδ/bcde", vec![10, 11, 12]),
("c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", vec![0, 23, 46]),
]
);
}
fn match_query<'a>(
query: &str,
smart_case: bool,
paths: &[&'a str],
) -> Vec<(&'a str, Vec<usize>)> {
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let query_chars = CharBag::from(&lowercase_query[..]);
let path_arcs = paths
.iter()
.map(|path| Arc::from(PathBuf::from(path)))
.collect::<Vec<_>>();
let mut path_entries = Vec::new();
for (i, path) in paths.iter().enumerate() {
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
let char_bag = CharBag::from(lowercase_path.as_slice());
path_entries.push(PathMatchCandidate {
char_bag,
path: path_arcs.get(i).unwrap(),
});
}
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
let cancel_flag = AtomicBool::new(false);
let mut results = Vec::new();
matcher.match_paths(
0,
"".into(),
path_entries.into_iter(),
&mut results,
&cancel_flag,
);
results
.into_iter()
.map(|result| {
(
paths
.iter()
.copied()
.find(|p| result.path.as_ref() == Path::new(p))
.unwrap(),
result.positions,
)
})
.collect()
}
}
pub use paths::{match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet};
pub use strings::{match_strings, StringMatch, StringMatchCandidate};

463
crates/fuzzy/src/matcher.rs Normal file
View File

@@ -0,0 +1,463 @@
use std::{
borrow::Cow,
sync::atomic::{self, AtomicBool},
};
use crate::CharBag;
const BASE_DISTANCE_PENALTY: f64 = 0.6;
const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
const MIN_DISTANCE_PENALTY: f64 = 0.2;
pub struct Matcher<'a> {
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
min_score: f64,
match_positions: Vec<usize>,
last_positions: Vec<usize>,
score_matrix: Vec<Option<f64>>,
best_position_matrix: Vec<usize>,
}
pub trait Match: Ord {
fn score(&self) -> f64;
fn set_positions(&mut self, positions: Vec<usize>);
}
pub trait MatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool;
fn to_string(&self) -> Cow<'_, str>;
}
impl<'a> Matcher<'a> {
pub fn new(
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
) -> Self {
Self {
query,
lowercase_query,
query_char_bag,
min_score: 0.0,
last_positions: vec![0; query.len()],
match_positions: vec![0; query.len()],
score_matrix: Vec::new(),
best_position_matrix: Vec::new(),
smart_case,
max_results,
}
}
pub fn match_candidates<C: MatchCandidate, R, F>(
&mut self,
prefix: &[char],
lowercase_prefix: &[char],
candidates: impl Iterator<Item = C>,
results: &mut Vec<R>,
cancel_flag: &AtomicBool,
build_match: F,
) where
R: Match,
F: Fn(&C, f64) -> R,
{
let mut candidate_chars = Vec::new();
let mut lowercase_candidate_chars = Vec::new();
for candidate in candidates {
if !candidate.has_chars(self.query_char_bag) {
continue;
}
if cancel_flag.load(atomic::Ordering::Relaxed) {
break;
}
candidate_chars.clear();
lowercase_candidate_chars.clear();
for c in candidate.to_string().chars() {
candidate_chars.push(c);
lowercase_candidate_chars.push(c.to_ascii_lowercase());
}
if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) {
continue;
}
let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len());
self.score_matrix.clear();
self.score_matrix.resize(matrix_len, None);
self.best_position_matrix.clear();
self.best_position_matrix.resize(matrix_len, 0);
let score = self.score_match(
&candidate_chars,
&lowercase_candidate_chars,
prefix,
lowercase_prefix,
);
if score > 0.0 {
let mut mat = build_match(&candidate, score);
if let Err(i) = results.binary_search_by(|m| mat.cmp(m)) {
if results.len() < self.max_results {
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
} else if i < results.len() {
results.pop();
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
}
if results.len() == self.max_results {
self.min_score = results.last().unwrap().score();
}
}
}
}
}
fn find_last_positions(
&mut self,
lowercase_prefix: &[char],
lowercase_candidate: &[char],
) -> bool {
let mut lowercase_prefix = lowercase_prefix.iter();
let mut lowercase_candidate = lowercase_candidate.iter();
for (i, char) in self.lowercase_query.iter().enumerate().rev() {
if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
self.last_positions[i] = j + lowercase_prefix.len();
} else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
self.last_positions[i] = j;
} else {
return false;
}
}
true
}
fn score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
) -> f64 {
let score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
0,
0,
self.query.len() as f64,
) * self.query.len() as f64;
if score <= 0.0 {
return 0.0;
}
let path_len = prefix.len() + path.len();
let mut cur_start = 0;
let mut byte_ix = 0;
let mut char_ix = 0;
for i in 0..self.query.len() {
let match_char_ix = self.best_position_matrix[i * path_len + cur_start];
while char_ix < match_char_ix {
let ch = prefix
.get(char_ix)
.or_else(|| path.get(char_ix - prefix.len()))
.unwrap();
byte_ix += ch.len_utf8();
char_ix += 1;
}
cur_start = match_char_ix + 1;
self.match_positions[i] = byte_ix;
}
score
}
#[allow(clippy::too_many_arguments)]
fn recursive_score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
query_idx: usize,
path_idx: usize,
cur_score: f64,
) -> f64 {
if query_idx == self.query.len() {
return 1.0;
}
let path_len = prefix.len() + path.len();
if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] {
return memoized;
}
let mut score = 0.0;
let mut best_position = 0;
let query_char = self.lowercase_query[query_idx];
let limit = self.last_positions[query_idx];
let mut last_slash = 0;
for j in path_idx..=limit {
let path_char = if j < prefix.len() {
lowercase_prefix[j]
} else {
path_cased[j - prefix.len()]
};
let is_path_sep = path_char == '/' || path_char == '\\';
if query_idx == 0 && is_path_sep {
last_slash = j;
}
if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
let curr = if j < prefix.len() {
prefix[j]
} else {
path[j - prefix.len()]
};
let mut char_score = 1.0;
if j > path_idx {
let last = if j - 1 < prefix.len() {
prefix[j - 1]
} else {
path[j - 1 - prefix.len()]
};
if last == '/' {
char_score = 0.9;
} else if (last == '-' || last == '_' || last == ' ' || last.is_numeric())
|| (last.is_lowercase() && curr.is_uppercase())
{
char_score = 0.8;
} else if last == '.' {
char_score = 0.7;
} else if query_idx == 0 {
char_score = BASE_DISTANCE_PENALTY;
} else {
char_score = MIN_DISTANCE_PENALTY.max(
BASE_DISTANCE_PENALTY
- (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY,
);
}
}
// Apply a severe penalty if the case doesn't match.
// This will make the exact matches have higher score than the case-insensitive and the
// path insensitive matches.
if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
char_score *= 0.001;
}
let mut multiplier = char_score;
// Scale the score based on how deep within the path we found the match.
if query_idx == 0 {
multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
}
let mut next_score = 1.0;
if self.min_score > 0.0 {
next_score = cur_score * multiplier;
// Scores only decrease. If we can't pass the previous best, bail
if next_score < self.min_score {
// Ensure that score is non-zero so we use it in the memo table.
if score == 0.0 {
score = 1e-18;
}
continue;
}
}
let new_score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
query_idx + 1,
j + 1,
next_score,
) * multiplier;
if new_score > score {
score = new_score;
best_position = j;
// Optimization: can't score better than 1.
if new_score == 1.0 {
break;
}
}
}
}
if best_position != 0 {
self.best_position_matrix[query_idx * path_len + path_idx] = best_position;
}
self.score_matrix[query_idx * path_len + path_idx] = Some(score);
score
}
}
#[cfg(test)]
mod tests {
use crate::{PathMatch, PathMatchCandidate};
use super::*;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
#[test]
fn test_get_last_positions() {
let mut query: &[char] = &['d', 'c'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(!result);
query = &['c', 'd'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![2, 4]);
query = &['z', '/', 'z', 'f'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
}
#[test]
fn test_match_path_entries() {
let paths = vec![
"",
"a",
"ab",
"abC",
"abcd",
"alphabravocharlie",
"AlphaBravoCharlie",
"thisisatestdir",
"/////ThisIsATestDir",
"/this/is/a/test/dir",
"/test/tiatd",
];
assert_eq!(
match_single_path_query("abc", false, &paths),
vec![
("abC", vec![0, 1, 2]),
("abcd", vec![0, 1, 2]),
("AlphaBravoCharlie", vec![0, 5, 10]),
("alphabravocharlie", vec![4, 5, 10]),
]
);
assert_eq!(
match_single_path_query("t/i/a/t/d", false, &paths),
vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
);
assert_eq!(
match_single_path_query("tiatd", false, &paths),
vec![
("/test/tiatd", vec![6, 7, 8, 9, 10]),
("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
("thisisatestdir", vec![0, 2, 6, 7, 11]),
]
);
}
#[test]
fn test_match_multibyte_path_entries() {
let paths = vec!["aαbβ/cγ", "αβγδ/bcde", "c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", "/d/🆒/h"];
assert_eq!("1".len(), 7);
assert_eq!(
match_single_path_query("bcd", false, &paths),
vec![
("αβγδ/bcde", vec![9, 10, 11]),
("aαbβ/cγ", vec![3, 7, 10]),
]
);
assert_eq!(
match_single_path_query("cde", false, &paths),
vec![
("αβγδ/bcde", vec![10, 11, 12]),
("c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", vec![0, 23, 46]),
]
);
}
fn match_single_path_query<'a>(
query: &str,
smart_case: bool,
paths: &[&'a str],
) -> Vec<(&'a str, Vec<usize>)> {
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let query_chars = CharBag::from(&lowercase_query[..]);
let path_arcs: Vec<Arc<Path>> = paths
.iter()
.map(|path| Arc::from(PathBuf::from(path)))
.collect::<Vec<_>>();
let mut path_entries = Vec::new();
for (i, path) in paths.iter().enumerate() {
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
let char_bag = CharBag::from(lowercase_path.as_slice());
path_entries.push(PathMatchCandidate {
char_bag,
path: &path_arcs[i],
});
}
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
let cancel_flag = AtomicBool::new(false);
let mut results = Vec::new();
matcher.match_candidates(
&[],
&[],
path_entries.into_iter(),
&mut results,
&cancel_flag,
|candidate, score| PathMatch {
score,
worktree_id: 0,
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: "".into(),
},
);
results
.into_iter()
.map(|result| {
(
paths
.iter()
.copied()
.find(|p| result.path.as_ref() == Path::new(p))
.unwrap(),
result.positions,
)
})
.collect()
}
}

174
crates/fuzzy/src/paths.rs Normal file
View File

@@ -0,0 +1,174 @@
use std::{
borrow::Cow,
cmp::{self, Ordering},
path::Path,
sync::{atomic::AtomicBool, Arc},
};
use gpui::executor;
use crate::{
matcher::{Match, MatchCandidate, Matcher},
CharBag,
};
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
pub path: &'a Arc<Path>,
pub char_bag: CharBag,
}
#[derive(Clone, Debug)]
pub struct PathMatch {
pub score: f64,
pub positions: Vec<usize>,
pub worktree_id: usize,
pub path: Arc<Path>,
pub path_prefix: Arc<str>,
}
pub trait PathMatchCandidateSet<'a>: Send + Sync {
type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
fn id(&self) -> usize;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn prefix(&self) -> Arc<str>;
fn candidates(&'a self, start: usize) -> Self::Candidates;
}
impl Match for PathMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl<'a> MatchCandidate for PathMatchCandidate<'a> {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.path.to_string_lossy()
}
}
impl PartialEq for PathMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for PathMatch {}
impl PartialOrd for PathMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PathMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
.then_with(|| self.path.cmp(&other.path))
}
}
pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
candidate_sets: &'a [Set],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<PathMatch> {
let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
if path_count == 0 {
return Vec::new();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(path_count);
let segment_size = (path_count + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
scope.spawn(async move {
let segment_start = segment_idx * segment_size;
let segment_end = segment_start + segment_size;
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
let mut tree_start = 0;
for candidate_set in candidate_sets {
let tree_end = tree_start + candidate_set.len();
if tree_start < segment_end && segment_start < tree_end {
let start = cmp::max(tree_start, segment_start) - tree_start;
let end = cmp::min(tree_end, segment_end) - tree_start;
let candidates = candidate_set.candidates(start).take(end - start);
let worktree_id = candidate_set.id();
let prefix = candidate_set.prefix().chars().collect::<Vec<_>>();
let lowercase_prefix = prefix
.iter()
.map(|c| c.to_ascii_lowercase())
.collect::<Vec<_>>();
matcher.match_candidates(
&prefix,
&lowercase_prefix,
candidates,
results,
cancel_flag,
|candidate, score| PathMatch {
score,
worktree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: candidate_set.prefix(),
},
);
}
if tree_end >= segment_end {
break;
}
tree_start = tree_end;
}
})
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}

161
crates/fuzzy/src/strings.rs Normal file
View File

@@ -0,0 +1,161 @@
use std::{
borrow::Cow,
cmp::{self, Ordering},
sync::{atomic::AtomicBool, Arc},
};
use gpui::executor;
use crate::{
matcher::{Match, MatchCandidate, Matcher},
CharBag,
};
#[derive(Clone, Debug)]
pub struct StringMatchCandidate {
pub id: usize,
pub string: String,
pub char_bag: CharBag,
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl StringMatchCandidate {
pub fn new(id: usize, string: String) -> Self {
Self {
id,
char_bag: CharBag::from(string.as_str()),
string,
}
}
}
impl<'a> MatchCandidate for &'a StringMatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.string.as_str().into()
}
}
#[derive(Clone, Debug)]
pub struct StringMatch {
pub candidate_id: usize,
pub score: f64,
pub positions: Vec<usize>,
pub string: String,
}
impl PartialEq for StringMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for StringMatch {}
impl PartialOrd for StringMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StringMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.candidate_id.cmp(&other.candidate_id))
}
}
pub async fn match_strings(
candidates: &[StringMatchCandidate],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<StringMatch> {
if candidates.is_empty() || max_results == 0 {
return Default::default();
}
if query.is_empty() {
return candidates
.iter()
.map(|candidate| StringMatch {
candidate_id: candidate.id,
score: 0.,
positions: Default::default(),
string: candidate.string.clone(),
})
.collect();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(candidates.len());
let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results.min(candidates.len())))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
let cancel_flag = &cancel_flag;
scope.spawn(async move {
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
matcher.match_candidates(
&[],
&[],
candidates[segment_start..segment_end].iter(),
results,
cancel_flag,
|candidate, score| StringMatch {
candidate_id: candidate.id,
score,
positions: Vec::new(),
string: candidate.string.to_string(),
},
);
});
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}

View File

@@ -71,18 +71,26 @@ impl BufferDiff {
}
}
pub fn hunks_in_range<'a>(
pub fn hunks_in_row_range<'a>(
&'a self,
query_row_range: Range<u32>,
range: Range<u32>,
buffer: &'a BufferSnapshot,
reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let start = buffer.anchor_before(Point::new(query_row_range.start, 0));
let end = buffer.anchor_after(Point::new(query_row_range.end, 0));
let start = buffer.anchor_before(Point::new(range.start, 0));
let end = buffer.anchor_after(Point::new(range.end, 0));
self.hunks_intersecting_range(start..end, buffer, reversed)
}
pub fn hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
buffer: &'a BufferSnapshot,
reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
let before_start = summary.buffer_range.end.cmp(&start, buffer).is_lt();
let after_end = summary.buffer_range.start.cmp(&end, buffer).is_gt();
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
!before_start && !after_end
});
@@ -141,7 +149,9 @@ impl BufferDiff {
#[cfg(test)]
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
self.hunks_in_range(0..u32::MAX, text, false)
let start = text.anchor_before(Point::new(0, 0));
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
self.hunks_intersecting_range(start..end, text, false)
}
fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
@@ -355,7 +365,7 @@ mod tests {
assert_eq!(diff.hunks(&buffer).count(), 8);
assert_hunks(
diff.hunks_in_range(7..12, &buffer, false),
diff.hunks_in_row_range(7..12, &buffer, false),
&buffer,
&diff_base,
&[

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,44 @@
use crate::MutableAppContext;
use collections::{BTreeMap, HashMap, HashSet};
use parking_lot::Mutex;
use std::sync::Arc;
use std::{hash::Hash, sync::Weak};
use parking_lot::Mutex;
use collections::{btree_map, BTreeMap, HashMap};
use crate::MutableAppContext;
pub type Mapping<K, F> = Mutex<HashMap<K, BTreeMap<usize, Option<F>>>>;
pub struct CallbackCollection<K: Hash + Eq, F> {
internal: Arc<Mapping<K, F>>,
pub struct CallbackCollection<K: Clone + Hash + Eq, F> {
internal: Arc<Mutex<Mapping<K, F>>>,
}
impl<K: Hash + Eq, F> Clone for CallbackCollection<K, F> {
pub struct Subscription<K: Clone + Hash + Eq, F> {
key: K,
id: usize,
mapping: Option<Weak<Mutex<Mapping<K, F>>>>,
}
struct Mapping<K, F> {
callbacks: HashMap<K, BTreeMap<usize, F>>,
dropped_subscriptions: HashMap<K, HashSet<usize>>,
}
impl<K: Hash + Eq, F> Mapping<K, F> {
fn clear_dropped_state(&mut self, key: &K, subscription_id: usize) -> bool {
if let Some(subscriptions) = self.dropped_subscriptions.get_mut(&key) {
subscriptions.remove(&subscription_id)
} else {
false
}
}
}
impl<K, F> Default for Mapping<K, F> {
fn default() -> Self {
Self {
callbacks: Default::default(),
dropped_subscriptions: Default::default(),
}
}
}
impl<K: Clone + Hash + Eq, F> Clone for CallbackCollection<K, F> {
fn clone(&self) -> Self {
Self {
internal: self.internal.clone(),
@@ -21,7 +46,7 @@ impl<K: Hash + Eq, F> Clone for CallbackCollection<K, F> {
}
}
impl<K: Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
impl<K: Clone + Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
fn default() -> Self {
CallbackCollection {
internal: Arc::new(Mutex::new(Default::default())),
@@ -29,78 +54,114 @@ impl<K: Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
}
}
impl<K: Hash + Eq + Copy, F> CallbackCollection<K, F> {
pub fn downgrade(&self) -> Weak<Mapping<K, F>> {
Arc::downgrade(&self.internal)
}
impl<K: Clone + Hash + Eq + Copy, F> CallbackCollection<K, F> {
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.internal.lock().is_empty()
self.internal.lock().callbacks.is_empty()
}
pub fn add_callback(&mut self, id: K, subscription_id: usize, callback: F) {
self.internal
.lock()
.entry(id)
.or_default()
.insert(subscription_id, Some(callback));
}
pub fn remove(&mut self, id: K) {
self.internal.lock().remove(&id);
}
pub fn add_or_remove_callback(&mut self, id: K, subscription_id: usize, callback: F) {
match self
.internal
.lock()
.entry(id)
.or_default()
.entry(subscription_id)
{
btree_map::Entry::Vacant(entry) => {
entry.insert(Some(callback));
}
btree_map::Entry::Occupied(entry) => {
// TODO: This seems like it should never be called because no code
// should ever attempt to remove an existing callback
debug_assert!(entry.get().is_none());
entry.remove();
}
pub fn subscribe(&mut self, key: K, subscription_id: usize) -> Subscription<K, F> {
Subscription {
key,
id: subscription_id,
mapping: Some(Arc::downgrade(&self.internal)),
}
}
pub fn emit_and_cleanup<C: FnMut(&mut F, &mut MutableAppContext) -> bool>(
pub fn add_callback(&mut self, key: K, subscription_id: usize, callback: F) {
let mut this = self.internal.lock();
// If this callback's subscription was dropped before the callback was
// added, then just drop the callback.
if this.clear_dropped_state(&key, subscription_id) {
return;
}
this.callbacks
.entry(key)
.or_default()
.insert(subscription_id, callback);
}
pub fn remove(&mut self, key: K) {
// Drop these callbacks after releasing the lock, in case one of them
// owns a subscription to this callback collection.
let mut this = self.internal.lock();
let callbacks = this.callbacks.remove(&key);
this.dropped_subscriptions.remove(&key);
drop(this);
drop(callbacks);
}
pub fn emit<C: FnMut(&mut F, &mut MutableAppContext) -> bool>(
&mut self,
id: K,
key: K,
cx: &mut MutableAppContext,
mut call_callback: C,
) {
let callbacks = self.internal.lock().remove(&id);
let callbacks = self.internal.lock().callbacks.remove(&key);
if let Some(callbacks) = callbacks {
for (subscription_id, callback) in callbacks {
if let Some(mut callback) = callback {
let alive = call_callback(&mut callback, cx);
if alive {
match self
.internal
.lock()
.entry(id)
.or_default()
.entry(subscription_id)
{
btree_map::Entry::Vacant(entry) => {
entry.insert(Some(callback));
}
btree_map::Entry::Occupied(entry) => {
entry.remove();
}
}
}
for (subscription_id, mut callback) in callbacks {
// If this callback's subscription was dropped while invoking an
// earlier callback, then just drop the callback.
let mut this = self.internal.lock();
if this.clear_dropped_state(&key, subscription_id) {
continue;
}
drop(this);
let alive = call_callback(&mut callback, cx);
// If this callback's subscription was dropped while invoking the callback
// itself, or if the callback returns false, then just drop the callback.
let mut this = self.internal.lock();
if this.clear_dropped_state(&key, subscription_id) || !alive {
continue;
}
this.callbacks
.entry(key)
.or_default()
.insert(subscription_id, callback);
}
}
}
}
impl<K: Clone + Hash + Eq, F> Subscription<K, F> {
pub fn id(&self) -> usize {
self.id
}
pub fn detach(&mut self) {
self.mapping.take();
}
}
impl<K: Clone + Hash + Eq, F> Drop for Subscription<K, F> {
fn drop(&mut self) {
if let Some(mapping) = self.mapping.as_ref().and_then(|mapping| mapping.upgrade()) {
let mut mapping = mapping.lock();
// If the callback is present in the mapping, then just remove it.
if let Some(callbacks) = mapping.callbacks.get_mut(&self.key) {
let callback = callbacks.remove(&self.id);
if callback.is_some() {
drop(mapping);
drop(callback);
return;
}
}
// If this subscription's callback is not present, then either it has been
// temporarily removed during emit, or it has not yet been added. Record
// that this subscription has been dropped so that the callback can be
// removed later.
mapping
.dropped_subscriptions
.entry(self.key.clone())
.or_default()
.insert(self.id);
}
}
}

View File

@@ -47,6 +47,8 @@ pub fn key_to_native(key: &str) -> Cow<str> {
"right" => NSRightArrowFunctionKey,
"pageup" => NSPageUpFunctionKey,
"pagedown" => NSPageDownFunctionKey,
"home" => NSHomeFunctionKey,
"end" => NSEndFunctionKey,
"delete" => NSDeleteFunctionKey,
"f1" => NSF1FunctionKey,
"f2" => NSF2FunctionKey,
@@ -258,6 +260,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
Some(NSRightArrowFunctionKey) => "right".to_string(),
Some(NSPageUpFunctionKey) => "pageup".to_string(),
Some(NSPageDownFunctionKey) => "pagedown".to_string(),
Some(NSHomeFunctionKey) => "home".to_string(),
Some(NSEndFunctionKey) => "end".to_string(),
Some(NSDeleteFunctionKey) => "delete".to_string(),
Some(NSF1FunctionKey) => "f1".to_string(),
Some(NSF2FunctionKey) => "f2".to_string(),

View File

@@ -2310,13 +2310,21 @@ impl BufferSnapshot {
})
}
pub fn git_diff_hunks_in_range<'a>(
pub fn git_diff_hunks_in_row_range<'a>(
&'a self,
query_row_range: Range<u32>,
range: Range<u32>,
reversed: bool,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
self.git_diff.hunks_in_row_range(range, self, reversed)
}
pub fn git_diff_hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
reversed: bool,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
self.git_diff
.hunks_in_range(query_row_range, self, reversed)
.hunks_intersecting_range(range, self, reversed)
}
pub fn diagnostics_in_range<'a, T, O>(

View File

@@ -84,13 +84,13 @@ impl OutlineView {
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
{
let buffer = editor
let outline = editor
.read(cx)
.buffer()
.read(cx)
.snapshot(cx)
.outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
if let Some(outline) = buffer {
if let Some(outline) = outline {
workspace.toggle_modal(cx, |_, cx| {
let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx));
cx.subscribe(&view, Self::on_event).detach();

View File

@@ -0,0 +1,22 @@
[package]
name = "recent_projects"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/recent_projects.rs"
doctest = false
[dependencies]
db = { path = "../db" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
language = { path = "../language" }
picker = { path = "../picker" }
settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" }
ordered-float = "2.1.1"
postage = { version = "0.4", features = ["futures-traits"] }
smol = "1.2"

View File

@@ -0,0 +1,129 @@
use std::path::Path;
use fuzzy::StringMatch;
use gpui::{
elements::{Label, LabelStyle},
Element, ElementBox,
};
use workspace::WorkspaceLocation;
pub struct HighlightedText {
pub text: String,
pub highlight_positions: Vec<usize>,
char_count: usize,
}
impl HighlightedText {
fn join(components: impl Iterator<Item = Self>, separator: &str) -> Self {
let mut char_count = 0;
let separator_char_count = separator.chars().count();
let mut text = String::new();
let mut highlight_positions = Vec::new();
for component in components {
if char_count != 0 {
text.push_str(separator);
char_count += separator_char_count;
}
highlight_positions.extend(
component
.highlight_positions
.iter()
.map(|position| position + char_count),
);
text.push_str(&component.text);
char_count += component.text.chars().count();
}
Self {
text,
highlight_positions,
char_count,
}
}
pub fn render(self, style: impl Into<LabelStyle>) -> ElementBox {
Label::new(self.text, style)
.with_highlights(self.highlight_positions)
.boxed()
}
}
pub struct HighlightedWorkspaceLocation {
pub names: HighlightedText,
pub paths: Vec<HighlightedText>,
}
impl HighlightedWorkspaceLocation {
pub fn new(string_match: &StringMatch, location: &WorkspaceLocation) -> Self {
let mut path_start_offset = 0;
let (names, paths): (Vec<_>, Vec<_>) = location
.paths()
.iter()
.map(|path| {
let highlighted_text = Self::highlights_for_path(
path.as_ref(),
&string_match.positions,
path_start_offset,
);
path_start_offset += highlighted_text.1.char_count;
highlighted_text
})
.unzip();
Self {
names: HighlightedText::join(names.into_iter().filter_map(|name| name), ", "),
paths,
}
}
// Compute the highlighted text for the name and path
fn highlights_for_path(
path: &Path,
match_positions: &Vec<usize>,
path_start_offset: usize,
) -> (Option<HighlightedText>, HighlightedText) {
let path_string = path.to_string_lossy();
let path_char_count = path_string.chars().count();
// Get the subset of match highlight positions that line up with the given path.
// Also adjusts them to start at the path start
let path_positions = match_positions
.iter()
.copied()
.skip_while(|position| *position < path_start_offset)
.take_while(|position| *position < path_start_offset + path_char_count)
.map(|position| position - path_start_offset)
.collect::<Vec<_>>();
// Again subset the highlight positions to just those that line up with the file_name
// again adjusted to the start of the file_name
let file_name_text_and_positions = path.file_name().map(|file_name| {
let text = file_name.to_string_lossy();
let char_count = text.chars().count();
let file_name_start = path_char_count - char_count;
let highlight_positions = path_positions
.iter()
.copied()
.skip_while(|position| *position < file_name_start)
.take_while(|position| *position < file_name_start + char_count)
.map(|position| position - file_name_start)
.collect::<Vec<_>>();
HighlightedText {
text: text.to_string(),
highlight_positions,
char_count,
}
});
(
file_name_text_and_positions,
HighlightedText {
text: path_string.to_string(),
highlight_positions: path_positions,
char_count: path_char_count,
},
)
}
}

View File

@@ -0,0 +1,197 @@
mod highlighted_workspace_location;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions,
elements::{ChildView, Flex, ParentElement},
AnyViewHandle, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, View,
ViewContext, ViewHandle,
};
use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use settings::Settings;
use workspace::{OpenPaths, Workspace, WorkspaceLocation, WORKSPACE_DB};
actions!(recent_projects, [Toggle]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(RecentProjectsView::toggle);
Picker::<RecentProjectsView>::init(cx);
}
struct RecentProjectsView {
picker: ViewHandle<Picker<Self>>,
workspace_locations: Vec<WorkspaceLocation>,
selected_match_index: usize,
matches: Vec<StringMatch>,
}
impl RecentProjectsView {
fn new(workspace_locations: Vec<WorkspaceLocation>, cx: &mut ViewContext<Self>) -> Self {
let handle = cx.weak_handle();
Self {
picker: cx.add_view(|cx| {
Picker::new("Recent Projects...", handle, cx).with_max_size(800., 1200.)
}),
workspace_locations,
selected_match_index: 0,
matches: Default::default(),
}
}
fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
cx.spawn(|workspace, mut cx| async move {
let workspace_locations = cx
.background()
.spawn(async {
WORKSPACE_DB
.recent_workspaces_on_disk()
.await
.unwrap_or_default()
.into_iter()
.map(|(_, location)| location)
.collect()
})
.await;
workspace.update(&mut cx, |workspace, cx| {
workspace.toggle_modal(cx, |_, cx| {
let view = cx.add_view(|cx| Self::new(workspace_locations, cx));
cx.subscribe(&view, Self::on_event).detach();
view
});
})
})
.detach();
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => workspace.dismiss_modal(cx),
}
}
}
pub enum Event {
Dismissed,
}
impl Entity for RecentProjectsView {
type Event = Event;
}
impl View for RecentProjectsView {
fn ui_name() -> &'static str {
"RecentProjectsView"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(self.picker.clone(), cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl PickerDelegate for RecentProjectsView {
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<Self>) {
self.selected_match_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
let query = query.trim_start();
let smart_case = query.chars().any(|c| c.is_uppercase());
let candidates = self
.workspace_locations
.iter()
.enumerate()
.map(|(id, location)| {
let combined_string = location
.paths()
.iter()
.map(|path| path.to_string_lossy().to_owned())
.collect::<Vec<_>>()
.join("");
StringMatchCandidate::new(id, combined_string)
})
.collect::<Vec<_>>();
self.matches = smol::block_on(fuzzy::match_strings(
candidates.as_slice(),
query,
smart_case,
100,
&Default::default(),
cx.background().clone(),
));
self.matches.sort_unstable_by_key(|m| m.candidate_id);
self.selected_match_index = self
.matches
.iter()
.enumerate()
.max_by_key(|(_, m)| OrderedFloat(m.score))
.map(|(ix, _)| ix)
.unwrap_or(0);
Task::ready(())
}
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
let selected_match = &self.matches[self.selected_index()];
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
cx.dispatch_global_action(OpenPaths {
paths: workspace_location.paths().as_ref().clone(),
});
cx.emit(Event::Dismissed);
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut gpui::MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> ElementBox {
let settings = cx.global::<Settings>();
let string_match = &self.matches[ix];
let style = settings.theme.picker.item.style_for(mouse_state, selected);
let highlighted_location = HighlightedWorkspaceLocation::new(
&string_match,
&self.workspace_locations[string_match.candidate_id],
);
Flex::column()
.with_child(highlighted_location.names.render(style.label.clone()))
.with_children(
highlighted_location
.paths
.into_iter()
.map(|highlighted_path| highlighted_path.render(style.label.clone())),
)
.flex(1., false)
.contained()
.with_style(style.container)
.named("match")
}
}

View File

@@ -853,9 +853,10 @@ message UpdateView {
repeated ExcerptInsertion inserted_excerpts = 1;
repeated uint64 deleted_excerpts = 2;
repeated Selection selections = 3;
EditorAnchor scroll_top_anchor = 4;
float scroll_x = 5;
float scroll_y = 6;
optional Selection pending_selection = 4;
EditorAnchor scroll_top_anchor = 5;
float scroll_x = 6;
float scroll_y = 7;
}
}
@@ -872,9 +873,10 @@ message View {
optional string title = 2;
repeated Excerpt excerpts = 3;
repeated Selection selections = 4;
EditorAnchor scroll_top_anchor = 5;
float scroll_x = 6;
float scroll_y = 7;
optional Selection pending_selection = 5;
EditorAnchor scroll_top_anchor = 6;
float scroll_x = 7;
float scroll_y = 8;
}
}

View File

@@ -6,7 +6,6 @@ use prost::Message as _;
use serde::Serialize;
use std::any::{Any, TypeId};
use std::fmt;
use std::str::FromStr;
use std::{
cmp,
fmt::Debug,
@@ -119,23 +118,6 @@ impl fmt::Display for PeerId {
}
}
impl FromStr for PeerId {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut components = s.split('/');
let owner_id = components
.next()
.ok_or_else(|| anyhow!("invalid peer id {:?}", s))?
.parse()?;
let id = components
.next()
.ok_or_else(|| anyhow!("invalid peer id {:?}", s))?
.parse()?;
Ok(PeerId { owner_id, id })
}
}
messages!(
(Ack, Foreground),
(AddProjectCollaborator, Foreground),

View File

@@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
pub const PROTOCOL_VERSION: u32 = 42;
pub const PROTOCOL_VERSION: u32 = 44;

View File

@@ -106,73 +106,79 @@ impl View for BufferSearchBar {
.with_child(
Flex::row()
.with_child(
ChildView::new(&self.query_editor, cx)
.aligned()
.left()
.flex(1., true)
.boxed(),
)
.with_children(self.active_searchable_item.as_ref().and_then(
|searchable_item| {
let matches = self
.seachable_items_with_matches
.get(&searchable_item.downgrade())?;
let message = if let Some(match_ix) = self.active_match_index {
format!("{}/{}", match_ix + 1, matches.len())
} else {
"No matches".to_string()
};
Some(
Label::new(message, theme.search.match_index.text.clone())
.contained()
.with_style(theme.search.match_index.container)
Flex::row()
.with_child(
ChildView::new(&self.query_editor, cx)
.aligned()
.left()
.flex(1., true)
.boxed(),
)
},
))
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.flex(1., false)
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
)
.with_child(
Flex::row()
.with_children(self.render_search_option(
supported_options.case,
"Case",
SearchOption::CaseSensitive,
cx,
))
.with_children(self.render_search_option(
supported_options.word,
"Word",
SearchOption::WholeWord,
cx,
))
.with_children(self.render_search_option(
supported_options.regex,
"Regex",
SearchOption::Regex,
cx,
))
.contained()
.with_style(theme.search.option_button_group)
.aligned()
.with_children(self.active_searchable_item.as_ref().and_then(
|searchable_item| {
let matches = self
.seachable_items_with_matches
.get(&searchable_item.downgrade())?;
let message = if let Some(match_ix) = self.active_match_index {
format!("{}/{}", match_ix + 1, matches.len())
} else {
"No matches".to_string()
};
Some(
Label::new(message, theme.search.match_index.text.clone())
.contained()
.with_style(theme.search.match_index.container)
.aligned()
.boxed(),
)
},
))
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.flex(1., false)
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
)
.with_child(
Flex::row()
.with_children(self.render_search_option(
supported_options.case,
"Case",
SearchOption::CaseSensitive,
cx,
))
.with_children(self.render_search_option(
supported_options.word,
"Word",
SearchOption::WholeWord,
cx,
))
.with_children(self.render_search_option(
supported_options.regex,
"Regex",
SearchOption::Regex,
cx,
))
.contained()
.with_style(theme.search.option_button_group)
.aligned()
.boxed(),
)
.flex(1., true)
.boxed(),
)
.with_child(self.render_close_button(&theme.search, cx))
.contained()
.with_style(theme.search.container)
.named("search bar")
@@ -325,7 +331,7 @@ impl BufferSearchBar {
let is_active = self.is_search_option_enabled(option);
Some(
MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
let style = &cx
let style = cx
.global::<Settings>()
.theme
.search
@@ -373,7 +379,7 @@ impl BufferSearchBar {
enum NavButton {}
MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
let style = &cx
let style = cx
.global::<Settings>()
.theme
.search
@@ -399,6 +405,38 @@ impl BufferSearchBar {
.boxed()
}
fn render_close_button(
&self,
theme: &theme::Search,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let action = Box::new(Dismiss);
let tooltip = "Dismiss Buffer Search";
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
enum CloseButton {}
MouseEventHandler::<CloseButton>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.contained()
.with_style(style.container)
.boxed()
})
.on_click(MouseButton::Left, {
let action = action.boxed_clone();
move |_, cx| cx.dispatch_any_action(action.boxed_clone())
})
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<CloseButton, _>(0, tooltip.to_string(), Some(action), tooltip_style, cx)
.boxed()
}
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {

View File

@@ -334,6 +334,15 @@ impl Item for ProjectSearchView {
.update(cx, |editor, cx| editor.navigate(data, cx))
}
fn git_diff_recalc(
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.results_editor
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
}
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
match event {
ViewEvent::UpdateTab => vec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab],

View File

@@ -597,6 +597,10 @@ where
self.cursor.item()
}
pub fn item_summary(&self) -> Option<&'a T::Summary> {
self.cursor.item_summary()
}
pub fn next(&mut self, cx: &<T::Summary as Summary>::Context) {
self.cursor.next_internal(&mut self.filter_node, cx);
}

View File

@@ -247,6 +247,7 @@ pub struct Search {
pub results_status: TextStyle,
pub tab_icon_width: f32,
pub tab_icon_spacing: f32,
pub dismiss_button: Interactive<IconButton>,
}
#[derive(Clone, Deserialize, Default)]

View File

@@ -44,6 +44,8 @@ actions!(
ActivateLastItem,
CloseActiveItem,
CloseInactiveItems,
CloseCleanItems,
CloseAllItems,
ReopenClosedItem,
SplitLeft,
SplitUp,
@@ -122,6 +124,8 @@ pub fn init(cx: &mut MutableAppContext) {
});
cx.add_async_action(Pane::close_active_item);
cx.add_async_action(Pane::close_inactive_items);
cx.add_async_action(Pane::close_clean_items);
cx.add_async_action(Pane::close_all_items);
cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
let pane = action.pane.upgrade(cx)?;
let task = Pane::close_item(workspace, pane, action.item_id, cx);
@@ -258,6 +262,13 @@ pub enum ReorderBehavior {
MoveToIndex(usize),
}
enum ItemType {
Active,
Inactive,
Clean,
All,
}
impl Pane {
pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
let handle = cx.weak_handle();
@@ -696,40 +707,67 @@ impl Pane {
_: &CloseActiveItem,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
let pane_handle = workspace.active_pane().clone();
let pane = pane_handle.read(cx);
if pane.items.is_empty() {
None
} else {
let item_id_to_close = pane.items[pane.active_item_index].id();
let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
item_id == item_id_to_close
});
Some(cx.foreground().spawn(async move {
task.await?;
Ok(())
}))
}
Self::close_main(workspace, ItemType::Active, cx)
}
pub fn close_inactive_items(
workspace: &mut Workspace,
_: &CloseInactiveItems,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
Self::close_main(workspace, ItemType::Inactive, cx)
}
pub fn close_all_items(
workspace: &mut Workspace,
_: &CloseAllItems,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
Self::close_main(workspace, ItemType::All, cx)
}
pub fn close_clean_items(
workspace: &mut Workspace,
_: &CloseCleanItems,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
Self::close_main(workspace, ItemType::Clean, cx)
}
fn close_main(
workspace: &mut Workspace,
close_item_type: ItemType,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
let pane_handle = workspace.active_pane().clone();
let pane = pane_handle.read(cx);
if pane.items.is_empty() {
None
} else {
let active_item_id = pane.items[pane.active_item_index].id();
let task =
Self::close_items(workspace, pane_handle, cx, move |id| id != active_item_id);
Some(cx.foreground().spawn(async move {
task.await?;
Ok(())
}))
return None;
}
let active_item_id = pane.items[pane.active_item_index].id();
let clean_item_ids: Vec<_> = pane
.items()
.filter(|item| !item.is_dirty(cx))
.map(|item| item.id())
.collect();
let task =
Self::close_items(
workspace,
pane_handle,
cx,
move |item_id| match close_item_type {
ItemType::Active => item_id == active_item_id,
ItemType::Inactive => item_id != active_item_id,
ItemType::Clean => clean_item_ids.contains(&item_id),
ItemType::All => true,
},
);
Some(cx.foreground().spawn(async move {
task.await?;
Ok(())
}))
}
pub fn close_item(

View File

@@ -148,7 +148,7 @@ impl Member {
.and_then(|leader_id| {
let room = active_call?.read(cx).room()?.read(cx);
let collaborator = project.read(cx).collaborators().get(leader_id)?;
let participant = room.remote_participants().get(&leader_id)?;
let participant = room.remote_participant_for_peer_id(*leader_id)?;
Some((collaborator.replica_id, participant))
});

View File

@@ -8,7 +8,7 @@ use anyhow::{anyhow, bail, Context, Result};
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::Axis;
use util::{ unzip_option, ResultExt};
use util::{unzip_option, ResultExt};
use crate::dock::DockPosition;
use crate::WorkspaceId;
@@ -31,7 +31,7 @@ define_connection! {
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
) STRICT;
CREATE TABLE pane_groups(
group_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
@@ -43,7 +43,7 @@ define_connection! {
ON UPDATE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
CREATE TABLE panes(
pane_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
@@ -52,7 +52,7 @@ define_connection! {
ON DELETE CASCADE
ON UPDATE CASCADE
) STRICT;
CREATE TABLE center_panes(
pane_id INTEGER PRIMARY KEY,
parent_group_id INTEGER, // NULL means that this is a root pane
@@ -61,7 +61,7 @@ define_connection! {
ON DELETE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
CREATE TABLE items(
item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
workspace_id INTEGER NOT NULL,
@@ -96,10 +96,10 @@ impl WorkspaceDb {
WorkspaceLocation,
bool,
DockPosition,
) =
) =
self.select_row_bound(sql!{
SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
FROM workspaces
FROM workspaces
WHERE workspace_location = ?
})
.and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
@@ -119,7 +119,7 @@ impl WorkspaceDb {
.context("Getting center group")
.log_err()?,
dock_position,
left_sidebar_open
left_sidebar_open,
})
}
@@ -158,7 +158,12 @@ impl WorkspaceDb {
dock_visible = ?4,
dock_anchor = ?5,
timestamp = CURRENT_TIMESTAMP
))?((workspace.id, &workspace.location, workspace.left_sidebar_open, workspace.dock_position))
))?((
workspace.id,
&workspace.location,
workspace.left_sidebar_open,
workspace.dock_position,
))
.context("Updating workspace")?;
// Save center pane group and dock pane
@@ -190,15 +195,38 @@ impl WorkspaceDb {
}
query! {
pub fn recent_workspaces(limit: usize) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
SELECT workspace_id, workspace_location
fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
SELECT workspace_id, workspace_location
FROM workspaces
WHERE workspace_location IS NOT NULL
ORDER BY timestamp DESC
LIMIT ?
ORDER BY timestamp DESC
}
}
query! {
async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> {
DELETE FROM workspaces
WHERE workspace_id IS ?
}
}
// Returns the recent locations which are still valid on disk and deletes ones which no longer
// exist.
pub async fn recent_workspaces_on_disk(&self) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
let mut result = Vec::new();
let mut delete_tasks = Vec::new();
for (id, location) in self.recent_workspaces()? {
if location.paths().iter().all(|path| dbg!(path).exists()) {
result.push((id, location));
} else {
delete_tasks.push(self.delete_stale_workspace(id));
}
}
futures::future::join_all(delete_tasks).await;
Ok(result)
}
query! {
pub fn last_workspace() -> Result<Option<WorkspaceLocation>> {
SELECT workspace_location
@@ -210,10 +238,16 @@ impl WorkspaceDb {
}
fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
Ok(self.get_pane_group(workspace_id, None)?
Ok(self
.get_pane_group(workspace_id, None)?
.into_iter()
.next()
.unwrap_or_else(|| SerializedPaneGroup::Pane(SerializedPane { active: true, children: vec![] })))
.unwrap_or_else(|| {
SerializedPaneGroup::Pane(SerializedPane {
active: true,
children: vec![],
})
}))
}
fn get_pane_group(
@@ -225,7 +259,7 @@ impl WorkspaceDb {
type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
self.select_bound::<GroupKey, GroupOrPane>(sql!(
SELECT group_id, axis, pane_id, active
FROM (SELECT
FROM (SELECT
group_id,
axis,
NULL as pane_id,
@@ -233,18 +267,18 @@ impl WorkspaceDb {
position,
parent_group_id,
workspace_id
FROM pane_groups
FROM pane_groups
UNION
SELECT
SELECT
NULL,
NULL,
NULL,
center_panes.pane_id,
panes.active as active,
position,
parent_group_id,
panes.workspace_id as workspace_id
FROM center_panes
JOIN panes ON center_panes.pane_id = panes.pane_id)
JOIN panes ON center_panes.pane_id = panes.pane_id)
WHERE parent_group_id IS ? AND workspace_id = ?
ORDER BY position
))?((group_id, workspace_id))?
@@ -267,13 +301,12 @@ impl WorkspaceDb {
// Filter out panes and pane groups which don't have any children or items
.filter(|pane_group| match pane_group {
Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
_ => true,
})
.collect::<Result<_>>()
}
fn save_pane_group(
conn: &Connection,
workspace_id: WorkspaceId,
@@ -285,15 +318,10 @@ impl WorkspaceDb {
let (parent_id, position) = unzip_option(parent);
let group_id = conn.select_row_bound::<_, i64>(sql!(
INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
VALUES (?, ?, ?, ?)
INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
VALUES (?, ?, ?, ?)
RETURNING group_id
))?((
workspace_id,
parent_id,
position,
*axis,
))?
))?((workspace_id, parent_id, position, *axis))?
.ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
for (position, group) in children.iter().enumerate() {
@@ -314,9 +342,7 @@ impl WorkspaceDb {
SELECT pane_id, active
FROM panes
WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
))?(
workspace_id,
)?
))?(workspace_id)?
.context("No dock pane for workspace")?;
Ok(SerializedPane::new(
@@ -333,8 +359,8 @@ impl WorkspaceDb {
dock: bool,
) -> Result<PaneId> {
let pane_id = conn.select_row_bound::<_, i64>(sql!(
INSERT INTO panes(workspace_id, active)
VALUES (?, ?)
INSERT INTO panes(workspace_id, active)
VALUES (?, ?)
RETURNING pane_id
))?((workspace_id, pane.active))?
.ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
@@ -376,14 +402,13 @@ impl WorkspaceDb {
Ok(())
}
query!{
query! {
pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
UPDATE workspaces
SET timestamp = CURRENT_TIMESTAMP
WHERE workspace_id = ?
}
}
}
#[cfg(test)]
@@ -472,7 +497,7 @@ mod tests {
dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
center_group: Default::default(),
dock_pane: Default::default(),
left_sidebar_open: true
left_sidebar_open: true,
};
let mut workspace_2 = SerializedWorkspace {
@@ -481,7 +506,7 @@ mod tests {
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
center_group: Default::default(),
dock_pane: Default::default(),
left_sidebar_open: false
left_sidebar_open: false,
};
db.save_workspace(workspace_1.clone()).await;
@@ -587,7 +612,7 @@ mod tests {
dock_position: DockPosition::Shown(DockAnchor::Bottom),
center_group,
dock_pane,
left_sidebar_open: true
left_sidebar_open: true,
};
db.save_workspace(workspace.clone()).await;
@@ -660,7 +685,7 @@ mod tests {
dock_position: DockPosition::Shown(DockAnchor::Right),
center_group: Default::default(),
dock_pane: Default::default(),
left_sidebar_open: false
left_sidebar_open: false,
};
db.save_workspace(workspace_3.clone()).await;
@@ -695,7 +720,7 @@ mod tests {
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
center_group: center_group.clone(),
dock_pane,
left_sidebar_open: true
left_sidebar_open: true,
}
}

View File

@@ -60,7 +60,7 @@ pub use pane_group::*;
use persistence::{model::SerializedItem, DB};
pub use persistence::{
model::{ItemId, WorkspaceLocation},
WorkspaceDb,
WorkspaceDb, DB as WORKSPACE_DB,
};
use postage::prelude::Stream;
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
@@ -95,7 +95,7 @@ actions!(
ToggleLeftSidebar,
ToggleRightSidebar,
NewTerminal,
NewSearch,
NewSearch
]
);
@@ -2141,7 +2141,7 @@ impl Workspace {
let call = self.active_call()?;
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participants().get(&leader_id)?;
let participant = room.remote_participant_for_peer_id(leader_id)?;
let mut items_to_add = Vec::new();
match participant.location {
@@ -2190,7 +2190,7 @@ impl Workspace {
) -> Option<ViewHandle<SharedScreen>> {
let call = self.active_call()?;
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participants().get(&peer_id)?;
let participant = room.remote_participant_for_peer_id(peer_id)?;
let track = participant.tracks.values().next()?.clone();
let user = participant.user.clone();

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.67.2"
version = "0.68.1"
[lib]
name = "zed"
@@ -30,6 +30,7 @@ clock = { path = "../clock" }
diagnostics = { path = "../diagnostics" }
editor = { path = "../editor" }
file_finder = { path = "../file_finder" }
human_bytes = "0.4.1"
search = { path = "../search" }
fs = { path = "../fs" }
fsevent = { path = "../fsevent" }
@@ -44,9 +45,11 @@ plugin_runtime = { path = "../plugin_runtime" }
project = { path = "../project" }
project_panel = { path = "../project_panel" }
project_symbols = { path = "../project_symbols" }
recent_projects = { path = "../recent_projects" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" }
sysinfo = "0.27.1"
text = { path = "../text" }
terminal_view = { path = "../terminal_view" }
theme = { path = "../theme" }
@@ -107,6 +110,7 @@ tree-sitter-html = "0.19.0"
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9"}
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"}
url = "2.2"
urlencoding = "2.1.2"
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }

View File

@@ -93,7 +93,7 @@ impl LspAdapter for RustLspAdapter {
}
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
Some("rust-analyzer/checkOnSave".into())
Some("rust-analyzer/flycheck".into())
}
async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {

View File

@@ -128,8 +128,14 @@ impl LspAdapter for TypeScriptLspAdapter {
Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
_ => None,
}?;
let text = match &item.detail {
Some(detail) => format!("{} {}", item.label, detail),
None => item.label.clone(),
};
Some(language::CodeLabel {
text: item.label.clone(),
text,
runs: vec![(0..len, highlight_id)],
filter_range: 0..len,
})

View File

@@ -123,6 +123,7 @@ fn main() {
vim::init(cx);
terminal_view::init(cx);
theme_testbench::init(cx);
recent_projects::init(cx);
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
.detach();

View File

@@ -79,6 +79,11 @@ pub fn menus() -> Vec<Menu<'static>> {
name: "Open…",
action: Box::new(workspace::Open),
},
MenuItem::Action {
name: "Open Recent...",
action: Box::new(recent_projects::Toggle),
},
MenuItem::Separator,
MenuItem::Action {
name: "Add Folder to Project…",
action: Box::new(workspace::AddFolderToProject),
@@ -333,18 +338,25 @@ pub fn menus() -> Vec<Menu<'static>> {
action: Box::new(crate::OpenTelemetryLog),
},
MenuItem::Separator,
MenuItem::Action {
name: "Copy System Specs Into Clipboard",
action: Box::new(crate::CopySystemSpecsIntoClipboard),
},
MenuItem::Action {
name: "File Bug Report",
action: Box::new(crate::FileBugReport),
},
MenuItem::Action {
name: "Request Feature",
action: Box::new(crate::RequestFeature),
},
MenuItem::Separator,
MenuItem::Action {
name: "Documentation",
action: Box::new(crate::OpenBrowser {
url: "https://zed.dev/docs".into(),
}),
},
MenuItem::Action {
name: "Give Feedback",
action: Box::new(crate::OpenBrowser {
url: super::feedback::NEW_ISSUE_URL.into(),
}),
},
MenuItem::Action {
name: "Zed Twitter",
action: Box::new(crate::OpenBrowser {

View File

@@ -0,0 +1,52 @@
use std::{env, fmt::Display};
use gpui::AppContext;
use human_bytes::human_bytes;
use sysinfo::{System, SystemExt};
use util::channel::ReleaseChannel;
pub struct SystemSpecs {
app_version: &'static str,
release_channel: &'static str,
os_name: &'static str,
os_version: Option<String>,
memory: u64,
architecture: &'static str,
}
impl SystemSpecs {
pub fn new(cx: &AppContext) -> Self {
let platform = cx.platform();
let system = System::new_all();
SystemSpecs {
app_version: env!("CARGO_PKG_VERSION"),
release_channel: cx.global::<ReleaseChannel>().dev_name(),
os_name: platform.os_name(),
os_version: platform
.os_version()
.ok()
.map(|os_version| os_version.to_string()),
memory: system.total_memory(),
architecture: env::consts::ARCH,
}
}
}
impl Display for SystemSpecs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let os_information = match &self.os_version {
Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
None => format!("OS: {}", self.os_name),
};
let system_specs = [
format!("Zed: {} ({})", self.app_version, self.release_channel),
os_information,
format!("Memory: {}", human_bytes(self.memory as f64)),
format!("Architecture: {}", self.architecture),
]
.join("\n");
write!(f, "{system_specs}")
}
}

View File

@@ -1,6 +1,7 @@
mod feedback;
pub mod languages;
pub mod menus;
pub mod system_specs;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
@@ -21,7 +22,7 @@ use gpui::{
},
impl_actions,
platform::{WindowBounds, WindowOptions},
AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind,
AssetSource, AsyncAppContext, ClipboardItem, TitlebarOptions, ViewContext, WindowKind,
};
use language::Rope;
use lazy_static::lazy_static;
@@ -33,6 +34,7 @@ use serde::Deserialize;
use serde_json::to_string_pretty;
use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
use std::{env, path::Path, str, sync::Arc};
use system_specs::SystemSpecs;
use util::{channel::ReleaseChannel, paths, ResultExt};
pub use workspace;
use workspace::{sidebar::SidebarSide, AppState, Workspace};
@@ -67,6 +69,9 @@ actions!(
ResetBufferFontSize,
InstallCommandLineInterface,
ResetDatabase,
CopySystemSpecsIntoClipboard,
RequestFeature,
FileBugReport
]
);
@@ -245,6 +250,41 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
},
);
cx.add_action(
|_: &mut Workspace, _: &CopySystemSpecsIntoClipboard, cx: &mut ViewContext<Workspace>| {
let system_specs = SystemSpecs::new(cx).to_string();
let item = ClipboardItem::new(system_specs.clone());
cx.prompt(
gpui::PromptLevel::Info,
&format!("Copied into clipboard:\n\n{system_specs}"),
&["OK"],
);
cx.write_to_clipboard(item);
},
);
cx.add_action(
|_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
let url = "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
cx.dispatch_action(OpenBrowser {
url: url.into(),
});
},
);
cx.add_action(
|_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
let system_specs_text = SystemSpecs::new(cx).to_string();
let url = format!(
"https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
urlencoding::encode(&system_specs_text)
);
cx.dispatch_action(OpenBrowser {
url: url.into(),
});
},
);
activity_indicator::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
settings::KeymapFileContent::load_defaults(cx);

View File

@@ -36,6 +36,7 @@ position_1=0,0
position_2=${width},0
# Authenticate using the collab server's admin secret.
export ZED_STATELESS=1
export ZED_ADMIN_API_TOKEN=secret
export ZED_SERVER_URL=http://localhost:8080
export ZED_WINDOW_SIZE=${width},${height}

View File

@@ -32,13 +32,13 @@ export default function contactNotification(colorScheme: ColorScheme): Object {
},
},
dismissButton: {
color: foreground(layer, "on"),
color: foreground(layer, "variant"),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
hover: {
color: foreground(layer, "on", "hovered"),
color: foreground(layer, "hovered"),
},
},
};

View File

@@ -257,7 +257,6 @@ export default function editor(colorScheme: ColorScheme) {
right: 6,
},
hover: {
color: foreground(layer, "on", "hovered"),
background: background(layer, "on", "hovered"),
},
},

View File

@@ -80,5 +80,17 @@ export default function search(colorScheme: ColorScheme) {
...text(layer, "mono", "on"),
size: 18,
},
dismissButton: {
color: foreground(layer, "variant"),
iconWidth: 12,
buttonWidth: 14,
padding: {
left: 10,
right: 10,
},
hover: {
color: foreground(layer, "hovered"),
},
},
};
}

View File

@@ -26,7 +26,7 @@ export default function tabBar(colorScheme: ColorScheme) {
// Close icons
iconWidth: 8,
iconClose: foreground(layer, "variant"),
iconCloseActive: foreground(layer),
iconCloseActive: foreground(layer, "hovered"),
// Indicators
iconConflict: foreground(layer, "warning"),