Merge pull request #293 from zed-industries/project-diagnostics

Project diagnostics: First pass
This commit is contained in:
Nathan Sobo
2021-12-24 16:42:00 -07:00
committed by GitHub
73 changed files with 11581 additions and 5802 deletions
Generated
+31
View File
@@ -1399,6 +1399,24 @@ dependencies = [
"const-oid",
]
[[package]]
name = "diagnostics"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"editor",
"gpui",
"language",
"postage",
"project",
"serde_json",
"unindent",
"util",
"workspace",
]
[[package]]
name = "digest"
version = "0.8.1"
@@ -1525,9 +1543,11 @@ dependencies = [
"aho-corasick",
"anyhow",
"clock",
"collections",
"ctor",
"env_logger",
"gpui",
"itertools",
"language",
"lazy_static",
"log",
@@ -2585,7 +2605,11 @@ name = "language"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"clock",
"collections",
"ctor",
"env_logger",
"futures",
"gpui",
"lazy_static",
@@ -2597,7 +2621,9 @@ dependencies = [
"rpc",
"serde",
"similar",
"smallvec",
"smol",
"sum_tree",
"text",
"theme",
"tree-sitter",
@@ -3449,6 +3475,7 @@ dependencies = [
"async-trait",
"client",
"clock",
"collections",
"fsevent",
"futures",
"fuzzy",
@@ -4834,6 +4861,7 @@ dependencies = [
"ctor",
"env_logger",
"gpui",
"lazy_static",
"log",
"parking_lot",
"rand 0.8.3",
@@ -5628,6 +5656,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"clock",
"gpui",
"language",
"log",
@@ -5673,9 +5702,11 @@ dependencies = [
"chat_panel",
"client",
"clock",
"collections",
"contacts_panel",
"crossbeam-channel",
"ctor",
"diagnostics",
"dirs 3.0.1",
"easy-parallel",
"editor",
+2 -2
View File
@@ -56,14 +56,14 @@ impl ChatPanel {
4,
{
let settings = settings.clone();
move |_| {
Arc::new(move |_| {
let settings = settings.borrow();
EditorSettings {
tab_size: settings.tab_size,
style: settings.theme.chat_panel.input_editor.as_editor(),
soft_wrap: editor::SoftWrap::EditorWidth,
}
}
})
},
cx,
)
+12 -12
View File
@@ -22,14 +22,14 @@ pub struct User {
#[derive(Debug)]
pub struct Contact {
pub user: Arc<User>,
pub worktrees: Vec<WorktreeMetadata>,
pub projects: Vec<ProjectMetadata>,
}
#[derive(Debug)]
pub struct WorktreeMetadata {
pub struct ProjectMetadata {
pub id: u64,
pub root_name: String,
pub is_shared: bool,
pub worktree_root_names: Vec<String>,
pub guests: Vec<Arc<User>>,
}
@@ -112,7 +112,7 @@ impl UserStore {
let mut user_ids = HashSet::new();
for contact in &message.contacts {
user_ids.insert(contact.user_id);
user_ids.extend(contact.worktrees.iter().flat_map(|w| &w.guests).copied());
user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied());
}
let load_users = self.load_users(user_ids.into_iter().collect(), cx);
@@ -221,10 +221,10 @@ impl Contact {
user_store.fetch_user(contact.user_id, cx)
})
.await?;
let mut worktrees = Vec::new();
for worktree in contact.worktrees {
let mut projects = Vec::new();
for project in contact.projects {
let mut guests = Vec::new();
for participant_id in worktree.guests {
for participant_id in project.guests {
guests.push(
user_store
.update(cx, |user_store, cx| {
@@ -233,14 +233,14 @@ impl Contact {
.await?,
);
}
worktrees.push(WorktreeMetadata {
id: worktree.id,
root_name: worktree.root_name,
is_shared: worktree.is_shared,
projects.push(ProjectMetadata {
id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
is_shared: project.is_shared,
guests,
});
}
Ok(Self { user, worktrees })
Ok(Self { user, projects })
}
}
+9
View File
@@ -21,6 +21,15 @@ pub struct Lamport {
}
impl Local {
pub const MIN: Self = Self {
replica_id: ReplicaId::MIN,
value: Seq::MIN,
};
pub const MAX: Self = Self {
replica_id: ReplicaId::MAX,
value: Seq::MAX,
};
pub fn new(replica_id: ReplicaId) -> Self {
Self {
replica_id,
+41 -107
View File
@@ -1,27 +1,15 @@
use std::sync::Arc;
use client::{Contact, UserStore};
use gpui::{
action,
elements::*,
geometry::{rect::RectF, vector::vec2f},
platform::CursorStyle,
Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext,
Subscription, View, ViewContext,
Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View,
ViewContext,
};
use postage::watch;
use theme::Theme;
use workspace::{Settings, Workspace};
action!(JoinWorktree, u64);
action!(LeaveWorktree, u64);
action!(ShareWorktree, u64);
action!(UnshareWorktree, u64);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsPanel::share_worktree);
cx.add_action(ContactsPanel::unshare_worktree);
cx.add_action(ContactsPanel::join_worktree);
cx.add_action(ContactsPanel::leave_worktree);
}
use workspace::{AppState, JoinProject, JoinProjectParams, Settings};
pub struct ContactsPanel {
contacts: ListState,
@@ -31,78 +19,33 @@ pub struct ContactsPanel {
}
impl ContactsPanel {
pub fn new(
user_store: ModelHandle<UserStore>,
settings: watch::Receiver<Settings>,
cx: &mut ViewContext<Self>,
) -> Self {
pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
Self {
contacts: ListState::new(
user_store.read(cx).contacts().len(),
app_state.user_store.read(cx).contacts().len(),
Orientation::Top,
1000.,
{
let user_store = user_store.clone();
let settings = settings.clone();
let app_state = app_state.clone();
move |ix, cx| {
let user_store = user_store.read(cx);
let user_store = app_state.user_store.read(cx);
let contacts = user_store.contacts().clone();
let current_user_id = user_store.current_user().map(|user| user.id);
Self::render_collaborator(
&contacts[ix],
current_user_id,
&settings.borrow().theme,
app_state.clone(),
cx,
)
}
},
),
_maintain_contacts: cx.observe(&user_store, Self::update_contacts),
user_store,
settings,
_maintain_contacts: cx.observe(&app_state.user_store, Self::update_contacts),
user_store: app_state.user_store.clone(),
settings: app_state.settings.clone(),
}
}
fn share_worktree(
workspace: &mut Workspace,
action: &ShareWorktree,
cx: &mut ViewContext<Workspace>,
) {
workspace
.project()
.update(cx, |p, cx| p.share_worktree(action.0, cx));
}
fn unshare_worktree(
workspace: &mut Workspace,
action: &UnshareWorktree,
cx: &mut ViewContext<Workspace>,
) {
workspace
.project()
.update(cx, |p, cx| p.unshare_worktree(action.0, cx));
}
fn join_worktree(
workspace: &mut Workspace,
action: &JoinWorktree,
cx: &mut ViewContext<Workspace>,
) {
workspace
.project()
.update(cx, |p, cx| p.add_remote_worktree(action.0, cx).detach());
}
fn leave_worktree(
workspace: &mut Workspace,
action: &LeaveWorktree,
cx: &mut ViewContext<Workspace>,
) {
workspace
.project()
.update(cx, |p, cx| p.close_remote_worktree(action.0, cx));
}
fn update_contacts(&mut self, _: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) {
self.contacts
.reset(self.user_store.read(cx).contacts().len());
@@ -112,20 +55,16 @@ impl ContactsPanel {
fn render_collaborator(
collaborator: &Contact,
current_user_id: Option<u64>,
theme: &Theme,
app_state: Arc<AppState>,
cx: &mut LayoutContext,
) -> ElementBox {
let theme = &theme.contacts_panel;
let worktree_count = collaborator.worktrees.len();
let theme = &app_state.settings.borrow().theme.contacts_panel;
let project_count = collaborator.projects.len();
let font_cache = cx.font_cache();
let line_height = theme.unshared_worktree.name.text.line_height(font_cache);
let cap_height = theme.unshared_worktree.name.text.cap_height(font_cache);
let baseline_offset = theme
.unshared_worktree
.name
.text
.baseline_offset(font_cache)
+ (theme.unshared_worktree.height - line_height) / 2.;
let line_height = theme.unshared_project.name.text.line_height(font_cache);
let cap_height = theme.unshared_project.name.text.cap_height(font_cache);
let baseline_offset = theme.unshared_project.name.text.baseline_offset(font_cache)
+ (theme.unshared_project.height - line_height) / 2.;
let tree_branch_width = theme.tree_branch_width;
let tree_branch_color = theme.tree_branch_color;
let host_avatar_height = theme
@@ -161,11 +100,11 @@ impl ContactsPanel {
)
.with_children(
collaborator
.worktrees
.projects
.iter()
.enumerate()
.map(|(ix, worktree)| {
let worktree_id = worktree.id;
.map(|(ix, project)| {
let project_id = project.id;
Flex::row()
.with_child(
@@ -182,7 +121,7 @@ impl ContactsPanel {
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch_width,
if ix + 1 == worktree_count {
if ix + 1 == project_count {
end_y
} else {
bounds.max_y()
@@ -210,28 +149,28 @@ impl ContactsPanel {
.with_child({
let is_host = Some(collaborator.user.id) == current_user_id;
let is_guest = !is_host
&& worktree
&& project
.guests
.iter()
.any(|guest| Some(guest.id) == current_user_id);
let is_shared = worktree.is_shared;
let is_shared = project.is_shared;
let app_state = app_state.clone();
MouseEventHandler::new::<ContactsPanel, _, _, _>(
worktree_id as usize,
project_id as usize,
cx,
|mouse_state, _| {
let style = match (worktree.is_shared, mouse_state.hovered)
{
(false, false) => &theme.unshared_worktree,
(false, true) => &theme.hovered_unshared_worktree,
(true, false) => &theme.shared_worktree,
(true, true) => &theme.hovered_shared_worktree,
let style = match (project.is_shared, mouse_state.hovered) {
(false, false) => &theme.unshared_project,
(false, true) => &theme.hovered_unshared_project,
(true, false) => &theme.shared_project,
(true, true) => &theme.hovered_shared_project,
};
Flex::row()
.with_child(
Label::new(
worktree.root_name.clone(),
project.worktree_root_names.join(", "),
style.name.text.clone(),
)
.aligned()
@@ -240,7 +179,7 @@ impl ContactsPanel {
.with_style(style.name.container)
.boxed(),
)
.with_children(worktree.guests.iter().filter_map(
.with_children(project.guests.iter().filter_map(
|participant| {
participant.avatar.clone().map(|avatar| {
Image::new(avatar)
@@ -268,23 +207,18 @@ impl ContactsPanel {
CursorStyle::Arrow
})
.on_click(move |cx| {
if is_shared {
if is_host {
cx.dispatch_action(UnshareWorktree(worktree_id));
} else if is_guest {
cx.dispatch_action(LeaveWorktree(worktree_id));
} else {
cx.dispatch_action(JoinWorktree(worktree_id))
}
} else if is_host {
cx.dispatch_action(ShareWorktree(worktree_id));
if !is_host && !is_guest {
cx.dispatch_global_action(JoinProject(JoinProjectParams {
project_id,
app_state: app_state.clone(),
}));
}
})
.expanded(1.0)
.boxed()
})
.constrained()
.with_height(theme.unshared_worktree.height)
.with_height(theme.unshared_project.height)
.boxed()
}),
)
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "diagnostics"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/diagnostics.rs"
[dependencies]
anyhow = "1.0"
collections = { path = "../collections" }
editor = { path = "../editor" }
language = { path = "../language" }
gpui = { path = "../gpui" }
project = { path = "../project" }
util = { path = "../util" }
workspace = { path = "../workspace" }
postage = { version = "0.4", features = ["futures-traits"] }
[dev-dependencies]
unindent = "0.1"
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
serde_json = { version = "1", features = ["preserve_order"] }
+870
View File
@@ -0,0 +1,870 @@
use anyhow::Result;
use collections::{HashMap, HashSet};
use editor::{
context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
display_map::{BlockDisposition, BlockId, BlockProperties},
BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer,
};
use gpui::{
action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Task, View, ViewContext, ViewHandle,
};
use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point};
use postage::watch;
use project::Project;
use std::{cmp::Ordering, ops::Range, path::Path, sync::Arc};
use util::TryFutureExt;
use workspace::Workspace;
action!(Toggle);
action!(ClearInvalid);
const CONTEXT_LINE_COUNT: u32 = 1;
pub fn init(cx: &mut MutableAppContext) {
cx.add_bindings([
Binding::new("alt-shift-D", Toggle, None),
Binding::new(
"alt-shift-C",
ClearInvalid,
Some("ProjectDiagnosticsEditor"),
),
]);
cx.add_action(ProjectDiagnosticsEditor::toggle);
cx.add_action(ProjectDiagnosticsEditor::clear_invalid);
}
type Event = editor::Event;
struct ProjectDiagnostics {
project: ModelHandle<Project>,
}
struct ProjectDiagnosticsEditor {
editor: ViewHandle<Editor>,
excerpts: ModelHandle<MultiBuffer>,
path_states: Vec<(Arc<Path>, Vec<DiagnosticGroupState>)>,
build_settings: BuildSettings,
}
struct DiagnosticGroupState {
primary_diagnostic: DiagnosticEntry<language::Anchor>,
excerpts: Vec<ExcerptId>,
blocks: HashMap<BlockId, DiagnosticBlock>,
block_count: usize,
is_valid: bool,
}
enum DiagnosticBlock {
Header(Diagnostic),
Inline(Diagnostic),
Context,
}
impl ProjectDiagnostics {
fn new(project: ModelHandle<Project>) -> Self {
Self { project }
}
}
impl Entity for ProjectDiagnostics {
type Event = ();
}
impl Entity for ProjectDiagnosticsEditor {
type Event = Event;
}
impl View for ProjectDiagnosticsEditor {
fn ui_name() -> &'static str {
"ProjectDiagnosticsEditor"
}
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(self.editor.id()).boxed()
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.focus(&self.editor);
}
}
impl ProjectDiagnosticsEditor {
fn new(
project: ModelHandle<Project>,
settings: watch::Receiver<workspace::Settings>,
cx: &mut ViewContext<Self>,
) -> Self {
let project_paths = project
.read(cx)
.diagnostic_summaries(cx)
.map(|e| e.0)
.collect::<Vec<_>>();
cx.spawn(|this, mut cx| {
let project = project.clone();
async move {
for project_path in project_paths {
let buffer = project
.update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
.await?;
this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
}
Result::<_, anyhow::Error>::Ok(())
}
})
.detach();
cx.subscribe(&project, |_, project, event, cx| {
if let project::Event::DiagnosticsUpdated(project_path) = event {
let project_path = project_path.clone();
cx.spawn(|this, mut cx| {
async move {
let buffer = project
.update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
.await?;
this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx));
Ok(())
}
.log_err()
})
.detach();
}
})
.detach();
let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
let editor =
cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
.detach();
Self {
excerpts,
editor,
build_settings,
path_states: Default::default(),
}
}
#[cfg(test)]
fn text(&self, cx: &AppContext) -> String {
self.editor.read(cx).text(cx)
}
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
workspace.add_item(diagnostics, cx);
}
fn clear_invalid(&mut self, _: &ClearInvalid, cx: &mut ViewContext<Self>) {
let mut blocks_to_delete = HashSet::default();
let mut excerpts_to_delete = Vec::new();
let mut path_ixs_to_delete = Vec::new();
for (ix, (_, groups)) in self.path_states.iter_mut().enumerate() {
groups.retain(|group| {
if group.is_valid {
true
} else {
blocks_to_delete.extend(group.blocks.keys().copied());
excerpts_to_delete.extend(group.excerpts.iter().cloned());
false
}
});
if groups.is_empty() {
path_ixs_to_delete.push(ix);
}
}
for ix in path_ixs_to_delete.into_iter().rev() {
self.path_states.remove(ix);
}
self.excerpts.update(cx, |excerpts, cx| {
excerpts_to_delete.sort_unstable();
excerpts.remove_excerpts(&excerpts_to_delete, cx)
});
self.editor
.update(cx, |editor, cx| editor.remove_blocks(blocks_to_delete, cx));
}
fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
let snapshot;
let path;
{
let buffer = buffer.read(cx);
snapshot = buffer.snapshot();
if let Some(file) = buffer.file() {
path = file.path().clone();
} else {
return;
}
}
let path_ix = match self
.path_states
.binary_search_by_key(&path.as_ref(), |e| e.0.as_ref())
{
Ok(ix) => ix,
Err(ix) => {
self.path_states
.insert(ix, (path.clone(), Default::default()));
ix
}
};
let mut prev_excerpt_id = if path_ix > 0 {
let prev_path_last_group = &self.path_states[path_ix - 1].1.last().unwrap();
prev_path_last_group.excerpts.last().unwrap().clone()
} else {
ExcerptId::min()
};
let groups = &mut self.path_states[path_ix].1;
let mut groups_to_add = Vec::new();
let mut group_ixs_to_remove = Vec::new();
let mut blocks_to_add = Vec::new();
let mut blocks_to_restyle = HashMap::default();
let mut blocks_to_remove = HashSet::default();
let selected_excerpts = self
.editor
.read(cx)
.local_anchor_selections()
.iter()
.flat_map(|s| [s.start.excerpt_id().clone(), s.end.excerpt_id().clone()])
.collect::<HashSet<_>>();
let mut diagnostic_blocks = Vec::new();
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
let mut old_groups = groups.iter_mut().enumerate().peekable();
let mut new_groups = snapshot
.diagnostic_groups()
.into_iter()
.filter(|group| group.entries[group.primary_ix].diagnostic.is_disk_based)
.peekable();
loop {
let mut to_insert = None;
let mut to_invalidate = None;
let mut to_validate = None;
match (old_groups.peek(), new_groups.peek()) {
(None, None) => break,
(None, Some(_)) => to_insert = new_groups.next(),
(Some(_), None) => to_invalidate = old_groups.next(),
(Some((_, old_group)), Some(new_group)) => {
let old_primary = &old_group.primary_diagnostic;
let new_primary = &new_group.entries[new_group.primary_ix];
match compare_diagnostics(old_primary, new_primary, &snapshot) {
Ordering::Less => to_invalidate = old_groups.next(),
Ordering::Equal => {
to_validate = old_groups.next();
new_groups.next();
}
Ordering::Greater => to_insert = new_groups.next(),
}
}
}
if let Some(group) = to_insert {
let mut group_state = DiagnosticGroupState {
primary_diagnostic: group.entries[group.primary_ix].clone(),
excerpts: Default::default(),
blocks: Default::default(),
block_count: 0,
is_valid: true,
};
let mut pending_range: Option<(Range<Point>, usize)> = None;
let mut is_first_excerpt_for_group = true;
for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
if let Some((range, start_ix)) = &mut pending_range {
if let Some(entry) = resolved_entry.as_ref() {
if entry.range.start.row
<= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
{
range.end = range.end.max(entry.range.end);
continue;
}
}
let excerpt_start =
Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
let excerpt_end = snapshot.clip_point(
Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
Bias::Left,
);
let excerpt_id = excerpts.insert_excerpt_after(
&prev_excerpt_id,
ExcerptProperties {
buffer: &buffer,
range: excerpt_start..excerpt_end,
},
excerpts_cx,
);
prev_excerpt_id = excerpt_id.clone();
group_state.excerpts.push(excerpt_id.clone());
let header_position = (excerpt_id.clone(), language::Anchor::min());
if is_first_excerpt_for_group {
is_first_excerpt_for_group = false;
let primary = &group.entries[group.primary_ix].diagnostic;
group_state.block_count += 1;
diagnostic_blocks.push(DiagnosticBlock::Header(primary.clone()));
blocks_to_add.push(BlockProperties {
position: header_position,
height: 2,
render: diagnostic_header_renderer(
buffer.clone(),
primary.clone(),
true,
self.build_settings.clone(),
),
disposition: BlockDisposition::Above,
});
} else {
group_state.block_count += 1;
diagnostic_blocks.push(DiagnosticBlock::Context);
blocks_to_add.push(BlockProperties {
position: header_position,
height: 1,
render: context_header_renderer(self.build_settings.clone()),
disposition: BlockDisposition::Above,
});
}
for entry in &group.entries[*start_ix..ix] {
if !entry.diagnostic.is_primary {
group_state.block_count += 1;
diagnostic_blocks
.push(DiagnosticBlock::Inline(entry.diagnostic.clone()));
blocks_to_add.push(BlockProperties {
position: (excerpt_id.clone(), entry.range.start.clone()),
height: entry.diagnostic.message.matches('\n').count()
as u8
+ 1,
render: diagnostic_block_renderer(
entry.diagnostic.clone(),
true,
self.build_settings.clone(),
),
disposition: BlockDisposition::Below,
});
}
}
pending_range.take();
}
if let Some(entry) = resolved_entry {
pending_range = Some((entry.range.clone(), ix));
}
}
groups_to_add.push(group_state);
} else if let Some((group_ix, group_state)) = to_invalidate {
if group_state
.excerpts
.iter()
.any(|excerpt_id| selected_excerpts.contains(excerpt_id))
{
for (block_id, block) in &group_state.blocks {
match block {
DiagnosticBlock::Header(diagnostic) => {
blocks_to_restyle.insert(
*block_id,
diagnostic_header_renderer(
buffer.clone(),
diagnostic.clone(),
false,
self.build_settings.clone(),
),
);
}
DiagnosticBlock::Inline(diagnostic) => {
blocks_to_restyle.insert(
*block_id,
diagnostic_block_renderer(
diagnostic.clone(),
false,
self.build_settings.clone(),
),
);
}
DiagnosticBlock::Context => {}
}
}
group_state.is_valid = false;
prev_excerpt_id = group_state.excerpts.last().unwrap().clone();
} else {
excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
group_ixs_to_remove.push(group_ix);
blocks_to_remove.extend(group_state.blocks.keys().copied());
}
} else if let Some((_, group_state)) = to_validate {
for (block_id, block) in &group_state.blocks {
match block {
DiagnosticBlock::Header(diagnostic) => {
blocks_to_restyle.insert(
*block_id,
diagnostic_header_renderer(
buffer.clone(),
diagnostic.clone(),
true,
self.build_settings.clone(),
),
);
}
DiagnosticBlock::Inline(diagnostic) => {
blocks_to_restyle.insert(
*block_id,
diagnostic_block_renderer(
diagnostic.clone(),
true,
self.build_settings.clone(),
),
);
}
DiagnosticBlock::Context => {}
}
}
group_state.is_valid = true;
prev_excerpt_id = group_state.excerpts.last().unwrap().clone();
} else {
unreachable!();
}
}
excerpts.snapshot(excerpts_cx)
});
self.editor.update(cx, |editor, cx| {
editor.remove_blocks(blocks_to_remove, cx);
editor.replace_blocks(blocks_to_restyle, cx);
let mut block_ids = editor
.insert_blocks(
blocks_to_add.into_iter().map(|block| {
let (excerpt_id, text_anchor) = block.position;
BlockProperties {
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
height: block.height,
render: block.render,
disposition: block.disposition,
}
}),
cx,
)
.into_iter()
.zip(diagnostic_blocks);
for group_state in &mut groups_to_add {
group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
}
});
for ix in group_ixs_to_remove.into_iter().rev() {
groups.remove(ix);
}
groups.extend(groups_to_add);
groups.sort_unstable_by(|a, b| {
let range_a = &a.primary_diagnostic.range;
let range_b = &b.primary_diagnostic.range;
range_a
.start
.cmp(&range_b.start, &snapshot)
.unwrap()
.then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
});
if groups.is_empty() {
self.path_states.remove(path_ix);
}
cx.notify();
}
}
impl workspace::Item for ProjectDiagnostics {
type View = ProjectDiagnosticsEditor;
fn build_view(
handle: ModelHandle<Self>,
settings: watch::Receiver<workspace::Settings>,
cx: &mut ViewContext<Self::View>,
) -> Self::View {
let project = handle.read(cx).project.clone();
ProjectDiagnosticsEditor::new(project, settings, cx)
}
fn project_path(&self) -> Option<project::ProjectPath> {
None
}
}
impl workspace::ItemView for ProjectDiagnosticsEditor {
fn title(&self, _: &AppContext) -> String {
"Project Diagnostics".to_string()
}
fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
None
}
fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
}
fn save_as(
&mut self,
_: ModelHandle<project::Worktree>,
_: &std::path::Path,
_: &mut ViewContext<Self>,
) -> Task<Result<()>> {
unreachable!()
}
fn is_dirty(&self, cx: &AppContext) -> bool {
self.excerpts.read(cx).read(cx).is_dirty()
}
fn has_conflict(&self, cx: &AppContext) -> bool {
self.excerpts.read(cx).read(cx).has_conflict()
}
fn should_update_tab_on_event(event: &Event) -> bool {
matches!(
event,
Event::Saved | Event::Dirtied | Event::FileHandleChanged
)
}
fn can_save(&self, _: &AppContext) -> bool {
true
}
fn can_save_as(&self, _: &AppContext) -> bool {
false
}
}
fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
lhs: &DiagnosticEntry<L>,
rhs: &DiagnosticEntry<R>,
snapshot: &language::BufferSnapshot,
) -> Ordering {
lhs.range
.start
.to_offset(&snapshot)
.cmp(&rhs.range.start.to_offset(snapshot))
.then_with(|| {
lhs.range
.end
.to_offset(&snapshot)
.cmp(&rhs.range.end.to_offset(snapshot))
})
.then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
}
#[cfg(test)]
mod tests {
use super::*;
use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
use gpui::TestAppContext;
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry};
use project::FakeFs;
use serde_json::json;
use std::sync::Arc;
use unindent::Unindent as _;
use workspace::WorkspaceParams;
#[gpui::test]
async fn test_diagnostics(mut cx: TestAppContext) {
let settings = cx.update(WorkspaceParams::test).settings;
let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
let client = Client::new();
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let fs = Arc::new(FakeFs::new());
let project = cx.update(|cx| {
Project::local(
client.clone(),
user_store,
Arc::new(LanguageRegistry::new()),
fs.clone(),
cx,
)
});
fs.insert_tree(
"/test",
json!({
"a.rs": "
const a: i32 = 'a';
".unindent(),
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
// comment 1
// comment 2
c(y);
d(x);
}
"
.unindent(),
}),
)
.await;
let worktree = project
.update(&mut cx, |project, cx| {
project.add_local_worktree("/test", cx)
})
.await
.unwrap();
worktree.update(&mut cx, |worktree, cx| {
worktree
.update_diagnostics_from_provider(
Arc::from("/test/main.rs".as_ref()),
vec![
DiagnosticEntry {
range: 20..21,
diagnostic: Diagnostic {
message:
"move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
.to_string(),
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 1,
..Default::default()
},
},
DiagnosticEntry {
range: 40..41,
diagnostic: Diagnostic {
message:
"move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
.to_string(),
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 0,
..Default::default()
},
},
DiagnosticEntry {
range: 58..59,
diagnostic: Diagnostic {
message: "value moved here".to_string(),
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 1,
..Default::default()
},
},
DiagnosticEntry {
range: 68..69,
diagnostic: Diagnostic {
message: "value moved here".to_string(),
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 0,
..Default::default()
},
},
DiagnosticEntry {
range: 112..113,
diagnostic: Diagnostic {
message: "use of moved value".to_string(),
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 0,
..Default::default()
},
},
DiagnosticEntry {
range: 112..113,
diagnostic: Diagnostic {
message: "value used here after move".to_string(),
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 0,
..Default::default()
},
},
DiagnosticEntry {
range: 122..123,
diagnostic: Diagnostic {
message: "use of moved value".to_string(),
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 1,
..Default::default()
},
},
DiagnosticEntry {
range: 122..123,
diagnostic: Diagnostic {
message: "value used here after move".to_string(),
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 1,
..Default::default()
},
},
],
cx,
)
.unwrap();
});
let view = cx.add_view(Default::default(), |cx| {
ProjectDiagnosticsEditor::new(project.clone(), settings, cx)
});
view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
.await;
view.update(&mut cx, |view, cx| {
let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
assert_eq!(
editor.text(),
concat!(
//
// main.rs, diagnostic group 1
//
"\n", // primary message
"\n", // filename
" let x = vec![];\n",
" let y = vec![];\n",
"\n", // supporting diagnostic
" a(x);\n",
" b(y);\n",
"\n", // supporting diagnostic
" // comment 1\n",
" // comment 2\n",
" c(y);\n",
"\n", // supporting diagnostic
" d(x);\n",
//
// main.rs, diagnostic group 2
//
"\n", // primary message
"\n", // filename
"fn main() {\n",
" let x = vec![];\n",
"\n", // supporting diagnostic
" let y = vec![];\n",
" a(x);\n",
"\n", // supporting diagnostic
" b(y);\n",
"\n", // context ellipsis
" c(y);\n",
" d(x);\n",
"\n", // supporting diagnostic
"}"
)
);
});
worktree.update(&mut cx, |worktree, cx| {
worktree
.update_diagnostics_from_provider(
Arc::from("/test/a.rs".as_ref()),
vec![
DiagnosticEntry {
range: 15..15,
diagnostic: Diagnostic {
message: "mismatched types".to_string(),
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 0,
..Default::default()
},
},
DiagnosticEntry {
range: 15..15,
diagnostic: Diagnostic {
message: "expected `usize`, found `char`".to_string(),
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 0,
..Default::default()
},
},
],
cx,
)
.unwrap();
});
view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
.await;
view.update(&mut cx, |view, cx| {
let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
assert_eq!(
editor.text(),
concat!(
//
// a.rs
//
"\n", // primary message
"\n", // filename
"const a: i32 = 'a';\n",
"\n", // supporting diagnostic
"\n", // context line
//
// main.rs, diagnostic group 1
//
"\n", // primary message
"\n", // filename
" let x = vec![];\n",
" let y = vec![];\n",
"\n", // supporting diagnostic
" a(x);\n",
" b(y);\n",
"\n", // supporting diagnostic
" // comment 1\n",
" // comment 2\n",
" c(y);\n",
"\n", // supporting diagnostic
" d(x);\n",
//
// main.rs, diagnostic group 2
//
"\n", // primary message
"\n", // filename
"fn main() {\n",
" let x = vec![];\n",
"\n", // supporting diagnostic
" let y = vec![];\n",
" a(x);\n",
"\n", // supporting diagnostic
" b(y);\n",
"\n", // context ellipsis
" c(y);\n",
" d(x);\n",
"\n", // supporting diagnostic
"}"
)
);
});
}
}
+6
View File
@@ -8,14 +8,17 @@ path = "src/editor.rs"
[features]
test-support = [
"rand",
"text/test-support",
"language/test-support",
"gpui/test-support",
"util/test-support",
]
[dependencies]
text = { path = "../text" }
clock = { path = "../clock" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
language = { path = "../language" }
project = { path = "../project" }
@@ -25,10 +28,12 @@ util = { path = "../util" }
workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow = "1.0"
itertools = "0.10"
lazy_static = "1.4"
log = "0.4"
parking_lot = "0.11"
postage = { version = "0.4", features = ["futures-traits"] }
rand = { version = "0.8.3", optional = true }
serde = { version = "1", features = ["derive", "rc"] }
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
@@ -37,6 +42,7 @@ smol = "1.2"
text = { path = "../text", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.8"
rand = "0.8"
+173 -91
View File
@@ -3,28 +3,29 @@ mod fold_map;
mod tab_map;
mod wrap_map;
pub use block_map::{
AlignedBlock, BlockContext, BlockDisposition, BlockId, BlockProperties, BufferRows, Chunks,
};
use crate::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
use fold_map::{FoldMap, ToFoldPoint as _};
use gpui::{fonts::FontId, ElementBox, Entity, ModelContext, ModelHandle};
use language::{Anchor, Buffer, Point, Subscription as BufferSubscription, ToOffset, ToPoint};
use std::{
collections::{HashMap, HashSet},
ops::Range,
};
use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
use language::{Point, Subscription as BufferSubscription};
use std::ops::Range;
use sum_tree::Bias;
use tab_map::TabMap;
use theme::SyntaxTheme;
use wrap_map::WrapMap;
pub use block_map::{
AlignedBlock, BlockBufferRows as DisplayBufferRows, BlockChunks as DisplayChunks, BlockContext,
BlockDisposition, BlockId, BlockProperties, RenderBlock,
};
pub trait ToDisplayPoint {
fn to_display_point(&self, map: &DisplayMapSnapshot) -> DisplayPoint;
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
}
pub struct DisplayMap {
buffer: ModelHandle<Buffer>,
buffer: ModelHandle<MultiBuffer>,
buffer_subscription: BufferSubscription,
fold_map: FoldMap,
tab_map: TabMap,
@@ -38,7 +39,7 @@ impl Entity for DisplayMap {
impl DisplayMap {
pub fn new(
buffer: ModelHandle<Buffer>,
buffer: ModelHandle<MultiBuffer>,
tab_size: usize,
font_id: FontId,
font_size: f32,
@@ -46,10 +47,10 @@ impl DisplayMap {
cx: &mut ModelContext<Self>,
) -> Self {
let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot());
let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
let block_map = BlockMap::new(buffer.clone(), snapshot);
let block_map = BlockMap::new(snapshot);
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
DisplayMap {
buffer,
@@ -61,18 +62,18 @@ impl DisplayMap {
}
}
pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplayMapSnapshot {
let buffer_snapshot = self.buffer.read(cx).snapshot();
pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (folds_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits);
let (wraps_snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
let blocks_snapshot = self.block_map.read(wraps_snapshot.clone(), edits, cx);
let blocks_snapshot = self.block_map.read(wraps_snapshot.clone(), edits);
DisplayMapSnapshot {
buffer_snapshot: self.buffer.read(cx).snapshot(),
DisplaySnapshot {
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
folds_snapshot,
tabs_snapshot,
wraps_snapshot,
@@ -85,20 +86,20 @@ impl DisplayMap {
ranges: impl IntoIterator<Item = Range<T>>,
cx: &mut ModelContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot();
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits, cx);
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.fold(ranges);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits, cx);
self.block_map.read(snapshot, edits);
}
pub fn unfold<T: ToOffset>(
@@ -106,20 +107,20 @@ impl DisplayMap {
ranges: impl IntoIterator<Item = Range<T>>,
cx: &mut ModelContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot();
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits, cx);
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.unfold(ranges);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits, cx);
self.block_map.read(snapshot, edits);
}
pub fn insert_blocks<P>(
@@ -130,34 +131,31 @@ impl DisplayMap {
where
P: ToOffset + Clone,
{
let snapshot = self.buffer.read(cx).snapshot();
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits, cx);
block_map.insert(blocks, cx)
let mut block_map = self.block_map.write(snapshot, edits);
block_map.insert(blocks)
}
pub fn replace_blocks<F>(&mut self, styles: HashMap<BlockId, F>)
where
F: 'static + Fn(&BlockContext) -> ElementBox,
{
pub fn replace_blocks(&mut self, styles: HashMap<BlockId, RenderBlock>) {
self.block_map.replace(styles);
}
pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot();
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits, cx);
block_map.remove(ids, cx);
let mut block_map = self.block_map.write(snapshot, edits);
block_map.remove(ids);
}
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
@@ -176,15 +174,15 @@ impl DisplayMap {
}
}
pub struct DisplayMapSnapshot {
pub buffer_snapshot: language::Snapshot,
folds_snapshot: fold_map::Snapshot,
tabs_snapshot: tab_map::Snapshot,
wraps_snapshot: wrap_map::Snapshot,
pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot,
folds_snapshot: fold_map::FoldSnapshot,
tabs_snapshot: tab_map::TabSnapshot,
wraps_snapshot: wrap_map::WrapSnapshot,
blocks_snapshot: block_map::BlockSnapshot,
}
impl DisplayMapSnapshot {
impl DisplaySnapshot {
#[cfg(test)]
pub fn fold_count(&self) -> usize {
self.folds_snapshot.fold_count()
@@ -194,56 +192,74 @@ impl DisplayMapSnapshot {
self.buffer_snapshot.len() == 0
}
pub fn buffer_rows<'a>(&'a self, start_row: u32) -> BufferRows<'a> {
pub fn buffer_rows<'a>(&'a self, start_row: u32) -> DisplayBufferRows<'a> {
self.blocks_snapshot.buffer_rows(start_row)
}
pub fn buffer_row_count(&self) -> u32 {
self.buffer_snapshot.max_point().row + 1
pub fn max_buffer_row(&self) -> u32 {
self.buffer_snapshot.max_buffer_row()
}
pub fn prev_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) {
pub fn prev_row_boundary(&self, input_display_point: DisplayPoint) -> (DisplayPoint, Point) {
let mut display_point = input_display_point;
loop {
*display_point.column_mut() = 0;
let mut point = display_point.to_point(self);
point = self.buffer_snapshot.clip_point(point, Bias::Left);
point.column = 0;
let next_display_point = self.point_to_display_point(point, Bias::Left);
let next_display_point = self.point_to_display_point_with_clipping(point, Bias::Left);
if next_display_point == display_point {
return (display_point, point);
}
if next_display_point > display_point {
panic!("invalid display point {:?}", input_display_point);
}
display_point = next_display_point;
}
}
pub fn next_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) {
pub fn next_row_boundary(&self, input_display_point: DisplayPoint) -> (DisplayPoint, Point) {
let mut display_point = input_display_point;
loop {
*display_point.column_mut() = self.line_len(display_point.row());
let mut point = display_point.to_point(self);
let mut point = self.display_point_to_point(display_point, Bias::Right);
point = self.buffer_snapshot.clip_point(point, Bias::Right);
point.column = self.buffer_snapshot.line_len(point.row);
let next_display_point = self.point_to_display_point(point, Bias::Right);
if next_display_point == display_point {
return (display_point, point);
}
if next_display_point < display_point {
panic!("invalid display point {:?}", input_display_point);
}
display_point = next_display_point;
}
}
fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
DisplayPoint(
self.blocks_snapshot.to_block_point(
self.wraps_snapshot.from_tab_point(
self.tabs_snapshot
.to_tab_point(point.to_fold_point(&self.folds_snapshot, bias)),
),
),
)
let fold_point = point.to_fold_point(&self.folds_snapshot, bias);
let tab_point = self.tabs_snapshot.to_tab_point(fold_point);
let wrap_point = self.wraps_snapshot.from_tab_point(tab_point);
let block_point = self.blocks_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
}
fn point_to_display_point_with_clipping(&self, point: Point, bias: Bias) -> DisplayPoint {
let fold_point = point.to_fold_point(&self.folds_snapshot, bias);
let tab_point = self.tabs_snapshot.to_tab_point(fold_point);
let wrap_point = self
.wraps_snapshot
.from_tab_point_with_clipping(tab_point, bias);
let block_point = self.blocks_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
}
fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
let unblocked_point = self.blocks_snapshot.to_wrap_point(point.0);
let unwrapped_point = self.wraps_snapshot.to_tab_point(unblocked_point);
let unexpanded_point = self.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
unexpanded_point.to_buffer_point(&self.folds_snapshot)
let block_point = point.0;
let wrap_point = self.blocks_snapshot.to_wrap_point(block_point);
let tab_point = self.wraps_snapshot.to_tab_point(wrap_point);
let fold_point = self.tabs_snapshot.to_fold_point(tab_point, bias).0;
fold_point.to_buffer_point(&self.folds_snapshot)
}
pub fn max_point(&self) -> DisplayPoint {
@@ -260,7 +276,7 @@ impl DisplayMapSnapshot {
&'a self,
display_rows: Range<u32>,
theme: Option<&'a SyntaxTheme>,
) -> block_map::Chunks<'a> {
) -> DisplayChunks<'a> {
self.blocks_snapshot.chunks(display_rows, theme)
}
@@ -420,11 +436,11 @@ impl DisplayPoint {
&mut self.0.column
}
pub fn to_point(self, map: &DisplayMapSnapshot) -> Point {
pub fn to_point(self, map: &DisplaySnapshot) -> Point {
map.display_point_to_point(self, Bias::Left)
}
pub fn to_offset(self, map: &DisplayMapSnapshot, bias: Bias) -> usize {
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
let unblocked_point = map.blocks_snapshot.to_wrap_point(self.0);
let unwrapped_point = map.wraps_snapshot.to_tab_point(unblocked_point);
let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
@@ -433,19 +449,19 @@ impl DisplayPoint {
}
impl ToDisplayPoint for usize {
fn to_display_point(&self, map: &DisplayMapSnapshot) -> DisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
map.point_to_display_point(self.to_point(&map.buffer_snapshot), Bias::Left)
}
}
impl ToDisplayPoint for Point {
fn to_display_point(&self, map: &DisplayMapSnapshot) -> DisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
map.point_to_display_point(*self, Bias::Left)
}
}
impl ToDisplayPoint for Anchor {
fn to_display_point(&self, map: &DisplayMapSnapshot) -> DisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
self.to_point(&map.buffer_snapshot).to_display_point(map)
}
}
@@ -454,15 +470,16 @@ impl ToDisplayPoint for Anchor {
mod tests {
use super::*;
use crate::{movement, test::*};
use gpui::{color::Color, MutableAppContext};
use language::{Language, LanguageConfig, RandomCharIter, SelectionGoal};
use rand::{prelude::StdRng, Rng};
use gpui::{color::Color, elements::*, MutableAppContext};
use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
use rand::{prelude::*, Rng};
use std::{env, sync::Arc};
use theme::SyntaxTheme;
use util::test::sample_text;
use Bias::*;
#[gpui::test(iterations = 100)]
async fn test_random(mut cx: gpui::TestAppContext, mut rng: StdRng) {
async fn test_random_display_map(mut cx: gpui::TestAppContext, mut rng: StdRng) {
cx.foreground().set_block_on_ticks(0..=50);
cx.foreground().forbid_parking();
let operations = env::var("OPERATIONS")
@@ -486,10 +503,14 @@ mod tests {
log::info!("tab size: {}", tab_size);
log::info!("wrap width: {:?}", wrap_width);
let buffer = cx.add_model(|cx| {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
Buffer::new(0, text, cx)
let buffer = cx.update(|cx| {
if rng.gen() {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)
}
});
let map = cx.add_model(|cx| {
@@ -497,6 +518,15 @@ mod tests {
});
let (_observer, notifications) = Observer::new(&map, &mut cx);
let mut fold_count = 0;
let mut blocks = Vec::new();
let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.folds_snapshot.text());
log::info!("tab text: {:?}", snapshot.tabs_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wraps_snapshot.text());
log::info!("block text: {:?}", snapshot.blocks_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
for _i in 0..operations {
match rng.gen_range(0..100) {
@@ -509,10 +539,55 @@ mod tests {
log::info!("setting wrap width to {:?}", wrap_width);
map.update(&mut cx, |map, cx| map.set_wrap_width(wrap_width, cx));
}
20..=80 => {
20..=44 => {
map.update(&mut cx, |map, cx| {
if rng.gen() || blocks.is_empty() {
let buffer = map.snapshot(cx).buffer_snapshot;
let block_properties = (0..rng.gen_range(1..=1))
.map(|_| {
let position =
buffer.anchor_after(buffer.clip_offset(
rng.gen_range(0..=buffer.len()),
Bias::Left,
));
let disposition = if rng.gen() {
BlockDisposition::Above
} else {
BlockDisposition::Below
};
let height = rng.gen_range(1..5);
log::info!(
"inserting block {:?} {:?} with height {}",
disposition,
position.to_point(&buffer),
height
);
BlockProperties {
position,
height,
disposition,
render: Arc::new(|_| Empty::new().boxed()),
}
})
.collect::<Vec<_>>();
blocks.extend(map.insert_blocks(block_properties, cx));
} else {
blocks.shuffle(&mut rng);
let remove_count = rng.gen_range(1..=4.min(blocks.len()));
let block_ids_to_remove = (0..remove_count)
.map(|_| blocks.remove(rng.gen_range(0..blocks.len())))
.collect();
log::info!("removing block ids {:?}", block_ids_to_remove);
map.remove_blocks(block_ids_to_remove, cx);
}
});
}
45..=79 => {
let mut ranges = Vec::new();
for _ in 0..rng.gen_range(1..=3) {
buffer.read_with(&cx, |buffer, _| {
buffer.read_with(&cx, |buffer, cx| {
let buffer = buffer.read(cx);
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
ranges.push(start..end);
@@ -542,7 +617,11 @@ mod tests {
let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
fold_count = snapshot.fold_count();
log::info!("buffer text: {:?}", buffer.read_with(&cx, |b, _| b.text()));
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.folds_snapshot.text());
log::info!("tab text: {:?}", snapshot.tabs_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wraps_snapshot.text());
log::info!("block text: {:?}", snapshot.blocks_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
// Line boundaries
@@ -558,12 +637,11 @@ mod tests {
assert!(next_display_bound >= point);
assert_eq!(prev_buffer_bound.column, 0);
assert_eq!(prev_display_bound.column(), 0);
if next_display_bound < snapshot.max_point() {
if next_buffer_bound < snapshot.buffer_snapshot.max_point() {
assert_eq!(
buffer
.read_with(&cx, |buffer, _| buffer.chars_at(next_buffer_bound).next()),
snapshot.buffer_snapshot.chars_at(next_buffer_bound).next(),
Some('\n')
)
);
}
assert_eq!(
@@ -597,6 +675,8 @@ mod tests {
}
// Movement
let min_point = snapshot.clip_point(DisplayPoint::new(0, 0), Left);
let max_point = snapshot.clip_point(snapshot.max_point(), Right);
for _ in 0..5 {
let row = rng.gen_range(0..=snapshot.max_point().row());
let column = rng.gen_range(0..=snapshot.line_len(row));
@@ -606,7 +686,7 @@ mod tests {
let moved_right = movement::right(&snapshot, point).unwrap();
log::info!("Right {:?}", moved_right);
if point < snapshot.max_point() {
if point < max_point {
assert!(moved_right > point);
if point.column() == snapshot.line_len(point.row())
|| snapshot.soft_wrap_indent(point.row()).is_some()
@@ -620,13 +700,13 @@ mod tests {
let moved_left = movement::left(&snapshot, point).unwrap();
log::info!("Left {:?}", moved_left);
if !point.is_zero() {
if point > min_point {
assert!(moved_left < point);
if point.column() == 0 {
assert!(moved_left.row() < point.row());
}
} else {
assert!(moved_left.is_zero());
assert_eq!(moved_left, point);
}
}
}
@@ -648,7 +728,7 @@ mod tests {
let wrap_width = Some(64.);
let text = "one two three four five\nsix seven eight";
let buffer = cx.add_model(|cx| Buffer::new(0, text.to_string(), cx));
let buffer = MultiBuffer::build_simple(text, cx);
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
});
@@ -697,8 +777,8 @@ mod tests {
(DisplayPoint::new(2, 4), SelectionGoal::Column(10))
);
let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
buffer.update(cx, |buffer, cx| {
let ix = buffer.text().find("seven").unwrap();
buffer.edit(vec![ix..ix], "and ", cx);
});
@@ -720,8 +800,8 @@ mod tests {
#[gpui::test]
fn test_text_chunks(cx: &mut gpui::MutableAppContext) {
let text = sample_text(6, 6);
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let text = sample_text(6, 6, 'a');
let buffer = MultiBuffer::build_simple(&text, cx);
let tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
@@ -800,6 +880,7 @@ mod tests {
let buffer =
cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx));
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let tab_size = 2;
let font_cache = cx.font_cache();
@@ -887,6 +968,7 @@ mod tests {
let buffer =
cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx));
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let font_cache = cx.font_cache();
@@ -932,7 +1014,7 @@ mod tests {
let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n";
let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n";
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let buffer = MultiBuffer::build_simple(text, cx);
let tab_size = 4;
let font_cache = cx.font_cache();
@@ -976,7 +1058,7 @@ mod tests {
#[gpui::test]
fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
let text = "\t\tα\nβ\t\n🏀β\t\tγ";
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let buffer = MultiBuffer::build_simple(text, cx);
let tab_size = 4;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
@@ -1035,7 +1117,7 @@ mod tests {
#[gpui::test]
fn test_max_point(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "aaa\n\t\tbbb", cx));
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
let tab_size = 4;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+200 -183
View File
@@ -1,10 +1,11 @@
use super::wrap_map::{self, Edit as WrapEdit, Snapshot as WrapSnapshot, WrapPoint};
use gpui::{AppContext, ElementBox, ModelHandle};
use language::{Buffer, Chunk};
use super::wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot};
use crate::{Anchor, ToOffset, ToPoint as _};
use collections::{HashMap, HashSet};
use gpui::{AppContext, ElementBox};
use language::Chunk;
use parking_lot::Mutex;
use std::{
cmp::{self, Ordering},
collections::{HashMap, HashSet},
fmt::Debug,
ops::{Deref, Range},
sync::{
@@ -12,14 +13,13 @@ use std::{
Arc,
},
};
use sum_tree::SumTree;
use text::{Anchor, Bias, Edit, Point, ToOffset, ToPoint as _};
use sum_tree::{Bias, SumTree};
use text::{Edit, Point};
use theme::SyntaxTheme;
const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize];
pub struct BlockMap {
buffer: ModelHandle<Buffer>,
next_block_id: AtomicUsize,
wrap_snapshot: Mutex<WrapSnapshot>,
blocks: Vec<Arc<Block>>,
@@ -45,11 +45,13 @@ struct BlockRow(u32);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct WrapRow(u32);
pub type RenderBlock = Arc<dyn Fn(&BlockContext) -> ElementBox>;
pub struct Block {
id: BlockId,
position: Anchor,
height: u8,
render: Mutex<Arc<dyn Fn(&BlockContext) -> ElementBox>>,
render: Mutex<RenderBlock>,
disposition: BlockDisposition,
}
@@ -93,25 +95,24 @@ struct TransformSummary {
output_rows: u32,
}
pub struct Chunks<'a> {
pub struct BlockChunks<'a> {
transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
input_chunks: wrap_map::Chunks<'a>,
input_chunks: wrap_map::WrapChunks<'a>,
input_chunk: Chunk<'a>,
output_row: u32,
max_output_row: u32,
}
pub struct BufferRows<'a> {
pub struct BlockBufferRows<'a> {
transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
input_buffer_rows: wrap_map::BufferRows<'a>,
input_buffer_rows: wrap_map::WrapBufferRows<'a>,
output_row: u32,
started: bool,
}
impl BlockMap {
pub fn new(buffer: ModelHandle<Buffer>, wrap_snapshot: WrapSnapshot) -> Self {
pub fn new(wrap_snapshot: WrapSnapshot) -> Self {
Self {
buffer,
next_block_id: AtomicUsize::new(0),
blocks: Vec::new(),
transforms: Mutex::new(SumTree::from_item(
@@ -122,13 +123,8 @@ impl BlockMap {
}
}
pub fn read(
&self,
wrap_snapshot: WrapSnapshot,
edits: Vec<WrapEdit>,
cx: &AppContext,
) -> BlockSnapshot {
self.sync(&wrap_snapshot, edits, cx);
pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Vec<WrapEdit>) -> BlockSnapshot {
self.sync(&wrap_snapshot, edits);
*self.wrap_snapshot.lock() = wrap_snapshot.clone();
BlockSnapshot {
wrap_snapshot,
@@ -136,23 +132,18 @@ impl BlockMap {
}
}
pub fn write(
&mut self,
wrap_snapshot: WrapSnapshot,
edits: Vec<WrapEdit>,
cx: &AppContext,
) -> BlockMapWriter {
self.sync(&wrap_snapshot, edits, cx);
pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Vec<WrapEdit>) -> BlockMapWriter {
self.sync(&wrap_snapshot, edits);
*self.wrap_snapshot.lock() = wrap_snapshot;
BlockMapWriter(self)
}
fn sync(&self, wrap_snapshot: &WrapSnapshot, edits: Vec<WrapEdit>, cx: &AppContext) {
fn sync(&self, wrap_snapshot: &WrapSnapshot, edits: Vec<WrapEdit>) {
if edits.is_empty() {
return;
}
let buffer = self.buffer.read(cx);
let buffer = wrap_snapshot.buffer_snapshot();
let mut transforms = self.transforms.lock();
let mut new_transforms = SumTree::new();
let old_row_count = transforms.summary().input_rows;
@@ -236,26 +227,29 @@ impl BlockMap {
}
// Find the blocks within this edited region.
let new_start = wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left);
let start_anchor = buffer.anchor_before(new_start);
let new_buffer_start =
wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left);
let start_anchor = buffer.anchor_before(new_buffer_start);
let start_block_ix = match self.blocks[last_block_ix..].binary_search_by(|probe| {
probe
.position
.cmp(&start_anchor, buffer)
.cmp(&start_anchor, &buffer)
.unwrap()
.then(Ordering::Greater)
}) {
Ok(ix) | Err(ix) => last_block_ix + ix,
};
let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() {
self.blocks.len()
} else {
let new_end = wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left);
let end_anchor = buffer.anchor_before(new_end);
let new_buffer_end =
wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left);
let end_anchor = buffer.anchor_before(new_buffer_end);
match self.blocks[start_block_ix..].binary_search_by(|probe| {
probe
.position
.cmp(&end_anchor, buffer)
.cmp(&end_anchor, &buffer)
.unwrap()
.then(Ordering::Greater)
}) {
@@ -263,12 +257,13 @@ impl BlockMap {
}
};
last_block_ix = end_block_ix;
blocks_in_edit.clear();
debug_assert!(blocks_in_edit.is_empty());
blocks_in_edit.extend(
self.blocks[start_block_ix..end_block_ix]
.iter()
.map(|block| {
let mut position = block.position.to_point(buffer);
let mut position = block.position.to_point(&buffer);
let column = wrap_snapshot.from_point(position, Bias::Left).column();
match block.disposition {
BlockDisposition::Above => position.column = 0,
@@ -277,22 +272,21 @@ impl BlockMap {
}
}
let position = wrap_snapshot.from_point(position, Bias::Left);
(position.row(), column, block)
(position.row(), column, block.clone())
}),
);
blocks_in_edit
.sort_unstable_by_key(|(row, _, block)| (*row, block.disposition, block.id));
blocks_in_edit.sort_by_key(|(row, _, block)| (*row, block.disposition, block.id));
// For each of these blocks, insert a new isomorphic transform preceding the block,
// and then insert the block itself.
for (block_row, column, block) in blocks_in_edit.iter().copied() {
for (block_row, column, block) in blocks_in_edit.drain(..) {
let insertion_row = match block.disposition {
BlockDisposition::Above => block_row,
BlockDisposition::Below => block_row + 1,
};
let extent_before_block = insertion_row - new_transforms.summary().input_rows;
push_isomorphic(&mut new_transforms, extent_before_block);
new_transforms.push(Transform::block(block.clone(), column), &());
new_transforms.push(Transform::block(block, column), &());
}
old_end = WrapRow(old_end.0.min(old_row_count));
@@ -317,13 +311,10 @@ impl BlockMap {
*transforms = new_transforms;
}
pub fn replace<F>(&mut self, mut element_builders: HashMap<BlockId, F>)
where
F: 'static + Fn(&BlockContext) -> ElementBox,
{
pub fn replace(&mut self, mut renderers: HashMap<BlockId, RenderBlock>) {
for block in &self.blocks {
if let Some(build_element) = element_builders.remove(&block.id) {
*block.render.lock() = Arc::new(build_element);
if let Some(render) = renderers.remove(&block.id) {
*block.render.lock() = render;
}
}
}
@@ -374,37 +365,33 @@ impl<'a> BlockMapWriter<'a> {
pub fn insert<P>(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<P>>,
cx: &AppContext,
) -> Vec<BlockId>
where
P: ToOffset + Clone,
{
let buffer = self.0.buffer.read(cx);
let mut ids = Vec::new();
let mut edits = Vec::<Edit<u32>>::new();
let wrap_snapshot = &*self.0.wrap_snapshot.lock();
let buffer = wrap_snapshot.buffer_snapshot();
for block in blocks {
let id = BlockId(self.0.next_block_id.fetch_add(1, SeqCst));
ids.push(id);
let position = buffer.anchor_after(block.position);
let point = position.to_point(buffer);
let start_row = wrap_snapshot
let point = position.to_point(&buffer);
let wrap_row = wrap_snapshot
.from_point(Point::new(point.row, 0), Bias::Left)
.row();
let end_row = if point.row == buffer.max_point().row {
wrap_snapshot.max_point().row() + 1
} else {
wrap_snapshot
.from_point(Point::new(point.row + 1, 0), Bias::Left)
.row()
};
let start_row = wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0));
let end_row = wrap_snapshot
.next_row_boundary(WrapPoint::new(wrap_row, 0))
.unwrap_or(wrap_snapshot.max_point().row() + 1);
let block_ix = match self
.0
.blocks
.binary_search_by(|probe| probe.position.cmp(&position, buffer).unwrap())
.binary_search_by(|probe| probe.position.cmp(&position, &buffer).unwrap())
{
Ok(ix) | Err(ix) => ix,
};
@@ -430,30 +417,27 @@ impl<'a> BlockMapWriter<'a> {
}
}
self.0.sync(wrap_snapshot, edits, cx);
self.0.sync(wrap_snapshot, edits);
ids
}
pub fn remove(&mut self, block_ids: HashSet<BlockId>, cx: &AppContext) {
let buffer = self.0.buffer.read(cx);
pub fn remove(&mut self, block_ids: HashSet<BlockId>) {
let wrap_snapshot = &*self.0.wrap_snapshot.lock();
let buffer = wrap_snapshot.buffer_snapshot();
let mut edits = Vec::new();
let mut last_block_buffer_row = None;
self.0.blocks.retain(|block| {
if block_ids.contains(&block.id) {
let buffer_row = block.position.to_point(buffer).row;
let buffer_row = block.position.to_point(&buffer).row;
if last_block_buffer_row != Some(buffer_row) {
last_block_buffer_row = Some(buffer_row);
let start_row = wrap_snapshot
let wrap_row = wrap_snapshot
.from_point(Point::new(buffer_row, 0), Bias::Left)
.row();
let start_row = wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0));
let end_row = wrap_snapshot
.from_point(
Point::new(buffer_row, buffer.line_len(buffer_row)),
Bias::Left,
)
.row()
+ 1;
.next_row_boundary(WrapPoint::new(wrap_row, 0))
.unwrap_or(wrap_snapshot.max_point().row() + 1);
edits.push(Edit {
old: start_row..end_row,
new: start_row..end_row,
@@ -464,19 +448,23 @@ impl<'a> BlockMapWriter<'a> {
true
}
});
self.0.sync(wrap_snapshot, edits, cx);
self.0.sync(wrap_snapshot, edits);
}
}
impl BlockSnapshot {
#[cfg(test)]
fn text(&mut self) -> String {
pub fn text(&self) -> String {
self.chunks(0..self.transforms.summary().output_rows, None)
.map(|chunk| chunk.text)
.collect()
}
pub fn chunks<'a>(&'a self, rows: Range<u32>, theme: Option<&'a SyntaxTheme>) -> Chunks<'a> {
pub fn chunks<'a>(
&'a self,
rows: Range<u32>,
theme: Option<&'a SyntaxTheme>,
) -> BlockChunks<'a> {
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
let input_end = {
@@ -503,7 +491,7 @@ impl BlockSnapshot {
};
cursor.start().1 .0 + overshoot
};
Chunks {
BlockChunks {
input_chunks: self.wrap_snapshot.chunks(input_start..input_end, theme),
input_chunk: Default::default(),
transforms: cursor,
@@ -512,7 +500,7 @@ impl BlockSnapshot {
}
}
pub fn buffer_rows<'a>(&'a self, start_row: u32) -> BufferRows<'a> {
pub fn buffer_rows<'a>(&'a self, start_row: u32) -> BlockBufferRows<'a> {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
cursor.seek(&BlockRow(start_row), Bias::Right, &());
let (output_start, input_start) = cursor.start();
@@ -522,7 +510,7 @@ impl BlockSnapshot {
0
};
let input_start_row = input_start.0 + overshoot;
BufferRows {
BlockBufferRows {
transforms: cursor,
input_buffer_rows: self.wrap_snapshot.buffer_rows(input_start_row),
output_row: start_row,
@@ -590,41 +578,45 @@ impl BlockSnapshot {
cursor.seek(&BlockRow(point.row), Bias::Right, &());
let max_input_row = WrapRow(self.transforms.summary().input_rows);
let search_left =
let mut search_left =
(bias == Bias::Left && cursor.start().1 .0 > 0) || cursor.end(&()).1 == max_input_row;
let mut reversed = false;
loop {
if let Some(transform) = cursor.item() {
if transform.is_isomorphic() {
let (output_start_row, input_start_row) = cursor.start();
let (output_end_row, input_end_row) = cursor.end(&());
if point.row >= output_end_row.0 {
return BlockPoint::new(
output_end_row.0 - 1,
self.wrap_snapshot.line_len(input_end_row.0 - 1),
);
}
let output_start = Point::new(output_start_row.0, 0);
if point.0 > output_start {
let output_overshoot = point.0 - output_start;
let input_start = Point::new(input_start_row.0, 0);
let input_point = self
.wrap_snapshot
.clip_point(WrapPoint(input_start + output_overshoot), bias);
let input_overshoot = input_point.0 - input_start;
return BlockPoint(output_start + input_overshoot);
let input_start = Point::new(input_start_row.0, 0);
let input_end = Point::new(input_end_row.0, 0);
let input_point = if point.row >= output_end_row.0 {
let line_len = self.wrap_snapshot.line_len(input_end_row.0 - 1);
self.wrap_snapshot
.clip_point(WrapPoint::new(input_end_row.0 - 1, line_len), bias)
} else {
return BlockPoint(output_start);
let output_overshoot = point.0.saturating_sub(output_start);
self.wrap_snapshot
.clip_point(WrapPoint(input_start + output_overshoot), bias)
};
if (input_start..input_end).contains(&input_point.0) {
let input_overshoot = input_point.0.saturating_sub(input_start);
return BlockPoint(output_start + input_overshoot);
}
} else if search_left {
}
if search_left {
cursor.prev(&());
} else {
cursor.next(&());
}
} else {
} else if reversed {
return self.max_point();
} else {
reversed = true;
search_left = !search_left;
cursor.seek(&BlockRow(point.row), Bias::Right, &());
}
}
}
@@ -693,7 +685,7 @@ impl Transform {
}
}
impl<'a> Iterator for Chunks<'a> {
impl<'a> Iterator for BlockChunks<'a> {
type Item = Chunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
@@ -756,7 +748,7 @@ impl<'a> Iterator for Chunks<'a> {
}
}
impl<'a> Iterator for BufferRows<'a> {
impl<'a> Iterator for BlockBufferRows<'a> {
type Item = Option<u32>;
fn next(&mut self) -> Option<Self::Item> {
@@ -840,6 +832,14 @@ impl Deref for AlignedBlock {
}
}
impl<'a> Deref for BlockContext<'a> {
type Target = AppContext;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl Debug for Block {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Block")
@@ -872,8 +872,8 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
mod tests {
use super::*;
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
use crate::multi_buffer::MultiBuffer;
use gpui::{elements::Empty, Element};
use language::Buffer;
use rand::prelude::*;
use std::env;
use text::RandomCharIter;
@@ -902,38 +902,36 @@ mod tests {
let text = "aaa\nbbb\nccc\nddd";
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let (fold_map, folds_snapshot) = FoldMap::new(buffer.read(cx).snapshot());
let buffer = MultiBuffer::build_simple(text, cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (fold_map, folds_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1);
let (wrap_map, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, None, cx);
let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot.clone());
let mut block_map = BlockMap::new(wraps_snapshot.clone());
let mut writer = block_map.write(wraps_snapshot.clone(), vec![], cx);
writer.insert(
vec![
BlockProperties {
position: Point::new(1, 0),
height: 1,
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().named("block 1")),
},
BlockProperties {
position: Point::new(1, 2),
height: 2,
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().named("block 2")),
},
BlockProperties {
position: Point::new(3, 3),
height: 3,
disposition: BlockDisposition::Below,
render: Arc::new(|_| Empty::new().named("block 3")),
},
],
cx,
);
let mut writer = block_map.write(wraps_snapshot.clone(), vec![]);
writer.insert(vec![
BlockProperties {
position: Point::new(1, 0),
height: 1,
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().named("block 1")),
},
BlockProperties {
position: Point::new(1, 2),
height: 2,
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().named("block 2")),
},
BlockProperties {
position: Point::new(3, 3),
height: 3,
disposition: BlockDisposition::Below,
render: Arc::new(|_| Empty::new().named("block 3")),
},
]);
let mut snapshot = block_map.read(wraps_snapshot, vec![], cx);
let snapshot = block_map.read(wraps_snapshot, vec![]);
assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
let blocks = snapshot
@@ -1046,20 +1044,19 @@ mod tests {
]
);
// Insert a line break, separating two block decorations into separate
// lines.
let (buffer_snapshot, buffer_edits) = buffer.update(cx, |buffer, cx| {
let v0 = buffer.version();
// Insert a line break, separating two block decorations into separate lines.
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
buffer.edit([Point::new(1, 1)..Point::new(1, 1)], "!!!\n", cx);
(buffer.snapshot(), buffer.edits_since(&v0).collect())
buffer.snapshot(cx)
});
let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot, buffer_edits);
let (folds_snapshot, fold_edits) =
fold_map.read(buffer_snapshot, subscription.consume().into_inner());
let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
let mut snapshot = block_map.read(wraps_snapshot, wrap_edits, cx);
let snapshot = block_map.read(wraps_snapshot, wrap_edits);
assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
}
@@ -1073,34 +1070,31 @@ mod tests {
let text = "one two three\nfour five six\nseven eight";
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let (_, folds_snapshot) = FoldMap::new(buffer.read(cx).snapshot());
let buffer = MultiBuffer::build_simple(text, cx);
let (_, folds_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1);
let (_, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, Some(60.), cx);
let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot.clone());
let mut block_map = BlockMap::new(wraps_snapshot.clone());
let mut writer = block_map.write(wraps_snapshot.clone(), vec![], cx);
writer.insert(
vec![
BlockProperties {
position: Point::new(1, 12),
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().named("block 1")),
height: 1,
},
BlockProperties {
position: Point::new(1, 1),
disposition: BlockDisposition::Below,
render: Arc::new(|_| Empty::new().named("block 2")),
height: 1,
},
],
cx,
);
let mut writer = block_map.write(wraps_snapshot.clone(), vec![]);
writer.insert(vec![
BlockProperties {
position: Point::new(1, 12),
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().named("block 1")),
height: 1,
},
BlockProperties {
position: Point::new(1, 1),
disposition: BlockDisposition::Below,
render: Arc::new(|_| Empty::new().named("block 2")),
height: 1,
},
]);
// Blocks with an 'above' disposition go above their corresponding buffer line.
// Blocks with a 'below' disposition go below their corresponding buffer line.
let mut snapshot = block_map.read(wraps_snapshot, vec![], cx);
let snapshot = block_map.read(wraps_snapshot, vec![]);
assert_eq!(
snapshot.text(),
"one two \nthree\n\nfour five \nsix\n\nseven \neight"
@@ -1128,18 +1122,21 @@ mod tests {
log::info!("Wrap width: {:?}", wrap_width);
let buffer = cx.add_model(|cx| {
let buffer = if rng.gen() {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
log::info!("initial buffer text: {:?}", text);
Buffer::new(0, text, cx)
});
let mut buffer_snapshot = buffer.read(cx).snapshot();
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)
};
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
let (fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
let (wrap_map, wraps_snapshot) =
WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx);
let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot);
let mut block_map = BlockMap::new(wraps_snapshot);
let mut expected_blocks = Vec::new();
for _ in 0..operations {
@@ -1155,10 +1152,10 @@ mod tests {
wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
}
20..=39 => {
let block_count = rng.gen_range(1..=1);
let block_count = rng.gen_range(1..=5);
let block_properties = (0..block_count)
.map(|_| {
let buffer = buffer.read(cx);
let buffer = buffer.read(cx).read(cx);
let position = buffer.anchor_after(
buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left),
);
@@ -1172,7 +1169,7 @@ mod tests {
log::info!(
"inserting block {:?} {:?} with height {}",
disposition,
position.to_point(buffer),
position.to_point(&buffer),
height
);
BlockProperties {
@@ -1190,8 +1187,8 @@ mod tests {
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits, cx);
let block_ids = block_map.insert(block_properties.clone(), cx);
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
let block_ids = block_map.insert(block_properties.clone());
for (block_id, props) in block_ids.into_iter().zip(block_properties) {
expected_blocks.push((block_id, props));
}
@@ -1212,17 +1209,17 @@ mod tests {
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits, cx);
block_map.remove(block_ids_to_remove, cx);
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
block_map.remove(block_ids_to_remove);
}
_ => {
buffer.update(cx, |buffer, cx| {
let v0 = buffer.version();
let edit_count = rng.gen_range(1..=5);
let subscription = buffer.subscribe();
buffer.randomly_edit(&mut rng, edit_count, cx);
log::info!("buffer text: {:?}", buffer.text());
buffer_edits.extend(buffer.edits_since(&v0));
buffer_snapshot = buffer.snapshot();
buffer_snapshot = buffer.snapshot(cx);
buffer_edits.extend(subscription.consume());
log::info!("buffer text: {:?}", buffer_snapshot.text());
});
}
}
@@ -1232,26 +1229,25 @@ mod tests {
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
let mut blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits, cx);
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
assert_eq!(
blocks_snapshot.transforms.summary().input_rows,
wraps_snapshot.max_point().row() + 1
);
log::info!("blocks text: {:?}", blocks_snapshot.text());
let buffer = buffer.read(cx);
let mut sorted_blocks = expected_blocks
.iter()
.cloned()
.map(|(id, block)| {
let mut position = block.position.to_point(buffer);
let mut position = block.position.to_point(&buffer_snapshot);
let column = wraps_snapshot.from_point(position, Bias::Left).column();
match block.disposition {
BlockDisposition::Above => {
position.column = 0;
}
BlockDisposition::Below => {
position.column = buffer.line_len(position.row);
position.column = buffer_snapshot.line_len(position.row);
}
};
let row = wraps_snapshot.from_point(position, Bias::Left).row();
@@ -1270,6 +1266,7 @@ mod tests {
.sort_unstable_by_key(|(id, block)| (block.position.row, block.disposition, *id));
let mut sorted_blocks = sorted_blocks.into_iter().peekable();
let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::<Vec<_>>();
let mut expected_buffer_rows = Vec::new();
let mut expected_text = String::new();
let input_text = wraps_snapshot.text();
@@ -1279,9 +1276,9 @@ mod tests {
expected_text.push('\n');
}
let buffer_row = wraps_snapshot
let buffer_row = input_buffer_rows[wraps_snapshot
.to_point(WrapPoint::new(row, 0), Bias::Left)
.row;
.row as usize];
while let Some((_, block)) = sorted_blocks.peek() {
if block.position.row == row && block.disposition == BlockDisposition::Above {
@@ -1297,7 +1294,7 @@ mod tests {
}
let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0;
expected_buffer_rows.push(if soft_wrapped { None } else { Some(buffer_row) });
expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });
expected_text.push_str(input_line);
while let Some((_, block)) = sorted_blocks.peek() {
@@ -1377,16 +1374,30 @@ mod tests {
let mut block_point = BlockPoint::new(0, 0);
for c in expected_text.chars() {
let left_point = blocks_snapshot.clip_point(block_point, Bias::Left);
let right_point = blocks_snapshot.clip_point(block_point, Bias::Right);
let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left);
assert_eq!(
blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)),
left_point
);
assert_eq!(
left_buffer_point,
buffer_snapshot.clip_point(left_buffer_point, Bias::Right),
"{:?} is not valid in buffer coordinates",
left_point
);
let right_point = blocks_snapshot.clip_point(block_point, Bias::Right);
let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right);
assert_eq!(
blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)),
right_point
);
assert_eq!(
right_buffer_point,
buffer_snapshot.clip_point(right_buffer_point, Bias::Left),
"{:?} is not valid in buffer coordinates",
right_point
);
if c == '\n' {
block_point.0 += Point::new(1, 0);
@@ -1396,4 +1407,10 @@ mod tests {
}
}
}
impl BlockSnapshot {
fn to_point(&self, point: BlockPoint, bias: Bias) -> Point {
self.wrap_snapshot.to_point(self.to_wrap_point(point), bias)
}
}
}
+196 -166
View File
@@ -1,19 +1,20 @@
use language::{
Anchor, AnchorRangeExt, Chunk, Edit, Point, PointUtf16, Snapshot as BufferSnapshot,
TextSummary, ToOffset,
use crate::{
multi_buffer::MultiBufferRows, Anchor, AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot,
ToOffset,
};
use language::{Chunk, Edit, Point, PointUtf16, TextSummary};
use parking_lot::Mutex;
use std::{
cmp::{self, Ordering},
iter,
ops::Range,
ops::{Range, Sub},
sync::atomic::{AtomicUsize, Ordering::SeqCst},
};
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
use theme::SyntaxTheme;
pub trait ToFoldPoint {
fn to_fold_point(&self, snapshot: &Snapshot, bias: Bias) -> FoldPoint;
fn to_fold_point(&self, snapshot: &FoldSnapshot, bias: Bias) -> FoldPoint;
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
@@ -41,23 +42,23 @@ impl FoldPoint {
&mut self.0.column
}
pub fn to_buffer_point(&self, snapshot: &Snapshot) -> Point {
pub fn to_buffer_point(&self, snapshot: &FoldSnapshot) -> Point {
let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>();
cursor.seek(self, Bias::Right, &());
let overshoot = self.0 - cursor.start().0 .0;
cursor.start().1 + overshoot
}
pub fn to_buffer_offset(&self, snapshot: &Snapshot) -> usize {
pub fn to_buffer_offset(&self, snapshot: &FoldSnapshot) -> usize {
let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>();
cursor.seek(self, Bias::Right, &());
let overshoot = self.0 - cursor.start().0 .0;
snapshot
.buffer_snapshot
.to_offset(cursor.start().1 + overshoot)
.point_to_offset(cursor.start().1 + overshoot)
}
pub fn to_offset(&self, snapshot: &Snapshot) -> FoldOffset {
pub fn to_offset(&self, snapshot: &FoldSnapshot) -> FoldOffset {
let mut cursor = snapshot
.transforms
.cursor::<(FoldPoint, TransformSummary)>();
@@ -69,7 +70,7 @@ impl FoldPoint {
assert!(transform.output_text.is_none());
let end_buffer_offset = snapshot
.buffer_snapshot
.to_offset(cursor.start().1.input.lines + overshoot);
.point_to_offset(cursor.start().1.input.lines + overshoot);
offset += end_buffer_offset - cursor.start().1.input.bytes;
}
FoldOffset(offset)
@@ -77,7 +78,7 @@ impl FoldPoint {
}
impl ToFoldPoint for Point {
fn to_fold_point(&self, snapshot: &Snapshot, bias: Bias) -> FoldPoint {
fn to_fold_point(&self, snapshot: &FoldSnapshot, bias: Bias) -> FoldPoint {
let mut cursor = snapshot.transforms.cursor::<(Point, FoldPoint)>();
cursor.seek(self, Bias::Right, &());
if cursor.item().map_or(false, |t| t.is_fold()) {
@@ -102,7 +103,7 @@ impl<'a> FoldMapWriter<'a> {
pub fn fold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
) -> (Snapshot, Vec<FoldEdit>) {
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let mut folds = Vec::new();
let buffer = self.0.buffer.lock().clone();
@@ -133,7 +134,7 @@ impl<'a> FoldMapWriter<'a> {
consolidate_buffer_edits(&mut edits);
let edits = self.0.sync(buffer.clone(), edits);
let snapshot = Snapshot {
let snapshot = FoldSnapshot {
transforms: self.0.transforms.lock().clone(),
folds: self.0.folds.clone(),
buffer_snapshot: buffer,
@@ -145,7 +146,7 @@ impl<'a> FoldMapWriter<'a> {
pub fn unfold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
) -> (Snapshot, Vec<FoldEdit>) {
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let mut fold_ixs_to_delete = Vec::new();
let buffer = self.0.buffer.lock().clone();
@@ -154,10 +155,12 @@ impl<'a> FoldMapWriter<'a> {
let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, true);
while let Some(fold) = folds_cursor.item() {
let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer);
edits.push(text::Edit {
old: offset_range.clone(),
new: offset_range,
});
if offset_range.end > offset_range.start {
edits.push(text::Edit {
old: offset_range.clone(),
new: offset_range,
});
}
fold_ixs_to_delete.push(*folds_cursor.start());
folds_cursor.next(&buffer);
}
@@ -179,7 +182,7 @@ impl<'a> FoldMapWriter<'a> {
consolidate_buffer_edits(&mut edits);
let edits = self.0.sync(buffer.clone(), edits);
let snapshot = Snapshot {
let snapshot = FoldSnapshot {
transforms: self.0.transforms.lock().clone(),
folds: self.0.folds.clone(),
buffer_snapshot: buffer,
@@ -190,14 +193,14 @@ impl<'a> FoldMapWriter<'a> {
}
pub struct FoldMap {
buffer: Mutex<BufferSnapshot>,
buffer: Mutex<MultiBufferSnapshot>,
transforms: Mutex<SumTree<Transform>>,
folds: SumTree<Fold>,
version: AtomicUsize,
}
impl FoldMap {
pub fn new(buffer: BufferSnapshot) -> (Self, Snapshot) {
pub fn new(buffer: MultiBufferSnapshot) -> (Self, FoldSnapshot) {
let this = Self {
buffer: Mutex::new(buffer.clone()),
folds: Default::default(),
@@ -214,7 +217,7 @@ impl FoldMap {
version: Default::default(),
};
let snapshot = Snapshot {
let snapshot = FoldSnapshot {
transforms: this.transforms.lock().clone(),
folds: this.folds.clone(),
buffer_snapshot: this.buffer.lock().clone(),
@@ -225,12 +228,12 @@ impl FoldMap {
pub fn read(
&self,
buffer: BufferSnapshot,
buffer: MultiBufferSnapshot,
edits: Vec<Edit<usize>>,
) -> (Snapshot, Vec<FoldEdit>) {
) -> (FoldSnapshot, Vec<FoldEdit>) {
let edits = self.sync(buffer, edits);
self.check_invariants();
let snapshot = Snapshot {
let snapshot = FoldSnapshot {
transforms: self.transforms.lock().clone(),
folds: self.folds.clone(),
buffer_snapshot: self.buffer.lock().clone(),
@@ -241,9 +244,9 @@ impl FoldMap {
pub fn write(
&mut self,
buffer: BufferSnapshot,
buffer: MultiBufferSnapshot,
edits: Vec<Edit<usize>>,
) -> (FoldMapWriter, Snapshot, Vec<FoldEdit>) {
) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
let (snapshot, edits) = self.read(buffer, edits);
(FoldMapWriter(self), snapshot, edits)
}
@@ -260,7 +263,7 @@ impl FoldMap {
fn sync(
&self,
new_buffer: BufferSnapshot,
new_buffer: MultiBufferSnapshot,
buffer_edits: Vec<text::Edit<usize>>,
) -> Vec<FoldEdit> {
if buffer_edits.is_empty() {
@@ -457,8 +460,8 @@ impl FoldMap {
new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0);
fold_edits.push(FoldEdit {
old_bytes: FoldOffset(old_start)..FoldOffset(old_end),
new_bytes: FoldOffset(new_start)..FoldOffset(new_end),
old: FoldOffset(old_start)..FoldOffset(old_end),
new: FoldOffset(new_start)..FoldOffset(new_end),
});
}
@@ -474,14 +477,18 @@ impl FoldMap {
}
#[derive(Clone)]
pub struct Snapshot {
pub struct FoldSnapshot {
transforms: SumTree<Transform>,
folds: SumTree<Fold>,
buffer_snapshot: language::Snapshot,
buffer_snapshot: MultiBufferSnapshot,
pub version: usize,
}
impl Snapshot {
impl FoldSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
&self.buffer_snapshot
}
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(FoldOffset(0)..self.len(), None)
@@ -553,15 +560,24 @@ impl Snapshot {
(line_end - line_start) as u32
}
pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
pub fn buffer_rows(&self, start_row: u32) -> FoldBufferRows {
if start_row > self.transforms.summary().output.lines.row {
panic!("invalid display row {}", start_row);
}
let fold_point = FoldPoint::new(start_row, 0);
let mut cursor = self.transforms.cursor();
let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
cursor.seek(&fold_point, Bias::Left, &());
BufferRows { fold_point, cursor }
let overshoot = fold_point.0 - cursor.start().0 .0;
let buffer_point = cursor.start().1 + overshoot;
let input_buffer_rows = self.buffer_snapshot.buffer_rows(buffer_point.row);
FoldBufferRows {
fold_point,
input_buffer_rows,
cursor,
}
}
pub fn max_point(&self) -> FoldPoint {
@@ -624,7 +640,7 @@ impl Snapshot {
&'a self,
range: Range<FoldOffset>,
theme: Option<&'a SyntaxTheme>,
) -> Chunks<'a> {
) -> FoldChunks<'a> {
let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>();
transform_cursor.seek(&range.end, Bias::Right, &());
@@ -635,7 +651,7 @@ impl Snapshot {
let overshoot = range.start.0 - transform_cursor.start().0 .0;
let buffer_start = transform_cursor.start().1 + overshoot;
Chunks {
FoldChunks {
transform_cursor,
buffer_chunks: self.buffer_snapshot.chunks(buffer_start..buffer_end, theme),
buffer_chunk: None,
@@ -687,11 +703,7 @@ impl Snapshot {
let buffer_position = cursor.start().1 + overshoot;
let clipped_buffer_position =
self.buffer_snapshot.clip_point(buffer_position, bias);
FoldPoint::new(
point.row(),
((point.column() as i32) + clipped_buffer_position.column as i32
- buffer_position.column as i32) as u32,
)
FoldPoint(cursor.start().0 .0 + (clipped_buffer_position - cursor.start().1))
}
} else {
FoldPoint(self.transforms.summary().output.lines)
@@ -700,7 +712,7 @@ impl Snapshot {
}
fn intersecting_folds<'a, T>(
buffer: &'a text::Snapshot,
buffer: &'a MultiBufferSnapshot,
folds: &'a SumTree<Fold>,
range: Range<T>,
inclusive: bool,
@@ -750,20 +762,20 @@ fn consolidate_buffer_edits(edits: &mut Vec<text::Edit<usize>>) {
fn consolidate_fold_edits(edits: &mut Vec<FoldEdit>) {
edits.sort_unstable_by(|a, b| {
a.old_bytes
a.old
.start
.cmp(&b.old_bytes.start)
.then_with(|| b.old_bytes.end.cmp(&a.old_bytes.end))
.cmp(&b.old.start)
.then_with(|| b.old.end.cmp(&a.old.end))
});
let mut i = 1;
while i < edits.len() {
let edit = edits[i].clone();
let prev_edit = &mut edits[i - 1];
if prev_edit.old_bytes.end >= edit.old_bytes.start {
prev_edit.old_bytes.end = prev_edit.old_bytes.end.max(edit.old_bytes.end);
prev_edit.new_bytes.start = prev_edit.new_bytes.start.min(edit.new_bytes.start);
prev_edit.new_bytes.end = prev_edit.new_bytes.end.max(edit.new_bytes.end);
if prev_edit.old.end >= edit.old.start {
prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
edits.remove(i);
continue;
}
@@ -851,9 +863,9 @@ impl Default for FoldSummary {
}
impl sum_tree::Summary for FoldSummary {
type Context = text::Snapshot;
type Context = MultiBufferSnapshot;
fn add_summary(&mut self, other: &Self, buffer: &text::Snapshot) {
fn add_summary(&mut self, other: &Self, buffer: &MultiBufferSnapshot) {
if other.min_start.cmp(&self.min_start, buffer).unwrap() == Ordering::Less {
self.min_start = other.min_start.clone();
}
@@ -877,62 +889,66 @@ impl sum_tree::Summary for FoldSummary {
}
impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold {
fn add_summary(&mut self, summary: &'a FoldSummary, _: &text::Snapshot) {
fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
self.0.start = summary.start.clone();
self.0.end = summary.end.clone();
}
}
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold {
fn cmp(&self, other: &Self, buffer: &text::Snapshot) -> Ordering {
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
self.0.cmp(&other.0, buffer).unwrap()
}
}
impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
fn add_summary(&mut self, summary: &'a FoldSummary, _: &text::Snapshot) {
fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
*self += summary.count;
}
}
pub struct BufferRows<'a> {
pub struct FoldBufferRows<'a> {
cursor: Cursor<'a, Transform, (FoldPoint, Point)>,
input_buffer_rows: MultiBufferRows<'a>,
fold_point: FoldPoint,
}
impl<'a> Iterator for BufferRows<'a> {
type Item = u32;
impl<'a> Iterator for FoldBufferRows<'a> {
type Item = Option<u32>;
fn next(&mut self) -> Option<Self::Item> {
let mut traversed_fold = false;
while self.fold_point > self.cursor.end(&()).0 {
self.cursor.next(&());
traversed_fold = true;
if self.cursor.item().is_none() {
// TODO: Return a bool from next?
break;
}
}
if self.cursor.item().is_some() {
let overshoot = self.fold_point.0 - self.cursor.start().0 .0;
let buffer_point = self.cursor.start().1 + overshoot;
if traversed_fold {
self.input_buffer_rows.seek(self.cursor.start().1.row);
self.input_buffer_rows.next();
}
*self.fold_point.row_mut() += 1;
Some(buffer_point.row)
self.input_buffer_rows.next()
} else {
None
}
}
}
pub struct Chunks<'a> {
pub struct FoldChunks<'a> {
transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>,
buffer_chunks: language::Chunks<'a>,
buffer_chunks: MultiBufferChunks<'a>,
buffer_chunk: Option<(usize, Chunk<'a>)>,
buffer_offset: usize,
output_offset: usize,
max_output_offset: usize,
}
impl<'a> Iterator for Chunks<'a> {
impl<'a> Iterator for FoldChunks<'a> {
type Item = Chunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
@@ -1006,7 +1022,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint {
pub struct FoldOffset(pub usize);
impl FoldOffset {
pub fn to_point(&self, snapshot: &Snapshot) -> FoldPoint {
pub fn to_point(&self, snapshot: &FoldSnapshot) -> FoldPoint {
let mut cursor = snapshot
.transforms
.cursor::<(FoldOffset, TransformSummary)>();
@@ -1015,13 +1031,21 @@ impl FoldOffset {
Point::new(0, (self.0 - cursor.start().0 .0) as u32)
} else {
let buffer_offset = cursor.start().1.input.bytes + self.0 - cursor.start().0 .0;
let buffer_point = snapshot.buffer_snapshot.to_point(buffer_offset);
let buffer_point = snapshot.buffer_snapshot.offset_to_point(buffer_offset);
buffer_point - cursor.start().1.input.lines
};
FoldPoint(cursor.start().1.output.lines + overshoot)
}
}
impl Sub for FoldOffset {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldOffset {
fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
self.0 += &summary.output.bytes;
@@ -1040,41 +1064,23 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize {
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FoldEdit {
pub old_bytes: Range<FoldOffset>,
pub new_bytes: Range<FoldOffset>,
}
#[cfg(test)]
impl FoldEdit {
pub fn delta(&self) -> isize {
self.inserted_bytes() as isize - self.deleted_bytes() as isize
}
pub fn deleted_bytes(&self) -> usize {
self.old_bytes.end.0 - self.old_bytes.start.0
}
pub fn inserted_bytes(&self) -> usize {
self.new_bytes.end.0 - self.new_bytes.start.0
}
}
pub type FoldEdit = Edit<FoldOffset>;
#[cfg(test)]
mod tests {
use super::*;
use crate::{test::sample_text, ToPoint};
use language::Buffer;
use crate::{MultiBuffer, ToPoint};
use rand::prelude::*;
use std::{env, mem};
use text::RandomCharIter;
use util::test::sample_text;
use Bias::{Left, Right};
#[gpui::test]
fn test_basic_folds(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6), cx));
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let mut map = FoldMap::new(buffer_snapshot.clone()).0;
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
@@ -1087,18 +1093,17 @@ mod tests {
edits,
&[
FoldEdit {
old_bytes: FoldOffset(2)..FoldOffset(16),
new_bytes: FoldOffset(2)..FoldOffset(5),
old: FoldOffset(2)..FoldOffset(16),
new: FoldOffset(2)..FoldOffset(5),
},
FoldEdit {
old_bytes: FoldOffset(18)..FoldOffset(29),
new_bytes: FoldOffset(7)..FoldOffset(10)
old: FoldOffset(18)..FoldOffset(29),
new: FoldOffset(7)..FoldOffset(10)
},
]
);
let (buffer_snapshot, edits) = buffer.update(cx, |buffer, cx| {
let v0 = buffer.version();
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
buffer.edit(
vec![
Point::new(0, 0)..Point::new(0, 1),
@@ -1107,30 +1112,30 @@ mod tests {
"123",
cx,
);
(buffer.snapshot(), buffer.edits_since(&v0).collect())
buffer.snapshot(cx)
});
let (snapshot3, edits) = map.read(buffer_snapshot.clone(), edits);
let (snapshot3, edits) =
map.read(buffer_snapshot.clone(), subscription.consume().into_inner());
assert_eq!(snapshot3.text(), "123a…c123c…eeeee");
assert_eq!(
edits,
&[
FoldEdit {
old_bytes: FoldOffset(0)..FoldOffset(1),
new_bytes: FoldOffset(0)..FoldOffset(3),
old: FoldOffset(0)..FoldOffset(1),
new: FoldOffset(0)..FoldOffset(3),
},
FoldEdit {
old_bytes: FoldOffset(6)..FoldOffset(6),
new_bytes: FoldOffset(8)..FoldOffset(11),
old: FoldOffset(6)..FoldOffset(6),
new: FoldOffset(8)..FoldOffset(11),
},
]
);
let (buffer_snapshot, edits) = buffer.update(cx, |buffer, cx| {
let v0 = buffer.version();
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
buffer.edit(vec![Point::new(2, 6)..Point::new(4, 3)], "456", cx);
(buffer.snapshot(), buffer.edits_since(&v0).collect())
buffer.snapshot(cx)
});
let (snapshot4, _) = map.read(buffer_snapshot.clone(), edits);
let (snapshot4, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner());
assert_eq!(snapshot4.text(), "123a…c123456eee");
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
@@ -1141,8 +1146,9 @@ mod tests {
#[gpui::test]
fn test_adjacent_folds(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "abcdefghijkl", cx));
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
{
let mut map = FoldMap::new(buffer_snapshot.clone()).0;
@@ -1175,20 +1181,20 @@ mod tests {
assert_eq!(snapshot.text(), "…fghijkl");
// Edit within one of the folds.
let (buffer_snapshot, edits) = buffer.update(cx, |buffer, cx| {
let v0 = buffer.version();
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
buffer.edit(vec![0..1], "12345", cx);
(buffer.snapshot(), buffer.edits_since(&v0).collect())
buffer.snapshot(cx)
});
let (snapshot, _) = map.read(buffer_snapshot.clone(), edits);
let (snapshot, _) =
map.read(buffer_snapshot.clone(), subscription.consume().into_inner());
assert_eq!(snapshot.text(), "12345…fghijkl");
}
}
#[gpui::test]
fn test_overlapping_folds(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6), cx));
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let mut map = FoldMap::new(buffer_snapshot.clone()).0;
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.fold(vec![
@@ -1203,8 +1209,9 @@ mod tests {
#[gpui::test]
fn test_merging_folds_via_edit(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6), cx));
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let mut map = FoldMap::new(buffer_snapshot.clone()).0;
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
@@ -1215,21 +1222,19 @@ mod tests {
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee");
let (buffer_snapshot, edits) = buffer.update(cx, |buffer, cx| {
let v0 = buffer.version();
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
buffer.edit(Some(Point::new(2, 2)..Point::new(3, 1)), "", cx);
(buffer.snapshot(), buffer.edits_since(&v0).collect())
buffer.snapshot(cx)
});
let (snapshot, _) = map.read(buffer_snapshot.clone(), edits);
let (snapshot, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner());
assert_eq!(snapshot.text(), "aa…eeeee");
}
#[gpui::test]
fn test_folds_in_range(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(5, 6), cx));
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let mut map = FoldMap::new(buffer_snapshot.clone()).0;
let buffer = buffer.read(cx);
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.fold(vec![
@@ -1241,7 +1246,7 @@ mod tests {
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
let fold_ranges = snapshot
.folds_in_range(Point::new(1, 0)..Point::new(1, 3))
.map(|fold| fold.start.to_point(buffer)..fold.end.to_point(buffer))
.map(|fold| fold.start.to_point(&buffer_snapshot)..fold.end.to_point(&buffer_snapshot))
.collect::<Vec<_>>();
assert_eq!(
fold_ranges,
@@ -1258,55 +1263,73 @@ mod tests {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let buffer = cx.add_model(|cx| {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
Buffer::new(0, text, cx)
});
let buffer_snapshot = buffer.read(cx).snapshot();
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
let buffer = if rng.gen() {
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)
};
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
let mut map = FoldMap::new(buffer_snapshot.clone()).0;
let (mut initial_snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
let mut snapshot_edits = Vec::new();
for _ in 0..operations {
log::info!("text: {:?}", buffer.read(cx).text());
let buffer_edits = match rng.gen_range(0..=100) {
log::info!("text: {:?}", buffer_snapshot.text());
let mut buffer_edits = Vec::new();
match rng.gen_range(0..=100) {
0..=59 => {
snapshot_edits.extend(map.randomly_mutate(&mut rng));
vec![]
}
_ => buffer.update(cx, |buffer, cx| {
let start_version = buffer.version.clone();
let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5);
buffer.randomly_edit(&mut rng, edit_count, cx);
let edits = buffer
.edits_since::<Point>(&start_version)
.collect::<Vec<_>>();
buffer_snapshot = buffer.snapshot(cx);
let edits = subscription.consume().into_inner();
log::info!("editing {:?}", edits);
buffer.edits_since::<usize>(&start_version).collect()
buffer_edits.extend(edits);
}),
};
let buffer_snapshot = buffer.read(cx).snapshot();
let (snapshot, edits) = map.read(buffer_snapshot.clone(), buffer_edits);
snapshot_edits.push((snapshot.clone(), edits));
let mut expected_text: String = buffer_snapshot.text().to_string();
let mut expected_buffer_rows = Vec::new();
let mut next_row = buffer_snapshot.max_point().row;
for fold_range in map.merged_fold_ranges().into_iter().rev() {
let fold_start = buffer_snapshot.point_for_offset(fold_range.start).unwrap();
let fold_end = buffer_snapshot.point_for_offset(fold_range.end).unwrap();
expected_buffer_rows.extend((fold_end.row + 1..=next_row).rev());
next_row = fold_start.row;
expected_text.replace_range(fold_range.start..fold_range.end, "");
}
expected_buffer_rows.extend((0..=next_row).rev());
expected_buffer_rows.reverse();
assert_eq!(snapshot.text(), expected_text);
log::info!(
"fold text {:?} ({} lines)",
expected_text,
expected_text.matches('\n').count() + 1
);
let mut prev_row = 0;
let mut expected_buffer_rows = Vec::new();
for fold_range in map.merged_fold_ranges().into_iter() {
let fold_start = buffer_snapshot.offset_to_point(fold_range.start).row;
let fold_end = buffer_snapshot.offset_to_point(fold_range.end).row;
expected_buffer_rows.extend(
buffer_snapshot
.buffer_rows(prev_row)
.take((1 + fold_start - prev_row) as usize),
);
prev_row = 1 + fold_end;
}
expected_buffer_rows.extend(buffer_snapshot.buffer_rows(prev_row));
assert_eq!(
expected_buffer_rows.len(),
expected_text.matches('\n').count() + 1,
"wrong expected buffer rows {:?}. text: {:?}",
expected_buffer_rows,
expected_text
);
for (output_row, line) in expected_text.lines().enumerate() {
let line_len = snapshot.line_len(output_row as u32);
@@ -1375,7 +1398,6 @@ mod tests {
}
let text = &expected_text[start.0..end.0];
log::info!("slicing {:?}..{:?} (text: {:?})", start, end, text);
assert_eq!(
snapshot
.chunks(start..end, None)
@@ -1385,14 +1407,19 @@ mod tests {
);
}
for (idx, buffer_row) in expected_buffer_rows.iter().enumerate() {
let fold_row = Point::new(*buffer_row, 0)
.to_fold_point(&snapshot, Right)
let mut fold_row = 0;
while fold_row < expected_buffer_rows.len() as u32 {
fold_row = snapshot
.clip_point(FoldPoint::new(fold_row, 0), Bias::Right)
.row();
eprintln!("fold_row: {} of {}", fold_row, expected_buffer_rows.len());
assert_eq!(
snapshot.buffer_rows(fold_row).collect::<Vec<_>>(),
expected_buffer_rows[idx..],
expected_buffer_rows[(fold_row as usize)..],
"wrong buffer rows starting at fold row {}",
fold_row,
);
fold_row += 1;
}
for fold_range in map.merged_fold_ranges() {
@@ -1454,12 +1481,9 @@ mod tests {
let mut text = initial_snapshot.text();
for (snapshot, edits) in snapshot_edits.drain(..) {
let new_text = snapshot.text();
let mut delta = 0isize;
for edit in edits {
let old_bytes = ((edit.old_bytes.start.0 as isize) + delta) as usize
..((edit.old_bytes.end.0 as isize) + delta) as usize;
let new_bytes = edit.new_bytes.start.0..edit.new_bytes.end.0;
delta += edit.delta();
let old_bytes = edit.new.start.0..edit.new.start.0 + edit.old_len().0;
let new_bytes = edit.new.start.0..edit.new.end.0;
text.replace_range(old_bytes, &new_text[new_bytes]);
}
@@ -1471,10 +1495,10 @@ mod tests {
#[gpui::test]
fn test_buffer_rows(cx: &mut gpui::MutableAppContext) {
let text = sample_text(6, 6) + "\n";
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let text = sample_text(6, 6, 'a') + "\n";
let buffer = MultiBuffer::build_simple(&text, cx);
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let mut map = FoldMap::new(buffer_snapshot.clone()).0;
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
@@ -1485,8 +1509,11 @@ mod tests {
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee\nffffff\n");
assert_eq!(snapshot.buffer_rows(0).collect::<Vec<_>>(), [0, 3, 5, 6]);
assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [6]);
assert_eq!(
snapshot.buffer_rows(0).collect::<Vec<_>>(),
[Some(0), Some(3), Some(5), Some(6)]
);
assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [Some(6)]);
}
impl FoldMap {
@@ -1519,7 +1546,10 @@ mod tests {
merged_ranges
}
pub fn randomly_mutate(&mut self, rng: &mut impl Rng) -> Vec<(Snapshot, Vec<FoldEdit>)> {
pub fn randomly_mutate(
&mut self,
rng: &mut impl Rng,
) -> Vec<(FoldSnapshot, Vec<FoldEdit>)> {
let mut snapshot_edits = Vec::new();
match rng.gen_range(0..=100) {
0..=39 if !self.folds.is_empty() => {
+50 -58
View File
@@ -1,4 +1,5 @@
use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot, ToFoldPoint};
use super::fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot, ToFoldPoint};
use crate::MultiBufferSnapshot;
use language::{rope, Chunk};
use parking_lot::Mutex;
use std::{cmp, mem, ops::Range};
@@ -6,11 +7,11 @@ use sum_tree::Bias;
use text::Point;
use theme::SyntaxTheme;
pub struct TabMap(Mutex<Snapshot>);
pub struct TabMap(Mutex<TabSnapshot>);
impl TabMap {
pub fn new(input: FoldSnapshot, tab_size: usize) -> (Self, Snapshot) {
let snapshot = Snapshot {
pub fn new(input: FoldSnapshot, tab_size: usize) -> (Self, TabSnapshot) {
let snapshot = TabSnapshot {
fold_snapshot: input,
tab_size,
};
@@ -21,10 +22,10 @@ impl TabMap {
&self,
fold_snapshot: FoldSnapshot,
mut fold_edits: Vec<FoldEdit>,
) -> (Snapshot, Vec<Edit>) {
) -> (TabSnapshot, Vec<TabEdit>) {
let mut old_snapshot = self.0.lock();
let max_offset = old_snapshot.fold_snapshot.len();
let new_snapshot = Snapshot {
let new_snapshot = TabSnapshot {
fold_snapshot,
tab_size: old_snapshot.tab_size,
};
@@ -34,13 +35,13 @@ impl TabMap {
let mut delta = 0;
for chunk in old_snapshot
.fold_snapshot
.chunks(fold_edit.old_bytes.end..max_offset, None)
.chunks(fold_edit.old.end..max_offset, None)
{
let patterns: &[_] = &['\t', '\n'];
if let Some(ix) = chunk.text.find(patterns) {
if &chunk.text[ix..ix + 1] == "\t" {
fold_edit.old_bytes.end.0 += delta + ix + 1;
fold_edit.new_bytes.end.0 += delta + ix + 1;
fold_edit.old.end.0 += delta + ix + 1;
fold_edit.new.end.0 += delta + ix + 1;
}
break;
@@ -55,9 +56,9 @@ impl TabMap {
let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
let prev_edit = prev_edits.last_mut().unwrap();
let edit = &next_edits[0];
if prev_edit.old_bytes.end >= edit.old_bytes.start {
prev_edit.old_bytes.end = edit.old_bytes.end;
prev_edit.new_bytes.end = edit.new_bytes.end;
if prev_edit.old.end >= edit.old.start {
prev_edit.old.end = edit.old.end;
prev_edit.new.end = edit.new.end;
fold_edits.remove(ix);
} else {
ix += 1;
@@ -65,25 +66,13 @@ impl TabMap {
}
for fold_edit in fold_edits {
let old_start = fold_edit
.old_bytes
.start
.to_point(&old_snapshot.fold_snapshot);
let old_end = fold_edit
.old_bytes
.end
.to_point(&old_snapshot.fold_snapshot);
let new_start = fold_edit
.new_bytes
.start
.to_point(&new_snapshot.fold_snapshot);
let new_end = fold_edit
.new_bytes
.end
.to_point(&new_snapshot.fold_snapshot);
tab_edits.push(Edit {
old_lines: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
new_lines: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
tab_edits.push(TabEdit {
old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
});
}
@@ -93,12 +82,16 @@ impl TabMap {
}
#[derive(Clone)]
pub struct Snapshot {
pub struct TabSnapshot {
pub fold_snapshot: FoldSnapshot,
pub tab_size: usize,
}
impl Snapshot {
impl TabSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
self.fold_snapshot.buffer_snapshot()
}
pub fn text_summary(&self) -> TextSummary {
self.text_summary_for_range(TabPoint::zero()..self.max_point())
}
@@ -155,7 +148,7 @@ impl Snapshot {
&'a self,
range: Range<TabPoint>,
theme: Option<&'a SyntaxTheme>,
) -> Chunks<'a> {
) -> TabChunks<'a> {
let (input_start, expanded_char_column, to_next_stop) =
self.to_fold_point(range.start, Bias::Left);
let input_start = input_start.to_offset(&self.fold_snapshot);
@@ -169,7 +162,7 @@ impl Snapshot {
to_next_stop
};
Chunks {
TabChunks {
fold_chunks: self.fold_snapshot.chunks(input_start..input_end, theme),
column: expanded_char_column,
output_position: range.start.0,
@@ -183,7 +176,7 @@ impl Snapshot {
}
}
pub fn buffer_rows(&self, row: u32) -> fold_map::BufferRows {
pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows {
self.fold_snapshot.buffer_rows(row)
}
@@ -322,11 +315,7 @@ impl From<super::Point> for TabPoint {
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Edit {
pub old_lines: Range<TabPoint>,
pub new_lines: Range<TabPoint>,
}
pub type TabEdit = text::Edit<TabPoint>;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TextSummary {
@@ -380,8 +369,8 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
// Handles a tab width <= 16
const SPACES: &'static str = " ";
pub struct Chunks<'a> {
fold_chunks: fold_map::Chunks<'a>,
pub struct TabChunks<'a> {
fold_chunks: fold_map::FoldChunks<'a>,
chunk: Chunk<'a>,
column: usize,
output_position: Point,
@@ -390,7 +379,7 @@ pub struct Chunks<'a> {
skip_leading_tab: bool,
}
impl<'a> Iterator for Chunks<'a> {
impl<'a> Iterator for TabChunks<'a> {
type Item = Chunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
@@ -450,28 +439,29 @@ impl<'a> Iterator for Chunks<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::display_map::fold_map::FoldMap;
use language::Buffer;
use crate::{display_map::fold_map::FoldMap, MultiBuffer};
use rand::{prelude::StdRng, Rng};
use text::{RandomCharIter, Rope};
#[test]
fn test_expand_tabs() {
assert_eq!(Snapshot::expand_tabs("\t".chars(), 0, 4), 0);
assert_eq!(Snapshot::expand_tabs("\t".chars(), 1, 4), 4);
assert_eq!(Snapshot::expand_tabs("\ta".chars(), 2, 4), 5);
assert_eq!(TabSnapshot::expand_tabs("\t".chars(), 0, 4), 0);
assert_eq!(TabSnapshot::expand_tabs("\t".chars(), 1, 4), 4);
assert_eq!(TabSnapshot::expand_tabs("\ta".chars(), 2, 4), 5);
}
#[gpui::test(iterations = 100)]
fn test_random(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
fn test_random_tabs(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
let tab_size = rng.gen_range(1..=4);
let buffer = cx.add_model(|cx| {
let len = rng.gen_range(0..30);
let len = rng.gen_range(0..30);
let buffer = if rng.gen() {
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
Buffer::new(0, text, cx)
});
let buffer_snapshot = buffer.read(cx).snapshot();
log::info!("Buffer text: {:?}", buffer.read(cx).text());
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)
};
let buffer_snapshot = buffer.read(cx).snapshot(cx);
log::info!("Buffer text: {:?}", buffer_snapshot.text());
let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone());
fold_map.randomly_mutate(&mut rng);
@@ -502,13 +492,15 @@ mod tests {
.chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0))
.collect::<String>();
let expected_summary = TextSummary::from(expected_text.as_str());
log::info!("slicing {:?}..{:?} (text: {:?})", start, end, text);
assert_eq!(
expected_text,
tabs_snapshot
.chunks(start..end, None)
.map(|c| c.text)
.collect::<String>()
.collect::<String>(),
"chunks({:?}..{:?})",
start,
end
);
let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
+139 -85
View File
@@ -1,24 +1,25 @@
use super::{
fold_map,
tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint},
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
};
use crate::{MultiBufferSnapshot, Point};
use gpui::{
fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext,
Task,
};
use language::{Chunk, Point};
use language::Chunk;
use lazy_static::lazy_static;
use smol::future::yield_now;
use std::{collections::VecDeque, mem, ops::Range, time::Duration};
use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
use sum_tree::{Bias, Cursor, SumTree};
use text::Patch;
use theme::SyntaxTheme;
pub use super::tab_map::TextSummary;
pub type Edit = text::Edit<u32>;
pub type WrapEdit = text::Edit<u32>;
pub struct WrapMap {
snapshot: Snapshot,
snapshot: WrapSnapshot,
pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
interpolated_edits: Patch<u32>,
edits_since_sync: Patch<u32>,
@@ -32,7 +33,7 @@ impl Entity for WrapMap {
}
#[derive(Clone)]
pub struct Snapshot {
pub struct WrapSnapshot {
tab_snapshot: TabSnapshot,
transforms: SumTree<Transform>,
interpolated: bool,
@@ -53,17 +54,17 @@ struct TransformSummary {
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct WrapPoint(pub super::Point);
pub struct Chunks<'a> {
input_chunks: tab_map::Chunks<'a>,
pub struct WrapChunks<'a> {
input_chunks: tab_map::TabChunks<'a>,
input_chunk: Chunk<'a>,
output_position: WrapPoint,
max_output_row: u32,
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
}
pub struct BufferRows<'a> {
input_buffer_rows: fold_map::BufferRows<'a>,
input_buffer_row: u32,
pub struct WrapBufferRows<'a> {
input_buffer_rows: fold_map::FoldBufferRows<'a>,
input_buffer_row: Option<u32>,
output_row: u32,
soft_wrapped: bool,
max_output_row: u32,
@@ -77,7 +78,7 @@ impl WrapMap {
font_size: f32,
wrap_width: Option<f32>,
cx: &mut MutableAppContext,
) -> (ModelHandle<Self>, Snapshot) {
) -> (ModelHandle<Self>, WrapSnapshot) {
let handle = cx.add_model(|cx| {
let mut this = Self {
font: (font_id, font_size),
@@ -85,7 +86,7 @@ impl WrapMap {
pending_edits: Default::default(),
interpolated_edits: Default::default(),
edits_since_sync: Default::default(),
snapshot: Snapshot::new(tab_snapshot),
snapshot: WrapSnapshot::new(tab_snapshot),
background_task: None,
};
this.set_wrap_width(wrap_width, cx);
@@ -106,7 +107,7 @@ impl WrapMap {
tab_snapshot: TabSnapshot,
edits: Vec<TabEdit>,
cx: &mut ModelContext<Self>,
) -> (Snapshot, Vec<Edit>) {
) -> (WrapSnapshot, Vec<WrapEdit>) {
if self.wrap_width.is_some() {
self.pending_edits.push_back((tab_snapshot, edits));
self.flush_edits(cx);
@@ -157,8 +158,8 @@ impl WrapMap {
.update(
tab_snapshot,
&[TabEdit {
old_lines: range.clone(),
new_lines: range.clone(),
old: range.clone(),
new: range.clone(),
}],
wrap_width,
&mut line_wrapper,
@@ -203,7 +204,7 @@ impl WrapMap {
}
let new_rows = self.snapshot.transforms.summary().output.lines.row + 1;
self.snapshot.interpolated = false;
self.edits_since_sync = self.edits_since_sync.compose(&Patch::new(vec![Edit {
self.edits_since_sync = self.edits_since_sync.compose(&Patch::new(vec![WrapEdit {
old: 0..old_rows,
new: 0..new_rows,
}]));
@@ -291,7 +292,7 @@ impl WrapMap {
}
}
impl Snapshot {
impl WrapSnapshot {
fn new(tab_snapshot: TabSnapshot) -> Self {
let mut transforms = SumTree::new();
let extent = tab_snapshot.text_summary();
@@ -305,6 +306,10 @@ impl Snapshot {
}
}
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
self.tab_snapshot.buffer_snapshot()
}
fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch<u32> {
let mut new_transforms;
if tab_edits.is_empty() {
@@ -313,47 +318,44 @@ impl Snapshot {
let mut old_cursor = self.transforms.cursor::<TabPoint>();
let mut tab_edits_iter = tab_edits.iter().peekable();
new_transforms = old_cursor.slice(
&tab_edits_iter.peek().unwrap().old_lines.start,
Bias::Right,
&(),
);
new_transforms =
old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right, &());
while let Some(edit) = tab_edits_iter.next() {
if edit.new_lines.start > TabPoint::from(new_transforms.summary().input.lines) {
if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) {
let summary = new_tab_snapshot.text_summary_for_range(
TabPoint::from(new_transforms.summary().input.lines)..edit.new_lines.start,
TabPoint::from(new_transforms.summary().input.lines)..edit.new.start,
);
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
if !edit.new_lines.is_empty() {
if !edit.new.is_empty() {
new_transforms.push_or_extend(Transform::isomorphic(
new_tab_snapshot.text_summary_for_range(edit.new_lines.clone()),
new_tab_snapshot.text_summary_for_range(edit.new.clone()),
));
}
old_cursor.seek_forward(&edit.old_lines.end, Bias::Right, &());
old_cursor.seek_forward(&edit.old.end, Bias::Right, &());
if let Some(next_edit) = tab_edits_iter.peek() {
if next_edit.old_lines.start > old_cursor.end(&()) {
if old_cursor.end(&()) > edit.old_lines.end {
if next_edit.old.start > old_cursor.end(&()) {
if old_cursor.end(&()) > edit.old.end {
let summary = self
.tab_snapshot
.text_summary_for_range(edit.old_lines.end..old_cursor.end(&()));
.text_summary_for_range(edit.old.end..old_cursor.end(&()));
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
old_cursor.next(&());
new_transforms.push_tree(
old_cursor.slice(&next_edit.old_lines.start, Bias::Right, &()),
old_cursor.slice(&next_edit.old.start, Bias::Right, &()),
&(),
);
}
} else {
if old_cursor.end(&()) > edit.old_lines.end {
if old_cursor.end(&()) > edit.old.end {
let summary = self
.tab_snapshot
.text_summary_for_range(edit.old_lines.end..old_cursor.end(&()));
.text_summary_for_range(edit.old.end..old_cursor.end(&()));
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
old_cursor.next(&());
@@ -364,7 +366,7 @@ impl Snapshot {
let old_snapshot = mem::replace(
self,
Snapshot {
WrapSnapshot {
tab_snapshot: new_tab_snapshot,
transforms: new_transforms,
interpolated: true,
@@ -391,14 +393,14 @@ impl Snapshot {
let mut row_edits = Vec::new();
while let Some(edit) = tab_edits_iter.next() {
let mut row_edit = RowEdit {
old_rows: edit.old_lines.start.row()..edit.old_lines.end.row() + 1,
new_rows: edit.new_lines.start.row()..edit.new_lines.end.row() + 1,
old_rows: edit.old.start.row()..edit.old.end.row() + 1,
new_rows: edit.new.start.row()..edit.new.end.row() + 1,
};
while let Some(next_edit) = tab_edits_iter.peek() {
if next_edit.old_lines.start.row() <= row_edit.old_rows.end {
row_edit.old_rows.end = next_edit.old_lines.end.row() + 1;
row_edit.new_rows.end = next_edit.new_lines.end.row() + 1;
if next_edit.old.start.row() <= row_edit.old_rows.end {
row_edit.old_rows.end = next_edit.old.end.row() + 1;
row_edit.new_rows.end = next_edit.new.end.row() + 1;
tab_edits_iter.next();
} else {
break;
@@ -513,7 +515,7 @@ impl Snapshot {
let old_snapshot = mem::replace(
self,
Snapshot {
WrapSnapshot {
tab_snapshot: new_tab_snapshot,
transforms: new_transforms,
interpolated: false,
@@ -523,33 +525,33 @@ impl Snapshot {
old_snapshot.compute_edits(tab_edits, self)
}
fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &Snapshot) -> Patch<u32> {
fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &WrapSnapshot) -> Patch<u32> {
let mut wrap_edits = Vec::new();
let mut old_cursor = self.transforms.cursor::<TransformSummary>();
let mut new_cursor = new_snapshot.transforms.cursor::<TransformSummary>();
for mut tab_edit in tab_edits.iter().cloned() {
tab_edit.old_lines.start.0.column = 0;
tab_edit.old_lines.end.0 += Point::new(1, 0);
tab_edit.new_lines.start.0.column = 0;
tab_edit.new_lines.end.0 += Point::new(1, 0);
tab_edit.old.start.0.column = 0;
tab_edit.old.end.0 += Point::new(1, 0);
tab_edit.new.start.0.column = 0;
tab_edit.new.end.0 += Point::new(1, 0);
old_cursor.seek(&tab_edit.old_lines.start, Bias::Right, &());
old_cursor.seek(&tab_edit.old.start, Bias::Right, &());
let mut old_start = old_cursor.start().output.lines;
old_start += tab_edit.old_lines.start.0 - old_cursor.start().input.lines;
old_start += tab_edit.old.start.0 - old_cursor.start().input.lines;
old_cursor.seek(&tab_edit.old_lines.end, Bias::Right, &());
old_cursor.seek(&tab_edit.old.end, Bias::Right, &());
let mut old_end = old_cursor.start().output.lines;
old_end += tab_edit.old_lines.end.0 - old_cursor.start().input.lines;
old_end += tab_edit.old.end.0 - old_cursor.start().input.lines;
new_cursor.seek(&tab_edit.new_lines.start, Bias::Right, &());
new_cursor.seek(&tab_edit.new.start, Bias::Right, &());
let mut new_start = new_cursor.start().output.lines;
new_start += tab_edit.new_lines.start.0 - new_cursor.start().input.lines;
new_start += tab_edit.new.start.0 - new_cursor.start().input.lines;
new_cursor.seek(&tab_edit.new_lines.end, Bias::Right, &());
new_cursor.seek(&tab_edit.new.end, Bias::Right, &());
let mut new_end = new_cursor.start().output.lines;
new_end += tab_edit.new_lines.end.0 - new_cursor.start().input.lines;
new_end += tab_edit.new.end.0 - new_cursor.start().input.lines;
wrap_edits.push(Edit {
wrap_edits.push(WrapEdit {
old: old_start.row..old_end.row,
new: new_start.row..new_end.row,
});
@@ -564,7 +566,11 @@ impl Snapshot {
.map(|h| h.text)
}
pub fn chunks<'a>(&'a self, rows: Range<u32>, theme: Option<&'a SyntaxTheme>) -> Chunks<'a> {
pub fn chunks<'a>(
&'a self,
rows: Range<u32>,
theme: Option<&'a SyntaxTheme>,
) -> WrapChunks<'a> {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>();
@@ -576,7 +582,7 @@ impl Snapshot {
let input_end = self
.to_tab_point(output_end)
.min(self.tab_snapshot.max_point());
Chunks {
WrapChunks {
input_chunks: self.tab_snapshot.chunks(input_start..input_end, theme),
input_chunk: Default::default(),
output_position: output_start,
@@ -622,7 +628,7 @@ impl Snapshot {
self.transforms.summary().output.longest_row
}
pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows {
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>();
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
let mut input_row = transforms.start().1.row();
@@ -632,7 +638,7 @@ impl Snapshot {
let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic());
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row);
let input_buffer_row = input_buffer_rows.next().unwrap();
BufferRows {
WrapBufferRows {
transforms,
input_buffer_row,
input_buffer_rows,
@@ -666,6 +672,15 @@ impl Snapshot {
WrapPoint(cursor.start().1 .0 + (point.0 - cursor.start().0 .0))
}
pub fn from_tab_point_with_clipping(&self, point: TabPoint, bias: Bias) -> WrapPoint {
let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>();
cursor.seek(&point, bias, &());
self.clip_point(
WrapPoint(cursor.start().1 .0 + (point.0 - cursor.start().0 .0)),
bias,
)
}
pub fn clip_point(&self, mut point: WrapPoint, bias: Bias) -> WrapPoint {
if bias == Bias::Left {
let mut cursor = self.transforms.cursor::<WrapPoint>();
@@ -679,6 +694,46 @@ impl Snapshot {
self.from_tab_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
}
pub fn prev_row_boundary(&self, mut point: WrapPoint) -> u32 {
if self.transforms.is_empty() {
return 0;
}
*point.column_mut() = 0;
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
cursor.seek(&point, Bias::Right, &());
if cursor.item().is_none() {
cursor.prev(&());
}
while let Some(transform) = cursor.item() {
if transform.is_isomorphic() && cursor.start().1.column() == 0 {
return cmp::min(cursor.end(&()).0.row(), point.row());
} else {
cursor.prev(&());
}
}
unreachable!()
}
pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<u32> {
point.0 += Point::new(1, 0);
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
cursor.seek(&point, Bias::Right, &());
while let Some(transform) = cursor.item() {
if transform.is_isomorphic() && cursor.start().1.column() == 0 {
return Some(cmp::max(cursor.start().0.row(), point.row()));
} else {
cursor.next(&());
}
}
None
}
fn check_invariants(&self) {
#[cfg(test)]
{
@@ -696,22 +751,19 @@ impl Snapshot {
}
}
let input_buffer_rows = self.buffer_snapshot().buffer_rows(0).collect::<Vec<_>>();
let mut expected_buffer_rows = Vec::new();
let mut buffer_row = 0;
let mut prev_tab_row = 0;
for display_row in 0..=self.max_point().row() {
let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
let soft_wrapped;
if tab_point.row() == prev_tab_row {
soft_wrapped = display_row != 0;
if tab_point.row() == prev_tab_row && display_row != 0 {
expected_buffer_rows.push(None);
} else {
let fold_point = self.tab_snapshot.to_fold_point(tab_point, Bias::Left).0;
let buffer_point = fold_point.to_buffer_point(&self.tab_snapshot.fold_snapshot);
buffer_row = buffer_point.row;
expected_buffer_rows.push(input_buffer_rows[buffer_point.row as usize]);
prev_tab_row = tab_point.row();
soft_wrapped = false;
}
expected_buffer_rows.push(if soft_wrapped { None } else { Some(buffer_row) });
}
for start_display_row in 0..expected_buffer_rows.len() {
@@ -727,7 +779,7 @@ impl Snapshot {
}
}
impl<'a> Iterator for Chunks<'a> {
impl<'a> Iterator for WrapChunks<'a> {
type Item = Chunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
@@ -790,7 +842,7 @@ impl<'a> Iterator for Chunks<'a> {
}
}
impl<'a> Iterator for BufferRows<'a> {
impl<'a> Iterator for WrapBufferRows<'a> {
type Item = Option<u32>;
fn next(&mut self) -> Option<Self::Item> {
@@ -811,7 +863,7 @@ impl<'a> Iterator for BufferRows<'a> {
self.soft_wrapped = true;
}
Some(if soft_wrapped { None } else { Some(buffer_row) })
Some(if soft_wrapped { None } else { buffer_row })
}
}
@@ -951,7 +1003,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapPoint {
}
}
fn consolidate_wrap_edits(edits: &mut Vec<Edit>) {
fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
let mut i = 1;
while i < edits.len() {
let edit = edits[i].clone();
@@ -972,8 +1024,9 @@ mod tests {
use crate::{
display_map::{fold_map::FoldMap, tab_map::TabMap},
test::Observer,
MultiBuffer,
};
use language::{Buffer, RandomCharIter};
use language::RandomCharIter;
use rand::prelude::*;
use std::{cmp, env};
use text::Rope;
@@ -1003,18 +1056,19 @@ mod tests {
log::info!("Tab size: {}", tab_size);
log::info!("Wrap width: {:?}", wrap_width);
let buffer = cx.add_model(|cx| {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
Buffer::new(0, text, cx)
let buffer = cx.update(|cx| {
if rng.gen() {
MultiBuffer::build_random(&mut rng, cx)
} else {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
MultiBuffer::build_simple(&text, cx)
}
});
let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
let mut buffer_snapshot = buffer.read_with(&cx, |buffer, cx| buffer.snapshot(cx));
let (mut fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
log::info!(
"Unwrapped text (no folds): {:?}",
buffer.read_with(&cx, |buf, _| buf.text())
);
log::info!("Unwrapped text (no folds): {:?}", buffer_snapshot.text());
log::info!(
"Unwrapped text (unexpanded tabs): {:?}",
folds_snapshot.text()
@@ -1073,17 +1127,17 @@ mod tests {
}
_ => {
buffer.update(&mut cx, |buffer, cx| {
let v0 = buffer.version();
let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5);
buffer.randomly_edit(&mut rng, edit_count, cx);
buffer_edits.extend(buffer.edits_since(&v0));
buffer_snapshot = buffer.snapshot(cx);
buffer_edits.extend(subscription.consume());
});
}
}
let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
log::info!("Unwrapped text (no folds): {:?}", buffer_snapshot.text());
let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot, buffer_edits);
let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
log::info!(
"Unwrapped text (unexpanded tabs): {:?}",
folds_snapshot.text()
@@ -1224,7 +1278,7 @@ mod tests {
}
}
impl Snapshot {
impl WrapSnapshot {
pub fn text(&self) -> String {
self.text_chunks(0).collect()
}
+1139 -667
View File
File diff suppressed because it is too large Load Diff
+25 -37
View File
@@ -1,10 +1,10 @@
use crate::display_map::{BlockContext, ToDisplayPoint};
use super::{
DisplayPoint, Editor, EditorMode, EditorSettings, EditorStyle, Input, Scroll, Select,
SelectPhase, Snapshot, SoftWrap, MAX_LINE_LEN,
display_map::{BlockContext, ToDisplayPoint},
DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, Input, Scroll,
Select, SelectPhase, SoftWrap, ToPoint, MAX_LINE_LEN,
};
use clock::ReplicaId;
use collections::{BTreeMap, HashMap};
use gpui::{
color::Color,
geometry::{
@@ -19,11 +19,10 @@ use gpui::{
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
};
use json::json;
use language::{Chunk, ToPoint};
use language::Chunk;
use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
collections::{BTreeMap, HashMap},
fmt::Write,
ops::Range,
};
@@ -49,7 +48,7 @@ impl EditorElement {
self.view.upgrade(cx).unwrap().update(cx, f)
}
fn snapshot(&self, cx: &mut MutableAppContext) -> Snapshot {
fn snapshot(&self, cx: &mut MutableAppContext) -> EditorSnapshot {
self.update_view(cx, |view, cx| view.snapshot(cx))
}
@@ -434,8 +433,8 @@ impl EditorElement {
}
}
fn max_line_number_width(&self, snapshot: &Snapshot, cx: &LayoutContext) -> f32 {
let digit_count = (snapshot.buffer_row_count() as f32).log10().floor() as usize + 1;
fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &LayoutContext) -> f32 {
let digit_count = (snapshot.max_buffer_row() as f32).log10().floor() as usize + 1;
let style = &self.settings.style;
cx.text_layout_cache
@@ -458,7 +457,7 @@ impl EditorElement {
&self,
rows: Range<u32>,
active_rows: &BTreeMap<u32, bool>,
snapshot: &Snapshot,
snapshot: &EditorSnapshot,
cx: &LayoutContext,
) -> Vec<Option<text_layout::Line>> {
let style = &self.settings.style;
@@ -504,7 +503,7 @@ impl EditorElement {
fn layout_lines(
&mut self,
mut rows: Range<u32>,
snapshot: &mut Snapshot,
snapshot: &mut EditorSnapshot,
cx: &LayoutContext,
) -> Vec<text_layout::Line> {
rows.end = cmp::min(rows.end, snapshot.max_point().row() + 1);
@@ -623,7 +622,7 @@ impl EditorElement {
fn layout_blocks(
&mut self,
rows: Range<u32>,
snapshot: &Snapshot,
snapshot: &EditorSnapshot,
text_width: f32,
line_height: f32,
style: &EditorStyle,
@@ -732,21 +731,14 @@ impl Element for EditorElement {
let scroll_top = scroll_position.y() * line_height;
let end_row = ((scroll_top + size.y()) / line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
let mut selections = HashMap::new();
let mut active_rows = BTreeMap::new();
let mut highlighted_row = None;
self.update_view(cx.app, |view, cx| {
let selections = self.update_view(cx.app, |view, cx| {
highlighted_row = view.highlighted_row();
for selection_set_id in view.active_selection_sets(cx).collect::<Vec<_>>() {
let replica_selections = view
.intersecting_selections(
selection_set_id,
DisplayPoint::new(start_row, 0)..DisplayPoint::new(end_row, 0),
cx,
)
.collect::<Vec<_>>();
for selection in &replica_selections {
if selection_set_id == view.selection_set_id {
let selections = view.visible_selections(start_row..end_row, cx);
for (replica_id, selections) in &selections {
if *replica_id == view.replica_id(cx) {
for selection in selections {
let is_empty = selection.start == selection.end;
let selection_start = snapshot.prev_row_boundary(selection.start).0;
let selection_end = snapshot.next_row_boundary(selection.end).0;
@@ -759,9 +751,8 @@ impl Element for EditorElement {
}
}
}
selections.insert(selection_set_id.replica_id, replica_selections);
}
selections
});
let line_number_layouts = self.layout_rows(start_row..end_row, &active_rows, &snapshot, cx);
@@ -923,7 +914,7 @@ pub struct LayoutState {
gutter_padding: f32,
text_size: Vector2F,
style: EditorStyle,
snapshot: Snapshot,
snapshot: EditorSnapshot,
active_rows: BTreeMap<u32, bool>,
highlighted_row: Option<u32>,
line_layouts: Vec<text_layout::Line>,
@@ -961,7 +952,7 @@ impl LayoutState {
fn layout_line(
row: u32,
snapshot: &Snapshot,
snapshot: &EditorSnapshot,
style: &EditorStyle,
layout_cache: &TextLayoutCache,
) -> text_layout::Line {
@@ -998,7 +989,7 @@ pub struct PaintState {
impl PaintState {
fn point_for_position(
&self,
snapshot: &Snapshot,
snapshot: &EditorSnapshot,
layout: &LayoutState,
position: Vector2F,
) -> (DisplayPoint, u32) {
@@ -1164,23 +1155,20 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 {
#[cfg(test)]
mod tests {
use super::*;
use crate::{
test::sample_text,
{Editor, EditorSettings},
};
use language::Buffer;
use crate::{Editor, EditorSettings, MultiBuffer};
use std::sync::Arc;
use util::test::sample_text;
#[gpui::test]
fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
let settings = EditorSettings::test(cx);
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
Editor::for_buffer(
buffer,
{
let settings = settings.clone();
move |_| settings.clone()
Arc::new(move |_| settings.clone())
},
cx,
)
+47 -68
View File
@@ -1,28 +1,27 @@
use crate::{Editor, EditorSettings, Event};
use crate::{Editor, Event};
use crate::{MultiBuffer, ToPoint as _};
use anyhow::Result;
use gpui::{
elements::*, fonts::TextStyle, AppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
WeakModelHandle,
elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext,
Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle,
};
use language::{Buffer, Diagnostic, File as _};
use language::{Diagnostic, File as _};
use postage::watch;
use project::{ProjectPath, Worktree};
use std::fmt::Write;
use std::path::Path;
use text::{Point, Selection, ToPoint};
use text::{Point, Selection};
use workspace::{
settings, EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView,
WeakItemHandle,
EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView, WeakItemHandle,
};
pub struct BufferOpener;
#[derive(Clone)]
pub struct BufferItemHandle(pub ModelHandle<Buffer>);
pub struct BufferItemHandle(pub ModelHandle<MultiBuffer>);
#[derive(Clone)]
struct WeakBufferItemHandle(WeakModelHandle<Buffer>);
struct WeakBufferItemHandle(WeakModelHandle<MultiBuffer>);
impl EntryOpener for BufferOpener {
fn open(
@@ -32,10 +31,10 @@ impl EntryOpener for BufferOpener {
cx: &mut ModelContext<Worktree>,
) -> Option<Task<Result<Box<dyn ItemHandle>>>> {
let buffer = worktree.open_buffer(project_path.path, cx);
let task = cx.spawn(|_, _| async move {
buffer
.await
.map(|buffer| Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
let task = cx.spawn(|_, mut cx| async move {
let buffer = buffer.await?;
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
Ok(Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
});
Some(task)
}
@@ -52,42 +51,7 @@ impl ItemHandle for BufferItemHandle {
Box::new(cx.add_view(window_id, |cx| {
Editor::for_buffer(
self.0.clone(),
move |cx| {
let settings = settings.borrow();
let font_cache = cx.font_cache();
let font_family_id = settings.buffer_font_family;
let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
let font_properties = Default::default();
let font_id = font_cache
.select_font(font_family_id, &font_properties)
.unwrap();
let font_size = settings.buffer_font_size;
let mut theme = settings.theme.editor.clone();
theme.text = TextStyle {
color: theme.text.color,
font_family_name,
font_family_id,
font_id,
font_size,
font_properties,
underline: None,
};
let language = buffer.upgrade(cx).and_then(|buf| buf.read(cx).language());
let soft_wrap = match settings.soft_wrap(language) {
settings::SoftWrap::None => crate::SoftWrap::None,
settings::SoftWrap::EditorWidth => crate::SoftWrap::EditorWidth,
settings::SoftWrap::PreferredLineLength => crate::SoftWrap::Column(
settings.preferred_line_length(language).saturating_sub(1),
),
};
EditorSettings {
tab_size: settings.tab_size,
soft_wrap,
style: theme,
}
},
crate::settings_builder(buffer, settings),
cx,
)
}))
@@ -102,7 +66,7 @@ impl ItemHandle for BufferItemHandle {
}
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
self.0.read(cx).file().map(|f| ProjectPath {
self.0.read(cx).file(cx).map(|f| ProjectPath {
worktree_id: f.worktree_id(),
path: f.path().clone(),
})
@@ -137,7 +101,7 @@ impl ItemView for Editor {
let filename = self
.buffer()
.read(cx)
.file()
.file(cx)
.and_then(|file| file.file_name());
if let Some(name) = filename {
name.to_string_lossy().into()
@@ -147,7 +111,7 @@ impl ItemView for Editor {
}
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
self.buffer().read(cx).file().map(|file| ProjectPath {
self.buffer().read(cx).file(cx).map(|file| ProjectPath {
worktree_id: file.worktree_id(),
path: file.path().clone(),
})
@@ -174,7 +138,14 @@ impl ItemView for Editor {
path: &Path,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.buffer().update(cx, |buffer, cx| {
let buffer = self
.buffer()
.read(cx)
.as_singleton()
.expect("cannot call save_as on an excerpt list")
.clone();
buffer.update(cx, |buffer, cx| {
let handle = cx.handle();
let text = buffer.as_rope().clone();
let version = buffer.version();
@@ -191,12 +162,12 @@ impl ItemView for Editor {
let (language, language_server) = worktree.update(&mut cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
let language = worktree
.languages()
.language_registry()
.select_language(new_file.full_path())
.cloned();
let language_server = language
.as_ref()
.and_then(|language| worktree.ensure_language_server(language, cx));
.and_then(|language| worktree.register_language(language, cx));
(language, language_server.clone())
});
@@ -210,11 +181,19 @@ impl ItemView for Editor {
}
fn is_dirty(&self, cx: &AppContext) -> bool {
self.buffer().read(cx).is_dirty()
self.buffer().read(cx).read(cx).is_dirty()
}
fn has_conflict(&self, cx: &AppContext) -> bool {
self.buffer().read(cx).has_conflict()
self.buffer().read(cx).read(cx).has_conflict()
}
fn can_save(&self, cx: &AppContext) -> bool {
self.project_path(cx).is_some()
}
fn can_save_as(&self, _: &AppContext) -> bool {
true
}
}
@@ -237,11 +216,11 @@ impl CursorPosition {
fn update_position(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let buffer = editor.buffer().read(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
self.selected_count = 0;
let mut last_selection: Option<Selection<usize>> = None;
for selection in editor.selections::<usize>(cx) {
for selection in editor.local_selections::<usize>(cx) {
self.selected_count += selection.end - selection.start;
if last_selection
.as_ref()
@@ -250,7 +229,7 @@ impl CursorPosition {
last_selection = Some(selection);
}
}
self.position = last_selection.map(|s| s.head().to_point(buffer));
self.position = last_selection.map(|s| s.head().to_point(&buffer));
cx.notify();
}
@@ -314,14 +293,14 @@ impl DiagnosticMessage {
fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let cursor_position = editor.newest_selection(cx).head();
let new_diagnostic = editor
.buffer()
let buffer = editor.buffer().read(cx);
let cursor_position = editor.newest_selection::<usize>(&buffer.read(cx)).head();
let new_diagnostic = buffer
.read(cx)
.diagnostics_in_range::<usize, usize>(cursor_position..cursor_position)
.filter(|(range, _)| !range.is_empty())
.min_by_key(|(range, diagnostic)| (diagnostic.severity, range.len()))
.map(|(_, diagnostic)| diagnostic.clone());
.diagnostics_in_range::<_, usize>(cursor_position..cursor_position)
.filter(|(_, entry)| !entry.range.is_empty())
.min_by_key(|(_, entry)| (entry.diagnostic.severity, entry.range.len()))
.map(|(_, entry)| entry.diagnostic);
if new_diagnostic != self.diagnostic {
self.diagnostic = new_diagnostic;
cx.notify();
+151 -44
View File
@@ -1,9 +1,9 @@
use super::{Bias, DisplayMapSnapshot, DisplayPoint, SelectionGoal, ToDisplayPoint};
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::ToPoint;
use anyhow::Result;
use std::{cmp, ops::Range};
use text::ToPoint;
pub fn left(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
if point.column() > 0 {
*point.column_mut() -= 1;
} else if point.row() > 0 {
@@ -13,7 +13,7 @@ pub fn left(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<Display
Ok(map.clip_point(point, Bias::Left))
}
pub fn right(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
let max_column = map.line_len(point.row());
if point.column() < max_column {
*point.column_mut() += 1;
@@ -25,27 +25,26 @@ pub fn right(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<Displa
}
pub fn up(
map: &DisplayMapSnapshot,
mut point: DisplayPoint,
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
) -> Result<(DisplayPoint, SelectionGoal)> {
let goal_column = if let SelectionGoal::Column(column) = goal {
let mut goal_column = if let SelectionGoal::Column(column) = goal {
column
} else {
map.column_to_chars(point.row(), point.column())
map.column_to_chars(start.row(), start.column())
};
loop {
if point.row() > 0 {
*point.row_mut() -= 1;
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
if !map.is_block_line(point.row()) {
break;
}
} else {
point = DisplayPoint::new(0, 0);
break;
}
let prev_row = start.row().saturating_sub(1);
let mut point = map.clip_point(
DisplayPoint::new(prev_row, map.line_len(prev_row)),
Bias::Left,
);
if point.row() < start.row() {
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
} else {
point = DisplayPoint::new(0, 0);
goal_column = 0;
}
let clip_bias = if point.column() == map.line_len(point.row()) {
@@ -61,28 +60,23 @@ pub fn up(
}
pub fn down(
map: &DisplayMapSnapshot,
mut point: DisplayPoint,
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
) -> Result<(DisplayPoint, SelectionGoal)> {
let max_point = map.max_point();
let goal_column = if let SelectionGoal::Column(column) = goal {
let mut goal_column = if let SelectionGoal::Column(column) = goal {
column
} else {
map.column_to_chars(point.row(), point.column())
map.column_to_chars(start.row(), start.column())
};
loop {
if point.row() < max_point.row() {
*point.row_mut() += 1;
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
if !map.is_block_line(point.row()) {
break;
}
} else {
point = max_point;
break;
}
let next_row = start.row() + 1;
let mut point = map.clip_point(DisplayPoint::new(next_row, 0), Bias::Right);
if point.row() > start.row() {
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
} else {
point = map.max_point();
goal_column = map.column_to_chars(point.row(), point.column())
}
let clip_bias = if point.column() == map.line_len(point.row()) {
@@ -98,7 +92,7 @@ pub fn down(
}
pub fn line_beginning(
map: &DisplayMapSnapshot,
map: &DisplaySnapshot,
point: DisplayPoint,
toggle_indent: bool,
) -> DisplayPoint {
@@ -110,12 +104,12 @@ pub fn line_beginning(
}
}
pub fn line_end(map: &DisplayMapSnapshot, point: DisplayPoint) -> DisplayPoint {
pub fn line_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let line_end = DisplayPoint::new(point.row(), map.line_len(point.row()));
map.clip_point(line_end, Bias::Left)
}
pub fn prev_word_boundary(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> DisplayPoint {
pub fn prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
let mut line_start = 0;
if point.row() > 0 {
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
@@ -154,7 +148,7 @@ pub fn prev_word_boundary(map: &DisplayMapSnapshot, mut point: DisplayPoint) ->
boundary
}
pub fn next_word_boundary(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> DisplayPoint {
pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
let mut prev_char_kind = None;
for c in map.chars_at(point) {
let char_kind = char_kind(c);
@@ -181,7 +175,7 @@ pub fn next_word_boundary(map: &DisplayMapSnapshot, mut point: DisplayPoint) ->
point
}
pub fn is_inside_word(map: &DisplayMapSnapshot, point: DisplayPoint) -> bool {
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
let text = &map.buffer_snapshot;
let next_char_kind = text.chars_at(ix).next().map(char_kind);
@@ -189,7 +183,7 @@ pub fn is_inside_word(map: &DisplayMapSnapshot, point: DisplayPoint) -> bool {
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
}
pub fn surrounding_word(map: &DisplayMapSnapshot, point: DisplayPoint) -> Range<DisplayPoint> {
pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range<DisplayPoint> {
let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
let mut end = start;
@@ -244,7 +238,120 @@ fn char_kind(c: char) -> CharKind {
#[cfg(test)]
mod tests {
use super::*;
use crate::{display_map::DisplayMap, Buffer};
use crate::{
display_map::{BlockDisposition, BlockProperties},
Buffer, DisplayMap, ExcerptProperties, MultiBuffer,
};
use gpui::{elements::Empty, Element};
use language::Point;
use std::sync::Arc;
#[gpui::test]
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
let mut excerpt1_header_position = None;
let mut excerpt2_header_position = None;
let multibuffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0);
let excerpt1_id = multibuffer.push_excerpt(
ExcerptProperties {
buffer: &buffer,
range: Point::new(0, 0)..Point::new(1, 4),
},
cx,
);
let excerpt2_id = multibuffer.push_excerpt(
ExcerptProperties {
buffer: &buffer,
range: Point::new(2, 0)..Point::new(3, 2),
},
cx,
);
excerpt1_header_position = Some(
multibuffer
.read(cx)
.anchor_in_excerpt(excerpt1_id, language::Anchor::min()),
);
excerpt2_header_position = Some(
multibuffer
.read(cx)
.anchor_in_excerpt(excerpt2_id, language::Anchor::min()),
);
multibuffer
});
let display_map =
cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, cx));
display_map.update(cx, |display_map, cx| {
display_map.insert_blocks(
[
BlockProperties {
position: excerpt1_header_position.unwrap(),
height: 2,
render: Arc::new(|_| Empty::new().boxed()),
disposition: BlockDisposition::Above,
},
BlockProperties {
position: excerpt2_header_position.unwrap(),
height: 3,
render: Arc::new(|_| Empty::new().boxed()),
disposition: BlockDisposition::Above,
},
],
cx,
)
});
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\n\nhijkl\nmn");
// Can't move up into the first excerpt's header
assert_eq!(
up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(),
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
);
assert_eq!(
up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(),
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
);
// Move up and down within first excerpt
assert_eq!(
up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(),
(DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
);
assert_eq!(
down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(),
(DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
);
// Move up and down across second excerpt's header
assert_eq!(
up(&snapshot, DisplayPoint::new(7, 5), SelectionGoal::Column(5)).unwrap(),
(DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
);
assert_eq!(
down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
(DisplayPoint::new(7, 5), SelectionGoal::Column(5)),
);
// Can't move down off the end
assert_eq!(
down(&snapshot, DisplayPoint::new(8, 0), SelectionGoal::Column(0)).unwrap(),
(DisplayPoint::new(8, 2), SelectionGoal::Column(2)),
);
assert_eq!(
down(&snapshot, DisplayPoint::new(8, 2), SelectionGoal::Column(2)).unwrap(),
(DisplayPoint::new(8, 2), SelectionGoal::Column(2)),
);
}
#[gpui::test]
fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
@@ -256,7 +363,7 @@ mod tests {
.unwrap();
let font_size = 14.0;
let buffer = cx.add_model(|cx| Buffer::new(0, "a bcΔ defγ hi—jk", cx));
let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
let display_map =
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -312,7 +419,7 @@ mod tests {
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer = cx.add_model(|cx| Buffer::new(0, "lorem ipsum dolor\n sit", cx));
let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx);
let display_map =
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint};
use anyhow::{anyhow, Result};
use std::{
cmp::Ordering,
ops::{Range, Sub},
};
use sum_tree::Bias;
use text::{rope::TextDimension, Point};
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub struct Anchor {
pub(crate) excerpt_id: ExcerptId,
pub(crate) text_anchor: text::Anchor,
}
impl Anchor {
pub fn min() -> Self {
Self {
excerpt_id: ExcerptId::min(),
text_anchor: text::Anchor::min(),
}
}
pub fn max() -> Self {
Self {
excerpt_id: ExcerptId::max(),
text_anchor: text::Anchor::max(),
}
}
pub fn excerpt_id(&self) -> &ExcerptId {
&self.excerpt_id
}
pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Result<Ordering> {
let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id);
if excerpt_id_cmp.is_eq() {
if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
Ok(Ordering::Equal)
} else {
self.text_anchor.cmp(
&other.text_anchor,
snapshot
.buffer_snapshot_for_excerpt(&self.excerpt_id)
.ok_or_else(|| anyhow!("excerpt {:?} not found", self.excerpt_id))?,
)
}
} else {
return Ok(excerpt_id_cmp);
}
}
pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
if self.text_anchor.bias != Bias::Left {
if let Some(buffer_snapshot) = snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id) {
return Self {
excerpt_id: self.excerpt_id.clone(),
text_anchor: self.text_anchor.bias_left(buffer_snapshot),
};
}
}
self.clone()
}
pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
if self.text_anchor.bias != Bias::Right {
if let Some(buffer_snapshot) = snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id) {
return Self {
excerpt_id: self.excerpt_id.clone(),
text_anchor: self.text_anchor.bias_right(buffer_snapshot),
};
}
}
self.clone()
}
pub fn summary<D>(&self, snapshot: &MultiBufferSnapshot) -> D
where
D: TextDimension + Ord + Sub<D, Output = D>,
{
snapshot.summary_for_anchor(self)
}
}
impl ToOffset for Anchor {
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
self.summary(snapshot)
}
}
impl ToPoint for Anchor {
fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point {
self.summary(snapshot)
}
}
pub trait AnchorRangeExt {
fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering>;
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
}
impl AnchorRangeExt for Range<Anchor> {
fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering> {
Ok(match self.start.cmp(&other.start, buffer)? {
Ordering::Equal => other.end.cmp(&self.end, buffer)?,
ord @ _ => ord,
})
}
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize> {
self.start.to_offset(&content)..self.end.to_offset(&content)
}
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point> {
self.start.to_point(&content)..self.end.to_point(&content)
}
}
-13
View File
@@ -9,19 +9,6 @@ fn init_logger() {
env_logger::init();
}
pub fn sample_text(rows: usize, cols: usize) -> String {
let mut text = String::new();
for row in 0..rows {
let c: char = ('a' as u32 + row as u32) as u8 as char;
let mut line = c.to_string().repeat(cols);
if row < rows - 1 {
line.push('\n');
}
text += &line;
}
text
}
pub struct Observer<T>(PhantomData<T>);
impl<T: 'static> Entity for Observer<T> {
+2 -2
View File
@@ -270,14 +270,14 @@ impl FileFinder {
Editor::single_line(
{
let settings = settings.clone();
move |_| {
Arc::new(move |_| {
let settings = settings.borrow();
EditorSettings {
style: settings.theme.selector.input_editor.as_editor(),
tab_size: settings.tab_size,
soft_wrap: editor::SoftWrap::None,
}
}
})
},
cx,
)
+13 -9
View File
@@ -1,10 +1,11 @@
use text::{Bias, Point, Selection};
use editor::{display_map::ToDisplayPoint, Autoscroll, Editor, EditorSettings};
use gpui::{
action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity,
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
};
use postage::watch;
use std::sync::Arc;
use text::{Bias, Point, Selection};
use workspace::{Settings, Workspace};
action!(Toggle);
@@ -49,14 +50,14 @@ impl GoToLine {
Editor::single_line(
{
let settings = settings.clone();
move |_| {
Arc::new(move |_| {
let settings = settings.borrow();
EditorSettings {
tab_size: settings.tab_size,
style: settings.theme.selector.input_editor.as_editor(),
soft_wrap: editor::SoftWrap::None,
}
}
})
},
cx,
)
@@ -67,13 +68,14 @@ impl GoToLine {
let (restore_state, cursor_point, max_point) = active_editor.update(cx, |editor, cx| {
let restore_state = Some(RestoreState {
scroll_position: editor.scroll_position(cx),
selections: editor.selections::<usize>(cx).collect(),
selections: editor.local_selections::<usize>(cx),
});
let buffer = editor.buffer().read(cx).read(cx);
(
restore_state,
editor.newest_selection(cx).head(),
editor.buffer().read(cx).max_point(),
editor.newest_selection(&buffer).head(),
buffer.max_point(),
)
});
@@ -127,7 +129,7 @@ impl GoToLine {
match event {
editor::Event::Blurred => cx.emit(Event::Dismissed),
editor::Event::Edited => {
let line_editor = self.line_editor.read(cx).buffer().read(cx).text();
let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text();
let mut components = line_editor.trim().split(&[',', ':'][..]);
let row = components.next().and_then(|row| row.parse::<u32>().ok());
let column = components.next().and_then(|row| row.parse::<u32>().ok());
@@ -143,7 +145,7 @@ impl GoToLine {
let display_point = point.to_display_point(&snapshot);
active_editor.select_ranges([point..point], Some(Autoscroll::Center), cx);
active_editor.set_highlighted_row(Some(display_point.row()));
Some(active_editor.newest_selection(cx))
Some(active_editor.newest_selection(&snapshot.buffer_snapshot))
});
cx.notify();
}
@@ -162,7 +164,9 @@ impl Entity for GoToLine {
self.active_editor.update(cx, |editor, cx| {
editor.set_highlighted_row(None);
if let Some((line_selection, restore_state)) = line_selection.zip(restore_state) {
if line_selection.id == editor.newest_selection::<usize>(cx).id {
let newest_selection =
editor.newest_selection::<usize>(&editor.buffer().read(cx).read(cx));
if line_selection.id == newest_selection.id {
editor.set_scroll_position(restore_state.scroll_position, cx);
editor.update_selections(restore_state.selections, None, cx);
}
+8
View File
@@ -54,6 +54,7 @@ type AnyLocalTask = async_task::Task<Box<dyn Any + 'static>>;
#[must_use]
pub enum Task<T> {
Ready(Option<T>),
Local {
any_task: AnyLocalTask,
result_type: PhantomData<T>,
@@ -594,6 +595,10 @@ pub fn deterministic(seed: u64) -> (Rc<Foreground>, Arc<Background>) {
}
impl<T> Task<T> {
pub fn ready(value: T) -> Self {
Self::Ready(Some(value))
}
fn local(any_task: AnyLocalTask) -> Self {
Self::Local {
any_task,
@@ -603,6 +608,7 @@ impl<T> Task<T> {
pub fn detach(self) {
match self {
Task::Ready(_) => {}
Task::Local { any_task, .. } => any_task.detach(),
Task::Send { any_task, .. } => any_task.detach(),
}
@@ -621,6 +627,7 @@ impl<T: Send> Task<T> {
impl<T: fmt::Debug> fmt::Debug for Task<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Task::Ready(value) => value.fmt(f),
Task::Local { any_task, .. } => any_task.fmt(f),
Task::Send { any_task, .. } => any_task.fmt(f),
}
@@ -632,6 +639,7 @@ impl<T: 'static> Future for Task<T> {
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match unsafe { self.get_unchecked_mut() } {
Task::Ready(value) => Poll::Ready(value.take().unwrap()),
Task::Local { any_task, .. } => {
any_task.poll(cx).map(|value| *value.downcast().unwrap())
}
+1 -1
View File
@@ -57,7 +57,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
if let Some(Some(Ok(item))) = opened.first() {
if let Some(editor) = item.to_any().downcast::<Editor>() {
editor.update(&mut cx, |editor, cx| {
let len = editor.buffer().read(cx).len();
let len = editor.buffer().read(cx).read(cx).len();
editor.select_ranges([len..len], Some(Autoscroll::Center), cx);
if len > 0 {
editor.insert("\n\n", cx);
+14 -4
View File
@@ -1,7 +1,7 @@
[package]
name = "language"
version = "0.1.0"
edition = "2018"
edition = "2021"
[lib]
path = "src/language.rs"
@@ -9,20 +9,25 @@ path = "src/language.rs"
[features]
test-support = [
"rand",
"text/test-support",
"collections/test-support",
"lsp/test-support",
"text/test-support",
"tree-sitter-rust",
"util/test-support",
]
[dependencies]
text = { path = "../text" }
clock = { path = "../clock" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
lsp = { path = "../lsp" }
rpc = { path = "../rpc" }
sum_tree = { path = "../sum_tree" }
text = { path = "../text" }
theme = { path = "../theme" }
util = { path = "../util" }
anyhow = "1.0.38"
async-trait = "0.1"
futures = "0.3"
lazy_static = "1.4"
log = "0.4"
@@ -31,14 +36,19 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
rand = { version = "0.8.3", optional = true }
serde = { version = "1", features = ["derive"] }
similar = "1.3"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
tree-sitter = "0.20.0"
tree-sitter-rust = { version = "0.20.0", optional = true }
[dev-dependencies]
text = { path = "../text", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
text = { path = "../text", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.8"
rand = "0.8.3"
tree-sitter-rust = "0.20.0"
unindent = "0.1.7"
-1
View File
@@ -3,4 +3,3 @@ fn main() {
println!("cargo:rustc-env=ZED_BUNDLE={}", bundled);
}
}
File diff suppressed because it is too large Load Diff
+223
View File
@@ -0,0 +1,223 @@
use crate::Diagnostic;
use collections::HashMap;
use std::{
cmp::{Ordering, Reverse},
iter,
ops::Range,
sync::Arc,
};
use sum_tree::{self, Bias, SumTree};
use text::{Anchor, FromAnchor, Point, ToOffset};
#[derive(Clone, Debug)]
pub struct DiagnosticSet {
provider_name: Arc<str>,
diagnostics: SumTree<DiagnosticEntry<Anchor>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiagnosticEntry<T> {
pub range: Range<T>,
pub diagnostic: Diagnostic,
}
pub struct DiagnosticGroup<T> {
pub entries: Vec<DiagnosticEntry<T>>,
pub primary_ix: usize,
}
#[derive(Clone, Debug)]
pub struct Summary {
start: Anchor,
end: Anchor,
min_start: Anchor,
max_end: Anchor,
count: usize,
}
impl DiagnosticSet {
pub fn provider_name(&self) -> &str {
&self.provider_name
}
pub fn from_sorted_entries<I>(
provider_name: impl Into<Arc<str>>,
iter: I,
buffer: &text::BufferSnapshot,
) -> Self
where
I: IntoIterator<Item = DiagnosticEntry<Anchor>>,
{
Self {
provider_name: provider_name.into(),
diagnostics: SumTree::from_iter(iter, buffer),
}
}
pub fn new<I>(provider_name: Arc<str>, iter: I, buffer: &text::BufferSnapshot) -> Self
where
I: IntoIterator<Item = DiagnosticEntry<Point>>,
{
let mut entries = iter.into_iter().collect::<Vec<_>>();
entries.sort_unstable_by_key(|entry| (entry.range.start, Reverse(entry.range.end)));
Self {
provider_name,
diagnostics: SumTree::from_iter(
entries.into_iter().map(|entry| DiagnosticEntry {
range: buffer.anchor_before(entry.range.start)
..buffer.anchor_after(entry.range.end),
diagnostic: entry.diagnostic,
}),
buffer,
),
}
}
pub fn iter(&self) -> impl Iterator<Item = &DiagnosticEntry<Anchor>> {
self.diagnostics.iter()
}
pub fn range<'a, T, O>(
&'a self,
range: Range<T>,
buffer: &'a text::BufferSnapshot,
inclusive: bool,
) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
where
T: 'a + ToOffset,
O: FromAnchor,
{
let end_bias = if inclusive { Bias::Right } else { Bias::Left };
let range = buffer.anchor_before(range.start)..buffer.anchor_at(range.end, end_bias);
let mut cursor = self.diagnostics.filter::<_, ()>(
{
move |summary: &Summary| {
let start_cmp = range.start.cmp(&summary.max_end, buffer).unwrap();
let end_cmp = range.end.cmp(&summary.min_start, buffer).unwrap();
if inclusive {
start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
} else {
start_cmp == Ordering::Less && end_cmp == Ordering::Greater
}
}
},
buffer,
);
iter::from_fn({
move || {
if let Some(diagnostic) = cursor.item() {
cursor.next(buffer);
Some(diagnostic.resolve(buffer))
} else {
None
}
}
})
}
pub fn groups(&self, output: &mut Vec<DiagnosticGroup<Anchor>>, buffer: &text::BufferSnapshot) {
let mut groups = HashMap::default();
for entry in self.diagnostics.iter() {
groups
.entry(entry.diagnostic.group_id)
.or_insert(Vec::new())
.push(entry.clone());
}
let start_ix = output.len();
output.extend(groups.into_values().filter_map(|mut entries| {
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer).unwrap());
entries
.iter()
.position(|entry| entry.diagnostic.is_primary)
.map(|primary_ix| DiagnosticGroup {
entries,
primary_ix,
})
}));
output[start_ix..].sort_unstable_by(|a, b| {
a.entries[a.primary_ix]
.range
.start
.cmp(&b.entries[b.primary_ix].range.start, buffer)
.unwrap()
});
}
pub fn group<'a, O: FromAnchor>(
&'a self,
group_id: usize,
buffer: &'a text::BufferSnapshot,
) -> impl 'a + Iterator<Item = DiagnosticEntry<O>> {
self.iter()
.filter(move |entry| entry.diagnostic.group_id == group_id)
.map(|entry| entry.resolve(buffer))
}
}
impl Default for DiagnosticSet {
fn default() -> Self {
Self {
provider_name: "".into(),
diagnostics: Default::default(),
}
}
}
impl sum_tree::Item for DiagnosticEntry<Anchor> {
type Summary = Summary;
fn summary(&self) -> Self::Summary {
Summary {
start: self.range.start.clone(),
end: self.range.end.clone(),
min_start: self.range.start.clone(),
max_end: self.range.end.clone(),
count: 1,
}
}
}
impl DiagnosticEntry<Anchor> {
pub fn resolve<O: FromAnchor>(&self, buffer: &text::BufferSnapshot) -> DiagnosticEntry<O> {
DiagnosticEntry {
range: O::from_anchor(&self.range.start, buffer)
..O::from_anchor(&self.range.end, buffer),
diagnostic: self.diagnostic.clone(),
}
}
}
impl Default for Summary {
fn default() -> Self {
Self {
start: Anchor::min(),
end: Anchor::max(),
min_start: Anchor::max(),
max_end: Anchor::min(),
count: 0,
}
}
}
impl sum_tree::Summary for Summary {
type Context = text::BufferSnapshot;
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
if other
.min_start
.cmp(&self.min_start, buffer)
.unwrap()
.is_lt()
{
self.min_start = other.min_start.clone();
}
if other.max_end.cmp(&self.max_end, buffer).unwrap().is_gt() {
self.max_end = other.max_end.clone();
}
self.start = other.start.clone();
self.end = other.end.clone();
self.count += other.count;
}
}
+29 -5
View File
@@ -1,19 +1,22 @@
mod buffer;
mod diagnostic_set;
mod highlight_map;
pub mod proto;
#[cfg(test)]
mod tests;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
pub use buffer::Operation;
pub use buffer::*;
use gpui::{executor::Background, AppContext};
use collections::{HashMap, HashSet};
pub use diagnostic_set::DiagnosticEntry;
use gpui::AppContext;
use highlight_map::HighlightMap;
use lazy_static::lazy_static;
use lsp::LanguageServer;
use parking_lot::Mutex;
use serde::Deserialize;
use std::{collections::HashSet, path::Path, str, sync::Arc};
use std::{path::Path, str, sync::Arc};
use theme::SyntaxTheme;
use tree_sitter::{self, Query};
pub use tree_sitter::{Parser, Tree};
@@ -46,7 +49,7 @@ pub struct LanguageServerConfig {
pub disk_based_diagnostic_sources: HashSet<String>,
#[cfg(any(test, feature = "test-support"))]
#[serde(skip)]
pub fake_server: Option<(Arc<LanguageServer>, Arc<std::sync::atomic::AtomicBool>)>,
pub fake_server: Option<(Arc<lsp::LanguageServer>, Arc<std::sync::atomic::AtomicBool>)>,
}
#[derive(Clone, Debug, Deserialize)]
@@ -57,9 +60,18 @@ pub struct BracketPair {
pub newline: bool,
}
#[async_trait]
pub trait DiagnosticProvider: 'static + Send + Sync {
async fn diagnose(
&self,
path: Arc<Path>,
) -> Result<HashMap<Arc<Path>, Vec<DiagnosticEntry<usize>>>>;
}
pub struct Language {
pub(crate) config: LanguageConfig,
pub(crate) grammar: Option<Arc<Grammar>>,
pub(crate) diagnostic_provider: Option<Arc<dyn DiagnosticProvider>>,
}
pub struct Grammar {
@@ -124,6 +136,7 @@ impl Language {
highlight_map: Default::default(),
})
}),
diagnostic_provider: None,
}
}
@@ -157,6 +170,11 @@ impl Language {
Ok(self)
}
pub fn with_diagnostic_provider(mut self, source: impl DiagnosticProvider) -> Self {
self.diagnostic_provider = Some(Arc::new(source));
self
}
pub fn name(&self) -> &str {
self.config.name.as_str()
}
@@ -190,6 +208,10 @@ impl Language {
}
}
pub fn diagnostic_provider(&self) -> Option<&Arc<dyn DiagnosticProvider>> {
self.diagnostic_provider.as_ref()
}
pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet<String>> {
self.config
.language_server
@@ -217,7 +239,9 @@ impl Grammar {
#[cfg(any(test, feature = "test-support"))]
impl LanguageServerConfig {
pub async fn fake(executor: Arc<Background>) -> (Self, lsp::FakeLanguageServer) {
pub async fn fake(
executor: Arc<gpui::executor::Background>,
) -> (Self, lsp::FakeLanguageServer) {
let (server, fake) = lsp::LanguageServer::fake(executor).await;
fake.started
.store(false, std::sync::atomic::Ordering::SeqCst);
+153 -157
View File
@@ -1,13 +1,12 @@
use std::sync::Arc;
use crate::{Diagnostic, Operation};
use crate::{diagnostic_set::DiagnosticEntry, Diagnostic, Operation};
use anyhow::{anyhow, Result};
use clock::ReplicaId;
use lsp::DiagnosticSeverity;
use rpc::proto;
use std::sync::Arc;
use text::*;
pub use proto::Buffer;
pub use proto::{Buffer, SelectionSet};
pub fn serialize_operation(operation: &Operation) -> proto::Operation {
proto::Operation {
@@ -41,46 +40,34 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
.collect(),
version: From::from(&undo.version),
}),
Operation::Buffer(text::Operation::UpdateSelections {
set_id,
Operation::UpdateSelections {
replica_id,
selections,
lamport_timestamp,
}) => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
replica_id: set_id.replica_id as u32,
local_timestamp: set_id.value,
} => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
replica_id: *replica_id as u32,
lamport_timestamp: lamport_timestamp.value,
version: selections.version().into(),
selections: selections
.full_offset_ranges()
.map(|(range, state)| proto::Selection {
id: state.id as u64,
start: range.start.0 as u64,
end: range.end.0 as u64,
reversed: state.reversed,
})
.collect(),
selections: serialize_selections(selections),
}),
Operation::Buffer(text::Operation::RemoveSelections {
set_id,
Operation::RemoveSelections {
replica_id,
lamport_timestamp,
}) => proto::operation::Variant::RemoveSelections(proto::operation::RemoveSelections {
replica_id: set_id.replica_id as u32,
local_timestamp: set_id.value,
} => proto::operation::Variant::RemoveSelections(proto::operation::RemoveSelections {
replica_id: *replica_id as u32,
lamport_timestamp: lamport_timestamp.value,
}),
Operation::Buffer(text::Operation::SetActiveSelections {
set_id,
Operation::UpdateDiagnostics {
provider_name,
diagnostics,
lamport_timestamp,
}) => proto::operation::Variant::SetActiveSelections(
proto::operation::SetActiveSelections {
replica_id: lamport_timestamp.replica_id as u32,
local_timestamp: set_id.map(|set_id| set_id.value),
lamport_timestamp: lamport_timestamp.value,
},
),
Operation::UpdateDiagnostics(diagnostic_set) => {
proto::operation::Variant::UpdateDiagnostics(serialize_diagnostics(diagnostic_set))
}
} => proto::operation::Variant::UpdateDiagnosticSet(proto::UpdateDiagnosticSet {
replica_id: lamport_timestamp.replica_id as u32,
lamport_timestamp: lamport_timestamp.value,
diagnostic_set: Some(serialize_diagnostic_set(
provider_name.clone(),
diagnostics.iter(),
)),
}),
}),
}
}
@@ -104,48 +91,59 @@ pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::
}
}
pub fn serialize_selection_set(set: &SelectionSet) -> proto::SelectionSet {
let version = set.selections.version();
let entries = set.selections.full_offset_ranges();
proto::SelectionSet {
replica_id: set.id.replica_id as u32,
lamport_timestamp: set.id.value as u32,
is_active: set.active,
version: version.into(),
selections: entries
.map(|(range, state)| proto::Selection {
id: state.id as u64,
start: range.start.0 as u64,
end: range.end.0 as u64,
reversed: state.reversed,
})
.collect(),
}
pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
selections
.iter()
.map(|selection| proto::Selection {
id: selection.id as u64,
start: Some(serialize_anchor(&selection.start)),
end: Some(serialize_anchor(&selection.end)),
reversed: selection.reversed,
})
.collect()
}
pub fn serialize_diagnostics(map: &AnchorRangeMultimap<Diagnostic>) -> proto::DiagnosticSet {
pub fn serialize_diagnostic_set<'a>(
provider_name: String,
diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<Anchor>>,
) -> proto::DiagnosticSet {
proto::DiagnosticSet {
version: map.version().into(),
diagnostics: map
.full_offset_ranges()
.map(|(range, diagnostic)| proto::Diagnostic {
start: range.start.0 as u64,
end: range.end.0 as u64,
message: diagnostic.message.clone(),
severity: match diagnostic.severity {
provider_name,
diagnostics: diagnostics
.into_iter()
.map(|entry| proto::Diagnostic {
start: Some(serialize_anchor(&entry.range.start)),
end: Some(serialize_anchor(&entry.range.end)),
message: entry.diagnostic.message.clone(),
severity: match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error,
DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning,
DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information,
DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint,
_ => proto::diagnostic::Severity::None,
} as i32,
group_id: diagnostic.group_id as u64,
is_primary: diagnostic.is_primary,
group_id: entry.diagnostic.group_id as u64,
is_primary: entry.diagnostic.is_primary,
is_valid: entry.diagnostic.is_valid,
code: entry.diagnostic.code.clone(),
is_disk_based: entry.diagnostic.is_disk_based,
})
.collect(),
}
}
fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
proto::Anchor {
replica_id: anchor.timestamp.replica_id as u32,
local_timestamp: anchor.timestamp.value,
offset: anchor.offset as u64,
bias: match anchor.bias {
Bias::Left => proto::Bias::Left as i32,
Bias::Right => proto::Bias::Right as i32,
},
}
}
pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
Ok(
match message
@@ -187,66 +185,50 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
},
}),
proto::operation::Variant::UpdateSelections(message) => {
let version = message.version.into();
let entries = message
let selections = message
.selections
.iter()
.map(|selection| {
let range = FullOffset(selection.start as usize)
..FullOffset(selection.end as usize);
let state = SelectionState {
.into_iter()
.filter_map(|selection| {
Some(Selection {
id: selection.id as usize,
start: deserialize_anchor(selection.start?)?,
end: deserialize_anchor(selection.end?)?,
reversed: selection.reversed,
goal: SelectionGoal::None,
};
(range, state)
})
})
.collect();
let selections = AnchorRangeMap::from_full_offset_ranges(
version,
Bias::Left,
Bias::Left,
entries,
);
.collect::<Vec<_>>();
Operation::Buffer(text::Operation::UpdateSelections {
set_id: clock::Lamport {
replica_id: message.replica_id as ReplicaId,
value: message.local_timestamp,
},
Operation::UpdateSelections {
replica_id: message.replica_id as ReplicaId,
lamport_timestamp: clock::Lamport {
replica_id: message.replica_id as ReplicaId,
value: message.lamport_timestamp,
},
selections: Arc::from(selections),
})
}
}
proto::operation::Variant::RemoveSelections(message) => {
Operation::Buffer(text::Operation::RemoveSelections {
set_id: clock::Lamport {
replica_id: message.replica_id as ReplicaId,
value: message.local_timestamp,
},
proto::operation::Variant::RemoveSelections(message) => Operation::RemoveSelections {
replica_id: message.replica_id as ReplicaId,
lamport_timestamp: clock::Lamport {
replica_id: message.replica_id as ReplicaId,
value: message.lamport_timestamp,
},
},
proto::operation::Variant::UpdateDiagnosticSet(message) => {
let (provider_name, diagnostics) = deserialize_diagnostic_set(
message
.diagnostic_set
.ok_or_else(|| anyhow!("missing diagnostic set"))?,
);
Operation::UpdateDiagnostics {
provider_name,
diagnostics,
lamport_timestamp: clock::Lamport {
replica_id: message.replica_id as ReplicaId,
value: message.lamport_timestamp,
},
})
}
proto::operation::Variant::SetActiveSelections(message) => {
Operation::Buffer(text::Operation::SetActiveSelections {
set_id: message.local_timestamp.map(|value| clock::Lamport {
replica_id: message.replica_id as ReplicaId,
value,
}),
lamport_timestamp: clock::Lamport {
replica_id: message.replica_id as ReplicaId,
value: message.lamport_timestamp,
},
})
}
proto::operation::Variant::UpdateDiagnostics(message) => {
Operation::UpdateDiagnostics(deserialize_diagnostics(message))
}
}
},
)
@@ -270,55 +252,69 @@ pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation
}
}
pub fn deserialize_selection_set(set: proto::SelectionSet) -> SelectionSet {
SelectionSet {
id: clock::Lamport {
replica_id: set.replica_id as u16,
value: set.lamport_timestamp,
},
active: set.is_active,
selections: Arc::new(AnchorRangeMap::from_full_offset_ranges(
set.version.into(),
Bias::Left,
Bias::Left,
set.selections
.into_iter()
.map(|selection| {
let range =
FullOffset(selection.start as usize)..FullOffset(selection.end as usize);
let state = SelectionState {
id: selection.id as usize,
reversed: selection.reversed,
goal: SelectionGoal::None,
};
(range, state)
pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selection<Anchor>]> {
Arc::from(
selections
.into_iter()
.filter_map(|selection| {
Some(Selection {
id: selection.id as usize,
start: deserialize_anchor(selection.start?)?,
end: deserialize_anchor(selection.end?)?,
reversed: selection.reversed,
goal: SelectionGoal::None,
})
.collect(),
)),
}
}
pub fn deserialize_diagnostics(message: proto::DiagnosticSet) -> AnchorRangeMultimap<Diagnostic> {
AnchorRangeMultimap::from_full_offset_ranges(
message.version.into(),
Bias::Left,
Bias::Right,
message.diagnostics.into_iter().filter_map(|diagnostic| {
Some((
FullOffset(diagnostic.start as usize)..FullOffset(diagnostic.end as usize),
Diagnostic {
severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? {
proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR,
proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING,
proto::diagnostic::Severity::Information => DiagnosticSeverity::INFORMATION,
proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT,
proto::diagnostic::Severity::None => return None,
},
message: diagnostic.message,
group_id: diagnostic.group_id as usize,
is_primary: diagnostic.is_primary,
},
))
}),
})
.collect::<Vec<_>>(),
)
}
pub fn deserialize_diagnostic_set(
message: proto::DiagnosticSet,
) -> (String, Arc<[DiagnosticEntry<Anchor>]>) {
(
message.provider_name,
message
.diagnostics
.into_iter()
.filter_map(|diagnostic| {
Some(DiagnosticEntry {
range: deserialize_anchor(diagnostic.start?)?
..deserialize_anchor(diagnostic.end?)?,
diagnostic: Diagnostic {
severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)?
{
proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR,
proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING,
proto::diagnostic::Severity::Information => {
DiagnosticSeverity::INFORMATION
}
proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT,
proto::diagnostic::Severity::None => return None,
},
message: diagnostic.message,
group_id: diagnostic.group_id as usize,
code: diagnostic.code,
is_valid: diagnostic.is_valid,
is_primary: diagnostic.is_primary,
is_disk_based: diagnostic.is_disk_based,
},
})
})
.collect(),
)
}
fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {
Some(Anchor {
timestamp: clock::Local {
replica_id: anchor.replica_id as ReplicaId,
value: anchor.local_timestamp,
},
offset: anchor.offset as usize,
bias: match proto::Bias::from_i32(anchor.bias)? {
proto::Bias::Left => Bias::Left,
proto::Bias::Right => Bias::Right,
},
})
}
+242 -450
View File
@@ -1,17 +1,21 @@
use super::*;
use gpui::{ModelHandle, MutableAppContext, Task};
use gpui::{ModelHandle, MutableAppContext};
use std::{
any::Any,
cell::RefCell,
ffi::OsString,
iter::FromIterator,
ops::Range,
path::PathBuf,
rc::Rc,
time::{Duration, Instant, SystemTime},
time::{Duration, Instant},
};
use unindent::Unindent as _;
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
// std::env::set_var("RUST_LOG", "info");
env_logger::init();
}
#[test]
fn test_select_language() {
let registry = LanguageRegistry {
@@ -85,15 +89,15 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) {
buffer.edit(Some(2..4), "XYZ", cx);
// An empty transaction does not emit any events.
buffer.start_transaction(None).unwrap();
buffer.end_transaction(None, cx).unwrap();
buffer.start_transaction();
buffer.end_transaction(cx);
// A transaction containing two edits emits one edited event.
now += Duration::from_secs(1);
buffer.start_transaction_at(None, now).unwrap();
buffer.start_transaction_at(now);
buffer.edit(Some(5..5), "u", cx);
buffer.edit(Some(6..6), "w", cx);
buffer.end_transaction_at(None, now, cx).unwrap();
buffer.end_transaction_at(now, cx);
// Undoing a transaction emits one edited event.
buffer.undo(cx);
@@ -160,7 +164,7 @@ async fn test_reparse(mut cx: gpui::TestAppContext) {
// Perform some edits (add parameter and variable reference)
// Parsing doesn't begin until the transaction is complete
buffer.update(&mut cx, |buf, cx| {
buf.start_transaction(None).unwrap();
buf.start_transaction();
let offset = buf.text().find(")").unwrap();
buf.edit(vec![offset..offset], "b: C", cx);
@@ -170,7 +174,7 @@ async fn test_reparse(mut cx: gpui::TestAppContext) {
buf.edit(vec![offset..offset], " d; ", cx);
assert!(!buf.is_parsing());
buf.end_transaction(None, cx).unwrap();
buf.end_transaction(cx);
assert_eq!(buf.text(), "fn a(b: C) { d; }");
assert!(buf.is_parsing());
});
@@ -326,58 +330,62 @@ fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
});
}
#[gpui::test]
fn test_autoindent_moves_selections(cx: &mut MutableAppContext) {
cx.add_model(|cx| {
let text = "fn a() {}";
// We need another approach to managing selections with auto-indent
let mut buffer =
Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
// #[gpui::test]
// fn test_autoindent_moves_selections(cx: &mut MutableAppContext) {
// cx.add_model(|cx| {
// let text = "fn a() {}";
let selection_set_id = buffer.add_selection_set::<usize>(&[], cx);
buffer.start_transaction(Some(selection_set_id)).unwrap();
buffer.edit_with_autoindent([5..5, 9..9], "\n\n", cx);
buffer
.update_selection_set(
selection_set_id,
&[
Selection {
id: 0,
start: Point::new(1, 0),
end: Point::new(1, 0),
reversed: false,
goal: SelectionGoal::None,
},
Selection {
id: 1,
start: Point::new(4, 0),
end: Point::new(4, 0),
reversed: false,
goal: SelectionGoal::None,
},
],
cx,
)
.unwrap();
assert_eq!(buffer.text(), "fn a(\n\n) {}\n\n");
// let mut buffer =
// Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
// Ending the transaction runs the auto-indent. The selection
// at the start of the auto-indented row is pushed to the right.
buffer.end_transaction(Some(selection_set_id), cx).unwrap();
assert_eq!(buffer.text(), "fn a(\n \n) {}\n\n");
let selection_ranges = buffer
.selection_set(selection_set_id)
.unwrap()
.selections::<Point>(&buffer)
.map(|selection| selection.point_range(&buffer))
.collect::<Vec<_>>();
// let selection_set_id = buffer.add_selection_set::<usize>(&[], cx);
// buffer.start_transaction();
// buffer.edit_with_autoindent([5..5, 9..9], "\n\n", cx);
// buffer
// .update_selection_set(
// selection_set_id,
// &[
// Selection {
// id: 0,
// start: Point::new(1, 0),
// end: Point::new(1, 0),
// reversed: false,
// goal: SelectionGoal::None,
// },
// Selection {
// id: 1,
// start: Point::new(4, 0),
// end: Point::new(4, 0),
// reversed: false,
// goal: SelectionGoal::None,
// },
// ],
// cx,
// )
// .unwrap();
// assert_eq!(buffer.text(), "fn a(\n\n) {}\n\n");
assert_eq!(selection_ranges[0], empty(Point::new(1, 4)));
assert_eq!(selection_ranges[1], empty(Point::new(4, 0)));
// // TODO! Come up with a different approach to moving selections now that we don't manage selection sets in the buffer
buffer
});
}
// // Ending the transaction runs the auto-indent. The selection
// // at the start of the auto-indented row is pushed to the right.
// buffer.end_transaction(cx);
// assert_eq!(buffer.text(), "fn a(\n \n) {}\n\n");
// let selection_ranges = buffer
// .selection_set(selection_set_id)
// .unwrap()
// .selections::<Point>(&buffer)
// .map(|selection| selection.start.to_point(&buffer)..selection.end.to_point(&buffer))
// .collect::<Vec<_>>();
// assert_eq!(selection_ranges[0], empty(Point::new(1, 4)));
// assert_eq!(selection_ranges[1], empty(Point::new(4, 0)));
// buffer
// });
// }
#[gpui::test]
fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) {
@@ -504,25 +512,41 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
// Receive diagnostics for an earlier version of the buffer.
buffer
.update_diagnostics(
"lsp".into(),
Some(open_notification.text_document.version),
vec![
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "undefined variable 'A'".to_string(),
..Default::default()
DiagnosticEntry {
range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'A'".to_string(),
is_disk_based: true,
group_id: 0,
is_primary: true,
..Default::default()
},
},
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "undefined variable 'BB'".to_string(),
..Default::default()
DiagnosticEntry {
range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'BB'".to_string(),
is_disk_based: true,
group_id: 1,
is_primary: true,
..Default::default()
},
},
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "undefined variable 'CCC'".to_string(),
..Default::default()
DiagnosticEntry {
range: PointUtf16::new(2, 9)..PointUtf16::new(2, 12),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
is_disk_based: true,
message: "undefined variable 'CCC'".to_string(),
group_id: 2,
is_primary: true,
..Default::default()
},
},
],
cx,
@@ -532,25 +556,36 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
// The diagnostics have moved down since they were created.
assert_eq!(
buffer
.diagnostics_in_range(Point::new(3, 0)..Point::new(5, 0))
.snapshot()
.diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0))
.collect::<Vec<_>>(),
&[
(
Point::new(3, 9)..Point::new(3, 11),
&Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'BB'".to_string(),
group_id: 1,
is_primary: true,
},
"lsp",
DiagnosticEntry {
range: Point::new(3, 9)..Point::new(3, 11),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'BB'".to_string(),
is_disk_based: true,
group_id: 1,
is_primary: true,
..Default::default()
},
}
),
(
Point::new(4, 9)..Point::new(4, 12),
&Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'CCC'".to_string(),
group_id: 2,
is_primary: true,
"lsp",
DiagnosticEntry {
range: Point::new(4, 9)..Point::new(4, 12),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'CCC'".to_string(),
is_disk_based: true,
group_id: 2,
is_primary: true,
..Default::default()
}
}
)
]
@@ -579,19 +614,29 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
// Ensure overlapping diagnostics are highlighted correctly.
buffer
.update_diagnostics(
"lsp".into(),
Some(open_notification.text_document.version),
vec![
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "undefined variable 'A'".to_string(),
..Default::default()
DiagnosticEntry {
range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'A'".to_string(),
is_disk_based: true,
group_id: 0,
is_primary: true,
..Default::default()
},
},
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "unreachable statement".to_string(),
..Default::default()
DiagnosticEntry {
range: PointUtf16::new(0, 9)..PointUtf16::new(0, 12),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::WARNING,
message: "unreachable statement".to_string(),
group_id: 1,
is_primary: true,
..Default::default()
},
},
],
cx,
@@ -599,26 +644,36 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
.unwrap();
assert_eq!(
buffer
.diagnostics_in_range(Point::new(2, 0)..Point::new(3, 0))
.snapshot()
.diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0))
.collect::<Vec<_>>(),
&[
(
Point::new(2, 9)..Point::new(2, 12),
&Diagnostic {
severity: DiagnosticSeverity::WARNING,
message: "unreachable statement".to_string(),
group_id: 1,
is_primary: true,
"lsp",
DiagnosticEntry {
range: Point::new(2, 9)..Point::new(2, 12),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::WARNING,
message: "unreachable statement".to_string(),
group_id: 1,
is_primary: true,
..Default::default()
}
}
),
(
Point::new(2, 9)..Point::new(2, 10),
&Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'A'".to_string(),
group_id: 0,
is_primary: true,
},
"lsp",
DiagnosticEntry {
range: Point::new(2, 9)..Point::new(2, 10),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'A'".to_string(),
is_disk_based: true,
group_id: 0,
is_primary: true,
..Default::default()
},
}
)
]
);
@@ -656,21 +711,30 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
buffer.update(&mut cx, |buffer, cx| {
buffer
.update_diagnostics(
"lsp".into(),
Some(change_notification_2.text_document.version),
vec![
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "undefined variable 'BB'".to_string(),
source: Some("disk".to_string()),
..Default::default()
DiagnosticEntry {
range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'BB'".to_string(),
is_disk_based: true,
group_id: 1,
is_primary: true,
..Default::default()
},
},
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "undefined variable 'A'".to_string(),
source: Some("disk".to_string()),
..Default::default()
DiagnosticEntry {
range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'A'".to_string(),
is_disk_based: true,
group_id: 0,
is_primary: true,
..Default::default()
},
},
],
cx,
@@ -678,26 +742,37 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
.unwrap();
assert_eq!(
buffer
.diagnostics_in_range(0..buffer.len())
.snapshot()
.diagnostics_in_range::<_, Point>(0..buffer.len())
.collect::<Vec<_>>(),
&[
(
Point::new(2, 21)..Point::new(2, 22),
&Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'A'".to_string(),
group_id: 0,
is_primary: true,
"lsp",
DiagnosticEntry {
range: Point::new(2, 21)..Point::new(2, 22),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'A'".to_string(),
is_disk_based: true,
group_id: 0,
is_primary: true,
..Default::default()
}
}
),
(
Point::new(3, 9)..Point::new(3, 11),
&Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'BB'".to_string(),
group_id: 1,
is_primary: true,
},
"lsp",
DiagnosticEntry {
range: Point::new(3, 9)..Point::new(3, 11),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'BB'".to_string(),
is_disk_based: true,
group_id: 1,
is_primary: true,
..Default::default()
},
}
)
]
);
@@ -717,25 +792,24 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
buffer
.update_diagnostics(
"lsp".into(),
None,
vec![
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 10),
lsp::Position::new(0, 10),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "syntax error 1".to_string(),
..Default::default()
DiagnosticEntry {
range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "syntax error 1".to_string(),
..Default::default()
},
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(1, 10),
lsp::Position::new(1, 10),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "syntax error 2".to_string(),
..Default::default()
DiagnosticEntry {
range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "syntax error 2".to_string(),
..Default::default()
},
},
],
cx,
@@ -753,9 +827,9 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
.collect::<Vec<_>>(),
&[
("let one = ", None),
(";", Some(lsp::DiagnosticSeverity::ERROR)),
(";", Some(DiagnosticSeverity::ERROR)),
("\nlet two =", None),
(" ", Some(lsp::DiagnosticSeverity::ERROR)),
(" ", Some(DiagnosticSeverity::ERROR)),
("\nlet three = 3;\n", None)
]
);
@@ -763,217 +837,6 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
cx.add_model(|cx| {
let text = "
fn foo(mut v: Vec<usize>) {
for x in &v {
v.push(1);
}
}
"
.unindent();
let file = FakeFile::new("/example.rs");
let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx);
buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
let diagnostics = vec![
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
severity: Some(DiagnosticSeverity::WARNING),
message: "error 1".to_string(),
related_information: Some(vec![lsp::DiagnosticRelatedInformation {
location: lsp::Location {
uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
},
message: "error 1 hint 1".to_string(),
}]),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
severity: Some(DiagnosticSeverity::HINT),
message: "error 1 hint 1".to_string(),
related_information: Some(vec![lsp::DiagnosticRelatedInformation {
location: lsp::Location {
uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
},
message: "original diagnostic".to_string(),
}]),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
severity: Some(DiagnosticSeverity::ERROR),
message: "error 2".to_string(),
related_information: Some(vec![
lsp::DiagnosticRelatedInformation {
location: lsp::Location {
uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
range: lsp::Range::new(
lsp::Position::new(1, 13),
lsp::Position::new(1, 15),
),
},
message: "error 2 hint 1".to_string(),
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location {
uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
range: lsp::Range::new(
lsp::Position::new(1, 13),
lsp::Position::new(1, 15),
),
},
message: "error 2 hint 2".to_string(),
},
]),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
severity: Some(DiagnosticSeverity::HINT),
message: "error 2 hint 1".to_string(),
related_information: Some(vec![lsp::DiagnosticRelatedInformation {
location: lsp::Location {
uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
},
message: "original diagnostic".to_string(),
}]),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
severity: Some(DiagnosticSeverity::HINT),
message: "error 2 hint 2".to_string(),
related_information: Some(vec![lsp::DiagnosticRelatedInformation {
location: lsp::Location {
uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
},
message: "original diagnostic".to_string(),
}]),
..Default::default()
},
];
buffer.update_diagnostics(None, diagnostics, cx).unwrap();
assert_eq!(
buffer
.diagnostics_in_range::<_, Point>(0..buffer.len())
.collect::<Vec<_>>(),
&[
(
Point::new(1, 8)..Point::new(1, 9),
&Diagnostic {
severity: DiagnosticSeverity::WARNING,
message: "error 1".to_string(),
group_id: 0,
is_primary: true,
}
),
(
Point::new(1, 8)..Point::new(1, 9),
&Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 1 hint 1".to_string(),
group_id: 0,
is_primary: false,
}
),
(
Point::new(1, 13)..Point::new(1, 15),
&Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 1".to_string(),
group_id: 1,
is_primary: false,
}
),
(
Point::new(1, 13)..Point::new(1, 15),
&Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 2".to_string(),
group_id: 1,
is_primary: false,
}
),
(
Point::new(2, 8)..Point::new(2, 17),
&Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "error 2".to_string(),
group_id: 1,
is_primary: true,
}
)
]
);
assert_eq!(
buffer.diagnostic_group(0).collect::<Vec<_>>(),
&[
(
Point::new(1, 8)..Point::new(1, 9),
&Diagnostic {
severity: DiagnosticSeverity::WARNING,
message: "error 1".to_string(),
group_id: 0,
is_primary: true,
}
),
(
Point::new(1, 8)..Point::new(1, 9),
&Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 1 hint 1".to_string(),
group_id: 0,
is_primary: false,
}
),
]
);
assert_eq!(
buffer.diagnostic_group(1).collect::<Vec<_>>(),
&[
(
Point::new(1, 13)..Point::new(1, 15),
&Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 1".to_string(),
group_id: 1,
is_primary: false,
}
),
(
Point::new(1, 13)..Point::new(1, 15),
&Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 2".to_string(),
group_id: 1,
is_primary: false,
}
),
(
Point::new(2, 8)..Point::new(2, 17),
&Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "error 2".to_string(),
group_id: 1,
is_primary: true,
}
)
]
);
buffer
});
}
fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
buffer: &Buffer,
range: Range<T>,
@@ -995,13 +858,17 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
#[test]
fn test_contiguous_ranges() {
assert_eq!(
contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12], 100).collect::<Vec<_>>(),
contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
&[1..4, 5..7, 9..13]
);
// Respects the `max_len` parameter
assert_eq!(
contiguous_ranges([2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31], 3).collect::<Vec<_>>(),
contiguous_ranges(
[2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
3
)
.collect::<Vec<_>>(),
&[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
);
}
@@ -1011,11 +878,13 @@ impl Buffer {
&self,
range: Range<T>,
) -> Option<(Range<Point>, Range<Point>)> {
self.enclosing_bracket_ranges(range).map(|(start, end)| {
let point_start = start.start.to_point(self)..start.end.to_point(self);
let point_end = end.start.to_point(self)..end.end.to_point(self);
(point_start, point_end)
})
self.snapshot()
.enclosing_bracket_ranges(range)
.map(|(start, end)| {
let point_start = start.start.to_point(self)..start.end.to_point(self);
let point_end = end.start.to_point(self)..end.end.to_point(self);
(point_start, point_end)
})
}
}
@@ -1045,80 +914,3 @@ fn rust_lang() -> Language {
fn empty(point: Point) -> Range<Point> {
point..point
}
#[derive(Clone)]
struct FakeFile {
abs_path: PathBuf,
}
impl FakeFile {
fn new(abs_path: impl Into<PathBuf>) -> Self {
Self {
abs_path: abs_path.into(),
}
}
}
impl File for FakeFile {
fn worktree_id(&self) -> usize {
todo!()
}
fn entry_id(&self) -> Option<usize> {
todo!()
}
fn mtime(&self) -> SystemTime {
SystemTime::now()
}
fn path(&self) -> &Arc<Path> {
todo!()
}
fn abs_path(&self) -> Option<PathBuf> {
Some(self.abs_path.clone())
}
fn full_path(&self) -> PathBuf {
todo!()
}
fn file_name(&self) -> Option<OsString> {
todo!()
}
fn is_deleted(&self) -> bool {
todo!()
}
fn save(
&self,
_: u64,
_: Rope,
_: clock::Global,
_: &mut MutableAppContext,
) -> Task<Result<(clock::Global, SystemTime)>> {
todo!()
}
fn load_local(&self, _: &AppContext) -> Option<Task<Result<String>>> {
todo!()
}
fn buffer_updated(&self, _: u64, _: super::Operation, _: &mut MutableAppContext) {
todo!()
}
fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {
todo!()
}
fn boxed_clone(&self) -> Box<dyn File> {
todo!()
}
fn as_any(&self) -> &dyn Any {
todo!()
}
}
+5 -1
View File
@@ -226,7 +226,11 @@ impl LanguageServer {
process_id: Default::default(),
root_path: Default::default(),
root_uri: Some(root_uri),
initialization_options: Default::default(),
initialization_options: Some(json!({
"checkOnSave": {
"enable": false
},
})),
capabilities: lsp_types::ClientCapabilities {
experimental: Some(json!({
"serverStatusNotification": true,
+2
View File
@@ -13,6 +13,7 @@ test-support = ["language/test-support", "text/test-support"]
text = { path = "../text" }
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
fsevent = { path = "../fsevent" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
@@ -37,6 +38,7 @@ toml = "0.5"
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
+1
View File
@@ -134,6 +134,7 @@ impl Fs for RealFs {
}
}
#[cfg(any(test, feature = "test-support"))]
#[derive(Clone, Debug)]
struct FakeFsEntry {
metadata: Metadata,
+683 -125
View File
@@ -2,34 +2,65 @@ pub mod fs;
mod ignore;
mod worktree;
use anyhow::Result;
use client::{Client, UserStore};
use anyhow::{anyhow, Result};
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use clock::ReplicaId;
use collections::HashMap;
use futures::Future;
use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
use language::LanguageRegistry;
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
};
use language::{Buffer, DiagnosticEntry, LanguageRegistry};
use lsp::DiagnosticSeverity;
use postage::{prelude::Stream, watch};
use std::{
path::Path,
sync::{atomic::AtomicBool, Arc},
};
use util::TryFutureExt as _;
use util::{ResultExt, TryFutureExt as _};
pub use fs::*;
pub use worktree::*;
pub struct Project {
worktrees: Vec<ModelHandle<Worktree>>,
active_worktree: Option<usize>,
active_entry: Option<ProjectEntry>,
languages: Arc<LanguageRegistry>,
client: Arc<client::Client>,
user_store: ModelHandle<UserStore>,
fs: Arc<dyn Fs>,
client_state: ProjectClientState,
collaborators: HashMap<PeerId, Collaborator>,
subscriptions: Vec<client::Subscription>,
}
enum ProjectClientState {
Local {
is_shared: bool,
remote_id_tx: watch::Sender<Option<u64>>,
remote_id_rx: watch::Receiver<Option<u64>>,
_maintain_remote_id_task: Task<Option<()>>,
},
Remote {
sharing_has_stopped: bool,
remote_id: u64,
replica_id: ReplicaId,
},
}
#[derive(Clone, Debug)]
pub struct Collaborator {
pub user: Arc<User>,
pub peer_id: PeerId,
pub replica_id: ReplicaId,
}
#[derive(Debug)]
pub enum Event {
ActiveEntryChanged(Option<ProjectEntry>),
WorktreeRemoved(usize),
DiagnosticsUpdated(ProjectPath),
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
@@ -38,6 +69,39 @@ pub struct ProjectPath {
pub path: Arc<Path>,
}
#[derive(Clone)]
pub struct DiagnosticSummary {
pub error_count: usize,
pub warning_count: usize,
pub info_count: usize,
pub hint_count: usize,
}
impl DiagnosticSummary {
fn new<'a, T: 'a>(diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<T>>) -> Self {
let mut this = Self {
error_count: 0,
warning_count: 0,
info_count: 0,
hint_count: 0,
};
for entry in diagnostics {
if entry.diagnostic.is_primary {
match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => this.error_count += 1,
DiagnosticSeverity::WARNING => this.warning_count += 1,
DiagnosticSeverity::INFORMATION => this.info_count += 1,
DiagnosticSeverity::HINT => this.hint_count += 1,
_ => {}
}
}
}
this
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ProjectEntry {
pub worktree_id: usize,
@@ -45,105 +109,388 @@ pub struct ProjectEntry {
}
impl Project {
pub fn new(
languages: Arc<LanguageRegistry>,
pub fn local(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
) -> Self {
Self {
worktrees: Default::default(),
active_worktree: None,
cx: &mut MutableAppContext,
) -> ModelHandle<Self> {
cx.add_model(|cx: &mut ModelContext<Self>| {
let (remote_id_tx, remote_id_rx) = watch::channel();
let _maintain_remote_id_task = cx.spawn_weak({
let rpc = client.clone();
move |this, mut cx| {
async move {
let mut status = rpc.status();
while let Some(status) = status.recv().await {
if let Some(this) = this.upgrade(&cx) {
let remote_id = if let client::Status::Connected { .. } = status {
let response = rpc.request(proto::RegisterProject {}).await?;
Some(response.project_id)
} else {
None
};
if let Some(project_id) = remote_id {
let mut registrations = Vec::new();
this.read_with(&cx, |this, cx| {
for worktree in &this.worktrees {
let worktree_id = worktree.id() as u64;
let worktree = worktree.read(cx).as_local().unwrap();
registrations.push(rpc.request(
proto::RegisterWorktree {
project_id,
worktree_id,
root_name: worktree.root_name().to_string(),
authorized_logins: worktree.authorized_logins(),
},
));
}
});
for registration in registrations {
registration.await?;
}
}
this.update(&mut cx, |this, cx| this.set_remote_id(remote_id, cx));
}
}
Ok(())
}
.log_err()
}
});
Self {
worktrees: Default::default(),
collaborators: Default::default(),
client_state: ProjectClientState::Local {
is_shared: false,
remote_id_tx,
remote_id_rx,
_maintain_remote_id_task,
},
subscriptions: Vec::new(),
active_entry: None,
languages,
client,
user_store,
fs,
}
})
}
pub async fn remote(
remote_id: u64,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> {
client.authenticate_and_connect(&cx).await?;
let response = client
.request(proto::JoinProject {
project_id: remote_id,
})
.await?;
let replica_id = response.replica_id as ReplicaId;
let mut worktrees = Vec::new();
for worktree in response.worktrees {
worktrees.push(
Worktree::remote(
remote_id,
replica_id,
worktree,
client.clone(),
user_store.clone(),
languages.clone(),
cx,
)
.await?,
);
}
let user_ids = response
.collaborators
.iter()
.map(|peer| peer.user_id)
.collect();
user_store
.update(cx, |user_store, cx| user_store.load_users(user_ids, cx))
.await?;
let mut collaborators = HashMap::default();
for message in response.collaborators {
let collaborator = Collaborator::from_proto(message, &user_store, cx).await?;
collaborators.insert(collaborator.peer_id, collaborator);
}
Ok(cx.add_model(|cx| Self {
worktrees,
active_entry: None,
collaborators,
languages,
client,
user_store,
fs,
subscriptions: vec![
client.subscribe_to_entity(remote_id, cx, Self::handle_unshare_project),
client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator),
client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator),
client.subscribe_to_entity(remote_id, cx, Self::handle_share_worktree),
client.subscribe_to_entity(remote_id, cx, Self::handle_unregister_worktree),
client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree),
client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
],
client,
client_state: ProjectClientState::Remote {
sharing_has_stopped: false,
remote_id,
replica_id,
},
}))
}
fn set_remote_id(&mut self, remote_id: Option<u64>, cx: &mut ModelContext<Self>) {
if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state {
*remote_id_tx.borrow_mut() = remote_id;
}
self.subscriptions.clear();
if let Some(remote_id) = remote_id {
let client = &self.client;
self.subscriptions.extend([
client.subscribe_to_entity(remote_id, cx, Self::handle_open_buffer),
client.subscribe_to_entity(remote_id, cx, Self::handle_close_buffer),
client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator),
client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator),
client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree),
client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
client.subscribe_to_entity(remote_id, cx, Self::handle_save_buffer),
client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
]);
}
}
pub fn remote_id(&self) -> Option<u64> {
match &self.client_state {
ProjectClientState::Local { remote_id_rx, .. } => *remote_id_rx.borrow(),
ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
}
}
pub fn next_remote_id(&self) -> impl Future<Output = u64> {
let mut id = None;
let mut watch = None;
match &self.client_state {
ProjectClientState::Local { remote_id_rx, .. } => watch = Some(remote_id_rx.clone()),
ProjectClientState::Remote { remote_id, .. } => id = Some(*remote_id),
}
async move {
if let Some(id) = id {
return id;
}
let mut watch = watch.unwrap();
loop {
let id = *watch.borrow();
if let Some(id) = id {
return id;
}
watch.recv().await;
}
}
}
pub fn replica_id(&self) -> ReplicaId {
match &self.client_state {
ProjectClientState::Local { .. } => 0,
ProjectClientState::Remote { replica_id, .. } => *replica_id,
}
}
pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
&self.collaborators
}
pub fn worktrees(&self) -> &[ModelHandle<Worktree>] {
&self.worktrees
}
pub fn worktree_for_id(&self, id: usize) -> Option<ModelHandle<Worktree>> {
pub fn worktree_for_id(&self, id: usize, cx: &AppContext) -> Option<ModelHandle<Worktree>> {
self.worktrees
.iter()
.find(|worktree| worktree.id() == id)
.find(|worktree| worktree.read(cx).id() == id)
.cloned()
}
pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<anyhow::Result<()>> {
let rpc = self.client.clone();
cx.spawn(|this, mut cx| async move {
let project_id = this.update(&mut cx, |this, _| {
if let ProjectClientState::Local {
is_shared,
remote_id_rx,
..
} = &mut this.client_state
{
*is_shared = true;
remote_id_rx
.borrow()
.ok_or_else(|| anyhow!("no project id"))
} else {
Err(anyhow!("can't share a remote project"))
}
})?;
rpc.request(proto::ShareProject { project_id }).await?;
let mut tasks = Vec::new();
this.update(&mut cx, |this, cx| {
for worktree in &this.worktrees {
worktree.update(cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
tasks.push(worktree.share(project_id, cx));
});
}
});
for task in tasks {
task.await?;
}
this.update(&mut cx, |_, cx| cx.notify());
Ok(())
})
}
pub fn unshare(&self, cx: &mut ModelContext<Self>) -> Task<anyhow::Result<()>> {
let rpc = self.client.clone();
cx.spawn(|this, mut cx| async move {
let project_id = this.update(&mut cx, |this, _| {
if let ProjectClientState::Local {
is_shared,
remote_id_rx,
..
} = &mut this.client_state
{
*is_shared = false;
remote_id_rx
.borrow()
.ok_or_else(|| anyhow!("no project id"))
} else {
Err(anyhow!("can't share a remote project"))
}
})?;
rpc.send(proto::UnshareProject { project_id }).await?;
this.update(&mut cx, |this, cx| {
this.collaborators.clear();
cx.notify()
});
Ok(())
})
}
pub fn is_read_only(&self) -> bool {
match &self.client_state {
ProjectClientState::Local { .. } => false,
ProjectClientState::Remote {
sharing_has_stopped,
..
} => *sharing_has_stopped,
}
}
pub fn is_local(&self) -> bool {
match &self.client_state {
ProjectClientState::Local { .. } => true,
ProjectClientState::Remote { .. } => false,
}
}
pub fn open_buffer(
&self,
path: ProjectPath,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Buffer>>> {
if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) {
worktree.update(cx, |worktree, cx| worktree.open_buffer(path.path, cx))
} else {
cx.spawn(|_, _| async move { Err(anyhow!("no such worktree")) })
}
}
pub fn is_shared(&self) -> bool {
match &self.client_state {
ProjectClientState::Local { is_shared, .. } => *is_shared,
ProjectClientState::Remote { .. } => false,
}
}
pub fn add_local_worktree(
&mut self,
abs_path: &Path,
abs_path: impl AsRef<Path>,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Worktree>>> {
let fs = self.fs.clone();
let client = self.client.clone();
let user_store = self.user_store.clone();
let languages = self.languages.clone();
let path = Arc::from(abs_path);
cx.spawn(|this, mut cx| async move {
let path = Arc::from(abs_path.as_ref());
cx.spawn(|project, mut cx| async move {
let worktree =
Worktree::open_local(client, user_store, path, fs, languages, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.add_worktree(worktree.clone(), cx);
});
Ok(worktree)
})
}
pub fn add_remote_worktree(
&mut self,
remote_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Worktree>>> {
let rpc = self.client.clone();
let languages = self.languages.clone();
let user_store = self.user_store.clone();
cx.spawn(|this, mut cx| async move {
rpc.authenticate_and_connect(&cx).await?;
let worktree =
Worktree::open_remote(rpc.clone(), remote_id, languages, user_store, &mut cx)
Worktree::open_local(client.clone(), user_store, path, fs, languages, &mut cx)
.await?;
this.update(&mut cx, |this, cx| {
cx.subscribe(&worktree, move |this, _, event, cx| match event {
worktree::Event::Closed => {
this.close_remote_worktree(remote_id, cx);
cx.notify();
}
})
.detach();
this.add_worktree(worktree.clone(), cx);
let (remote_project_id, is_shared) = project.update(&mut cx, |project, cx| {
project.add_worktree(worktree.clone(), cx);
(project.remote_id(), project.is_shared())
});
if let Some(project_id) = remote_project_id {
let worktree_id = worktree.id() as u64;
let register_message = worktree.update(&mut cx, |worktree, _| {
let worktree = worktree.as_local_mut().unwrap();
proto::RegisterWorktree {
project_id,
worktree_id,
root_name: worktree.root_name().to_string(),
authorized_logins: worktree.authorized_logins(),
}
});
client.request(register_message).await?;
if is_shared {
worktree
.update(&mut cx, |worktree, cx| {
worktree.as_local_mut().unwrap().share(project_id, cx)
})
.await?;
}
}
Ok(worktree)
})
}
fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
if self.active_worktree.is_none() {
self.set_active_worktree(Some(worktree.id()), cx);
}
cx.subscribe(&worktree, |_, worktree, event, cx| match event {
worktree::Event::DiagnosticsUpdated(path) => {
cx.emit(Event::DiagnosticsUpdated(ProjectPath {
worktree_id: worktree.id(),
path: path.clone(),
}));
}
})
.detach();
self.worktrees.push(worktree);
cx.notify();
}
fn set_active_worktree(&mut self, worktree_id: Option<usize>, cx: &mut ModelContext<Self>) {
if self.active_worktree != worktree_id {
self.active_worktree = worktree_id;
cx.notify();
}
}
pub fn active_worktree(&self) -> Option<ModelHandle<Worktree>> {
self.active_worktree
.and_then(|worktree_id| self.worktree_for_id(worktree_id))
}
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
let new_active_entry = entry.and_then(|project_path| {
let worktree = self.worktree_for_id(project_path.worktree_id)?;
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
let entry = worktree.read(cx).entry_for_path(project_path.path)?;
Some(ProjectEntry {
worktree_id: project_path.worktree_id,
@@ -151,92 +498,257 @@ impl Project {
})
});
if new_active_entry != self.active_entry {
if let Some(worktree_id) = new_active_entry.map(|e| e.worktree_id) {
self.set_active_worktree(Some(worktree_id), cx);
}
self.active_entry = new_active_entry;
cx.emit(Event::ActiveEntryChanged(new_active_entry));
}
}
pub fn diagnose(&self, cx: &mut ModelContext<Self>) {
for worktree_handle in &self.worktrees {
if let Some(worktree) = worktree_handle.read(cx).as_local() {
for language in worktree.languages() {
if let Some(provider) = language.diagnostic_provider().cloned() {
let worktree_path = worktree.abs_path().clone();
let worktree_handle = worktree_handle.downgrade();
cx.spawn_weak(|_, mut cx| async move {
let diagnostics = provider.diagnose(worktree_path).await.log_err()?;
let worktree_handle = worktree_handle.upgrade(&cx)?;
worktree_handle.update(&mut cx, |worktree, cx| {
for (path, diagnostics) in diagnostics {
worktree
.update_diagnostics_from_provider(
path.into(),
diagnostics,
cx,
)
.log_err()?;
}
Some(())
})
})
.detach();
}
}
}
}
}
pub fn diagnostic_summaries<'a>(
&'a self,
cx: &'a AppContext,
) -> impl Iterator<Item = (ProjectPath, DiagnosticSummary)> + 'a {
self.worktrees.iter().flat_map(move |worktree| {
let worktree_id = worktree.id();
worktree
.read(cx)
.diagnostic_summaries()
.map(move |(path, summary)| (ProjectPath { worktree_id, path }, summary))
})
}
pub fn active_entry(&self) -> Option<ProjectEntry> {
self.active_entry
}
pub fn share_worktree(&self, remote_id: u64, cx: &mut ModelContext<Self>) {
let rpc = self.client.clone();
// RPC message handlers
fn handle_unshare_project(
&mut self,
_: TypedEnvelope<proto::UnshareProject>,
_: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let ProjectClientState::Remote {
sharing_has_stopped,
..
} = &mut self.client_state
{
*sharing_has_stopped = true;
self.collaborators.clear();
cx.notify();
Ok(())
} else {
unreachable!()
}
}
fn handle_add_collaborator(
&mut self,
mut envelope: TypedEnvelope<proto::AddProjectCollaborator>,
_: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
let user_store = self.user_store.clone();
let collaborator = envelope
.payload
.collaborator
.take()
.ok_or_else(|| anyhow!("empty collaborator"))?;
cx.spawn(|this, mut cx| {
async move {
rpc.authenticate_and_connect(&cx).await?;
let task = this.update(&mut cx, |this, cx| {
for worktree in &this.worktrees {
let task = worktree.update(cx, |worktree, cx| {
worktree.as_local_mut().and_then(|worktree| {
if worktree.remote_id() == Some(remote_id) {
Some(worktree.share(cx))
} else {
None
}
})
});
if task.is_some() {
return task;
}
}
None
let collaborator =
Collaborator::from_proto(collaborator, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.collaborators
.insert(collaborator.peer_id, collaborator);
cx.notify();
});
if let Some(task) = task {
task.await?;
}
Ok(())
}
.log_err()
})
.detach();
Ok(())
}
pub fn unshare_worktree(&mut self, remote_id: u64, cx: &mut ModelContext<Self>) {
fn handle_remove_collaborator(
&mut self,
envelope: TypedEnvelope<proto::RemoveProjectCollaborator>,
_: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
let peer_id = PeerId(envelope.payload.peer_id);
let replica_id = self
.collaborators
.remove(&peer_id)
.ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?
.replica_id;
for worktree in &self.worktrees {
if worktree.update(cx, |worktree, cx| {
if let Some(worktree) = worktree.as_local_mut() {
if worktree.remote_id() == Some(remote_id) {
worktree.unshare(cx);
return true;
}
}
false
}) {
break;
worktree.update(cx, |worktree, cx| {
worktree.remove_collaborator(peer_id, replica_id, cx);
})
}
Ok(())
}
fn handle_share_worktree(
&mut self,
envelope: TypedEnvelope<proto::ShareWorktree>,
client: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
let remote_id = self.remote_id().ok_or_else(|| anyhow!("invalid project"))?;
let replica_id = self.replica_id();
let worktree = envelope
.payload
.worktree
.ok_or_else(|| anyhow!("invalid worktree"))?;
let user_store = self.user_store.clone();
let languages = self.languages.clone();
cx.spawn(|this, mut cx| {
async move {
let worktree = Worktree::remote(
remote_id, replica_id, worktree, client, user_store, languages, &mut cx,
)
.await?;
this.update(&mut cx, |this, cx| this.add_worktree(worktree, cx));
Ok(())
}
.log_err()
})
.detach();
Ok(())
}
fn handle_unregister_worktree(
&mut self,
envelope: TypedEnvelope<proto::UnregisterWorktree>,
_: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
self.worktrees.retain(|worktree| {
worktree.read(cx).as_remote().unwrap().remote_id() != envelope.payload.worktree_id
});
cx.notify();
Ok(())
}
fn handle_update_worktree(
&mut self,
envelope: TypedEnvelope<proto::UpdateWorktree>,
_: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize, cx) {
worktree.update(cx, |worktree, cx| {
let worktree = worktree.as_remote_mut().unwrap();
worktree.update_from_remote(envelope, cx)
})?;
}
Ok(())
}
pub fn handle_update_buffer(
&mut self,
envelope: TypedEnvelope<proto::UpdateBuffer>,
_: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize, cx) {
worktree.update(cx, |worktree, cx| {
worktree.handle_update_buffer(envelope, cx)
})?;
}
Ok(())
}
pub fn handle_save_buffer(
&mut self,
envelope: TypedEnvelope<proto::SaveBuffer>,
rpc: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize, cx) {
worktree.update(cx, |worktree, cx| {
worktree.handle_save_buffer(envelope, rpc, cx)
})?;
}
Ok(())
}
pub fn handle_open_buffer(
&mut self,
envelope: TypedEnvelope<proto::OpenBuffer>,
rpc: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> anyhow::Result<()> {
if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize, cx) {
return worktree.update(cx, |worktree, cx| {
worktree.handle_open_buffer(envelope, rpc, cx)
});
} else {
Err(anyhow!("no such worktree"))
}
}
pub fn close_remote_worktree(&mut self, id: u64, cx: &mut ModelContext<Self>) {
let mut reset_active = None;
self.worktrees.retain(|worktree| {
let keep = worktree.update(cx, |worktree, cx| {
if let Some(worktree) = worktree.as_remote_mut() {
if worktree.remote_id() == id {
worktree.close_all_buffers(cx);
return false;
}
}
true
});
if !keep {
cx.emit(Event::WorktreeRemoved(worktree.id()));
reset_active = Some(worktree.id());
}
keep
});
if self.active_worktree == reset_active {
self.active_worktree = self.worktrees.first().map(|w| w.id());
cx.notify();
pub fn handle_close_buffer(
&mut self,
envelope: TypedEnvelope<proto::CloseBuffer>,
rpc: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> anyhow::Result<()> {
if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize, cx) {
worktree.update(cx, |worktree, cx| {
worktree.handle_close_buffer(envelope, rpc, cx)
})?;
}
Ok(())
}
pub fn handle_buffer_saved(
&mut self,
envelope: TypedEnvelope<proto::BufferSaved>,
_: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize, cx) {
worktree.update(cx, |worktree, cx| {
worktree.handle_buffer_saved(envelope, cx)
})?;
}
Ok(())
}
pub fn match_paths<'a>(
@@ -335,6 +847,52 @@ impl<'a> Iterator for CandidateSetIter<'a> {
impl Entity for Project {
type Event = Event;
fn release(&mut self, cx: &mut gpui::MutableAppContext) {
match &self.client_state {
ProjectClientState::Local { remote_id_rx, .. } => {
if let Some(project_id) = *remote_id_rx.borrow() {
let rpc = self.client.clone();
cx.spawn(|_| async move {
if let Err(err) = rpc.send(proto::UnregisterProject { project_id }).await {
log::error!("error unregistering project: {}", err);
}
})
.detach();
}
}
ProjectClientState::Remote { remote_id, .. } => {
let rpc = self.client.clone();
let project_id = *remote_id;
cx.spawn(|_| async move {
if let Err(err) = rpc.send(proto::LeaveProject { project_id }).await {
log::error!("error leaving project: {}", err);
}
})
.detach();
}
}
}
}
impl Collaborator {
fn from_proto(
message: proto::Collaborator,
user_store: &ModelHandle<UserStore>,
cx: &mut AsyncAppContext,
) -> impl Future<Output = Result<Self>> {
let user = user_store.update(cx, |user_store, cx| {
user_store.fetch_user(message.user_id, cx)
});
async move {
Ok(Self {
peer_id: PeerId(message.peer_id),
user: user.await?,
replica_id: message.replica_id as ReplicaId,
})
}
}
}
#[cfg(test)]
@@ -449,6 +1007,6 @@ mod tests {
let client = client::Client::new();
let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
cx.add_model(|_| Project::new(languages, client, user_store, fs))
cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
}
}
File diff suppressed because it is too large Load Diff
+9 -8
View File
@@ -118,7 +118,7 @@ impl ProjectPanel {
worktree_id,
entry_id,
} => {
if let Some(worktree) = project.read(cx).worktree_for_id(*worktree_id) {
if let Some(worktree) = project.read(cx).worktree_for_id(*worktree_id, cx) {
if let Some(entry) = worktree.read(cx).entry_for_id(*entry_id) {
workspace
.open_entry(
@@ -307,7 +307,7 @@ impl ProjectPanel {
fn selected_entry<'a>(&self, cx: &'a AppContext) -> Option<(&'a Worktree, &'a project::Entry)> {
let selection = self.selection?;
let project = self.project.read(cx);
let worktree = project.worktree_for_id(selection.worktree_id)?.read(cx);
let worktree = project.worktree_for_id(selection.worktree_id, cx)?.read(cx);
Some((worktree, worktree.entry_for_id(selection.entry_id)?))
}
@@ -374,7 +374,7 @@ impl ProjectPanel {
fn expand_entry(&mut self, worktree_id: usize, entry_id: usize, cx: &mut ViewContext<Self>) {
let project = self.project.read(cx);
if let Some((worktree, expanded_dir_ids)) = project
.worktree_for_id(worktree_id)
.worktree_for_id(worktree_id, cx)
.zip(self.expanded_dir_ids.get_mut(&worktree_id))
{
let worktree = worktree.read(cx);
@@ -617,17 +617,18 @@ mod tests {
)
.await;
let project = cx.add_model(|_| {
Project::new(
params.languages.clone(),
let project = cx.update(|cx| {
Project::local(
params.client.clone(),
params.user_store.clone(),
params.languages.clone(),
params.fs.clone(),
cx,
)
});
let root1 = project
.update(&mut cx, |project, cx| {
project.add_local_worktree("/root1".as_ref(), cx)
project.add_local_worktree("/root1", cx)
})
.await
.unwrap();
@@ -636,7 +637,7 @@ mod tests {
.await;
let root2 = project
.update(&mut cx, |project, cx| {
project.add_local_worktree("/root2".as_ref(), cx)
project.add_local_worktree("/root2", cx)
})
.await
.unwrap();
+148 -105
View File
@@ -9,37 +9,46 @@ message Envelope {
Ack ack = 4;
Error error = 5;
Ping ping = 6;
ShareWorktree share_worktree = 7;
ShareWorktreeResponse share_worktree_response = 8;
JoinWorktree join_worktree = 9;
JoinWorktreeResponse join_worktree_response = 10;
UpdateWorktree update_worktree = 11;
CloseWorktree close_worktree = 12;
OpenBuffer open_buffer = 13;
OpenBufferResponse open_buffer_response = 14;
CloseBuffer close_buffer = 15;
UpdateBuffer update_buffer = 16;
SaveBuffer save_buffer = 17;
BufferSaved buffer_saved = 18;
AddCollaborator add_collaborator = 19;
RemoveCollaborator remove_collaborator = 20;
GetChannels get_channels = 21;
GetChannelsResponse get_channels_response = 22;
GetUsers get_users = 23;
GetUsersResponse get_users_response = 24;
JoinChannel join_channel = 25;
JoinChannelResponse join_channel_response = 26;
LeaveChannel leave_channel = 27;
SendChannelMessage send_channel_message = 28;
SendChannelMessageResponse send_channel_message_response = 29;
ChannelMessageSent channel_message_sent = 30;
GetChannelMessages get_channel_messages = 31;
GetChannelMessagesResponse get_channel_messages_response = 32;
OpenWorktree open_worktree = 33;
OpenWorktreeResponse open_worktree_response = 34;
UnshareWorktree unshare_worktree = 35;
UpdateContacts update_contacts = 36;
LeaveWorktree leave_worktree = 37;
RegisterProject register_project = 7;
RegisterProjectResponse register_project_response = 8;
UnregisterProject unregister_project = 9;
ShareProject share_project = 10;
UnshareProject unshare_project = 11;
JoinProject join_project = 12;
JoinProjectResponse join_project_response = 13;
LeaveProject leave_project = 14;
AddProjectCollaborator add_project_collaborator = 15;
RemoveProjectCollaborator remove_project_collaborator = 16;
RegisterWorktree register_worktree = 17;
UnregisterWorktree unregister_worktree = 18;
ShareWorktree share_worktree = 100;
UpdateWorktree update_worktree = 19;
UpdateDiagnosticSummary update_diagnostic_summary = 20;
OpenBuffer open_buffer = 22;
OpenBufferResponse open_buffer_response = 23;
CloseBuffer close_buffer = 24;
UpdateBuffer update_buffer = 25;
SaveBuffer save_buffer = 26;
BufferSaved buffer_saved = 27;
GetChannels get_channels = 28;
GetChannelsResponse get_channels_response = 29;
JoinChannel join_channel = 30;
JoinChannelResponse join_channel_response = 31;
LeaveChannel leave_channel = 32;
SendChannelMessage send_channel_message = 33;
SendChannelMessageResponse send_channel_message_response = 34;
ChannelMessageSent channel_message_sent = 35;
GetChannelMessages get_channel_messages = 36;
GetChannelMessagesResponse get_channel_messages_response = 37;
UpdateContacts update_contacts = 38;
GetUsers get_users = 39;
GetUsersResponse get_users_response = 40;
}
}
@@ -53,62 +62,77 @@ message Error {
string message = 1;
}
message OpenWorktree {
string root_name = 1;
repeated string authorized_logins = 2;
message RegisterProject {}
message RegisterProjectResponse {
uint64 project_id = 1;
}
message OpenWorktreeResponse {
uint64 worktree_id = 1;
message UnregisterProject {
uint64 project_id = 1;
}
message ShareWorktree {
Worktree worktree = 1;
message ShareProject {
uint64 project_id = 1;
}
message ShareWorktreeResponse {}
message UnshareWorktree {
uint64 worktree_id = 1;
message UnshareProject {
uint64 project_id = 1;
}
message JoinWorktree {
uint64 worktree_id = 1;
message JoinProject {
uint64 project_id = 1;
}
message LeaveWorktree {
uint64 worktree_id = 1;
}
message JoinWorktreeResponse {
Worktree worktree = 2;
uint32 replica_id = 3;
message JoinProjectResponse {
uint32 replica_id = 2;
repeated Worktree worktrees = 3;
repeated Collaborator collaborators = 4;
}
message LeaveProject {
uint64 project_id = 1;
}
message RegisterWorktree {
uint64 project_id = 1;
uint64 worktree_id = 2;
string root_name = 3;
repeated string authorized_logins = 4;
}
message UnregisterWorktree {
uint64 project_id = 1;
uint64 worktree_id = 2;
}
message ShareWorktree {
uint64 project_id = 1;
Worktree worktree = 2;
}
message UpdateWorktree {
uint64 worktree_id = 1;
repeated Entry updated_entries = 2;
repeated uint64 removed_entries = 3;
uint64 project_id = 1;
uint64 worktree_id = 2;
string root_name = 3;
repeated Entry updated_entries = 4;
repeated uint64 removed_entries = 5;
}
message CloseWorktree {
uint64 worktree_id = 1;
}
message AddCollaborator {
uint64 worktree_id = 1;
message AddProjectCollaborator {
uint64 project_id = 1;
Collaborator collaborator = 2;
}
message RemoveCollaborator {
uint64 worktree_id = 1;
message RemoveProjectCollaborator {
uint64 project_id = 1;
uint32 peer_id = 2;
}
message OpenBuffer {
uint64 worktree_id = 1;
string path = 2;
uint64 project_id = 1;
uint64 worktree_id = 2;
string path = 3;
}
message OpenBufferResponse {
@@ -116,26 +140,38 @@ message OpenBufferResponse {
}
message CloseBuffer {
uint64 worktree_id = 1;
uint64 buffer_id = 2;
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 buffer_id = 3;
}
message UpdateBuffer {
uint64 worktree_id = 1;
uint64 buffer_id = 2;
repeated Operation operations = 3;
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 buffer_id = 3;
repeated Operation operations = 4;
}
message SaveBuffer {
uint64 worktree_id = 1;
uint64 buffer_id = 2;
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 buffer_id = 3;
}
message BufferSaved {
uint64 worktree_id = 1;
uint64 buffer_id = 2;
repeated VectorClockEntry version = 3;
Timestamp mtime = 4;
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 buffer_id = 3;
repeated VectorClockEntry version = 4;
Timestamp mtime = 5;
}
message UpdateDiagnosticSummary {
uint64 project_id = 1;
uint64 worktree_id = 2;
string path = 3;
uint32 error_count = 4;
uint32 warning_count = 5;
}
message GetChannels {}
@@ -229,36 +265,55 @@ message Buffer {
string content = 2;
repeated Operation.Edit history = 3;
repeated SelectionSet selections = 4;
DiagnosticSet diagnostics = 5;
repeated DiagnosticSet diagnostic_sets = 5;
}
message SelectionSet {
uint32 replica_id = 1;
uint32 lamport_timestamp = 2;
bool is_active = 3;
repeated VectorClockEntry version = 4;
repeated Selection selections = 5;
repeated Selection selections = 2;
}
message Selection {
uint64 id = 1;
uint64 start = 2;
uint64 end = 3;
Anchor start = 2;
Anchor end = 3;
bool reversed = 4;
}
message Anchor {
uint32 replica_id = 1;
uint32 local_timestamp = 2;
uint64 offset = 3;
Bias bias = 4;
}
enum Bias {
Left = 0;
Right = 1;
}
message UpdateDiagnosticSet {
uint32 replica_id = 1;
uint32 lamport_timestamp = 2;
DiagnosticSet diagnostic_set = 3;
}
message DiagnosticSet {
repeated VectorClockEntry version = 1;
string provider_name = 1;
repeated Diagnostic diagnostics = 2;
}
message Diagnostic {
uint64 start = 1;
uint64 end = 2;
Anchor start = 1;
Anchor end = 2;
Severity severity = 3;
string message = 4;
uint64 group_id = 5;
bool is_primary = 6;
optional string code = 5;
uint64 group_id = 6;
bool is_primary = 7;
bool is_valid = 8;
bool is_disk_based = 9;
enum Severity {
None = 0;
Error = 1;
@@ -268,16 +323,13 @@ message Diagnostic {
}
}
message Operation {
oneof variant {
Edit edit = 1;
Undo undo = 2;
UpdateSelections update_selections = 3;
RemoveSelections remove_selections = 4;
SetActiveSelections set_active_selections = 5;
DiagnosticSet update_diagnostics = 6;
UpdateDiagnosticSet update_diagnostic_set = 5;
}
message Edit {
@@ -306,21 +358,12 @@ message Operation {
message UpdateSelections {
uint32 replica_id = 1;
uint32 local_timestamp = 2;
uint32 lamport_timestamp = 3;
repeated VectorClockEntry version = 4;
repeated Selection selections = 5;
repeated Selection selections = 4;
}
message RemoveSelections {
uint32 replica_id = 1;
uint32 local_timestamp = 2;
uint32 lamport_timestamp = 3;
}
message SetActiveSelections {
uint32 replica_id = 1;
optional uint32 local_timestamp = 2;
uint32 lamport_timestamp = 3;
}
}
@@ -360,12 +403,12 @@ message ChannelMessage {
message Contact {
uint64 user_id = 1;
repeated WorktreeMetadata worktrees = 2;
repeated ProjectMetadata projects = 2;
}
message WorktreeMetadata {
message ProjectMetadata {
uint64 id = 1;
string root_name = 2;
bool is_shared = 3;
bool is_shared = 2;
repeated string worktree_root_names = 3;
repeated uint64 guests = 4;
}
+6 -4
View File
@@ -388,6 +388,7 @@ mod tests {
.request(
client1_conn_id,
proto::OpenBuffer {
project_id: 0,
worktree_id: 1,
path: "path/one".to_string(),
},
@@ -400,7 +401,7 @@ mod tests {
content: "path/one content".to_string(),
history: vec![],
selections: vec![],
diagnostics: None,
diagnostic_sets: vec![],
}),
}
);
@@ -410,6 +411,7 @@ mod tests {
.request(
client2_conn_id,
proto::OpenBuffer {
project_id: 0,
worktree_id: 2,
path: "path/two".to_string(),
},
@@ -422,7 +424,7 @@ mod tests {
content: "path/two content".to_string(),
history: vec![],
selections: vec![],
diagnostics: None,
diagnostic_sets: vec![],
}),
}
);
@@ -453,7 +455,7 @@ mod tests {
content: "path/one content".to_string(),
history: vec![],
selections: vec![],
diagnostics: None,
diagnostic_sets: vec![],
}),
}
}
@@ -465,7 +467,7 @@ mod tests {
content: "path/two content".to_string(),
history: vec![],
selections: vec![],
diagnostics: None,
diagnostic_sets: vec![],
}),
}
}
+29 -24
View File
@@ -121,68 +121,73 @@ macro_rules! entity_messages {
messages!(
Ack,
AddCollaborator,
AddProjectCollaborator,
BufferSaved,
ChannelMessageSent,
CloseBuffer,
CloseWorktree,
Error,
GetChannelMessages,
GetChannelMessagesResponse,
GetChannels,
GetChannelsResponse,
UpdateContacts,
GetUsers,
GetUsersResponse,
JoinChannel,
JoinChannelResponse,
JoinWorktree,
JoinWorktreeResponse,
JoinProject,
JoinProjectResponse,
LeaveChannel,
LeaveWorktree,
LeaveProject,
OpenBuffer,
OpenBufferResponse,
OpenWorktree,
OpenWorktreeResponse,
RegisterProjectResponse,
Ping,
RemoveCollaborator,
RegisterProject,
RegisterWorktree,
RemoveProjectCollaborator,
SaveBuffer,
SendChannelMessage,
SendChannelMessageResponse,
ShareProject,
ShareWorktree,
ShareWorktreeResponse,
UnshareWorktree,
UnregisterProject,
UnregisterWorktree,
UnshareProject,
UpdateBuffer,
UpdateContacts,
UpdateWorktree,
);
request_messages!(
(GetChannelMessages, GetChannelMessagesResponse),
(GetChannels, GetChannelsResponse),
(GetUsers, GetUsersResponse),
(JoinChannel, JoinChannelResponse),
(JoinProject, JoinProjectResponse),
(OpenBuffer, OpenBufferResponse),
(JoinWorktree, JoinWorktreeResponse),
(OpenWorktree, OpenWorktreeResponse),
(Ping, Ack),
(RegisterProject, RegisterProjectResponse),
(RegisterWorktree, Ack),
(SaveBuffer, BufferSaved),
(UpdateBuffer, Ack),
(ShareWorktree, ShareWorktreeResponse),
(UnshareWorktree, Ack),
(SendChannelMessage, SendChannelMessageResponse),
(GetChannelMessages, GetChannelMessagesResponse),
(ShareProject, Ack),
(ShareWorktree, Ack),
(UpdateBuffer, Ack),
);
entity_messages!(
worktree_id,
AddCollaborator,
project_id,
AddProjectCollaborator,
RemoveProjectCollaborator,
JoinProject,
LeaveProject,
BufferSaved,
CloseBuffer,
CloseWorktree,
OpenBuffer,
JoinWorktree,
RemoveCollaborator,
CloseBuffer,
SaveBuffer,
UnshareWorktree,
ShareWorktree,
UnregisterWorktree,
UnshareProject,
UpdateBuffer,
UpdateWorktree,
);
+1 -1
View File
@@ -5,4 +5,4 @@ pub mod proto;
pub use conn::Connection;
pub use peer::*;
pub const PROTOCOL_VERSION: u32 = 3;
pub const PROTOCOL_VERSION: u32 = 4;
+3 -1
View File
@@ -443,7 +443,9 @@ impl Db {
macro_rules! id_type {
($name:ident) => {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, sqlx::Type, Serialize)]
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Serialize,
)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct $name(pub i32);
+3 -4
View File
@@ -2,16 +2,15 @@ use crate::{
auth::RequestExt as _, github::Release, AppState, LayoutData, Request, RequestExt as _,
};
use comrak::ComrakOptions;
use serde::{Serialize};
use serde::Serialize;
use std::sync::Arc;
use tide::{http::mime};
use tide::http::mime;
pub fn add_routes(releases: &mut tide::Server<Arc<AppState>>) {
releases.at("/releases").get(get_releases);
}
async fn get_releases(mut request: Request) -> tide::Result {
#[derive(Serialize)]
struct ReleasesData {
#[serde(flatten)]
@@ -52,4 +51,4 @@ async fn get_releases(mut request: Request) -> tide::Result {
.body(request.state().render_template("releases.hbs", &data)?)
.content_type(mime::HTML)
.build())
}
}
+563 -426
View File
File diff suppressed because it is too large Load Diff
+277 -221
View File
@@ -8,29 +8,38 @@ use std::collections::hash_map;
pub struct Store {
connections: HashMap<ConnectionId, ConnectionState>,
connections_by_user_id: HashMap<UserId, HashSet<ConnectionId>>,
worktrees: HashMap<u64, Worktree>,
visible_worktrees_by_user_id: HashMap<UserId, HashSet<u64>>,
projects: HashMap<u64, Project>,
visible_projects_by_user_id: HashMap<UserId, HashSet<u64>>,
channels: HashMap<ChannelId, Channel>,
next_worktree_id: u64,
next_project_id: u64,
}
struct ConnectionState {
user_id: UserId,
worktrees: HashSet<u64>,
projects: HashSet<u64>,
channels: HashSet<ChannelId>,
}
pub struct Worktree {
pub struct Project {
pub host_connection_id: ConnectionId,
pub host_user_id: UserId,
pub share: Option<ProjectShare>,
pub worktrees: HashMap<u64, Worktree>,
}
pub struct Worktree {
pub authorized_user_ids: Vec<UserId>,
pub root_name: String,
pub share: Option<WorktreeShare>,
}
pub struct WorktreeShare {
#[derive(Default)]
pub struct ProjectShare {
pub guests: HashMap<ConnectionId, (ReplicaId, UserId)>,
pub active_replica_ids: HashSet<ReplicaId>,
}
pub struct WorktreeShare {
pub entries: HashMap<u64, proto::Entry>,
}
@@ -43,14 +52,14 @@ pub type ReplicaId = u16;
#[derive(Default)]
pub struct RemovedConnectionState {
pub hosted_worktrees: HashMap<u64, Worktree>,
pub guest_worktree_ids: HashMap<u64, Vec<ConnectionId>>,
pub hosted_projects: HashMap<u64, Project>,
pub guest_project_ids: HashMap<u64, Vec<ConnectionId>>,
pub contact_ids: HashSet<UserId>,
}
pub struct JoinedWorktree<'a> {
pub struct JoinedProject<'a> {
pub replica_id: ReplicaId,
pub worktree: &'a Worktree,
pub project: &'a Project,
}
pub struct UnsharedWorktree {
@@ -58,7 +67,7 @@ pub struct UnsharedWorktree {
pub authorized_user_ids: Vec<UserId>,
}
pub struct LeftWorktree {
pub struct LeftProject {
pub connection_ids: Vec<ConnectionId>,
pub authorized_user_ids: Vec<UserId>,
}
@@ -69,7 +78,7 @@ impl Store {
connection_id,
ConnectionState {
user_id,
worktrees: Default::default(),
projects: Default::default(),
channels: Default::default(),
},
);
@@ -105,17 +114,15 @@ impl Store {
}
let mut result = RemovedConnectionState::default();
for worktree_id in connection.worktrees.clone() {
if let Ok(worktree) = self.remove_worktree(worktree_id, connection_id) {
for project_id in connection.projects.clone() {
if let Some(project) = self.unregister_project(project_id, connection_id) {
result.contact_ids.extend(project.authorized_user_ids());
result.hosted_projects.insert(project_id, project);
} else if let Some(project) = self.leave_project(connection_id, project_id) {
result
.contact_ids
.extend(worktree.authorized_user_ids.iter().copied());
result.hosted_worktrees.insert(worktree_id, worktree);
} else if let Some(worktree) = self.leave_worktree(connection_id, worktree_id) {
result
.guest_worktree_ids
.insert(worktree_id, worktree.connection_ids);
result.contact_ids.extend(worktree.authorized_user_ids);
.guest_project_ids
.insert(project_id, project.connection_ids);
result.contact_ids.extend(project.authorized_user_ids);
}
}
@@ -174,15 +181,15 @@ impl Store {
pub fn contacts_for_user(&self, user_id: UserId) -> Vec<proto::Contact> {
let mut contacts = HashMap::default();
for worktree_id in self
.visible_worktrees_by_user_id
for project_id in self
.visible_projects_by_user_id
.get(&user_id)
.unwrap_or(&HashSet::default())
{
let worktree = &self.worktrees[worktree_id];
let project = &self.projects[project_id];
let mut guests = HashSet::default();
if let Ok(share) = worktree.share() {
if let Ok(share) = project.share() {
for guest_connection_id in share.guests.keys() {
if let Ok(user_id) = self.user_id_for_connection(*guest_connection_id) {
guests.insert(user_id.to_proto());
@@ -190,18 +197,24 @@ impl Store {
}
}
if let Ok(host_user_id) = self.user_id_for_connection(worktree.host_connection_id) {
if let Ok(host_user_id) = self.user_id_for_connection(project.host_connection_id) {
let mut worktree_root_names = project
.worktrees
.values()
.map(|worktree| worktree.root_name.clone())
.collect::<Vec<_>>();
worktree_root_names.sort_unstable();
contacts
.entry(host_user_id)
.or_insert_with(|| proto::Contact {
user_id: host_user_id.to_proto(),
worktrees: Vec::new(),
projects: Vec::new(),
})
.worktrees
.push(proto::WorktreeMetadata {
id: *worktree_id,
root_name: worktree.root_name.clone(),
is_shared: worktree.share.is_some(),
.projects
.push(proto::ProjectMetadata {
id: *project_id,
worktree_root_names,
is_shared: project.share.is_some(),
guests: guests.into_iter().collect(),
});
}
@@ -210,107 +223,147 @@ impl Store {
contacts.into_values().collect()
}
pub fn add_worktree(&mut self, worktree: Worktree) -> u64 {
let worktree_id = self.next_worktree_id;
for authorized_user_id in &worktree.authorized_user_ids {
self.visible_worktrees_by_user_id
.entry(*authorized_user_id)
.or_default()
.insert(worktree_id);
}
self.next_worktree_id += 1;
if let Some(connection) = self.connections.get_mut(&worktree.host_connection_id) {
connection.worktrees.insert(worktree_id);
}
self.worktrees.insert(worktree_id, worktree);
#[cfg(test)]
self.check_invariants();
worktree_id
pub fn register_project(
&mut self,
host_connection_id: ConnectionId,
host_user_id: UserId,
) -> u64 {
let project_id = self.next_project_id;
self.projects.insert(
project_id,
Project {
host_connection_id,
host_user_id,
share: None,
worktrees: Default::default(),
},
);
self.next_project_id += 1;
project_id
}
pub fn remove_worktree(
pub fn register_worktree(
&mut self,
project_id: u64,
worktree_id: u64,
worktree: Worktree,
) -> bool {
if let Some(project) = self.projects.get_mut(&project_id) {
for authorized_user_id in &worktree.authorized_user_ids {
self.visible_projects_by_user_id
.entry(*authorized_user_id)
.or_default()
.insert(project_id);
}
if let Some(connection) = self.connections.get_mut(&project.host_connection_id) {
connection.projects.insert(project_id);
}
project.worktrees.insert(worktree_id, worktree);
#[cfg(test)]
self.check_invariants();
true
} else {
false
}
}
pub fn unregister_project(
&mut self,
project_id: u64,
connection_id: ConnectionId,
) -> Option<Project> {
match self.projects.entry(project_id) {
hash_map::Entry::Occupied(e) => {
if e.get().host_connection_id == connection_id {
for user_id in e.get().authorized_user_ids() {
if let hash_map::Entry::Occupied(mut projects) =
self.visible_projects_by_user_id.entry(user_id)
{
projects.get_mut().remove(&project_id);
}
}
Some(e.remove())
} else {
None
}
}
hash_map::Entry::Vacant(_) => None,
}
}
pub fn unregister_worktree(
&mut self,
project_id: u64,
worktree_id: u64,
acting_connection_id: ConnectionId,
) -> tide::Result<Worktree> {
let worktree = if let hash_map::Entry::Occupied(e) = self.worktrees.entry(worktree_id) {
if e.get().host_connection_id != acting_connection_id {
Err(anyhow!("not your worktree"))?;
}
e.remove()
} else {
return Err(anyhow!("no such worktree"))?;
};
if let Some(connection) = self.connections.get_mut(&worktree.host_connection_id) {
connection.worktrees.remove(&worktree_id);
) -> tide::Result<(Worktree, Vec<ConnectionId>)> {
let project = self
.projects
.get_mut(&project_id)
.ok_or_else(|| anyhow!("no such project"))?;
if project.host_connection_id != acting_connection_id {
Err(anyhow!("not your worktree"))?;
}
if let Some(share) = &worktree.share {
for connection_id in share.guests.keys() {
if let Some(connection) = self.connections.get_mut(connection_id) {
connection.worktrees.remove(&worktree_id);
let worktree = project
.worktrees
.remove(&worktree_id)
.ok_or_else(|| anyhow!("no such worktree"))?;
let mut guest_connection_ids = Vec::new();
if let Some(share) = &project.share {
guest_connection_ids.extend(share.guests.keys());
}
for authorized_user_id in &worktree.authorized_user_ids {
if let Some(visible_projects) =
self.visible_projects_by_user_id.get_mut(authorized_user_id)
{
if !project.has_authorized_user_id(*authorized_user_id) {
visible_projects.remove(&project_id);
}
}
}
for authorized_user_id in &worktree.authorized_user_ids {
if let Some(visible_worktrees) = self
.visible_worktrees_by_user_id
.get_mut(&authorized_user_id)
{
visible_worktrees.remove(&worktree_id);
}
}
#[cfg(test)]
self.check_invariants();
Ok(worktree)
Ok((worktree, guest_connection_ids))
}
pub fn share_worktree(
&mut self,
worktree_id: u64,
connection_id: ConnectionId,
entries: HashMap<u64, proto::Entry>,
) -> Option<Vec<UserId>> {
if let Some(worktree) = self.worktrees.get_mut(&worktree_id) {
if worktree.host_connection_id == connection_id {
worktree.share = Some(WorktreeShare {
guests: Default::default(),
active_replica_ids: Default::default(),
entries,
});
return Some(worktree.authorized_user_ids.clone());
pub fn share_project(&mut self, project_id: u64, connection_id: ConnectionId) -> bool {
if let Some(project) = self.projects.get_mut(&project_id) {
if project.host_connection_id == connection_id {
project.share = Some(ProjectShare::default());
return true;
}
}
None
false
}
pub fn unshare_worktree(
pub fn unshare_project(
&mut self,
worktree_id: u64,
project_id: u64,
acting_connection_id: ConnectionId,
) -> tide::Result<UnsharedWorktree> {
let worktree = if let Some(worktree) = self.worktrees.get_mut(&worktree_id) {
worktree
let project = if let Some(project) = self.projects.get_mut(&project_id) {
project
} else {
return Err(anyhow!("no such worktree"))?;
return Err(anyhow!("no such project"))?;
};
if worktree.host_connection_id != acting_connection_id {
return Err(anyhow!("not your worktree"))?;
if project.host_connection_id != acting_connection_id {
return Err(anyhow!("not your project"))?;
}
let connection_ids = worktree.connection_ids();
let authorized_user_ids = worktree.authorized_user_ids.clone();
if let Some(share) = worktree.share.take() {
let connection_ids = project.connection_ids();
let authorized_user_ids = project.authorized_user_ids();
if let Some(share) = project.share.take() {
for connection_id in share.guests.into_keys() {
if let Some(connection) = self.connections.get_mut(&connection_id) {
connection.worktrees.remove(&worktree_id);
connection.projects.remove(&project_id);
}
}
@@ -322,34 +375,51 @@ impl Store {
authorized_user_ids,
})
} else {
Err(anyhow!("worktree is not shared"))?
Err(anyhow!("project is not shared"))?
}
}
pub fn join_worktree(
pub fn share_worktree(
&mut self,
project_id: u64,
worktree_id: u64,
connection_id: ConnectionId,
entries: HashMap<u64, proto::Entry>,
) -> Option<Vec<UserId>> {
let project = self.projects.get_mut(&project_id)?;
let worktree = project.worktrees.get_mut(&worktree_id)?;
if project.host_connection_id == connection_id && project.share.is_some() {
worktree.share = Some(WorktreeShare { entries });
Some(project.authorized_user_ids())
} else {
None
}
}
pub fn join_project(
&mut self,
connection_id: ConnectionId,
user_id: UserId,
worktree_id: u64,
) -> tide::Result<JoinedWorktree> {
project_id: u64,
) -> tide::Result<JoinedProject> {
let connection = self
.connections
.get_mut(&connection_id)
.ok_or_else(|| anyhow!("no such connection"))?;
let worktree = self
.worktrees
.get_mut(&worktree_id)
.and_then(|worktree| {
if worktree.authorized_user_ids.contains(&user_id) {
Some(worktree)
let project = self
.projects
.get_mut(&project_id)
.and_then(|project| {
if project.has_authorized_user_id(user_id) {
Some(project)
} else {
None
}
})
.ok_or_else(|| anyhow!("no such worktree"))?;
.ok_or_else(|| anyhow!("no such project"))?;
let share = worktree.share_mut()?;
connection.worktrees.insert(worktree_id);
let share = project.share_mut()?;
connection.projects.insert(project_id);
let mut replica_id = 1;
while share.active_replica_ids.contains(&replica_id) {
@@ -361,33 +431,33 @@ impl Store {
#[cfg(test)]
self.check_invariants();
Ok(JoinedWorktree {
Ok(JoinedProject {
replica_id,
worktree: &self.worktrees[&worktree_id],
project: &self.projects[&project_id],
})
}
pub fn leave_worktree(
pub fn leave_project(
&mut self,
connection_id: ConnectionId,
worktree_id: u64,
) -> Option<LeftWorktree> {
let worktree = self.worktrees.get_mut(&worktree_id)?;
let share = worktree.share.as_mut()?;
project_id: u64,
) -> Option<LeftProject> {
let project = self.projects.get_mut(&project_id)?;
let share = project.share.as_mut()?;
let (replica_id, _) = share.guests.remove(&connection_id)?;
share.active_replica_ids.remove(&replica_id);
if let Some(connection) = self.connections.get_mut(&connection_id) {
connection.worktrees.remove(&worktree_id);
connection.projects.remove(&project_id);
}
let connection_ids = worktree.connection_ids();
let authorized_user_ids = worktree.authorized_user_ids.clone();
let connection_ids = project.connection_ids();
let authorized_user_ids = project.authorized_user_ids();
#[cfg(test)]
self.check_invariants();
Some(LeftWorktree {
Some(LeftProject {
connection_ids,
authorized_user_ids,
})
@@ -396,115 +466,75 @@ impl Store {
pub fn update_worktree(
&mut self,
connection_id: ConnectionId,
project_id: u64,
worktree_id: u64,
removed_entries: &[u64],
updated_entries: &[proto::Entry],
) -> tide::Result<Vec<ConnectionId>> {
let worktree = self.write_worktree(worktree_id, connection_id)?;
let share = worktree.share_mut()?;
) -> Option<Vec<ConnectionId>> {
let project = self.write_project(project_id, connection_id)?;
let share = project.worktrees.get_mut(&worktree_id)?.share.as_mut()?;
for entry_id in removed_entries {
share.entries.remove(&entry_id);
}
for entry in updated_entries {
share.entries.insert(entry.id, entry.clone());
}
Ok(worktree.connection_ids())
Some(project.connection_ids())
}
pub fn worktree_host_connection_id(
pub fn project_connection_ids(
&self,
connection_id: ConnectionId,
worktree_id: u64,
) -> tide::Result<ConnectionId> {
Ok(self
.read_worktree(worktree_id, connection_id)?
.host_connection_id)
}
pub fn worktree_guest_connection_ids(
&self,
connection_id: ConnectionId,
worktree_id: u64,
) -> tide::Result<Vec<ConnectionId>> {
Ok(self
.read_worktree(worktree_id, connection_id)?
.share()?
.guests
.keys()
.copied()
.collect())
}
pub fn worktree_connection_ids(
&self,
connection_id: ConnectionId,
worktree_id: u64,
) -> tide::Result<Vec<ConnectionId>> {
Ok(self
.read_worktree(worktree_id, connection_id)?
.connection_ids())
project_id: u64,
acting_connection_id: ConnectionId,
) -> Option<Vec<ConnectionId>> {
Some(
self.read_project(project_id, acting_connection_id)?
.connection_ids(),
)
}
pub fn channel_connection_ids(&self, channel_id: ChannelId) -> Option<Vec<ConnectionId>> {
Some(self.channels.get(&channel_id)?.connection_ids())
}
fn read_worktree(
&self,
worktree_id: u64,
connection_id: ConnectionId,
) -> tide::Result<&Worktree> {
let worktree = self
.worktrees
.get(&worktree_id)
.ok_or_else(|| anyhow!("worktree not found"))?;
if worktree.host_connection_id == connection_id
|| worktree.share()?.guests.contains_key(&connection_id)
pub fn read_project(&self, project_id: u64, connection_id: ConnectionId) -> Option<&Project> {
let project = self.projects.get(&project_id)?;
if project.host_connection_id == connection_id
|| project.share.as_ref()?.guests.contains_key(&connection_id)
{
Ok(worktree)
Some(project)
} else {
Err(anyhow!(
"{} is not a member of worktree {}",
connection_id,
worktree_id
))?
None
}
}
fn write_worktree(
fn write_project(
&mut self,
worktree_id: u64,
project_id: u64,
connection_id: ConnectionId,
) -> tide::Result<&mut Worktree> {
let worktree = self
.worktrees
.get_mut(&worktree_id)
.ok_or_else(|| anyhow!("worktree not found"))?;
if worktree.host_connection_id == connection_id
|| worktree
.share
.as_ref()
.map_or(false, |share| share.guests.contains_key(&connection_id))
) -> Option<&mut Project> {
let project = self.projects.get_mut(&project_id)?;
if project.host_connection_id == connection_id
|| project.share.as_ref()?.guests.contains_key(&connection_id)
{
Ok(worktree)
Some(project)
} else {
Err(anyhow!(
"{} is not a member of worktree {}",
connection_id,
worktree_id
))?
None
}
}
#[cfg(test)]
fn check_invariants(&self) {
for (connection_id, connection) in &self.connections {
for worktree_id in &connection.worktrees {
let worktree = &self.worktrees.get(&worktree_id).unwrap();
if worktree.host_connection_id != *connection_id {
assert!(worktree.share().unwrap().guests.contains_key(connection_id));
for project_id in &connection.projects {
let project = &self.projects.get(&project_id).unwrap();
if project.host_connection_id != *connection_id {
assert!(project
.share
.as_ref()
.unwrap()
.guests
.contains_key(connection_id));
}
}
for channel_id in &connection.channels {
@@ -527,22 +557,22 @@ impl Store {
}
}
for (worktree_id, worktree) in &self.worktrees {
let host_connection = self.connections.get(&worktree.host_connection_id).unwrap();
assert!(host_connection.worktrees.contains(worktree_id));
for (project_id, project) in &self.projects {
let host_connection = self.connections.get(&project.host_connection_id).unwrap();
assert!(host_connection.projects.contains(project_id));
for authorized_user_ids in &worktree.authorized_user_ids {
let visible_worktree_ids = self
.visible_worktrees_by_user_id
.get(authorized_user_ids)
for authorized_user_ids in project.authorized_user_ids() {
let visible_project_ids = self
.visible_projects_by_user_id
.get(&authorized_user_ids)
.unwrap();
assert!(visible_worktree_ids.contains(worktree_id));
assert!(visible_project_ids.contains(project_id));
}
if let Some(share) = &worktree.share {
if let Some(share) = &project.share {
for guest_connection_id in share.guests.keys() {
let guest_connection = self.connections.get(guest_connection_id).unwrap();
assert!(guest_connection.worktrees.contains(worktree_id));
assert!(guest_connection.projects.contains(project_id));
}
assert_eq!(share.active_replica_ids.len(), share.guests.len(),);
assert_eq!(
@@ -556,10 +586,10 @@ impl Store {
}
}
for (user_id, visible_worktree_ids) in &self.visible_worktrees_by_user_id {
for worktree_id in visible_worktree_ids {
let worktree = self.worktrees.get(worktree_id).unwrap();
assert!(worktree.authorized_user_ids.contains(user_id));
for (user_id, visible_project_ids) in &self.visible_projects_by_user_id {
for project_id in visible_project_ids {
let project = self.projects.get(project_id).unwrap();
assert!(project.authorized_user_ids().contains(user_id));
}
}
@@ -572,7 +602,33 @@ impl Store {
}
}
impl Worktree {
impl Project {
pub fn has_authorized_user_id(&self, user_id: UserId) -> bool {
self.worktrees
.values()
.any(|worktree| worktree.authorized_user_ids.contains(&user_id))
}
pub fn authorized_user_ids(&self) -> Vec<UserId> {
let mut ids = self
.worktrees
.values()
.flat_map(|worktree| worktree.authorized_user_ids.iter())
.copied()
.collect::<Vec<_>>();
ids.sort_unstable();
ids.dedup();
ids
}
pub fn guest_connection_ids(&self) -> Vec<ConnectionId> {
if let Some(share) = &self.share {
share.guests.keys().copied().collect()
} else {
Vec::new()
}
}
pub fn connection_ids(&self) -> Vec<ConnectionId> {
if let Some(share) = &self.share {
share
@@ -586,14 +642,14 @@ impl Worktree {
}
}
pub fn share(&self) -> tide::Result<&WorktreeShare> {
pub fn share(&self) -> tide::Result<&ProjectShare> {
Ok(self
.share
.as_ref()
.ok_or_else(|| anyhow!("worktree is not shared"))?)
}
fn share_mut(&mut self) -> tide::Result<&mut WorktreeShare> {
fn share_mut(&mut self) -> tide::Result<&mut ProjectShare> {
Ok(self
.share
.as_mut()
+70
View File
@@ -18,6 +18,11 @@ pub struct Cursor<'a, T: Item, D> {
at_end: bool,
}
pub struct Iter<'a, T: Item> {
tree: &'a SumTree<T>,
stack: ArrayVec<StackEntry<'a, T, ()>, 16>,
}
impl<'a, T, D> Cursor<'a, T, D>
where
T: Item,
@@ -487,6 +492,71 @@ where
}
}
impl<'a, T: Item> Iter<'a, T> {
pub(crate) fn new(tree: &'a SumTree<T>) -> Self {
Self {
tree,
stack: Default::default(),
}
}
}
impl<'a, T: Item> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
let mut descend = false;
if self.stack.is_empty() {
self.stack.push(StackEntry {
tree: self.tree,
index: 0,
position: (),
});
descend = true;
}
while self.stack.len() > 0 {
let new_subtree = {
let entry = self.stack.last_mut().unwrap();
match entry.tree.0.as_ref() {
Node::Internal { child_trees, .. } => {
if !descend {
entry.index += 1;
}
child_trees.get(entry.index)
}
Node::Leaf { items, .. } => {
if !descend {
entry.index += 1;
}
if let Some(next_item) = items.get(entry.index) {
return Some(next_item);
} else {
None
}
}
}
};
if let Some(subtree) = new_subtree {
descend = true;
self.stack.push(StackEntry {
tree: subtree,
index: 0,
position: (),
});
} else {
descend = false;
self.stack.pop();
}
}
None
}
}
impl<'a, T, S, D> Iterator for Cursor<'a, T, D>
where
T: Item<Summary = S>,
+11 -2
View File
@@ -1,10 +1,11 @@
mod cursor;
mod tree_map;
use arrayvec::ArrayVec;
pub use cursor::Cursor;
pub use cursor::FilterCursor;
pub use cursor::{Cursor, FilterCursor, Iter};
use std::marker::PhantomData;
use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc};
pub use tree_map::TreeMap;
#[cfg(test)]
const TREE_BASE: usize = 2;
@@ -156,6 +157,10 @@ impl<T: Item> SumTree<T> {
items
}
pub fn iter(&self) -> Iter<T> {
Iter::new(self)
}
pub fn cursor<'a, S>(&'a self) -> Cursor<T, S>
where
S: Dimension<'a, T::Summary>,
@@ -722,6 +727,10 @@ mod tests {
};
assert_eq!(tree.items(&()), reference_items);
assert_eq!(
tree.iter().collect::<Vec<_>>(),
tree.cursor::<()>().collect::<Vec<_>>()
);
let mut filter_cursor =
tree.filter::<_, Count>(|summary| summary.contains_even, &());
+152
View File
@@ -0,0 +1,152 @@
use std::{cmp::Ordering, fmt::Debug};
use crate::{Bias, Dimension, Item, KeyedItem, SeekTarget, SumTree, Summary};
#[derive(Clone)]
pub struct TreeMap<K, V>(SumTree<MapEntry<K, V>>)
where
K: Clone + Debug + Default + Ord,
V: Clone + Debug;
#[derive(Clone)]
pub struct MapEntry<K, V> {
key: K,
value: V,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct MapKey<K>(K);
#[derive(Clone, Debug, Default)]
pub struct MapKeyRef<'a, K>(Option<&'a K>);
impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
pub fn get<'a>(&self, key: &'a K) -> Option<&V> {
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
let key = MapKeyRef(Some(key));
cursor.seek(&key, Bias::Left, &());
if key.cmp(cursor.start(), &()) == Ordering::Equal {
Some(&cursor.item().unwrap().value)
} else {
None
}
}
pub fn insert(&mut self, key: K, value: V) {
self.0.insert_or_replace(MapEntry { key, value }, &());
}
pub fn remove<'a>(&mut self, key: &'a K) -> Option<V> {
let mut removed = None;
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
let key = MapKeyRef(Some(key));
let mut new_tree = cursor.slice(&key, Bias::Left, &());
if key.cmp(&cursor.end(&()), &()) == Ordering::Equal {
removed = Some(cursor.item().unwrap().value.clone());
cursor.next(&());
}
new_tree.push_tree(cursor.suffix(&()), &());
drop(cursor);
self.0 = new_tree;
removed
}
pub fn iter<'a>(&'a self) -> impl 'a + Iterator<Item = (&'a K, &'a V)> {
self.0.iter().map(|entry| (&entry.key, &entry.value))
}
}
impl<K, V> Default for TreeMap<K, V>
where
K: Clone + Debug + Default + Ord,
V: Clone + Debug,
{
fn default() -> Self {
Self(Default::default())
}
}
impl<K, V> Item for MapEntry<K, V>
where
K: Clone + Debug + Default + Ord,
V: Clone,
{
type Summary = MapKey<K>;
fn summary(&self) -> Self::Summary {
self.key()
}
}
impl<K, V> KeyedItem for MapEntry<K, V>
where
K: Clone + Debug + Default + Ord,
V: Clone,
{
type Key = MapKey<K>;
fn key(&self) -> Self::Key {
MapKey(self.key.clone())
}
}
impl<K> Summary for MapKey<K>
where
K: Clone + Debug + Default,
{
type Context = ();
fn add_summary(&mut self, summary: &Self, _: &()) {
*self = summary.clone()
}
}
impl<'a, K> Dimension<'a, MapKey<K>> for MapKeyRef<'a, K>
where
K: Clone + Debug + Default + Ord,
{
fn add_summary(&mut self, summary: &'a MapKey<K>, _: &()) {
self.0 = Some(&summary.0)
}
}
impl<'a, K> SeekTarget<'a, MapKey<K>, MapKeyRef<'a, K>> for MapKeyRef<'_, K>
where
K: Clone + Debug + Default + Ord,
{
fn cmp(&self, cursor_location: &MapKeyRef<K>, _: &()) -> Ordering {
self.0.cmp(&cursor_location.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
let mut map = TreeMap::default();
assert_eq!(map.iter().collect::<Vec<_>>(), vec![]);
map.insert(3, "c");
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&3, &"c")]);
map.insert(1, "a");
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a"), (&3, &"c")]);
map.insert(2, "b");
assert_eq!(
map.iter().collect::<Vec<_>>(),
vec![(&1, &"a"), (&2, &"b"), (&3, &"c")]
);
map.remove(&2);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a"), (&3, &"c")]);
map.remove(&3);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a")]);
map.remove(&1);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![]);
}
}
+1
View File
@@ -15,6 +15,7 @@ collections = { path = "../collections" }
sum_tree = { path = "../sum_tree" }
anyhow = "1.0.38"
arrayvec = "0.7.1"
lazy_static = "1.4"
log = "0.4"
parking_lot = "0.11"
rand = { version = "0.8.3", optional = true }
+31 -519
View File
@@ -1,105 +1,48 @@
use crate::{rope::TextDimension, Snapshot};
use super::{Buffer, FromAnchor, FullOffset, Point, ToOffset};
use super::{Point, ToOffset};
use crate::{rope::TextDimension, BufferSnapshot};
use anyhow::Result;
use std::{
cmp::Ordering,
fmt::{Debug, Formatter},
ops::Range,
};
use sum_tree::{Bias, SumTree};
use std::{cmp::Ordering, fmt::Debug, ops::Range};
use sum_tree::Bias;
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub struct Anchor {
pub full_offset: FullOffset,
pub timestamp: clock::Local,
pub offset: usize,
pub bias: Bias,
pub version: clock::Global,
}
#[derive(Clone)]
pub struct AnchorMap<T> {
pub(crate) version: clock::Global,
pub(crate) bias: Bias,
pub(crate) entries: Vec<(FullOffset, T)>,
}
#[derive(Clone)]
pub struct AnchorSet(pub(crate) AnchorMap<()>);
#[derive(Clone)]
pub struct AnchorRangeMap<T> {
pub(crate) version: clock::Global,
pub(crate) entries: Vec<(Range<FullOffset>, T)>,
pub(crate) start_bias: Bias,
pub(crate) end_bias: Bias,
}
#[derive(Clone)]
pub struct AnchorRangeSet(pub(crate) AnchorRangeMap<()>);
#[derive(Clone)]
pub struct AnchorRangeMultimap<T: Clone> {
pub(crate) entries: SumTree<AnchorRangeMultimapEntry<T>>,
pub(crate) version: clock::Global,
pub(crate) start_bias: Bias,
pub(crate) end_bias: Bias,
}
#[derive(Clone)]
pub(crate) struct AnchorRangeMultimapEntry<T> {
pub(crate) range: FullOffsetRange,
pub(crate) value: T,
}
#[derive(Clone, Debug)]
pub(crate) struct FullOffsetRange {
pub(crate) start: FullOffset,
pub(crate) end: FullOffset,
}
#[derive(Clone, Debug)]
pub(crate) struct AnchorRangeMultimapSummary {
start: FullOffset,
end: FullOffset,
min_start: FullOffset,
max_end: FullOffset,
count: usize,
}
impl Anchor {
pub fn min() -> Self {
Self {
full_offset: FullOffset(0),
timestamp: clock::Local::MIN,
offset: usize::MIN,
bias: Bias::Left,
version: Default::default(),
}
}
pub fn max() -> Self {
Self {
full_offset: FullOffset::MAX,
timestamp: clock::Local::MAX,
offset: usize::MAX,
bias: Bias::Right,
version: Default::default(),
}
}
pub fn cmp<'a>(&self, other: &Anchor, buffer: &Snapshot) -> Result<Ordering> {
if self == other {
return Ok(Ordering::Equal);
}
let offset_comparison = if self.version == other.version {
self.full_offset.cmp(&other.full_offset)
pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Result<Ordering> {
let fragment_id_comparison = if self.timestamp == other.timestamp {
Ordering::Equal
} else {
buffer
.full_offset_for_anchor(self)
.cmp(&buffer.full_offset_for_anchor(other))
.fragment_id_for_anchor(self)
.cmp(&buffer.fragment_id_for_anchor(other))
};
Ok(offset_comparison.then_with(|| self.bias.cmp(&other.bias)))
Ok(fragment_id_comparison
.then_with(|| self.offset.cmp(&other.offset))
.then_with(|| self.bias.cmp(&other.bias)))
}
pub fn bias_left(&self, buffer: &Buffer) -> Anchor {
pub fn bias_left(&self, buffer: &BufferSnapshot) -> Anchor {
if self.bias == Bias::Left {
self.clone()
} else {
@@ -107,7 +50,7 @@ impl Anchor {
}
}
pub fn bias_right(&self, buffer: &Buffer) -> Anchor {
pub fn bias_right(&self, buffer: &BufferSnapshot) -> Anchor {
if self.bias == Bias::Right {
self.clone()
} else {
@@ -115,464 +58,33 @@ impl Anchor {
}
}
pub fn summary<'a, D>(&self, content: &'a Snapshot) -> D
pub fn summary<'a, D>(&self, content: &'a BufferSnapshot) -> D
where
D: TextDimension<'a>,
D: TextDimension,
{
content.summary_for_anchor(self)
}
}
impl<T> AnchorMap<T> {
pub fn version(&self) -> &clock::Global {
&self.version
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn iter<'a, D>(&'a self, snapshot: &'a Snapshot) -> impl Iterator<Item = (D, &'a T)> + 'a
where
D: 'a + TextDimension<'a>,
{
snapshot
.summaries_for_anchors(
self.version.clone(),
self.bias,
self.entries.iter().map(|e| &e.0),
)
.zip(self.entries.iter().map(|e| &e.1))
}
}
impl AnchorSet {
pub fn version(&self) -> &clock::Global {
&self.0.version
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn iter<'a, D>(&'a self, content: &'a Snapshot) -> impl Iterator<Item = D> + 'a
where
D: 'a + TextDimension<'a>,
{
self.0.iter(content).map(|(position, _)| position)
}
}
impl<T> AnchorRangeMap<T> {
pub fn version(&self) -> &clock::Global {
&self.version
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn from_full_offset_ranges(
version: clock::Global,
start_bias: Bias,
end_bias: Bias,
entries: Vec<(Range<FullOffset>, T)>,
) -> Self {
Self {
version,
start_bias,
end_bias,
entries,
}
}
pub fn ranges<'a, D>(
&'a self,
content: &'a Snapshot,
) -> impl Iterator<Item = (Range<D>, &'a T)> + 'a
where
D: 'a + TextDimension<'a>,
{
content
.summaries_for_anchor_ranges(
self.version.clone(),
self.start_bias,
self.end_bias,
self.entries.iter().map(|e| &e.0),
)
.zip(self.entries.iter().map(|e| &e.1))
}
pub fn intersecting_ranges<'a, D, I>(
&'a self,
range: Range<(I, Bias)>,
content: &'a Snapshot,
) -> impl Iterator<Item = (Range<D>, &'a T)> + 'a
where
D: 'a + TextDimension<'a>,
I: ToOffset,
{
let range = content.anchor_at(range.start.0, range.start.1)
..content.anchor_at(range.end.0, range.end.1);
let mut probe_anchor = Anchor {
full_offset: Default::default(),
bias: self.start_bias,
version: self.version.clone(),
};
let start_ix = self.entries.binary_search_by(|probe| {
probe_anchor.full_offset = probe.0.end;
probe_anchor.cmp(&range.start, &content).unwrap()
});
match start_ix {
Ok(start_ix) | Err(start_ix) => content
.summaries_for_anchor_ranges(
self.version.clone(),
self.start_bias,
self.end_bias,
self.entries[start_ix..].iter().map(|e| &e.0),
)
.zip(self.entries.iter().map(|e| &e.1)),
}
}
pub fn full_offset_ranges(&self) -> impl Iterator<Item = &(Range<FullOffset>, T)> {
self.entries.iter()
}
pub fn min_by_key<'a, D, F, K>(
&self,
content: &'a Snapshot,
mut extract_key: F,
) -> Option<(Range<D>, &T)>
where
D: 'a + TextDimension<'a>,
F: FnMut(&T) -> K,
K: Ord,
{
self.entries
.iter()
.min_by_key(|(_, value)| extract_key(value))
.map(|(range, value)| (self.resolve_range(range, &content), value))
}
pub fn max_by_key<'a, D, F, K>(
&self,
content: &'a Snapshot,
mut extract_key: F,
) -> Option<(Range<D>, &T)>
where
D: 'a + TextDimension<'a>,
F: FnMut(&T) -> K,
K: Ord,
{
self.entries
.iter()
.max_by_key(|(_, value)| extract_key(value))
.map(|(range, value)| (self.resolve_range(range, &content), value))
}
fn resolve_range<'a, D>(&self, range: &Range<FullOffset>, content: &'a Snapshot) -> Range<D>
where
D: 'a + TextDimension<'a>,
{
let mut anchor = Anchor {
full_offset: range.start,
bias: self.start_bias,
version: self.version.clone(),
};
let start = content.summary_for_anchor(&anchor);
anchor.full_offset = range.end;
anchor.bias = self.end_bias;
let end = content.summary_for_anchor(&anchor);
start..end
}
}
impl<T: PartialEq> PartialEq for AnchorRangeMap<T> {
fn eq(&self, other: &Self) -> bool {
self.version == other.version && self.entries == other.entries
}
}
impl<T: Eq> Eq for AnchorRangeMap<T> {}
impl<T: Debug> Debug for AnchorRangeMap<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
let mut f = f.debug_map();
for (range, value) in &self.entries {
f.key(range);
f.value(value);
}
f.finish()
}
}
impl Debug for AnchorRangeSet {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_set();
for (range, _) in &self.0.entries {
f.entry(range);
}
f.finish()
}
}
impl AnchorRangeSet {
pub fn len(&self) -> usize {
self.0.len()
}
pub fn version(&self) -> &clock::Global {
self.0.version()
}
pub fn ranges<'a, D>(&'a self, content: &'a Snapshot) -> impl 'a + Iterator<Item = Range<Point>>
where
D: 'a + TextDimension<'a>,
{
self.0.ranges(content).map(|(range, _)| range)
}
}
impl<T: Clone> Default for AnchorRangeMultimap<T> {
fn default() -> Self {
Self {
entries: Default::default(),
version: Default::default(),
start_bias: Bias::Left,
end_bias: Bias::Left,
}
}
}
impl<T: Clone> AnchorRangeMultimap<T> {
pub fn version(&self) -> &clock::Global {
&self.version
}
pub fn intersecting_ranges<'a, I, O>(
&'a self,
range: Range<I>,
content: &'a Snapshot,
inclusive: bool,
) -> impl Iterator<Item = (usize, Range<O>, &T)> + 'a
where
I: ToOffset,
O: FromAnchor,
{
let end_bias = if inclusive { Bias::Right } else { Bias::Left };
let range = range.start.to_full_offset(&content, Bias::Left)
..range.end.to_full_offset(&content, end_bias);
let mut cursor = self.entries.filter::<_, usize>(
{
let mut endpoint = Anchor {
full_offset: FullOffset(0),
bias: Bias::Right,
version: self.version.clone(),
};
move |summary: &AnchorRangeMultimapSummary| {
endpoint.full_offset = summary.max_end;
endpoint.bias = self.end_bias;
let max_end = endpoint.to_full_offset(&content, self.end_bias);
let start_cmp = range.start.cmp(&max_end);
endpoint.full_offset = summary.min_start;
endpoint.bias = self.start_bias;
let min_start = endpoint.to_full_offset(&content, self.start_bias);
let end_cmp = range.end.cmp(&min_start);
if inclusive {
start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
} else {
start_cmp == Ordering::Less && end_cmp == Ordering::Greater
}
}
},
&(),
);
std::iter::from_fn({
let mut endpoint = Anchor {
full_offset: FullOffset(0),
bias: Bias::Left,
version: self.version.clone(),
};
move || {
if let Some(item) = cursor.item() {
let ix = *cursor.start();
endpoint.full_offset = item.range.start;
endpoint.bias = self.start_bias;
let start = O::from_anchor(&endpoint, &content);
endpoint.full_offset = item.range.end;
endpoint.bias = self.end_bias;
let end = O::from_anchor(&endpoint, &content);
let value = &item.value;
cursor.next(&());
Some((ix, start..end, value))
} else {
None
}
}
})
}
pub fn from_full_offset_ranges(
version: clock::Global,
start_bias: Bias,
end_bias: Bias,
entries: impl Iterator<Item = (Range<FullOffset>, T)>,
) -> Self {
Self {
version,
start_bias,
end_bias,
entries: SumTree::from_iter(
entries.map(|(range, value)| AnchorRangeMultimapEntry {
range: FullOffsetRange {
start: range.start,
end: range.end,
},
value,
}),
&(),
),
}
}
pub fn full_offset_ranges(&self) -> impl Iterator<Item = (Range<FullOffset>, &T)> {
self.entries
.cursor::<()>()
.map(|entry| (entry.range.start..entry.range.end, &entry.value))
}
pub fn filter<'a, O, F>(
&'a self,
content: &'a Snapshot,
mut f: F,
) -> impl 'a + Iterator<Item = (usize, Range<O>, &T)>
where
O: FromAnchor,
F: 'a + FnMut(&'a T) -> bool,
{
let mut endpoint = Anchor {
full_offset: FullOffset(0),
bias: Bias::Left,
version: self.version.clone(),
};
self.entries
.cursor::<()>()
.enumerate()
.filter_map(move |(ix, entry)| {
if f(&entry.value) {
endpoint.full_offset = entry.range.start;
endpoint.bias = self.start_bias;
let start = O::from_anchor(&endpoint, &content);
endpoint.full_offset = entry.range.end;
endpoint.bias = self.end_bias;
let end = O::from_anchor(&endpoint, &content);
Some((ix, start..end, &entry.value))
} else {
None
}
})
}
}
impl<T: Clone> sum_tree::Item for AnchorRangeMultimapEntry<T> {
type Summary = AnchorRangeMultimapSummary;
fn summary(&self) -> Self::Summary {
AnchorRangeMultimapSummary {
start: self.range.start,
end: self.range.end,
min_start: self.range.start,
max_end: self.range.end,
count: 1,
}
}
}
impl Default for AnchorRangeMultimapSummary {
fn default() -> Self {
Self {
start: FullOffset(0),
end: FullOffset::MAX,
min_start: FullOffset::MAX,
max_end: FullOffset(0),
count: 0,
}
}
}
impl sum_tree::Summary for AnchorRangeMultimapSummary {
type Context = ();
fn add_summary(&mut self, other: &Self, _: &Self::Context) {
self.min_start = self.min_start.min(other.min_start);
self.max_end = self.max_end.max(other.max_end);
#[cfg(debug_assertions)]
{
let start_comparison = self.start.cmp(&other.start);
assert!(start_comparison <= Ordering::Equal);
if start_comparison == Ordering::Equal {
assert!(self.end.cmp(&other.end) >= Ordering::Equal);
}
}
self.start = other.start;
self.end = other.end;
self.count += other.count;
}
}
impl Default for FullOffsetRange {
fn default() -> Self {
Self {
start: FullOffset(0),
end: FullOffset::MAX,
}
}
}
impl<'a> sum_tree::Dimension<'a, AnchorRangeMultimapSummary> for usize {
fn add_summary(&mut self, summary: &'a AnchorRangeMultimapSummary, _: &()) {
*self += summary.count;
}
}
impl<'a> sum_tree::Dimension<'a, AnchorRangeMultimapSummary> for FullOffsetRange {
fn add_summary(&mut self, summary: &'a AnchorRangeMultimapSummary, _: &()) {
self.start = summary.start;
self.end = summary.end;
}
}
impl<'a> sum_tree::SeekTarget<'a, AnchorRangeMultimapSummary, FullOffsetRange> for FullOffsetRange {
fn cmp(&self, cursor_location: &FullOffsetRange, _: &()) -> Ordering {
Ord::cmp(&self.start, &cursor_location.start)
.then_with(|| Ord::cmp(&cursor_location.end, &self.end))
}
}
pub trait AnchorRangeExt {
fn cmp(&self, b: &Range<Anchor>, buffer: &Snapshot) -> Result<Ordering>;
fn to_offset(&self, content: &Snapshot) -> Range<usize>;
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering>;
fn to_offset(&self, content: &BufferSnapshot) -> Range<usize>;
fn to_point(&self, content: &BufferSnapshot) -> Range<Point>;
}
impl AnchorRangeExt for Range<Anchor> {
fn cmp(&self, other: &Range<Anchor>, buffer: &Snapshot) -> Result<Ordering> {
fn cmp(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering> {
Ok(match self.start.cmp(&other.start, buffer)? {
Ordering::Equal => other.end.cmp(&self.end, buffer)?,
ord @ _ => ord,
})
}
fn to_offset(&self, content: &Snapshot) -> Range<usize> {
fn to_offset(&self, content: &BufferSnapshot) -> Range<usize> {
self.start.to_offset(&content)..self.end.to_offset(&content)
}
fn to_point(&self, content: &BufferSnapshot) -> Range<Point> {
self.start.summary::<Point>(&content)..self.end.summary::<Point>(&content)
}
}
+89
View File
@@ -0,0 +1,89 @@
use lazy_static::lazy_static;
use smallvec::{smallvec, SmallVec};
use std::iter;
lazy_static! {
pub static ref MIN: Locator = Locator::min();
pub static ref MAX: Locator = Locator::max();
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Locator(SmallVec<[u64; 4]>);
impl Locator {
pub fn min() -> Self {
Self(smallvec![u64::MIN])
}
pub fn max() -> Self {
Self(smallvec![u64::MAX])
}
pub fn assign(&mut self, other: &Self) {
self.0.resize(other.0.len(), 0);
self.0.copy_from_slice(&other.0);
}
pub fn between(lhs: &Self, rhs: &Self) -> Self {
let lhs = lhs.0.iter().copied().chain(iter::repeat(u64::MIN));
let rhs = rhs.0.iter().copied().chain(iter::repeat(u64::MAX));
let mut location = SmallVec::new();
for (lhs, rhs) in lhs.zip(rhs) {
let mid = lhs + ((rhs.saturating_sub(lhs)) >> 48);
location.push(mid);
if mid > lhs {
break;
}
}
Self(location)
}
pub fn len(&self) -> usize {
self.0.len()
}
}
impl Default for Locator {
fn default() -> Self {
Self::min()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::prelude::*;
use std::mem;
#[gpui::test(iterations = 100)]
fn test_locators(mut rng: StdRng) {
let mut lhs = Default::default();
let mut rhs = Default::default();
while lhs == rhs {
lhs = Locator(
(0..rng.gen_range(1..=5))
.map(|_| rng.gen_range(0..=100))
.collect(),
);
rhs = Locator(
(0..rng.gen_range(1..=5))
.map(|_| rng.gen_range(0..=100))
.collect(),
);
}
if lhs > rhs {
mem::swap(&mut lhs, &mut rhs);
}
let middle = Locator::between(&lhs, &rhs);
assert!(middle > lhs);
assert!(middle < rhs);
for ix in 0..middle.0.len() - 1 {
assert!(
middle.0[ix] == *lhs.0.get(ix).unwrap_or(&0)
|| middle.0[ix] == *rhs.0.get(ix).unwrap_or(&0)
);
}
}
}
+33 -17
View File
@@ -1,9 +1,15 @@
use super::Operation;
use std::{fmt::Debug, ops::Add};
use sum_tree::{Cursor, Dimension, Edit, Item, KeyedItem, SumTree, Summary};
use sum_tree::{Dimension, Edit, Item, KeyedItem, SumTree, Summary};
pub trait Operation: Clone + Debug {
fn lamport_timestamp(&self) -> clock::Lamport;
}
#[derive(Clone, Debug)]
pub struct OperationQueue(SumTree<Operation>);
struct OperationItem<T>(T);
#[derive(Clone, Debug)]
pub struct OperationQueue<T: Operation>(SumTree<OperationItem<T>>);
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct OperationKey(clock::Lamport);
@@ -20,7 +26,7 @@ impl OperationKey {
}
}
impl OperationQueue {
impl<T: Operation> OperationQueue<T> {
pub fn new() -> Self {
OperationQueue(SumTree::new())
}
@@ -29,11 +35,15 @@ impl OperationQueue {
self.0.summary().len
}
pub fn insert(&mut self, mut ops: Vec<Operation>) {
pub fn insert(&mut self, mut ops: Vec<T>) {
ops.sort_by_key(|op| op.lamport_timestamp());
ops.dedup_by_key(|op| op.lamport_timestamp());
self.0
.edit(ops.into_iter().map(Edit::Insert).collect(), &());
self.0.edit(
ops.into_iter()
.map(|op| Edit::Insert(OperationItem(op)))
.collect(),
&(),
);
}
pub fn drain(&mut self) -> Self {
@@ -42,8 +52,8 @@ impl OperationQueue {
clone
}
pub fn cursor(&self) -> Cursor<Operation, ()> {
self.0.cursor()
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.0.cursor::<()>().map(|i| &i.0)
}
}
@@ -76,22 +86,22 @@ impl<'a> Dimension<'a, OperationSummary> for OperationKey {
}
}
impl Item for Operation {
impl<T: Operation> Item for OperationItem<T> {
type Summary = OperationSummary;
fn summary(&self) -> Self::Summary {
OperationSummary {
key: OperationKey::new(self.lamport_timestamp()),
key: OperationKey::new(self.0.lamport_timestamp()),
len: 1,
}
}
}
impl KeyedItem for Operation {
impl<T: Operation> KeyedItem for OperationItem<T> {
type Key = OperationKey;
fn key(&self) -> Self::Key {
OperationKey::new(self.lamport_timestamp())
OperationKey::new(self.0.lamport_timestamp())
}
}
@@ -107,21 +117,27 @@ mod tests {
assert_eq!(queue.len(), 0);
queue.insert(vec![
Operation::Test(clock.tick()),
Operation::Test(clock.tick()),
TestOperation(clock.tick()),
TestOperation(clock.tick()),
]);
assert_eq!(queue.len(), 2);
queue.insert(vec![Operation::Test(clock.tick())]);
queue.insert(vec![TestOperation(clock.tick())]);
assert_eq!(queue.len(), 3);
drop(queue.drain());
assert_eq!(queue.len(), 0);
queue.insert(vec![Operation::Test(clock.tick())]);
queue.insert(vec![TestOperation(clock.tick())]);
assert_eq!(queue.len(), 1);
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct TestOperation(clock::Lamport);
impl Operation for TestOperation {
fn lamport_timestamp(&self) -> clock::Lamport {
self.0
}
}
}
+35 -3
View File
@@ -9,7 +9,8 @@ pub struct Patch<T>(Vec<Edit<T>>);
impl<T> Patch<T>
where
T: Clone
T: 'static
+ Clone
+ Copy
+ Ord
+ Sub<T, Output = T>
@@ -33,13 +34,17 @@ where
Self(edits)
}
pub fn edits(&self) -> &[Edit<T>] {
&self.0
}
pub fn into_inner(self) -> Vec<Edit<T>> {
self.0
}
pub fn compose(&self, other: &Self) -> Self {
pub fn compose(&self, new_edits_iter: impl IntoIterator<Item = Edit<T>>) -> Self {
let mut old_edits_iter = self.0.iter().cloned().peekable();
let mut new_edits_iter = other.0.iter().cloned().peekable();
let mut new_edits_iter = new_edits_iter.into_iter().peekable();
let mut composed = Patch(Vec::new());
let mut old_start = T::default();
@@ -196,6 +201,33 @@ where
}
}
impl<T: Clone> IntoIterator for Patch<T> {
type Item = Edit<T>;
type IntoIter = std::vec::IntoIter<Edit<T>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a, T: Clone> IntoIterator for &'a Patch<T> {
type Item = Edit<T>;
type IntoIter = std::iter::Cloned<std::slice::Iter<'a, Edit<T>>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter().cloned()
}
}
impl<'a, T: Clone> IntoIterator for &'a mut Patch<T> {
type Item = Edit<T>;
type IntoIter = std::iter::Cloned<std::slice::Iter<'a, Edit<T>>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter().cloned()
}
}
#[cfg(test)]
mod tests {
use super::*;
+8
View File
@@ -35,6 +35,14 @@ impl Point {
pub fn is_zero(&self) -> bool {
self.row == 0 && self.column == 0
}
pub fn saturating_sub(self, other: Self) -> Self {
if self < other {
Self::zero()
} else {
self - other
}
}
}
impl<'a> Add<&'a Self> for Point {
+8
View File
@@ -26,6 +26,14 @@ impl PointUtf16 {
pub fn is_zero(&self) -> bool {
self.row == 0 && self.column == 0
}
pub fn saturating_sub(self, other: Self) -> Self {
if self < other {
Self::zero()
} else {
self - other
}
}
}
impl<'a> Add<&'a Self> for PointUtf16 {
+10 -2
View File
@@ -12,14 +12,22 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
type Item = char;
fn next(&mut self) -> Option<Self::Item> {
if std::env::var("SIMPLE_TEXT").map_or(false, |v| !v.is_empty()) {
return if self.0.gen_range(0..100) < 5 {
Some('\n')
} else {
Some(self.0.gen_range(b'a'..b'z' + 1).into())
};
}
match self.0.gen_range(0..100) {
// whitespace
0..=19 => [' ', '\n', '\t'].choose(&mut self.0).copied(),
// two-byte greek letters
20..=32 => char::from_u32(self.0.gen_range(('α' as u32)..('ω' as u32 + 1))),
// three-byte characters
// // three-byte characters
33..=45 => ['✋', '✅', '❌', '❎', '⭐'].choose(&mut self.0).copied(),
// four-byte characters
// // four-byte characters
46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.0).copied(),
// ascii letters
_ => Some(self.0.gen_range(b'a'..b'z' + 1).into()),
+67 -7
View File
@@ -205,6 +205,19 @@ impl Rope {
.map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot))
}
pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point {
if point >= self.summary().lines_utf16 {
return self.summary().lines;
}
let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>();
cursor.seek(&point, Bias::Left, &());
let overshoot = point - cursor.start().0;
cursor.start().1
+ cursor
.item()
.map_or(Point::zero(), |chunk| chunk.point_utf16_to_point(overshoot))
}
pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize {
let mut cursor = self.chunks.cursor::<usize>();
cursor.seek(&offset, Bias::Left, &());
@@ -327,7 +340,7 @@ impl<'a> Cursor<'a> {
slice
}
pub fn summary<D: TextDimension<'a>>(&mut self, end_offset: usize) -> D {
pub fn summary<D: TextDimension>(&mut self, end_offset: usize) -> D {
debug_assert!(end_offset >= self.offset);
let mut summary = D::default();
@@ -583,6 +596,28 @@ impl Chunk {
offset
}
fn point_utf16_to_point(&self, target: PointUtf16) -> Point {
let mut point = Point::zero();
let mut point_utf16 = PointUtf16::zero();
for ch in self.0.chars() {
if point_utf16 >= target {
if point_utf16 > target {
panic!("point {:?} is inside of character {:?}", target, ch);
}
break;
}
if ch == '\n' {
point_utf16 += PointUtf16::new(1, 0);
point += Point::new(1, 0);
} else {
point_utf16 += PointUtf16::new(0, ch.len_utf16() as u32);
point += Point::new(0, ch.len_utf8() as u32);
}
}
point
}
fn clip_point(&self, target: Point, bias: Bias) -> Point {
for (row, line) in self.0.split('\n').enumerate() {
if row == target.row as usize {
@@ -685,6 +720,15 @@ impl sum_tree::Summary for TextSummary {
}
}
impl<'a> std::ops::Add<Self> for TextSummary {
type Output = Self;
fn add(mut self, rhs: Self) -> Self::Output {
self.add_assign(&rhs);
self
}
}
impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
fn add_assign(&mut self, other: &'a Self) {
let joined_chars = self.last_line_chars + other.first_line_chars;
@@ -719,12 +763,12 @@ impl std::ops::AddAssign<Self> for TextSummary {
}
}
pub trait TextDimension<'a>: Dimension<'a, TextSummary> {
pub trait TextDimension: 'static + for<'a> Dimension<'a, TextSummary> {
fn from_text_summary(summary: &TextSummary) -> Self;
fn add_assign(&mut self, other: &Self);
}
impl<'a, D1: TextDimension<'a>, D2: TextDimension<'a>> TextDimension<'a> for (D1, D2) {
impl<'a, D1: TextDimension, D2: TextDimension> TextDimension for (D1, D2) {
fn from_text_summary(summary: &TextSummary) -> Self {
(
D1::from_text_summary(summary),
@@ -738,7 +782,7 @@ impl<'a, D1: TextDimension<'a>, D2: TextDimension<'a>> TextDimension<'a> for (D1
}
}
impl<'a> TextDimension<'a> for TextSummary {
impl TextDimension for TextSummary {
fn from_text_summary(summary: &TextSummary) -> Self {
summary.clone()
}
@@ -754,7 +798,7 @@ impl<'a> sum_tree::Dimension<'a, TextSummary> for usize {
}
}
impl<'a> TextDimension<'a> for usize {
impl TextDimension for usize {
fn from_text_summary(summary: &TextSummary) -> Self {
summary.bytes
}
@@ -770,7 +814,7 @@ impl<'a> sum_tree::Dimension<'a, TextSummary> for Point {
}
}
impl<'a> TextDimension<'a> for Point {
impl TextDimension for Point {
fn from_text_summary(summary: &TextSummary) -> Self {
summary.lines
}
@@ -786,7 +830,7 @@ impl<'a> sum_tree::Dimension<'a, TextSummary> for PointUtf16 {
}
}
impl<'a> TextDimension<'a> for PointUtf16 {
impl TextDimension for PointUtf16 {
fn from_text_summary(summary: &TextSummary) -> Self {
summary.lines_utf16
}
@@ -949,6 +993,22 @@ mod tests {
}
}
let mut point_utf16 = PointUtf16::zero();
for unit in expected.encode_utf16() {
let left_point = actual.clip_point_utf16(point_utf16, Bias::Left);
let right_point = actual.clip_point_utf16(point_utf16, Bias::Right);
assert!(right_point >= left_point);
// Ensure translating UTF-16 points to offsets doesn't panic.
actual.point_utf16_to_offset(left_point);
actual.point_utf16_to_offset(right_point);
if unit == b'\n' as u16 {
point_utf16 += PointUtf16::new(1, 0);
} else {
point_utf16 += PointUtf16::new(0, 1);
}
}
for _ in 0..5 {
let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right);
let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left);
+15 -114
View File
@@ -1,12 +1,6 @@
use sum_tree::Bias;
use crate::{rope::TextDimension, Snapshot};
use super::{AnchorRangeMap, Buffer, Point, ToOffset, ToPoint};
use std::{cmp::Ordering, ops::Range, sync::Arc};
pub type SelectionSetId = clock::Lamport;
pub type SelectionsVersion = usize;
use crate::Anchor;
use crate::{rope::TextDimension, BufferSnapshot, ToOffset, ToPoint};
use std::cmp::Ordering;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum SelectionGoal {
@@ -24,20 +18,6 @@ pub struct Selection<T> {
pub goal: SelectionGoal,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SelectionSet {
pub id: SelectionSetId,
pub active: bool,
pub selections: Arc<AnchorRangeMap<SelectionState>>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct SelectionState {
pub id: usize,
pub reversed: bool,
pub goal: SelectionGoal,
}
impl<T: Clone> Selection<T> {
pub fn head(&self) -> T {
if self.reversed {
@@ -76,98 +56,19 @@ impl<T: ToOffset + ToPoint + Copy + Ord> Selection<T> {
self.end = head;
}
}
}
pub fn point_range(&self, buffer: &Buffer) -> Range<Point> {
let start = self.start.to_point(buffer);
let end = self.end.to_point(buffer);
if self.reversed {
end..start
} else {
start..end
}
}
pub fn offset_range(&self, buffer: &Buffer) -> Range<usize> {
let start = self.start.to_offset(buffer);
let end = self.end.to_offset(buffer);
if self.reversed {
end..start
} else {
start..end
impl Selection<Anchor> {
pub fn resolve<'a, D: 'a + TextDimension>(
&'a self,
snapshot: &'a BufferSnapshot,
) -> Selection<D> {
Selection {
id: self.id,
start: snapshot.summary_for_anchor(&self.start),
end: snapshot.summary_for_anchor(&self.end),
reversed: self.reversed,
goal: self.goal,
}
}
}
impl SelectionSet {
pub fn len(&self) -> usize {
self.selections.len()
}
pub fn selections<'a, D>(
&'a self,
content: &'a Snapshot,
) -> impl 'a + Iterator<Item = Selection<D>>
where
D: 'a + TextDimension<'a>,
{
self.selections
.ranges(content)
.map(|(range, state)| Selection {
id: state.id,
start: range.start,
end: range.end,
reversed: state.reversed,
goal: state.goal,
})
}
pub fn intersecting_selections<'a, D, I>(
&'a self,
range: Range<(I, Bias)>,
content: &'a Snapshot,
) -> impl 'a + Iterator<Item = Selection<D>>
where
D: 'a + TextDimension<'a>,
I: 'a + ToOffset,
{
self.selections
.intersecting_ranges(range, content)
.map(|(range, state)| Selection {
id: state.id,
start: range.start,
end: range.end,
reversed: state.reversed,
goal: state.goal,
})
}
pub fn oldest_selection<'a, D>(&'a self, content: &'a Snapshot) -> Option<Selection<D>>
where
D: 'a + TextDimension<'a>,
{
self.selections
.min_by_key(content, |selection| selection.id)
.map(|(range, state)| Selection {
id: state.id,
start: range.start,
end: range.end,
reversed: state.reversed,
goal: state.goal,
})
}
pub fn newest_selection<'a, D>(&'a self, content: &'a Snapshot) -> Option<Selection<D>>
where
D: 'a + TextDimension<'a>,
{
self.selections
.max_by_key(content, |selection| selection.id)
.map(|(range, state)| Selection {
id: state.id,
start: range.start,
end: range.end,
reversed: state.reversed,
goal: state.goal,
})
}
}
+48
View File
@@ -0,0 +1,48 @@
use crate::{Edit, Patch};
use parking_lot::Mutex;
use std::{
mem,
sync::{Arc, Weak},
};
#[derive(Default)]
pub struct Topic(Mutex<Vec<Weak<Mutex<Patch<usize>>>>>);
pub struct Subscription(Arc<Mutex<Patch<usize>>>);
impl Topic {
pub fn subscribe(&mut self) -> Subscription {
let subscription = Subscription(Default::default());
self.0.get_mut().push(Arc::downgrade(&subscription.0));
subscription
}
pub fn publish(&self, edits: impl Clone + IntoIterator<Item = Edit<usize>>) {
publish(&mut *self.0.lock(), edits);
}
pub fn publish_mut(&mut self, edits: impl Clone + IntoIterator<Item = Edit<usize>>) {
publish(self.0.get_mut(), edits);
}
}
impl Subscription {
pub fn consume(&self) -> Patch<usize> {
mem::take(&mut *self.0.lock())
}
}
fn publish(
subscriptions: &mut Vec<Weak<Mutex<Patch<usize>>>>,
edits: impl Clone + IntoIterator<Item = Edit<usize>>,
) {
subscriptions.retain(|subscription| {
if let Some(subscription) = subscription.upgrade() {
let mut patch = subscription.lock();
*patch = patch.compose(edits.clone());
true
} else {
false
}
});
}
+75 -46
View File
@@ -78,6 +78,8 @@ fn test_random_edits(mut rng: StdRng) {
TextSummary::from(&reference_string[range])
);
buffer.check_invariants();
if rng.gen_bool(0.3) {
buffer_versions.push((buffer.clone(), buffer.subscribe()));
}
@@ -102,6 +104,32 @@ fn test_random_edits(mut rng: StdRng) {
}
assert_eq!(text.to_string(), buffer.text());
for _ in 0..5 {
let end_ix = old_buffer.clip_offset(rng.gen_range(0..=old_buffer.len()), Bias::Right);
let start_ix = old_buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
let range = old_buffer.anchor_before(start_ix)..old_buffer.anchor_after(end_ix);
let mut old_text = old_buffer.text_for_range(range.clone()).collect::<String>();
let edits = buffer
.edits_since_in_range::<usize>(&old_buffer.version, range.clone())
.collect::<Vec<_>>();
log::info!(
"applying edits since version {:?} to old text in range {:?}: {:?}: {:?}",
old_buffer.version(),
start_ix..end_ix,
old_text,
edits,
);
let new_text = buffer.text_for_range(range).collect::<String>();
for edit in edits {
old_text.replace_range(
edit.new.start..edit.new.start + edit.old_len(),
&new_text[edit.new],
);
}
assert_eq!(old_text, new_text);
}
let subscription_edits = subscription.consume();
log::info!(
"applying subscription edits since version {:?} to old text: {:?}: {:?}",
@@ -432,63 +460,41 @@ fn test_history() {
let mut now = Instant::now();
let mut buffer = Buffer::new(0, 0, History::new("123456".into()));
let set_id = if let Operation::UpdateSelections { set_id, .. } =
buffer.add_selection_set(&buffer.selections_from_ranges(vec![4..4]).unwrap())
{
set_id
} else {
unreachable!()
};
buffer.start_transaction_at(Some(set_id), now).unwrap();
buffer.start_transaction_at(now);
buffer.edit(vec![2..4], "cd");
buffer.end_transaction_at(Some(set_id), now).unwrap();
buffer.end_transaction_at(now);
assert_eq!(buffer.text(), "12cd56");
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]);
buffer.start_transaction_at(Some(set_id), now).unwrap();
buffer
.update_selection_set(set_id, &buffer.selections_from_ranges(vec![1..3]).unwrap())
.unwrap();
buffer.start_transaction_at(now);
buffer.edit(vec![4..5], "e");
buffer.end_transaction_at(Some(set_id), now).unwrap();
buffer.end_transaction_at(now).unwrap();
assert_eq!(buffer.text(), "12cde6");
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]);
now += buffer.history.group_interval + Duration::from_millis(1);
buffer.start_transaction_at(Some(set_id), now).unwrap();
buffer
.update_selection_set(set_id, &buffer.selections_from_ranges(vec![2..2]).unwrap())
.unwrap();
buffer.start_transaction_at(now);
buffer.edit(vec![0..1], "a");
buffer.edit(vec![1..1], "b");
buffer.end_transaction_at(Some(set_id), now).unwrap();
buffer.end_transaction_at(now).unwrap();
assert_eq!(buffer.text(), "ab2cde6");
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]);
// Last transaction happened past the group interval, undo it on its
// own.
// Last transaction happened past the group interval, undo it on its own.
buffer.undo();
assert_eq!(buffer.text(), "12cde6");
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]);
// First two transactions happened within the group interval, undo them
// together.
// First two transactions happened within the group interval, undo them together.
buffer.undo();
assert_eq!(buffer.text(), "123456");
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]);
// Redo the first two transactions together.
buffer.redo();
assert_eq!(buffer.text(), "12cde6");
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]);
// Redo the last transaction on its own.
buffer.redo();
assert_eq!(buffer.text(), "ab2cde6");
assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]);
buffer.start_transaction_at(None, now).unwrap();
assert!(buffer.end_transaction_at(None, now).is_none());
buffer.start_transaction_at(now);
assert!(buffer.end_transaction_at(now).is_none());
buffer.undo();
assert_eq!(buffer.text(), "12cde6");
}
@@ -554,8 +560,8 @@ fn test_random_concurrent_edits(mut rng: StdRng) {
let buffer = &mut buffers[replica_index];
match rng.gen_range(0..=100) {
0..=50 if mutation_count != 0 => {
let ops = buffer.randomly_mutate(&mut rng);
network.broadcast(buffer.replica_id, ops);
let op = buffer.randomly_edit(&mut rng, 5).2;
network.broadcast(buffer.replica_id, vec![op]);
log::info!("buffer {} text: {:?}", buffer.replica_id, buffer.text());
mutation_count -= 1;
}
@@ -577,6 +583,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) {
}
_ => {}
}
buffer.check_invariants();
if mutation_count == 0 && network.is_idle() {
break;
@@ -591,18 +598,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) {
"Replica {} text != Replica 0 text",
buffer.replica_id
);
assert_eq!(
buffer.selection_sets().collect::<HashMap<_, _>>(),
first_buffer.selection_sets().collect::<HashMap<_, _>>()
);
assert_eq!(
buffer
.all_selection_ranges::<usize>()
.collect::<HashMap<_, _>>(),
first_buffer
.all_selection_ranges::<usize>()
.collect::<HashMap<_, _>>()
);
buffer.check_invariants();
}
}
@@ -618,6 +614,39 @@ struct Network<T: Clone, R: rand::Rng> {
rng: R,
}
impl Buffer {
fn check_invariants(&self) {
// Ensure every fragment is ordered by locator in the fragment tree and corresponds
// to an insertion fragment in the insertions tree.
let mut prev_fragment_id = Locator::min();
for fragment in self.snapshot.fragments.items(&None) {
assert!(fragment.id > prev_fragment_id);
prev_fragment_id = fragment.id.clone();
let insertion_fragment = self
.snapshot
.insertions
.get(
&InsertionFragmentKey {
timestamp: fragment.insertion_timestamp.local(),
split_offset: fragment.insertion_offset,
},
&(),
)
.unwrap();
assert_eq!(insertion_fragment.fragment_id, fragment.id);
}
let mut cursor = self.snapshot.fragments.cursor::<Option<&Locator>>();
for insertion_fragment in self.snapshot.insertions.cursor::<()>() {
cursor.seek(&Some(&insertion_fragment.fragment_id), Bias::Left, &None);
let fragment = cursor.item().unwrap();
assert_eq!(insertion_fragment.fragment_id, fragment.id);
assert_eq!(insertion_fragment.split_offset, fragment.insertion_offset);
}
}
}
impl<T: Clone, R: rand::Rng> Network<T, R> {
fn new(rng: R) -> Self {
Network {
+570 -762
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -155,10 +155,10 @@ pub struct ContactsPanel {
pub host_username: ContainedText,
pub tree_branch_width: f32,
pub tree_branch_color: Color,
pub shared_worktree: WorktreeRow,
pub hovered_shared_worktree: WorktreeRow,
pub unshared_worktree: WorktreeRow,
pub hovered_unshared_worktree: WorktreeRow,
pub shared_project: WorktreeRow,
pub hovered_shared_project: WorktreeRow,
pub unshared_project: WorktreeRow,
pub hovered_unshared_project: WorktreeRow,
}
#[derive(Deserialize, Default)]
+3 -3
View File
@@ -11,7 +11,7 @@ use parking_lot::Mutex;
use postage::watch;
use std::{cmp, sync::Arc};
use theme::ThemeRegistry;
use workspace::{Settings, Workspace, AppState};
use workspace::{AppState, Settings, Workspace};
#[derive(Clone)]
pub struct ThemeSelectorParams {
@@ -64,14 +64,14 @@ impl ThemeSelector {
Editor::single_line(
{
let settings = settings.clone();
move |_| {
Arc::new(move |_| {
let settings = settings.borrow();
EditorSettings {
tab_size: settings.tab_size,
style: settings.theme.selector.input_editor.as_editor(),
soft_wrap: editor::SoftWrap::None,
}
}
})
},
cx,
)
+13
View File
@@ -35,3 +35,16 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
panic!("You must pass a JSON object to this helper")
}
}
pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
let mut text = String::new();
for row in 0..rows {
let c: char = (start_char as u32 + row as u32) as u8 as char;
let mut line = c.to_string().repeat(cols);
if row < rows - 1 {
line.push('\n');
}
text += &line;
}
text
}
+1
View File
@@ -11,6 +11,7 @@ test-support = ["client/test-support", "project/test-support"]
[dependencies]
client = { path = "../client" }
clock = { path = "../clock" }
gpui = { path = "../gpui" }
language = { path = "../language" }
project = { path = "../project" }
+241 -139
View File
@@ -6,6 +6,7 @@ mod status_bar;
use anyhow::{anyhow, Result};
use client::{Authenticate, ChannelList, Client, User, UserStore};
use clock::ReplicaId;
use gpui::{
action,
color::Color,
@@ -30,7 +31,6 @@ use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItem
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
use std::{
collections::{hash_map::Entry, HashMap},
future::Future,
path::{Path, PathBuf},
sync::Arc,
@@ -40,18 +40,24 @@ use theme::{Theme, ThemeRegistry};
action!(Open, Arc<AppState>);
action!(OpenNew, Arc<AppState>);
action!(OpenPaths, OpenParams);
action!(ToggleShare);
action!(JoinProject, JoinProjectParams);
action!(Save);
action!(DebugElements);
pub fn init(cx: &mut MutableAppContext) {
cx.add_global_action(open);
cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
open_paths(&action.0.paths, &action.0.app_state, cx).detach()
open_paths(&action.0.paths, &action.0.app_state, cx).detach();
});
cx.add_global_action(move |action: &OpenNew, cx: &mut MutableAppContext| {
open_new(&action.0, cx)
});
cx.add_global_action(move |action: &JoinProject, cx: &mut MutableAppContext| {
join_project(action.0.project_id, &action.0.app_state, cx).detach();
});
cx.add_action(Workspace::toggle_share);
cx.add_action(Workspace::save_active_item);
cx.add_action(Workspace::debug_elements);
cx.add_action(Workspace::toggle_sidebar_item);
@@ -90,8 +96,11 @@ pub struct AppState {
pub channel_list: ModelHandle<client::ChannelList>,
pub entry_openers: Arc<[Box<dyn EntryOpener>]>,
pub build_window_options: &'static dyn Fn() -> WindowOptions<'static>,
pub build_workspace:
&'static dyn Fn(&WorkspaceParams, &mut ViewContext<Workspace>) -> Workspace,
pub build_workspace: &'static dyn Fn(
ModelHandle<Project>,
&Arc<AppState>,
&mut ViewContext<Workspace>,
) -> Workspace,
}
#[derive(Clone)]
@@ -100,6 +109,12 @@ pub struct OpenParams {
pub app_state: Arc<AppState>,
}
#[derive(Clone)]
pub struct JoinProjectParams {
pub project_id: u64,
pub app_state: Arc<AppState>,
}
pub trait EntryOpener {
fn open(
&self,
@@ -136,7 +151,9 @@ pub trait ItemView: View {
fn has_conflict(&self, _: &AppContext) -> bool {
false
}
fn can_save(&self, cx: &AppContext) -> bool;
fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>>;
fn can_save_as(&self, cx: &AppContext) -> bool;
fn save_as(
&mut self,
worktree: ModelHandle<Worktree>,
@@ -180,6 +197,8 @@ pub trait ItemViewHandle {
fn to_any(&self) -> AnyViewHandle;
fn is_dirty(&self, cx: &AppContext) -> bool;
fn has_conflict(&self, cx: &AppContext) -> bool;
fn can_save(&self, cx: &AppContext) -> bool;
fn can_save_as(&self, cx: &AppContext) -> bool;
fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>>;
fn save_as(
&self,
@@ -310,6 +329,14 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
fn to_any(&self) -> AnyViewHandle {
self.into()
}
fn can_save(&self, cx: &AppContext) -> bool {
self.read(cx).can_save(cx)
}
fn can_save_as(&self, cx: &AppContext) -> bool {
self.read(cx).can_save_as(cx)
}
}
impl Clone for Box<dyn ItemViewHandle> {
@@ -326,6 +353,7 @@ impl Clone for Box<dyn ItemHandle> {
#[derive(Clone)]
pub struct WorkspaceParams {
pub project: ModelHandle<Project>,
pub client: Arc<Client>,
pub fs: Arc<dyn Fs>,
pub languages: Arc<LanguageRegistry>,
@@ -338,7 +366,8 @@ pub struct WorkspaceParams {
impl WorkspaceParams {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut MutableAppContext) -> Self {
let languages = LanguageRegistry::new();
let fs = Arc::new(project::FakeFs::new());
let languages = Arc::new(LanguageRegistry::new());
let client = Client::new();
let http_client = client::test::FakeHttpClient::new(|_| async move {
Ok(client::http::ServerResponse::new(404))
@@ -347,17 +376,45 @@ impl WorkspaceParams {
gpui::fonts::with_font_cache(cx.font_cache().clone(), || theme::Theme::default());
let settings = Settings::new("Courier", cx.font_cache(), Arc::new(theme)).unwrap();
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let project = Project::local(
client.clone(),
user_store.clone(),
languages.clone(),
fs.clone(),
cx,
);
Self {
project,
channel_list: cx
.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
client,
fs: Arc::new(project::FakeFs::new()),
languages: Arc::new(languages),
fs,
languages,
settings: watch::channel_with(settings).1,
user_store,
entry_openers: Arc::from([]),
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn local(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Self {
Self {
project: Project::local(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx,
),
client: app_state.client.clone(),
fs: app_state.fs.clone(),
languages: app_state.languages.clone(),
settings: app_state.settings.clone(),
user_store: app_state.user_store.clone(),
channel_list: app_state.channel_list.clone(),
entry_openers: app_state.entry_openers.clone(),
}
}
}
pub struct Workspace {
@@ -375,24 +432,12 @@ pub struct Workspace {
project: ModelHandle<Project>,
entry_openers: Arc<[Box<dyn EntryOpener>]>,
items: Vec<Box<dyn WeakItemHandle>>,
loading_items: HashMap<
ProjectPath,
postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
>,
_observe_current_user: Task<()>,
}
impl Workspace {
pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
let project = cx.add_model(|_| {
Project::new(
params.languages.clone(),
params.client.clone(),
params.user_store.clone(),
params.fs.clone(),
)
});
cx.observe(&project, |_, _, cx| cx.notify()).detach();
cx.observe(&params.project, |_, _, cx| cx.notify()).detach();
let pane = cx.add_view(|_| Pane::new(params.settings.clone()));
let pane_id = pane.id();
@@ -438,10 +483,9 @@ impl Workspace {
fs: params.fs.clone(),
left_sidebar: Sidebar::new(Side::Left),
right_sidebar: Sidebar::new(Side::Right),
project,
project: params.project.clone(),
entry_openers: params.entry_openers.clone(),
items: Default::default(),
loading_items: Default::default(),
_observe_current_user,
}
}
@@ -636,7 +680,7 @@ impl Workspace {
let worktree = match self
.project
.read(cx)
.worktree_for_id(project_path.worktree_id)
.worktree_for_id(project_path.worktree_id, cx)
{
Some(worktree) => worktree,
None => {
@@ -645,47 +689,27 @@ impl Workspace {
}
};
if let Entry::Vacant(entry) = self.loading_items.entry(project_path.clone()) {
let (mut tx, rx) = postage::watch::channel();
entry.insert(rx);
let project_path = project_path.clone();
let entry_openers = self.entry_openers.clone();
cx.as_mut()
.spawn(|mut cx| async move {
let item = worktree.update(&mut cx, move |worktree, cx| {
for opener in entry_openers.iter() {
if let Some(task) = opener.open(worktree, project_path.clone(), cx) {
return task;
}
}
cx.spawn(|_, _| async move {
Err(anyhow!("no opener for path {:?} found", project_path))
})
});
*tx.borrow_mut() = Some(item.await.map_err(Arc::new));
})
.detach();
}
let project_path = project_path.clone();
let entry_openers = self.entry_openers.clone();
let task = worktree.update(cx, |worktree, cx| {
for opener in entry_openers.iter() {
if let Some(task) = opener.open(worktree, project_path.clone(), cx) {
return Some(task);
}
}
log::error!("no opener for path {:?} found", project_path);
None
})?;
let pane = pane.downgrade();
let mut watch = self.loading_items.get(&project_path).unwrap().clone();
Some(cx.spawn(|this, mut cx| async move {
let load_result = loop {
if let Some(load_result) = watch.borrow().as_ref() {
break load_result.clone();
}
watch.recv().await;
};
let load_result = task.await;
this.update(&mut cx, |this, cx| {
this.loading_items.remove(&project_path);
let pane = pane
.upgrade(&cx)
.ok_or_else(|| anyhow!("could not upgrade pane reference"))?;
let item = load_result?;
// By the time loading finishes, the entry could have been already added
// to the pane. If it was, we activate it, otherwise we'll store the
// item and add a new view for it.
@@ -694,7 +718,7 @@ impl Workspace {
{
Ok(existing)
} else {
Ok(this.add_item(item.boxed_clone(), cx))
Ok(this.add_item(item, cx))
}
})
}))
@@ -752,7 +776,43 @@ impl Workspace {
pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
if let Some(item) = self.active_item(cx) {
let handle = cx.handle();
if item.project_path(cx.as_ref()).is_none() {
if item.can_save(cx) {
if item.has_conflict(cx.as_ref()) {
const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
cx.prompt(
PromptLevel::Warning,
CONFLICT_MESSAGE,
&["Overwrite", "Cancel"],
move |answer, cx| {
if answer == 0 {
cx.spawn(|mut cx| async move {
if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await
{
error!("failed to save item: {:?}, ", error);
}
handle.update(&mut cx, |this, cx| {
this.project.update(cx, |project, cx| project.diagnose(cx))
});
})
.detach();
}
},
);
} else {
cx.spawn(|this, mut cx| async move {
if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
error!("failed to save item: {:?}, ", error);
}
this.update(&mut cx, |this, cx| {
this.project.update(cx, |project, cx| project.diagnose(cx))
});
})
.detach();
}
} else if item.can_save_as(cx) {
let worktree = self.worktrees(cx).first();
let start_abs_path = worktree
.and_then(|w| w.read(cx).as_local())
@@ -780,36 +840,14 @@ impl Workspace {
if let Err(error) = result {
error!("failed to save item: {:?}, ", error);
}
handle.update(&mut cx, |this, cx| {
this.project.update(cx, |project, cx| project.diagnose(cx))
});
})
.detach()
}
});
return;
} else if item.has_conflict(cx.as_ref()) {
const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
cx.prompt(
PromptLevel::Warning,
CONFLICT_MESSAGE,
&["Overwrite", "Cancel"],
move |answer, cx| {
if answer == 0 {
cx.spawn(|mut cx| async move {
if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
error!("failed to save item: {:?}, ", error);
}
})
.detach();
}
},
);
} else {
cx.spawn(|_, mut cx| async move {
if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
error!("failed to save item: {:?}, ", error);
}
})
.detach();
}
}
}
@@ -968,6 +1006,18 @@ impl Workspace {
&self.active_pane
}
fn toggle_share(&mut self, _: &ToggleShare, cx: &mut ViewContext<Self>) {
self.project.update(cx, |project, cx| {
if project.is_local() {
if project.is_shared() {
project.unshare(cx).detach();
} else {
project.share(cx).detach();
}
}
});
}
fn render_connection_status(&self) -> Option<ElementBox> {
let theme = &self.settings.borrow().theme;
match &*self.client.status().borrow() {
@@ -1019,18 +1069,14 @@ impl Workspace {
.with_child(
Align::new(
Flex::row()
.with_children(self.render_share_icon(cx))
.with_children(self.render_collaborators(theme, cx))
.with_child(
self.render_avatar(
self.user_store.read(cx).current_user().as_ref(),
self.project
.read(cx)
.active_worktree()
.map(|worktree| worktree.read(cx).replica_id()),
theme,
cx,
),
)
.with_child(self.render_avatar(
self.user_store.read(cx).current_user().as_ref(),
self.project.read(cx).replica_id(),
theme,
cx,
))
.with_children(self.render_connection_status())
.boxed(),
)
@@ -1047,30 +1093,26 @@ impl Workspace {
}
fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
let mut elements = Vec::new();
if let Some(active_worktree) = self.project.read(cx).active_worktree() {
let collaborators = active_worktree
.read(cx)
.collaborators()
.values()
.cloned()
.collect::<Vec<_>>();
for collaborator in collaborators {
elements.push(self.render_avatar(
Some(&collaborator.user),
Some(collaborator.replica_id),
theme,
cx,
));
}
}
elements
let mut collaborators = self
.project
.read(cx)
.collaborators()
.values()
.cloned()
.collect::<Vec<_>>();
collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
collaborators
.into_iter()
.map(|collaborator| {
self.render_avatar(Some(&collaborator.user), collaborator.replica_id, theme, cx)
})
.collect()
}
fn render_avatar(
&self,
user: Option<&Arc<User>>,
replica_id: Option<u16>,
replica_id: ReplicaId,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
@@ -1088,15 +1130,13 @@ impl Workspace {
.boxed(),
)
.with_child(
AvatarRibbon::new(replica_id.map_or(Default::default(), |id| {
theme.editor.replica_selection_style(id).cursor
}))
.constrained()
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
.aligned()
.bottom()
.boxed(),
AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor)
.constrained()
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
.aligned()
.bottom()
.boxed(),
)
.boxed(),
)
@@ -1120,6 +1160,35 @@ impl Workspace {
.boxed()
}
}
fn render_share_icon(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
if self.project().read(cx).is_local() && self.client.user_id().is_some() {
enum Share {}
let color = if self.project().read(cx).is_shared() {
Color::green()
} else {
Color::red()
};
Some(
MouseEventHandler::new::<Share, _, _, _>(0, cx, |_, _| {
Align::new(
ConstrainedBox::new(
Svg::new("icons/broadcast-24.svg").with_color(color).boxed(),
)
.with_width(24.)
.boxed(),
)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(|cx| cx.dispatch_action(ToggleShare))
.boxed(),
)
} else {
None
}
}
}
impl Entity for Workspace {
@@ -1281,20 +1350,6 @@ impl std::fmt::Debug for OpenParams {
}
}
impl<'a> From<&'a AppState> for WorkspaceParams {
fn from(state: &'a AppState) -> Self {
Self {
client: state.client.clone(),
fs: state.fs.clone(),
languages: state.languages.clone(),
settings: state.settings.clone(),
user_store: state.user_store.clone(),
channel_list: state.channel_list.clone(),
entry_openers: state.entry_openers.clone(),
}
}
}
fn open(action: &Open, cx: &mut MutableAppContext) {
let app_state = action.0.clone();
cx.prompt_for_paths(
@@ -1337,7 +1392,14 @@ pub fn open_paths(
let workspace = existing.unwrap_or_else(|| {
cx.add_window((app_state.build_window_options)(), |cx| {
(app_state.build_workspace)(&WorkspaceParams::from(app_state.as_ref()), cx)
let project = Project::local(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx,
);
(app_state.build_workspace)(project, &app_state, cx)
})
.1
});
@@ -1349,9 +1411,49 @@ pub fn open_paths(
})
}
pub fn join_project(
project_id: u64,
app_state: &Arc<AppState>,
cx: &mut MutableAppContext,
) -> Task<Result<ViewHandle<Workspace>>> {
for window_id in cx.window_ids().collect::<Vec<_>>() {
if let Some(workspace) = cx.root_view::<Workspace>(window_id) {
if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) {
return Task::ready(Ok(workspace));
}
}
}
let app_state = app_state.clone();
cx.spawn(|mut cx| async move {
let project = Project::remote(
project_id,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
&mut cx,
)
.await?;
let (_, workspace) = cx.update(|cx| {
cx.add_window((app_state.build_window_options)(), |cx| {
(app_state.build_workspace)(project, &app_state, cx)
})
});
Ok(workspace)
})
}
fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
(app_state.build_workspace)(&app_state.as_ref().into(), cx)
let project = Project::local(
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx,
);
(app_state.build_workspace)(project, &app_state, cx)
});
cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone()));
}
+2
View File
@@ -29,9 +29,11 @@ test-support = [
[dependencies]
chat_panel = { path = "../chat_panel" }
collections = { path = "../collections" }
client = { path = "../client" }
clock = { path = "../clock" }
contacts_panel = { path = "../contacts_panel" }
diagnostics = { path = "../diagnostics" }
editor = { path = "../editor" }
file_finder = { path = "../file_finder" }
fsevent = { path = "../fsevent" }
+6
View File
@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.87348 15.1266C4.04217 12.2953 4.04217 7.70484 6.87348 4.87354M17.1265 4.87354C19.9578 7.70484 19.9578 12.2953 17.1265 15.1266" stroke="#636B78" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.9948 13.0052C7.33507 11.3454 7.33507 8.65448 8.9948 6.99475M15.0052 6.99475C16.6649 8.65448 16.6649 11.3454 15.0052 13.0052" stroke="#636B78" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5 10C12.5 10.2761 12.2761 10.5 12 10.5C11.7239 10.5 11.5 10.2761 11.5 10C11.5 9.72386 11.7239 9.5 12 9.5C12.2761 9.5 12.5 9.72386 12.5 10Z" stroke="#636B78" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 13.75V19.25" stroke="#636B78" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 879 B

+10 -10
View File
@@ -156,30 +156,30 @@ host_username = { extends = "$text.0", padding.left = 8 }
tree_branch_width = 1
tree_branch_color = "$surface.2"
[contacts_panel.worktree]
[contacts_panel.project]
height = 24
padding = { left = 8 }
guest_avatar = { corner_radius = 8, width = 14 }
guest_avatar_spacing = 4
[contacts_panel.worktree.name]
[contacts_panel.project.name]
extends = "$text.1"
margin = { right = 6 }
[contacts_panel.unshared_worktree]
extends = "$contacts_panel.worktree"
[contacts_panel.unshared_project]
extends = "$contacts_panel.project"
[contacts_panel.hovered_unshared_worktree]
extends = "$contacts_panel.unshared_worktree"
[contacts_panel.hovered_unshared_project]
extends = "$contacts_panel.unshared_project"
background = "$state.hover"
corner_radius = 6
[contacts_panel.shared_worktree]
extends = "$contacts_panel.worktree"
[contacts_panel.shared_project]
extends = "$contacts_panel.project"
name.color = "$text.0.color"
[contacts_panel.hovered_shared_worktree]
extends = "$contacts_panel.shared_worktree"
[contacts_panel.hovered_shared_project]
extends = "$contacts_panel.shared_project"
background = "$state.hover"
corner_radius = 6
+179
View File
@@ -7,6 +7,184 @@ use std::{str, sync::Arc};
#[folder = "languages"]
struct LanguageDir;
mod rust {
use anyhow::Result;
use async_trait::async_trait;
use collections::{HashMap, HashSet};
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity};
use parking_lot::Mutex;
use serde::Deserialize;
use serde_json::Deserializer;
use smol::process::Command;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Default)]
pub struct DiagnosticProvider {
reported_paths: Mutex<HashSet<Arc<Path>>>,
}
#[derive(Debug, Deserialize)]
struct Check {
message: CompilerMessage,
}
#[derive(Debug, Deserialize)]
struct CompilerMessage {
code: Option<ErrorCode>,
spans: Vec<Span>,
message: String,
level: ErrorLevel,
children: Vec<CompilerMessage>,
}
#[derive(Debug, Deserialize)]
enum ErrorLevel {
#[serde(rename = "warning")]
Warning,
#[serde(rename = "error")]
Error,
#[serde(rename = "help")]
Help,
#[serde(rename = "note")]
Note,
}
#[derive(Debug, Deserialize)]
struct ErrorCode {
code: String,
}
#[derive(Clone, Debug, Deserialize)]
struct Span {
is_primary: bool,
file_name: PathBuf,
byte_start: usize,
byte_end: usize,
expansion: Option<Box<Expansion>>,
}
#[derive(Clone, Debug, Deserialize)]
struct Expansion {
span: Span,
}
#[async_trait]
impl language::DiagnosticProvider for DiagnosticProvider {
async fn diagnose(
&self,
root_path: Arc<Path>,
) -> Result<HashMap<Arc<Path>, Vec<DiagnosticEntry<usize>>>> {
let output = Command::new("cargo")
.arg("check")
.args(["--message-format", "json"])
.current_dir(&root_path)
.output()
.await?;
let mut group_id = 0;
let mut diagnostics_by_path = HashMap::default();
let mut new_reported_paths = HashSet::default();
for value in
Deserializer::from_slice(&output.stdout).into_iter::<&serde_json::value::RawValue>()
{
if let Ok(check) = serde_json::from_str::<Check>(value?.get()) {
let check_severity = match check.message.level {
ErrorLevel::Warning => DiagnosticSeverity::WARNING,
ErrorLevel::Error => DiagnosticSeverity::ERROR,
ErrorLevel::Help => DiagnosticSeverity::HINT,
ErrorLevel::Note => DiagnosticSeverity::INFORMATION,
};
let mut primary_span = None;
for mut span in check.message.spans {
if let Some(mut expansion) = span.expansion {
expansion.span.is_primary = span.is_primary;
span = expansion.span;
}
let span_path: Arc<Path> = span.file_name.as_path().into();
new_reported_paths.insert(span_path.clone());
diagnostics_by_path
.entry(span_path)
.or_insert(Vec::new())
.push(DiagnosticEntry {
range: span.byte_start..span.byte_end,
diagnostic: Diagnostic {
code: check.message.code.as_ref().map(|c| c.code.clone()),
severity: check_severity,
message: check.message.message.clone(),
group_id,
is_valid: true,
is_primary: span.is_primary,
is_disk_based: true,
},
});
if span.is_primary {
primary_span = Some(span);
}
}
for mut child in check.message.children {
if child.spans.is_empty() {
if let Some(primary_span) = primary_span.clone() {
child.spans.push(primary_span);
}
} else {
// TODO
continue;
}
let child_severity = match child.level {
ErrorLevel::Warning => DiagnosticSeverity::WARNING,
ErrorLevel::Error => DiagnosticSeverity::ERROR,
ErrorLevel::Help => DiagnosticSeverity::HINT,
ErrorLevel::Note => DiagnosticSeverity::INFORMATION,
};
for mut span in child.spans {
if let Some(expansion) = span.expansion {
span = expansion.span;
}
let span_path: Arc<Path> = span.file_name.as_path().into();
new_reported_paths.insert(span_path.clone());
diagnostics_by_path
.entry(span_path)
.or_insert(Vec::new())
.push(DiagnosticEntry {
range: span.byte_start..span.byte_end,
diagnostic: Diagnostic {
code: child.code.as_ref().map(|c| c.code.clone()),
severity: child_severity,
message: child.message.clone(),
group_id,
is_valid: true,
is_primary: false,
is_disk_based: true,
},
});
}
}
group_id += 1;
}
}
let reported_paths = &mut *self.reported_paths.lock();
for old_reported_path in reported_paths.iter() {
if !diagnostics_by_path.contains_key(old_reported_path) {
diagnostics_by_path.insert(old_reported_path.clone(), Default::default());
}
}
*reported_paths = new_reported_paths;
Ok(diagnostics_by_path)
}
}
}
pub fn build_language_registry() -> LanguageRegistry {
let mut languages = LanguageRegistry::default();
languages.add(Arc::new(rust()));
@@ -24,6 +202,7 @@ fn rust() -> Language {
.unwrap()
.with_indents_query(load_query("rust/indents.scm").as_ref())
.unwrap()
.with_diagnostic_provider(rust::DiagnosticProvider::default())
}
fn markdown() -> Language {
+1 -1
View File
@@ -58,9 +58,9 @@ fn main() {
editor::init(cx, &mut entry_openers);
go_to_line::init(cx);
file_finder::init(cx);
contacts_panel::init(cx);
chat_panel::init(cx);
project_panel::init(cx);
diagnostics::init(cx);
let app_state = Arc::new(AppState {
languages: languages.clone(),
+37 -23
View File
@@ -14,9 +14,10 @@ use gpui::{
geometry::vector::vec2f,
keymap::Binding,
platform::{WindowBounds, WindowOptions},
ViewContext,
ModelHandle, ViewContext,
};
pub use lsp;
use project::Project;
pub use project::{self, fs};
use project_panel::ProjectPanel;
use std::sync::Arc;
@@ -48,27 +49,39 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
])
}
pub fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext<Workspace>) -> Workspace {
let mut workspace = Workspace::new(params, cx);
pub fn build_workspace(
project: ModelHandle<Project>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> Workspace {
let workspace_params = WorkspaceParams {
project,
client: app_state.client.clone(),
fs: app_state.fs.clone(),
languages: app_state.languages.clone(),
settings: app_state.settings.clone(),
user_store: app_state.user_store.clone(),
channel_list: app_state.channel_list.clone(),
entry_openers: app_state.entry_openers.clone(),
};
let mut workspace = Workspace::new(&workspace_params, cx);
let project = workspace.project().clone();
workspace.left_sidebar_mut().add_item(
"icons/folder-tree-16.svg",
ProjectPanel::new(project, params.settings.clone(), cx).into(),
ProjectPanel::new(project, app_state.settings.clone(), cx).into(),
);
workspace.right_sidebar_mut().add_item(
"icons/user-16.svg",
cx.add_view(|cx| {
ContactsPanel::new(params.user_store.clone(), params.settings.clone(), cx)
})
.into(),
cx.add_view(|cx| ContactsPanel::new(app_state.clone(), cx))
.into(),
);
workspace.right_sidebar_mut().add_item(
"icons/comment-16.svg",
cx.add_view(|cx| {
ChatPanel::new(
params.client.clone(),
params.channel_list.clone(),
params.settings.clone(),
app_state.client.clone(),
app_state.channel_list.clone(),
app_state.settings.clone(),
cx,
)
})
@@ -76,9 +89,9 @@ pub fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext<Workspace>
);
let diagnostic =
cx.add_view(|_| editor::items::DiagnosticMessage::new(params.settings.clone()));
cx.add_view(|_| editor::items::DiagnosticMessage::new(app_state.settings.clone()));
let cursor_position =
cx.add_view(|_| editor::items::CursorPosition::new(params.settings.clone()));
cx.add_view(|_| editor::items::CursorPosition::new(app_state.settings.clone()));
workspace.status_bar().update(cx, |status_bar, cx| {
status_bar.add_left_item(diagnostic, cx);
status_bar.add_right_item(cursor_position, cx);
@@ -225,8 +238,8 @@ mod tests {
}),
)
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_worktree(Path::new("/root"), cx)
@@ -340,7 +353,8 @@ mod tests {
fs.insert_file("/dir1/a.txt", "".into()).await.unwrap();
fs.insert_file("/dir2/b.txt", "".into()).await.unwrap();
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_worktree("/dir1".as_ref(), cx)
@@ -378,7 +392,7 @@ mod tests {
.read(cx)
.worktrees(cx)
.iter()
.map(|w| w.read(cx).as_local().unwrap().abs_path())
.map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
.collect::<HashSet<_>>();
assert_eq!(
worktree_roots,
@@ -406,8 +420,8 @@ mod tests {
let fs = app_state.fs.as_fake();
fs.insert_tree("/root", json!({ "a.txt": "" })).await;
let (window_id, workspace) =
cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_worktree(Path::new("/root"), cx)
@@ -453,7 +467,7 @@ mod tests {
async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
let app_state = cx.update(test_app_state);
app_state.fs.as_fake().insert_dir("/root").await.unwrap();
let params = app_state.as_ref().into();
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
workspace
.update(&mut cx, |workspace, cx| {
@@ -570,7 +584,7 @@ mod tests {
) {
let app_state = cx.update(test_app_state);
app_state.fs.as_fake().insert_dir("/root").await.unwrap();
let params = app_state.as_ref().into();
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
// Create a new untitled buffer
@@ -628,8 +642,8 @@ mod tests {
)
.await;
let (window_id, workspace) =
cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_worktree(Path::new("/root"), cx)
+1 -2
View File
@@ -1,5 +1,4 @@
#!/bin/bash
set -e
cd server
cargo run --features seed-support --bin seed
cargo run --package=zed-server --features seed-support --bin seed