Start on editor2 (#3223)

Opening this pull request early on, as I added a bunch of APIs to
`gpui2` and uncommented code that might be useful to others in
`workspace2`.

Release Notes:

- N/A
This commit is contained in:
Antonio Scandurra 2023-11-03 15:37:48 +01:00 committed by GitHub
commit 0b3c888acc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 43893 additions and 915 deletions

58
Cargo.lock generated
View file

@ -1866,7 +1866,6 @@ dependencies = [
"async-tar",
"clock",
"collections",
"context_menu",
"fs",
"futures 0.3.28",
"gpui2",
@ -2622,6 +2621,60 @@ dependencies = [
"workspace",
]
[[package]]
name = "editor2"
version = "0.1.0"
dependencies = [
"aho-corasick",
"anyhow",
"client2",
"clock",
"collections",
"convert_case 0.6.0",
"copilot2",
"ctor",
"db2",
"drag_and_drop",
"env_logger 0.9.3",
"futures 0.3.28",
"fuzzy2",
"git",
"gpui2",
"indoc",
"itertools 0.10.5",
"language2",
"lazy_static",
"log",
"lsp2",
"multi_buffer2",
"ordered-float 2.10.0",
"parking_lot 0.11.2",
"postage",
"project2",
"rand 0.8.5",
"rich_text2",
"rpc2",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings2",
"smallvec",
"smol",
"snippet",
"sqlez",
"sum_tree",
"text2",
"theme2",
"tree-sitter",
"tree-sitter-html",
"tree-sitter-rust",
"tree-sitter-typescript",
"unindent",
"util",
"workspace2",
]
[[package]]
name = "either"
version = "1.9.0"
@ -3524,7 +3577,6 @@ dependencies = [
"foreign-types",
"futures 0.3.28",
"gpui2_macros",
"gpui_macros",
"image",
"itertools 0.10.5",
"lazy_static",
@ -4374,6 +4426,7 @@ dependencies = [
"lsp2",
"parking_lot 0.11.2",
"postage",
"pulldown-cmark",
"rand 0.8.5",
"regex",
"rpc2",
@ -11109,6 +11162,7 @@ dependencies = [
"copilot2",
"ctor",
"db2",
"editor2",
"env_logger 0.9.3",
"feature_flags2",
"fs2",

View file

@ -20,7 +20,7 @@ test-support = [
[dependencies]
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
# context_menu = { path = "../context_menu" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
settings = { package = "settings2", path = "../settings2" }

View file

@ -42,6 +42,11 @@ use util::{
// copilot,
// [Suggest, NextSuggestion, PreviousSuggestion, Reinstall]
// );
//
pub struct Suggest;
pub struct NextSuggestion;
pub struct PreviousSuggestion;
pub struct Reinstall;
pub fn init(
new_server_id: LanguageServerId,

93
crates/editor2/Cargo.toml Normal file
View file

@ -0,0 +1,93 @@
[package]
name = "editor2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/editor.rs"
doctest = false
[features]
test-support = [
"copilot/test-support",
"text/test-support",
"language/test-support",
"gpui/test-support",
"multi_buffer/test-support",
"project/test-support",
"util/test-support",
"workspace/test-support",
"tree-sitter-rust",
"tree-sitter-typescript"
]
[dependencies]
client = { package = "client2", path = "../client2" }
clock = { path = "../clock" }
copilot = { package="copilot2", path = "../copilot2" }
db = { package="db2", path = "../db2" }
drag_and_drop = { path = "../drag_and_drop" }
collections = { path = "../collections" }
# context_menu = { path = "../context_menu" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
git = { path = "../git" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
lsp = { package = "lsp2", path = "../lsp2" }
multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" }
project = { package = "project2", path = "../project2" }
rpc = { package = "rpc2", path = "../rpc2" }
rich_text = { package = "rich_text2", path = "../rich_text2" }
settings = { package="settings2", path = "../settings2" }
snippet = { path = "../snippet" }
sum_tree = { path = "../sum_tree" }
text = { package="text2", path = "../text2" }
theme = { package="theme2", path = "../theme2" }
util = { path = "../util" }
sqlez = { path = "../sqlez" }
workspace = { package = "workspace2", path = "../workspace2" }
aho-corasick = "1.1"
anyhow.workspace = true
convert_case = "0.6.0"
futures.workspace = true
indoc = "1.0.4"
itertools = "0.10"
lazy_static.workspace = true
log.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-html = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
[dev-dependencies]
copilot = { package="copilot2", path = "../copilot2", features = ["test-support"] }
text = { package="text2", path = "../text2", features = ["test-support"] }
language = { package="language2", path = "../language2", features = ["test-support"] }
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
project = { package = "project2", path = "../project2", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
rand.workspace = true
unindent.workspace = true
tree-sitter.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-html.workspace = true
tree-sitter-typescript.workspace = true

View file

@ -0,0 +1,102 @@
use crate::EditorSettings;
use gpui::ModelContext;
use settings::Settings;
use settings::SettingsStore;
use smol::Timer;
use std::time::Duration;
pub struct BlinkManager {
blink_interval: Duration,
blink_epoch: usize,
blinking_paused: bool,
visible: bool,
enabled: bool,
}
impl BlinkManager {
pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
// Make sure we blink the cursors if the setting is re-enabled
cx.observe_global::<SettingsStore>(move |this, cx| {
this.blink_cursors(this.blink_epoch, cx)
})
.detach();
Self {
blink_interval,
blink_epoch: 0,
blinking_paused: false,
visible: true,
enabled: false,
}
}
fn next_blink_epoch(&mut self) -> usize {
self.blink_epoch += 1;
self.blink_epoch
}
pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
self.show_cursor(cx);
let epoch = self.next_blink_epoch();
let interval = self.blink_interval;
cx.spawn(|this, mut cx| async move {
Timer::after(interval).await;
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
})
.detach();
}
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
if epoch == self.blink_epoch {
self.blinking_paused = false;
self.blink_cursors(epoch, cx);
}
}
fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
if EditorSettings::get_global(cx).cursor_blink {
if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
self.visible = !self.visible;
cx.notify();
let epoch = self.next_blink_epoch();
let interval = self.blink_interval;
cx.spawn(|this, mut cx| async move {
Timer::after(interval).await;
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
}
})
.detach();
}
} else {
self.show_cursor(cx);
}
}
pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) {
if !self.visible {
self.visible = true;
cx.notify();
}
}
pub fn enable(&mut self, cx: &mut ModelContext<Self>) {
self.enabled = true;
// Set cursors as invisible and start blinking: this causes cursors
// to be visible during the next render.
self.visible = false;
self.blink_cursors(self.blink_epoch, cx);
}
pub fn disable(&mut self, _cx: &mut ModelContext<Self>) {
self.enabled = false;
}
pub fn visible(&self) -> bool {
self.visible
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,765 @@
use super::{
fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
Highlights,
};
use crate::MultiBufferSnapshot;
use language::{Chunk, Point};
use std::{cmp, mem, num::NonZeroU32, ops::Range};
use sum_tree::Bias;
const MAX_EXPANSION_COLUMN: u32 = 256;
pub struct TabMap(TabSnapshot);
impl TabMap {
pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
let snapshot = TabSnapshot {
fold_snapshot,
tab_size,
max_expansion_column: MAX_EXPANSION_COLUMN,
version: 0,
};
(Self(snapshot.clone()), snapshot)
}
#[cfg(test)]
pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
self.0.max_expansion_column = column;
self.0.clone()
}
pub fn sync(
&mut self,
fold_snapshot: FoldSnapshot,
mut fold_edits: Vec<FoldEdit>,
tab_size: NonZeroU32,
) -> (TabSnapshot, Vec<TabEdit>) {
let old_snapshot = &mut self.0;
let mut new_snapshot = TabSnapshot {
fold_snapshot,
tab_size,
max_expansion_column: old_snapshot.max_expansion_column,
version: old_snapshot.version,
};
if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
new_snapshot.version += 1;
}
let mut tab_edits = Vec::with_capacity(fold_edits.len());
if old_snapshot.tab_size == new_snapshot.tab_size {
// Expand each edit to include the next tab on the same line as the edit,
// and any subsequent tabs on that line that moved across the tab expansion
// boundary.
for fold_edit in &mut fold_edits {
let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
let old_end_row_successor_offset = cmp::min(
FoldPoint::new(old_end.row() + 1, 0),
old_snapshot.fold_snapshot.max_point(),
)
.to_offset(&old_snapshot.fold_snapshot);
let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
let mut offset_from_edit = 0;
let mut first_tab_offset = None;
let mut last_tab_with_changed_expansion_offset = None;
'outer: for chunk in old_snapshot.fold_snapshot.chunks(
fold_edit.old.end..old_end_row_successor_offset,
false,
Highlights::default(),
) {
for (ix, _) in chunk.text.match_indices('\t') {
let offset_from_edit = offset_from_edit + (ix as u32);
if first_tab_offset.is_none() {
first_tab_offset = Some(offset_from_edit);
}
let old_column = old_end.column() + offset_from_edit;
let new_column = new_end.column() + offset_from_edit;
let was_expanded = old_column < old_snapshot.max_expansion_column;
let is_expanded = new_column < new_snapshot.max_expansion_column;
if was_expanded != is_expanded {
last_tab_with_changed_expansion_offset = Some(offset_from_edit);
} else if !was_expanded && !is_expanded {
break 'outer;
}
}
offset_from_edit += chunk.text.len() as u32;
if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
&& new_end.column() + offset_from_edit >= new_snapshot.max_expansion_column
{
break;
}
}
if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
fold_edit.old.end.0 += offset as usize + 1;
fold_edit.new.end.0 += offset as usize + 1;
}
}
// Combine any edits that overlap due to the expansion.
let mut ix = 1;
while ix < fold_edits.len() {
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.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;
}
}
for fold_edit in fold_edits {
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),
});
}
} else {
new_snapshot.version += 1;
tab_edits.push(TabEdit {
old: TabPoint::zero()..old_snapshot.max_point(),
new: TabPoint::zero()..new_snapshot.max_point(),
});
}
*old_snapshot = new_snapshot;
(old_snapshot.clone(), tab_edits)
}
}
#[derive(Clone)]
pub struct TabSnapshot {
pub fold_snapshot: FoldSnapshot,
pub tab_size: NonZeroU32,
pub max_expansion_column: u32,
pub version: usize,
}
impl TabSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
&self.fold_snapshot.inlay_snapshot.buffer
}
pub fn line_len(&self, row: u32) -> u32 {
let max_point = self.max_point();
if row < max_point.row() {
self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
.0
.column
} else {
max_point.column()
}
}
pub fn text_summary(&self) -> TextSummary {
self.text_summary_for_range(TabPoint::zero()..self.max_point())
}
pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
let input_start = self.to_fold_point(range.start, Bias::Left).0;
let input_end = self.to_fold_point(range.end, Bias::Right).0;
let input_summary = self
.fold_snapshot
.text_summary_for_range(input_start..input_end);
let mut first_line_chars = 0;
let line_end = if range.start.row() == range.end.row() {
range.end
} else {
self.max_point()
};
for c in self
.chunks(range.start..line_end, false, Highlights::default())
.flat_map(|chunk| chunk.text.chars())
{
if c == '\n' {
break;
}
first_line_chars += 1;
}
let mut last_line_chars = 0;
if range.start.row() == range.end.row() {
last_line_chars = first_line_chars;
} else {
for _ in self
.chunks(
TabPoint::new(range.end.row(), 0)..range.end,
false,
Highlights::default(),
)
.flat_map(|chunk| chunk.text.chars())
{
last_line_chars += 1;
}
}
TextSummary {
lines: range.end.0 - range.start.0,
first_line_chars,
last_line_chars,
longest_row: input_summary.longest_row,
longest_row_chars: input_summary.longest_row_chars,
}
}
pub fn chunks<'a>(
&'a self,
range: Range<TabPoint>,
language_aware: bool,
highlights: Highlights<'a>,
) -> TabChunks<'a> {
let (input_start, expanded_char_column, to_next_stop) =
self.to_fold_point(range.start, Bias::Left);
let input_column = input_start.column();
let input_start = input_start.to_offset(&self.fold_snapshot);
let input_end = self
.to_fold_point(range.end, Bias::Right)
.0
.to_offset(&self.fold_snapshot);
let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
range.end.column() - range.start.column()
} else {
to_next_stop
};
TabChunks {
fold_chunks: self.fold_snapshot.chunks(
input_start..input_end,
language_aware,
highlights,
),
input_column,
column: expanded_char_column,
max_expansion_column: self.max_expansion_column,
output_position: range.start.0,
max_output_position: range.end.0,
tab_size: self.tab_size,
chunk: Chunk {
text: &SPACES[0..(to_next_stop as usize)],
is_tab: true,
..Default::default()
},
inside_leading_tab: to_next_stop > 0,
}
}
pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
self.fold_snapshot.buffer_rows(row)
}
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(
TabPoint::zero()..self.max_point(),
false,
Highlights::default(),
)
.map(|chunk| chunk.text)
.collect()
}
pub fn max_point(&self) -> TabPoint {
self.to_tab_point(self.fold_snapshot.max_point())
}
pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
self.to_tab_point(
self.fold_snapshot
.clip_point(self.to_fold_point(point, bias).0, bias),
)
}
pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
let expanded = self.expand_tabs(chars, input.column());
TabPoint::new(input.row(), expanded)
}
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
let expanded = output.column();
let (collapsed, expanded_char_column, to_next_stop) =
self.collapse_tabs(chars, expanded, bias);
(
FoldPoint::new(output.row(), collapsed as u32),
expanded_char_column,
to_next_stop,
)
}
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
self.to_tab_point(fold_point)
}
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
let fold_point = self.to_fold_point(point, bias).0;
let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
self.fold_snapshot
.inlay_snapshot
.to_buffer_point(inlay_point)
}
fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
let tab_size = self.tab_size.get();
let mut expanded_chars = 0;
let mut expanded_bytes = 0;
let mut collapsed_bytes = 0;
let end_column = column.min(self.max_expansion_column);
for c in chars {
if collapsed_bytes >= end_column {
break;
}
if c == '\t' {
let tab_len = tab_size - expanded_chars % tab_size;
expanded_bytes += tab_len;
expanded_chars += tab_len;
} else {
expanded_bytes += c.len_utf8() as u32;
expanded_chars += 1;
}
collapsed_bytes += c.len_utf8() as u32;
}
expanded_bytes + column.saturating_sub(collapsed_bytes)
}
fn collapse_tabs(
&self,
chars: impl Iterator<Item = char>,
column: u32,
bias: Bias,
) -> (u32, u32, u32) {
let tab_size = self.tab_size.get();
let mut expanded_bytes = 0;
let mut expanded_chars = 0;
let mut collapsed_bytes = 0;
for c in chars {
if expanded_bytes >= column {
break;
}
if collapsed_bytes >= self.max_expansion_column {
break;
}
if c == '\t' {
let tab_len = tab_size - (expanded_chars % tab_size);
expanded_chars += tab_len;
expanded_bytes += tab_len;
if expanded_bytes > column {
expanded_chars -= expanded_bytes - column;
return match bias {
Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
};
}
} else {
expanded_chars += 1;
expanded_bytes += c.len_utf8() as u32;
}
if expanded_bytes > column && matches!(bias, Bias::Left) {
expanded_chars -= 1;
break;
}
collapsed_bytes += c.len_utf8() as u32;
}
(
collapsed_bytes + column.saturating_sub(expanded_bytes),
expanded_chars,
0,
)
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct TabPoint(pub Point);
impl TabPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(Point::new(row, column))
}
pub fn zero() -> Self {
Self::new(0, 0)
}
pub fn row(self) -> u32 {
self.0.row
}
pub fn column(self) -> u32 {
self.0.column
}
}
impl From<Point> for TabPoint {
fn from(point: Point) -> Self {
Self(point)
}
}
pub type TabEdit = text::Edit<TabPoint>;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TextSummary {
pub lines: Point,
pub first_line_chars: u32,
pub last_line_chars: u32,
pub longest_row: u32,
pub longest_row_chars: u32,
}
impl<'a> From<&'a str> for TextSummary {
fn from(text: &'a str) -> Self {
let sum = text::TextSummary::from(text);
TextSummary {
lines: sum.lines,
first_line_chars: sum.first_line_chars,
last_line_chars: sum.last_line_chars,
longest_row: sum.longest_row,
longest_row_chars: sum.longest_row_chars,
}
}
}
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;
if joined_chars > self.longest_row_chars {
self.longest_row = self.lines.row;
self.longest_row_chars = joined_chars;
}
if other.longest_row_chars > self.longest_row_chars {
self.longest_row = self.lines.row + other.longest_row;
self.longest_row_chars = other.longest_row_chars;
}
if self.lines.row == 0 {
self.first_line_chars += other.first_line_chars;
}
if other.lines.row == 0 {
self.last_line_chars += other.first_line_chars;
} else {
self.last_line_chars = other.last_line_chars;
}
self.lines += &other.lines;
}
}
// Handles a tab width <= 16
const SPACES: &str = " ";
pub struct TabChunks<'a> {
fold_chunks: FoldChunks<'a>,
chunk: Chunk<'a>,
column: u32,
max_expansion_column: u32,
output_position: Point,
input_column: u32,
max_output_position: Point,
tab_size: NonZeroU32,
inside_leading_tab: bool,
}
impl<'a> Iterator for TabChunks<'a> {
type Item = Chunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.chunk.text.is_empty() {
if let Some(chunk) = self.fold_chunks.next() {
self.chunk = chunk;
if self.inside_leading_tab {
self.chunk.text = &self.chunk.text[1..];
self.inside_leading_tab = false;
self.input_column += 1;
}
} else {
return None;
}
}
for (ix, c) in self.chunk.text.char_indices() {
match c {
'\t' => {
if ix > 0 {
let (prefix, suffix) = self.chunk.text.split_at(ix);
self.chunk.text = suffix;
return Some(Chunk {
text: prefix,
..self.chunk
});
} else {
self.chunk.text = &self.chunk.text[1..];
let tab_size = if self.input_column < self.max_expansion_column {
self.tab_size.get() as u32
} else {
1
};
let mut len = tab_size - self.column % tab_size;
let next_output_position = cmp::min(
self.output_position + Point::new(0, len),
self.max_output_position,
);
len = next_output_position.column - self.output_position.column;
self.column += len;
self.input_column += 1;
self.output_position = next_output_position;
return Some(Chunk {
text: &SPACES[..len as usize],
is_tab: true,
..self.chunk
});
}
}
'\n' => {
self.column = 0;
self.input_column = 0;
self.output_position += Point::new(1, 0);
}
_ => {
self.column += 1;
if !self.inside_leading_tab {
self.input_column += c.len_utf8() as u32;
}
self.output_position.column += c.len_utf8() as u32;
}
}
}
Some(mem::take(&mut self.chunk))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
display_map::{fold_map::FoldMap, inlay_map::InlayMap},
MultiBuffer,
};
use rand::{prelude::StdRng, Rng};
#[gpui::test]
fn test_expand_tabs(cx: &mut gpui::AppContext) {
let buffer = MultiBuffer::build_simple("", cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
}
#[gpui::test]
fn test_long_lines(cx: &mut gpui::AppContext) {
let max_expansion_column = 12;
let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
let output = "A BC DEF G HI J K L M";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), output);
for (ix, c) in input.char_indices() {
assert_eq!(
tab_snapshot
.chunks(
TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
false,
Highlights::default(),
)
.map(|c| c.text)
.collect::<String>(),
&output[ix..],
"text from index {ix}"
);
if c != '\t' {
let input_point = Point::new(0, ix as u32);
let output_point = Point::new(0, output.find(c).unwrap() as u32);
assert_eq!(
tab_snapshot.to_tab_point(FoldPoint(input_point)),
TabPoint(output_point),
"to_tab_point({input_point:?})"
);
assert_eq!(
tab_snapshot
.to_fold_point(TabPoint(output_point), Bias::Left)
.0,
FoldPoint(input_point),
"to_fold_point({output_point:?})"
);
}
}
}
#[gpui::test]
fn test_long_lines_with_character_spanning_max_expansion_column(cx: &mut gpui::AppContext) {
let max_expansion_column = 8;
let input = "abcdefg⋯hij";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), input);
}
#[gpui::test]
fn test_marking_tabs(cx: &mut gpui::AppContext) {
let input = "\t \thello";
let buffer = MultiBuffer::build_simple(&input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(
chunks(&tab_snapshot, TabPoint::zero()),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
(" ".to_string(), true),
("hello".to_string(), false),
]
);
assert_eq!(
chunks(&tab_snapshot, TabPoint::new(0, 2)),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
(" ".to_string(), true),
("hello".to_string(), false),
]
);
fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
let mut chunks = Vec::new();
let mut was_tab = false;
let mut text = String::new();
for chunk in snapshot.chunks(start..snapshot.max_point(), false, Highlights::default())
{
if chunk.is_tab != was_tab {
if !text.is_empty() {
chunks.push((mem::take(&mut text), was_tab));
}
was_tab = chunk.is_tab;
}
text.push_str(chunk.text);
}
if !text.is_empty() {
chunks.push((text, was_tab));
}
chunks
}
}
#[gpui::test(iterations = 100)]
fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) {
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
let len = rng.gen_range(0..30);
let buffer = if rng.gen() {
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
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 inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
fold_map.randomly_mutate(&mut rng);
let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
let text = text::Rope::from(tabs_snapshot.text().as_str());
log::info!(
"TabMap text (tab size: {}): {:?}",
tab_size,
tabs_snapshot.text(),
);
for _ in 0..5 {
let end_row = rng.gen_range(0..=text.max_point().row);
let end_column = rng.gen_range(0..=text.line_len(end_row));
let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
let start_row = rng.gen_range(0..=text.max_point().row);
let start_column = rng.gen_range(0..=text.line_len(start_row));
let mut start =
TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
if start > end {
mem::swap(&mut start, &mut end);
}
let expected_text = text
.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());
assert_eq!(
tabs_snapshot
.chunks(start..end, false, Highlights::default())
.map(|c| c.text)
.collect::<String>(),
expected_text,
"chunks({:?}..{:?})",
start,
end
);
let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
actual_summary.longest_row = expected_summary.longest_row;
actual_summary.longest_row_chars = expected_summary.longest_row_chars;
}
assert_eq!(actual_summary, expected_summary);
}
for row in 0..=text.max_point().row {
assert_eq!(
tabs_snapshot.line_len(row),
text.line_len(row),
"line_len({row})"
);
}
}
}

File diff suppressed because it is too large Load diff

10120
crates/editor2/src/editor.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
#[derive(Deserialize)]
pub struct EditorSettings {
pub cursor_blink: bool,
pub hover_popover_enabled: bool,
pub show_completions_on_input: bool,
pub show_completion_documentation: bool,
pub use_on_type_format: bool,
pub scrollbar: Scrollbar,
pub relative_line_numbers: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Scrollbar {
pub show: ShowScrollbar,
pub git_diff: bool,
pub selections: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ShowScrollbar {
Auto,
System,
Always,
Never,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct EditorSettingsContent {
pub cursor_blink: Option<bool>,
pub hover_popover_enabled: Option<bool>,
pub show_completions_on_input: Option<bool>,
pub show_completion_documentation: Option<bool>,
pub use_on_type_format: Option<bool>,
pub scrollbar: Option<ScrollbarContent>,
pub relative_line_numbers: Option<bool>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ScrollbarContent {
pub show: Option<ShowScrollbar>,
pub git_diff: Option<bool>,
pub selections: Option<bool>,
}
impl Settings for EditorSettings {
const KEY: Option<&'static str> = None;
type FileContent = EditorSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

282
crates/editor2/src/git.rs Normal file
View file

@ -0,0 +1,282 @@
use std::ops::Range;
use git::diff::{DiffHunk, DiffHunkStatus};
use language::Point;
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
AnchorRangeExt,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DisplayDiffHunk {
Folded {
display_row: u32,
},
Unfolded {
display_row_range: Range<u32>,
status: DiffHunkStatus,
},
}
impl DisplayDiffHunk {
pub fn start_display_row(&self) -> u32 {
match self {
&DisplayDiffHunk::Folded { display_row } => display_row,
DisplayDiffHunk::Unfolded {
display_row_range, ..
} => display_row_range.start,
}
}
pub fn contains_display_row(&self, display_row: u32) -> bool {
let range = match self {
&DisplayDiffHunk::Folded { display_row } => display_row..=display_row,
DisplayDiffHunk::Unfolded {
display_row_range, ..
} => display_row_range.start..=display_row_range.end,
};
range.contains(&display_row)
}
}
pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) -> DisplayDiffHunk {
let hunk_start_point = Point::new(hunk.buffer_range.start, 0);
let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
let hunk_end_point_sub = Point::new(
hunk.buffer_range
.end
.saturating_sub(1)
.max(hunk.buffer_range.start),
0,
);
let is_removal = hunk.status() == DiffHunkStatus::Removed;
let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(2), 0);
let folds_end = Point::new(hunk.buffer_range.end + 2, 0);
let folds_range = folds_start..folds_end;
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
let fold_point_range = fold_range.to_point(&snapshot.buffer_snapshot);
let fold_point_range = fold_point_range.start..=fold_point_range.end;
let folded_start = fold_point_range.contains(&hunk_start_point);
let folded_end = fold_point_range.contains(&hunk_end_point_sub);
let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
(folded_start && folded_end) || (is_removal && folded_start_sub)
});
if let Some(fold) = containing_fold {
let row = fold.start.to_display_point(snapshot).row();
DisplayDiffHunk::Folded { display_row: row }
} else {
let start = hunk_start_point.to_display_point(snapshot).row();
let hunk_end_row = hunk.buffer_range.end.max(hunk.buffer_range.start);
let hunk_end_point = Point::new(hunk_end_row, 0);
let end = hunk_end_point.to_display_point(snapshot).row();
DisplayDiffHunk::Unfolded {
display_row_range: start..end,
status: hunk.status(),
}
}
}
// #[cfg(any(test, feature = "test_support"))]
// mod tests {
// // use crate::editor_tests::init_test;
// use crate::Point;
// use gpui::TestAppContext;
// use multi_buffer::{ExcerptRange, MultiBuffer};
// use project::{FakeFs, Project};
// use unindent::Unindent;
// #[gpui::test]
// async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
// use git::diff::DiffHunkStatus;
// init_test(cx, |_| {});
// let fs = FakeFs::new(cx.background());
// let project = Project::test(fs, [], cx).await;
// // buffer has two modified hunks with two rows each
// let buffer_1 = project
// .update(cx, |project, cx| {
// project.create_buffer(
// "
// 1.zero
// 1.ONE
// 1.TWO
// 1.three
// 1.FOUR
// 1.FIVE
// 1.six
// "
// .unindent()
// .as_str(),
// None,
// cx,
// )
// })
// .unwrap();
// buffer_1.update(cx, |buffer, cx| {
// buffer.set_diff_base(
// Some(
// "
// 1.zero
// 1.one
// 1.two
// 1.three
// 1.four
// 1.five
// 1.six
// "
// .unindent(),
// ),
// cx,
// );
// });
// // buffer has a deletion hunk and an insertion hunk
// let buffer_2 = project
// .update(cx, |project, cx| {
// project.create_buffer(
// "
// 2.zero
// 2.one
// 2.two
// 2.three
// 2.four
// 2.five
// 2.six
// "
// .unindent()
// .as_str(),
// None,
// cx,
// )
// })
// .unwrap();
// buffer_2.update(cx, |buffer, cx| {
// buffer.set_diff_base(
// Some(
// "
// 2.zero
// 2.one
// 2.one-and-a-half
// 2.two
// 2.three
// 2.four
// 2.six
// "
// .unindent(),
// ),
// cx,
// );
// });
// cx.foreground().run_until_parked();
// let multibuffer = cx.add_model(|cx| {
// let mut multibuffer = MultiBuffer::new(0);
// multibuffer.push_excerpts(
// buffer_1.clone(),
// [
// // excerpt ends in the middle of a modified hunk
// ExcerptRange {
// context: Point::new(0, 0)..Point::new(1, 5),
// primary: Default::default(),
// },
// // excerpt begins in the middle of a modified hunk
// ExcerptRange {
// context: Point::new(5, 0)..Point::new(6, 5),
// primary: Default::default(),
// },
// ],
// cx,
// );
// multibuffer.push_excerpts(
// buffer_2.clone(),
// [
// // excerpt ends at a deletion
// ExcerptRange {
// context: Point::new(0, 0)..Point::new(1, 5),
// primary: Default::default(),
// },
// // excerpt starts at a deletion
// ExcerptRange {
// context: Point::new(2, 0)..Point::new(2, 5),
// primary: Default::default(),
// },
// // excerpt fully contains a deletion hunk
// ExcerptRange {
// context: Point::new(1, 0)..Point::new(2, 5),
// primary: Default::default(),
// },
// // excerpt fully contains an insertion hunk
// ExcerptRange {
// context: Point::new(4, 0)..Point::new(6, 5),
// primary: Default::default(),
// },
// ],
// cx,
// );
// multibuffer
// });
// let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
// assert_eq!(
// snapshot.text(),
// "
// 1.zero
// 1.ONE
// 1.FIVE
// 1.six
// 2.zero
// 2.one
// 2.two
// 2.one
// 2.two
// 2.four
// 2.five
// 2.six"
// .unindent()
// );
// let expected = [
// (DiffHunkStatus::Modified, 1..2),
// (DiffHunkStatus::Modified, 2..3),
// //TODO: Define better when and where removed hunks show up at range extremities
// (DiffHunkStatus::Removed, 6..6),
// (DiffHunkStatus::Removed, 8..8),
// (DiffHunkStatus::Added, 10..11),
// ];
// assert_eq!(
// snapshot
// .git_diff_hunks_in_range(0..12)
// .map(|hunk| (hunk.status(), hunk.buffer_range))
// .collect::<Vec<_>>(),
// &expected,
// );
// assert_eq!(
// snapshot
// .git_diff_hunks_in_range_rev(0..12)
// .map(|hunk| (hunk.status(), hunk.buffer_range))
// .collect::<Vec<_>>(),
// expected
// .iter()
// .rev()
// .cloned()
// .collect::<Vec<_>>()
// .as_slice(),
// );
// }
// }

View file

@ -0,0 +1,138 @@
use gpui::ViewContext;
use crate::{Editor, RangeToAnchorExt};
enum MatchingBracketHighlight {}
pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
// editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
let newest_selection = editor.selections.newest::<usize>(cx);
// Don't highlight brackets if the selection isn't empty
if !newest_selection.is_empty() {
return;
}
let head = newest_selection.head();
let snapshot = editor.snapshot(cx);
if let Some((opening_range, closing_range)) = snapshot
.buffer_snapshot
.innermost_enclosing_bracket_ranges(head..head)
{
editor.highlight_background::<MatchingBracketHighlight>(
vec![
opening_range.to_anchors(&snapshot.buffer_snapshot),
closing_range.to_anchors(&snapshot.buffer_snapshot),
],
|theme| todo!("theme.editor.document_highlight_read_background"),
cx,
)
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
// use indoc::indoc;
// use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
// #[gpui::test]
// async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
// init_test(cx, |_| {});
// let mut cx = EditorLspTestContext::new(
// Language::new(
// LanguageConfig {
// name: "Rust".into(),
// path_suffixes: vec!["rs".to_string()],
// brackets: BracketPairConfig {
// pairs: vec![
// BracketPair {
// start: "{".to_string(),
// end: "}".to_string(),
// close: false,
// newline: true,
// },
// BracketPair {
// start: "(".to_string(),
// end: ")".to_string(),
// close: false,
// newline: true,
// },
// ],
// ..Default::default()
// },
// ..Default::default()
// },
// Some(tree_sitter_rust::language()),
// )
// .with_brackets_query(indoc! {r#"
// ("{" @open "}" @close)
// ("(" @open ")" @close)
// "#})
// .unwrap(),
// Default::default(),
// cx,
// )
// .await;
// // positioning cursor inside bracket highlights both
// cx.set_state(indoc! {r#"
// pub fn test("Test ˇargument") {
// another_test(1, 2, 3);
// }
// "#});
// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
// pub fn test«(»"Test argument"«)» {
// another_test(1, 2, 3);
// }
// "#});
// cx.set_state(indoc! {r#"
// pub fn test("Test argument") {
// another_test(1, ˇ2, 3);
// }
// "#});
// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
// pub fn test("Test argument") {
// another_test«(»1, 2, 3«)»;
// }
// "#});
// cx.set_state(indoc! {r#"
// pub fn test("Test argument") {
// anotherˇ_test(1, 2, 3);
// }
// "#});
// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
// pub fn test("Test argument") «{»
// another_test(1, 2, 3);
// «}»
// "#});
// // positioning outside of brackets removes highlight
// cx.set_state(indoc! {r#"
// pub fˇn test("Test argument") {
// another_test(1, 2, 3);
// }
// "#});
// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
// pub fn test("Test argument") {
// another_test(1, 2, 3);
// }
// "#});
// // non empty selection dismisses highlight
// cx.set_state(indoc! {r#"
// pub fn test("Te«st argˇ»ument") {
// another_test(1, 2, 3);
// }
// "#});
// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
// pub fn test("Test argument") {
// another_test(1, 2, 3);
// }
// "#});
// }
// }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

1339
crates/editor2/src/items.rs Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,94 @@
use crate::{DisplayPoint, Editor, EditorMode, SelectMode};
use gpui::{Pixels, Point, ViewContext};
pub fn deploy_context_menu(
editor: &mut Editor,
position: Point<Pixels>,
point: DisplayPoint,
cx: &mut ViewContext<Editor>,
) {
todo!();
// if !editor.focused {
// cx.focus_self();
// }
// // Don't show context menu for inline editors
// if editor.mode() != EditorMode::Full {
// return;
// }
// // Don't show the context menu if there isn't a project associated with this editor
// if editor.project.is_none() {
// return;
// }
// // Move the cursor to the clicked location so that dispatched actions make sense
// editor.change_selections(None, cx, |s| {
// s.clear_disjoint();
// s.set_pending_display_range(point..point, SelectMode::Character);
// });
// editor.mouse_context_menu.update(cx, |menu, cx| {
// menu.show(
// position,
// AnchorCorner::TopLeft,
// vec![
// ContextMenuItem::action("Rename Symbol", Rename),
// ContextMenuItem::action("Go to Definition", GoToDefinition),
// ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
// ContextMenuItem::action("Find All References", FindAllReferences),
// ContextMenuItem::action(
// "Code Actions",
// ToggleCodeActions {
// deployed_from_indicator: false,
// },
// ),
// ContextMenuItem::Separator,
// ContextMenuItem::action("Reveal in Finder", RevealInFinder),
// ],
// cx,
// );
// });
// cx.notify();
}
// #[cfg(test)]
// mod tests {
// use super::*;
// use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
// use indoc::indoc;
// #[gpui::test]
// async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
// init_test(cx, |_| {});
// let mut cx = EditorLspTestContext::new_rust(
// lsp::ServerCapabilities {
// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
// ..Default::default()
// },
// cx,
// )
// .await;
// cx.set_state(indoc! {"
// fn teˇst() {
// do_work();
// }
// "});
// let point = cx.display_point(indoc! {"
// fn test() {
// do_wˇork();
// }
// "});
// cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
// cx.assert_editor_state(indoc! {"
// fn test() {
// do_wˇork();
// }
// "});
// cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible()));
// }
// }

View file

@ -0,0 +1,933 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
use gpui::{px, TextSystem};
use language::Point;
use serde::de::IntoDeserializer;
use std::ops::Range;
#[derive(Debug, PartialEq)]
pub enum FindRange {
SingleLine,
MultiLine,
}
/// TextLayoutDetails encompasses everything we need to move vertically
/// taking into account variable width characters.
pub struct TextLayoutDetails {
pub text_system: TextSystem,
pub editor_style: EditorStyle,
}
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
if point.column() > 0 {
*point.column_mut() -= 1;
} else if point.row() > 0 {
*point.row_mut() -= 1;
*point.column_mut() = map.line_len(point.row());
}
map.clip_point(point, Bias::Left)
}
pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
if point.column() > 0 {
*point.column_mut() -= 1;
}
map.clip_point(point, Bias::Left)
}
pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
let max_column = map.line_len(point.row());
if point.column() < max_column {
*point.column_mut() += 1;
} else if point.row() < map.max_point().row() {
*point.row_mut() += 1;
*point.column_mut() = 0;
}
map.clip_point(point, Bias::Right)
}
pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
*point.column_mut() += 1;
map.clip_point(point, Bias::Right)
}
pub fn up(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_start: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
up_by_rows(
map,
start,
1,
goal,
preserve_column_at_start,
text_layout_details,
)
}
pub fn down(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_end: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
down_by_rows(
map,
start,
1,
goal,
preserve_column_at_end,
text_layout_details,
)
}
pub fn up_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_start: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.")
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
_ => map.x_for_point(start, text_layout_details),
};
let prev_row = start.row().saturating_sub(row_count);
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_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_start {
return (start, goal);
} else {
point = DisplayPoint::new(0, 0);
goal_x = px(0.);
}
let mut clipped_point = map.clip_point(point, Bias::Left);
if clipped_point.row() < point.row() {
clipped_point = map.clip_point(point, Bias::Right);
}
(
clipped_point,
SelectionGoal::HorizontalPosition(goal_x.into()),
)
}
pub fn down_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_end: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x.into(),
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
_ => map.x_for_point(start, text_layout_details),
};
let new_row = start.row() + row_count;
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
if point.row() > start.row() {
*point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_end {
return (start, goal);
} else {
point = map.max_point();
goal_x = map.x_for_point(point, text_layout_details)
}
let mut clipped_point = map.clip_point(point, Bias::Right);
if clipped_point.row() > point.row() {
clipped_point = map.clip_point(point, Bias::Left);
}
(
clipped_point,
SelectionGoal::HorizontalPosition(goal_x.into()),
)
}
pub fn line_beginning(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let point = display_point.to_point(map);
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
let line_start = map.prev_line_boundary(point).1;
if stop_at_soft_boundaries && display_point != soft_line_start {
soft_line_start
} else {
line_start
}
}
pub fn indented_line_beginning(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let point = display_point.to_point(map);
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
let indent_start = Point::new(
point.row,
map.buffer_snapshot.indent_size_for_line(point.row).len,
)
.to_display_point(map);
let line_start = map.prev_line_boundary(point).1;
if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
{
soft_line_start
} else if stop_at_soft_boundaries && display_point != indent_start {
indent_start
} else {
line_start
}
}
pub fn line_end(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let soft_line_end = map.clip_point(
DisplayPoint::new(display_point.row(), map.line_len(display_point.row())),
Bias::Left,
);
if stop_at_soft_boundaries && display_point != soft_line_end {
soft_line_end
} else {
map.next_line_boundary(display_point.to_point(map)).1
}
}
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
(char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
|| left == '\n'
})
}
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
let is_word_start =
char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
let is_subword_start =
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start || left == '\n'
})
}
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_boundary(map, point, FindRange::MultiLine, |left, right| {
(char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
|| right == '\n'
})
}
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_boundary(map, point, FindRange::MultiLine, |left, right| {
let is_word_end =
(char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
let is_subword_end =
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end || right == '\n'
})
}
pub fn start_of_paragraph(
map: &DisplaySnapshot,
display_point: DisplayPoint,
mut count: usize,
) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == 0 {
return DisplayPoint::zero();
}
let mut found_non_blank_line = false;
for row in (0..point.row + 1).rev() {
let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank {
if count <= 1 {
return Point::new(row, 0).to_display_point(map);
}
count -= 1;
found_non_blank_line = false;
}
found_non_blank_line |= !blank;
}
DisplayPoint::zero()
}
pub fn end_of_paragraph(
map: &DisplaySnapshot,
display_point: DisplayPoint,
mut count: usize,
) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == map.max_buffer_row() {
return map.max_point();
}
let mut found_non_blank_line = false;
for row in point.row..map.max_buffer_row() + 1 {
let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank {
if count <= 1 {
return Point::new(row, 0).to_display_point(map);
}
count -= 1;
found_non_blank_line = false;
}
found_non_blank_line |= !blank;
}
map.max_point()
}
/// Scans for a boundary preceding the given start point `from` until a boundary is found,
/// indicated by the given predicate returning true.
/// The predicate is called with the character to the left and right of the candidate boundary location.
/// If FindRange::SingleLine is specified and no boundary is found before the start of the current line, the start of the current line will be returned.
pub fn find_preceding_boundary(
map: &DisplaySnapshot,
from: DisplayPoint,
find_range: FindRange,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut prev_ch = None;
let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot);
for ch in map.buffer_snapshot.reversed_chars_at(offset) {
if find_range == FindRange::SingleLine && ch == '\n' {
break;
}
if let Some(prev_ch) = prev_ch {
if is_boundary(ch, prev_ch) {
break;
}
}
offset -= ch.len_utf8();
prev_ch = Some(ch);
}
map.clip_point(offset.to_display_point(map), Bias::Left)
}
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
/// given predicate returning true. The predicate is called with the character to the left and right
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
/// or end of a line.
pub fn find_boundary(
map: &DisplaySnapshot,
from: DisplayPoint,
find_range: FindRange,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut offset = from.to_offset(&map, Bias::Right);
let mut prev_ch = None;
for ch in map.buffer_snapshot.chars_at(offset) {
if find_range == FindRange::SingleLine && ch == '\n' {
break;
}
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
break;
}
}
offset += ch.len_utf8();
prev_ch = Some(ch);
}
map.clip_point(offset.to_display_point(map), Bias::Right)
}
pub fn chars_after(
map: &DisplaySnapshot,
mut offset: usize,
) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
map.buffer_snapshot.chars_at(offset).map(move |ch| {
let before = offset;
offset = offset + ch.len_utf8();
(ch, before..offset)
})
}
pub fn chars_before(
map: &DisplaySnapshot,
mut offset: usize,
) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
map.buffer_snapshot
.reversed_chars_at(offset)
.map(move |ch| {
let after = offset;
offset = offset - ch.len_utf8();
(ch, offset..after)
})
}
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
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(|c| char_kind(&scope, c));
let prev_char_kind = text
.reversed_chars_at(ix)
.next()
.map(|c| char_kind(&scope, c));
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
}
pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
let position = map
.clip_point(position, Bias::Left)
.to_offset(map, Bias::Left);
let (range, _) = map.buffer_snapshot.surrounding_word(position);
let start = range
.start
.to_point(&map.buffer_snapshot)
.to_display_point(map);
let end = range
.end
.to_point(&map.buffer_snapshot)
.to_display_point(map);
start..end
}
pub fn split_display_range_by_lines(
map: &DisplaySnapshot,
range: Range<DisplayPoint>,
) -> Vec<Range<DisplayPoint>> {
let mut result = Vec::new();
let mut start = range.start;
// Loop over all the covered rows until the one containing the range end
for row in range.start.row()..range.end.row() {
let row_end_column = map.line_len(row);
let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left);
if start != end {
result.push(start..end);
}
start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left);
}
// Add the final range from the start of the last end to the original range end.
result.push(start..range.end);
result
}
// #[cfg(test)]
// mod tests {
// use super::*;
// use crate::{
// display_map::Inlay,
// test::{},
// Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
// };
// use project::Project;
// use settings::SettingsStore;
// use util::post_inc;
// #[gpui::test]
// fn test_previous_word_start(cx: &mut gpui::AppContext) {
// init_test(cx);
// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
// assert_eq!(
// previous_word_start(&snapshot, display_points[1]),
// display_points[0]
// );
// }
// assert("\nˇ ˇlorem", cx);
// assert("ˇ\nˇ lorem", cx);
// assert(" ˇloremˇ", cx);
// assert("ˇ ˇlorem", cx);
// assert(" ˇlorˇem", cx);
// assert("\nlorem\nˇ ˇipsum", cx);
// assert("\n\nˇ\nˇ", cx);
// assert(" ˇlorem ˇipsum", cx);
// assert("loremˇ-ˇipsum", cx);
// assert("loremˇ-#$@ˇipsum", cx);
// assert("ˇlorem_ˇipsum", cx);
// assert(" ˇdefγˇ", cx);
// assert(" ˇbcΔˇ", cx);
// assert(" abˇ——ˇcd", cx);
// }
// #[gpui::test]
// fn test_previous_subword_start(cx: &mut gpui::AppContext) {
// init_test(cx);
// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
// assert_eq!(
// previous_subword_start(&snapshot, display_points[1]),
// display_points[0]
// );
// }
// // Subword boundaries are respected
// assert("lorem_ˇipˇsum", cx);
// assert("lorem_ˇipsumˇ", cx);
// assert("ˇlorem_ˇipsum", cx);
// assert("lorem_ˇipsum_ˇdolor", cx);
// assert("loremˇIpˇsum", cx);
// assert("loremˇIpsumˇ", cx);
// // Word boundaries are still respected
// assert("\nˇ ˇlorem", cx);
// assert(" ˇloremˇ", cx);
// assert(" ˇlorˇem", cx);
// assert("\nlorem\nˇ ˇipsum", cx);
// assert("\n\nˇ\nˇ", cx);
// assert(" ˇlorem ˇipsum", cx);
// assert("loremˇ-ˇipsum", cx);
// assert("loremˇ-#$@ˇipsum", cx);
// assert(" ˇdefγˇ", cx);
// assert(" bcˇΔˇ", cx);
// assert(" ˇbcδˇ", cx);
// assert(" abˇ——ˇcd", cx);
// }
// #[gpui::test]
// fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
// init_test(cx);
// fn assert(
// marked_text: &str,
// cx: &mut gpui::AppContext,
// is_boundary: impl FnMut(char, char) -> bool,
// ) {
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
// assert_eq!(
// find_preceding_boundary(
// &snapshot,
// display_points[1],
// FindRange::MultiLine,
// is_boundary
// ),
// display_points[0]
// );
// }
// assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
// left == 'c' && right == 'd'
// });
// assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
// left == '\n' && right == 'g'
// });
// let mut line_count = 0;
// assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
// if left == '\n' {
// line_count += 1;
// line_count == 2
// } else {
// false
// }
// });
// }
// #[gpui::test]
// fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
// init_test(cx);
// let input_text = "abcdefghijklmnopqrstuvwxys";
// let family_id = cx
// .font_cache()
// .load_family(&["Helvetica"], &Default::default())
// .unwrap();
// let font_id = cx
// .font_cache()
// .select_font(family_id, &Default::default())
// .unwrap();
// let font_size = 14.0;
// let buffer = MultiBuffer::build_simple(input_text, cx);
// let buffer_snapshot = buffer.read(cx).snapshot(cx);
// let display_map =
// cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
// // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
// let mut id = 0;
// let inlays = (0..buffer_snapshot.len())
// .map(|offset| {
// [
// Inlay {
// id: InlayId::Suggestion(post_inc(&mut id)),
// position: buffer_snapshot.anchor_at(offset, Bias::Left),
// text: format!("test").into(),
// },
// Inlay {
// id: InlayId::Suggestion(post_inc(&mut id)),
// position: buffer_snapshot.anchor_at(offset, Bias::Right),
// text: format!("test").into(),
// },
// Inlay {
// id: InlayId::Hint(post_inc(&mut id)),
// position: buffer_snapshot.anchor_at(offset, Bias::Left),
// text: format!("test").into(),
// },
// Inlay {
// id: InlayId::Hint(post_inc(&mut id)),
// position: buffer_snapshot.anchor_at(offset, Bias::Right),
// text: format!("test").into(),
// },
// ]
// })
// .flatten()
// .collect();
// let snapshot = display_map.update(cx, |map, cx| {
// map.splice_inlays(Vec::new(), inlays, cx);
// map.snapshot(cx)
// });
// assert_eq!(
// find_preceding_boundary(
// &snapshot,
// buffer_snapshot.len().to_display_point(&snapshot),
// FindRange::MultiLine,
// |left, _| left == 'e',
// ),
// snapshot
// .buffer_snapshot
// .offset_to_point(5)
// .to_display_point(&snapshot),
// "Should not stop at inlays when looking for boundaries"
// );
// }
// #[gpui::test]
// fn test_next_word_end(cx: &mut gpui::AppContext) {
// init_test(cx);
// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
// assert_eq!(
// next_word_end(&snapshot, display_points[0]),
// display_points[1]
// );
// }
// assert("\nˇ loremˇ", cx);
// assert(" ˇloremˇ", cx);
// assert(" lorˇemˇ", cx);
// assert(" loremˇ ˇ\nipsum\n", cx);
// assert("\nˇ\nˇ\n\n", cx);
// assert("loremˇ ipsumˇ ", cx);
// assert("loremˇ-ˇipsum", cx);
// assert("loremˇ#$@-ˇipsum", cx);
// assert("loremˇ_ipsumˇ", cx);
// assert(" ˇbcΔˇ", cx);
// assert(" abˇ——ˇcd", cx);
// }
// #[gpui::test]
// fn test_next_subword_end(cx: &mut gpui::AppContext) {
// init_test(cx);
// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
// assert_eq!(
// next_subword_end(&snapshot, display_points[0]),
// display_points[1]
// );
// }
// // Subword boundaries are respected
// assert("loˇremˇ_ipsum", cx);
// assert("ˇloremˇ_ipsum", cx);
// assert("loremˇ_ipsumˇ", cx);
// assert("loremˇ_ipsumˇ_dolor", cx);
// assert("loˇremˇIpsum", cx);
// assert("loremˇIpsumˇDolor", cx);
// // Word boundaries are still respected
// assert("\nˇ loremˇ", cx);
// assert(" ˇloremˇ", cx);
// assert(" lorˇemˇ", cx);
// assert(" loremˇ ˇ\nipsum\n", cx);
// assert("\nˇ\nˇ\n\n", cx);
// assert("loremˇ ipsumˇ ", cx);
// assert("loremˇ-ˇipsum", cx);
// assert("loremˇ#$@-ˇipsum", cx);
// assert("loremˇ_ipsumˇ", cx);
// assert(" ˇbcˇΔ", cx);
// assert(" abˇ——ˇcd", cx);
// }
// #[gpui::test]
// fn test_find_boundary(cx: &mut gpui::AppContext) {
// init_test(cx);
// fn assert(
// marked_text: &str,
// cx: &mut gpui::AppContext,
// is_boundary: impl FnMut(char, char) -> bool,
// ) {
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
// assert_eq!(
// find_boundary(
// &snapshot,
// display_points[0],
// FindRange::MultiLine,
// is_boundary
// ),
// display_points[1]
// );
// }
// assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
// left == 'j' && right == 'k'
// });
// assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
// left == '\n' && right == 'i'
// });
// let mut line_count = 0;
// assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
// if left == '\n' {
// line_count += 1;
// line_count == 2
// } else {
// false
// }
// });
// }
// #[gpui::test]
// fn test_surrounding_word(cx: &mut gpui::AppContext) {
// init_test(cx);
// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
// assert_eq!(
// surrounding_word(&snapshot, display_points[1]),
// display_points[0]..display_points[2],
// "{}",
// marked_text.to_string()
// );
// }
// assert("ˇˇloremˇ ipsum", cx);
// assert("ˇloˇremˇ ipsum", cx);
// assert("ˇloremˇˇ ipsum", cx);
// assert("loremˇ ˇ ˇipsum", cx);
// assert("lorem\nˇˇˇ\nipsum", cx);
// assert("lorem\nˇˇipsumˇ", cx);
// assert("loremˇ,ˇˇ ipsum", cx);
// assert("ˇloremˇˇ, ipsum", cx);
// }
// #[gpui::test]
// async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
// cx.update(|cx| {
// init_test(cx);
// });
// let mut cx = EditorTestContext::new(cx).await;
// let editor = cx.editor.clone();
// let window = cx.window.clone();
// cx.update_window(window, |cx| {
// let text_layout_details =
// editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
// let family_id = cx
// .font_cache()
// .load_family(&["Helvetica"], &Default::default())
// .unwrap();
// let font_id = cx
// .font_cache()
// .select_font(family_id, &Default::default())
// .unwrap();
// let buffer =
// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
// let multibuffer = cx.add_model(|cx| {
// let mut multibuffer = MultiBuffer::new(0);
// multibuffer.push_excerpts(
// buffer.clone(),
// [
// ExcerptRange {
// context: Point::new(0, 0)..Point::new(1, 4),
// primary: None,
// },
// ExcerptRange {
// context: Point::new(2, 0)..Point::new(3, 2),
// primary: None,
// },
// ],
// cx,
// );
// multibuffer
// });
// let display_map =
// cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
// let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
// assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
// let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
// // Can't move up into the first excerpt's header
// assert_eq!(
// up(
// &snapshot,
// DisplayPoint::new(2, 2),
// SelectionGoal::HorizontalPosition(col_2_x),
// false,
// &text_layout_details
// ),
// (
// DisplayPoint::new(2, 0),
// SelectionGoal::HorizontalPosition(0.0)
// ),
// );
// assert_eq!(
// up(
// &snapshot,
// DisplayPoint::new(2, 0),
// SelectionGoal::None,
// false,
// &text_layout_details
// ),
// (
// DisplayPoint::new(2, 0),
// SelectionGoal::HorizontalPosition(0.0)
// ),
// );
// let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details);
// // Move up and down within first excerpt
// assert_eq!(
// up(
// &snapshot,
// DisplayPoint::new(3, 4),
// SelectionGoal::HorizontalPosition(col_4_x),
// false,
// &text_layout_details
// ),
// (
// DisplayPoint::new(2, 3),
// SelectionGoal::HorizontalPosition(col_4_x)
// ),
// );
// assert_eq!(
// down(
// &snapshot,
// DisplayPoint::new(2, 3),
// SelectionGoal::HorizontalPosition(col_4_x),
// false,
// &text_layout_details
// ),
// (
// DisplayPoint::new(3, 4),
// SelectionGoal::HorizontalPosition(col_4_x)
// ),
// );
// let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details);
// // Move up and down across second excerpt's header
// assert_eq!(
// up(
// &snapshot,
// DisplayPoint::new(6, 5),
// SelectionGoal::HorizontalPosition(col_5_x),
// false,
// &text_layout_details
// ),
// (
// DisplayPoint::new(3, 4),
// SelectionGoal::HorizontalPosition(col_5_x)
// ),
// );
// assert_eq!(
// down(
// &snapshot,
// DisplayPoint::new(3, 4),
// SelectionGoal::HorizontalPosition(col_5_x),
// false,
// &text_layout_details
// ),
// (
// DisplayPoint::new(6, 5),
// SelectionGoal::HorizontalPosition(col_5_x)
// ),
// );
// let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details);
// // Can't move down off the end
// assert_eq!(
// down(
// &snapshot,
// DisplayPoint::new(7, 0),
// SelectionGoal::HorizontalPosition(0.0),
// false,
// &text_layout_details
// ),
// (
// DisplayPoint::new(7, 2),
// SelectionGoal::HorizontalPosition(max_point_x)
// ),
// );
// assert_eq!(
// down(
// &snapshot,
// DisplayPoint::new(7, 2),
// SelectionGoal::HorizontalPosition(max_point_x),
// false,
// &text_layout_details
// ),
// (
// DisplayPoint::new(7, 2),
// SelectionGoal::HorizontalPosition(max_point_x)
// ),
// );
// });
// }
// fn init_test(cx: &mut gpui::AppContext) {
// cx.set_global(SettingsStore::test(cx));
// theme::init(cx);
// language::init(cx);
// crate::init(cx);
// Project::init_settings(cx);
// }
// }

View file

@ -0,0 +1,83 @@
use std::path::PathBuf;
use db::sqlez_macros::sql;
use db::{define_connection, query};
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
define_connection!(
// Current schema shape using pseudo-rust syntax:
// editors(
// item_id: usize,
// workspace_id: usize,
// path: PathBuf,
// scroll_top_row: usize,
// scroll_vertical_offset: f32,
// scroll_horizontal_offset: f32,
// )
pub static ref DB: EditorDb<WorkspaceDb> =
&[sql! (
CREATE TABLE editors(
item_id INTEGER NOT NULL,
workspace_id INTEGER NOT NULL,
path BLOB NOT NULL,
PRIMARY KEY(item_id, workspace_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) STRICT;
),
sql! (
ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
)];
);
impl EditorDb {
query! {
pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
SELECT path FROM editors
WHERE item_id = ? AND workspace_id = ?
}
}
query! {
pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> {
INSERT INTO editors
(item_id, workspace_id, path)
VALUES
(?1, ?2, ?3)
ON CONFLICT DO UPDATE SET
item_id = ?1,
workspace_id = ?2,
path = ?3
}
}
// Returns the scroll top row, and offset
query! {
pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
FROM editors
WHERE item_id = ? AND workspace_id = ?
}
}
query! {
pub async fn save_scroll_position(
item_id: ItemId,
workspace_id: WorkspaceId,
top_row: u32,
vertical_offset: f32,
horizontal_offset: f32
) -> Result<()> {
UPDATE OR IGNORE editors
SET
scroll_top_row = ?3,
scroll_horizontal_offset = ?4,
scroll_vertical_offset = ?5
WHERE item_id = ?1 AND workspace_id = ?2
}
}
}

View file

@ -0,0 +1,448 @@
pub mod actions;
pub mod autoscroll;
pub mod scroll_amount;
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover,
persistence::DB,
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
ToPoint,
};
use gpui::{point, px, AppContext, Entity, Pixels, Styled, Task, ViewContext};
use language::{Bias, Point};
use std::{
cmp::Ordering,
time::{Duration, Instant},
};
use util::ResultExt;
use workspace::{ItemId, WorkspaceId};
use self::{
autoscroll::{Autoscroll, AutoscrollStrategy},
scroll_amount::ScrollAmount,
};
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
pub const VERTICAL_SCROLL_MARGIN: f32 = 3.;
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Default)]
pub struct ScrollbarAutoHide(pub bool);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollAnchor {
pub offset: gpui::Point<f32>,
pub anchor: Anchor,
}
impl ScrollAnchor {
fn new() -> Self {
Self {
offset: gpui::Point::default(),
anchor: Anchor::min(),
}
}
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<f32> {
let mut scroll_position = self.offset;
if self.anchor != Anchor::min() {
let scroll_top = self.anchor.to_display_point(snapshot).row() as f32;
scroll_position.y = scroll_top + scroll_position.y;
} else {
scroll_position.y = 0.;
}
scroll_position
}
pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
self.anchor.to_point(buffer).row
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Axis {
Vertical,
Horizontal,
}
#[derive(Clone, Copy, Debug)]
pub struct OngoingScroll {
last_event: Instant,
axis: Option<Axis>,
}
impl OngoingScroll {
fn new() -> Self {
Self {
last_event: Instant::now() - SCROLL_EVENT_SEPARATION,
axis: None,
}
}
pub fn filter(&self, delta: &mut gpui::Point<Pixels>) -> Option<Axis> {
const UNLOCK_PERCENT: f32 = 1.9;
const UNLOCK_LOWER_BOUND: Pixels = px(6.);
let mut axis = self.axis;
let x = delta.x.abs();
let y = delta.y.abs();
let duration = Instant::now().duration_since(self.last_event);
if duration > SCROLL_EVENT_SEPARATION {
//New ongoing scroll will start, determine axis
axis = if x <= y {
Some(Axis::Vertical)
} else {
Some(Axis::Horizontal)
};
} else if x.max(y) >= UNLOCK_LOWER_BOUND {
//Check if the current ongoing will need to unlock
match axis {
Some(Axis::Vertical) => {
if x > y && x >= y * UNLOCK_PERCENT {
axis = None;
}
}
Some(Axis::Horizontal) => {
if y > x && y >= x * UNLOCK_PERCENT {
axis = None;
}
}
None => {}
}
}
match axis {
Some(Axis::Vertical) => {
*delta = point(px(0.), delta.y);
}
Some(Axis::Horizontal) => {
*delta = point(delta.x, px(0.));
}
None => {}
}
axis
}
}
pub struct ScrollManager {
vertical_scroll_margin: f32,
anchor: ScrollAnchor,
ongoing: OngoingScroll,
autoscroll_request: Option<(Autoscroll, bool)>,
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>,
visible_line_count: Option<f32>,
}
impl ScrollManager {
pub fn new() -> Self {
ScrollManager {
vertical_scroll_margin: VERTICAL_SCROLL_MARGIN,
anchor: ScrollAnchor::new(),
ongoing: OngoingScroll::new(),
autoscroll_request: None,
show_scrollbars: true,
hide_scrollbar_task: None,
last_autoscroll: None,
visible_line_count: None,
}
}
pub fn clone_state(&mut self, other: &Self) {
self.anchor = other.anchor;
self.ongoing = other.ongoing;
}
pub fn anchor(&self) -> ScrollAnchor {
self.anchor
}
pub fn ongoing_scroll(&self) -> OngoingScroll {
self.ongoing
}
pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) {
self.ongoing.last_event = Instant::now();
self.ongoing.axis = axis;
}
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<f32> {
self.anchor.scroll_position(snapshot)
}
fn set_scroll_position(
&mut self,
scroll_position: gpui::Point<f32>,
map: &DisplaySnapshot,
local: bool,
autoscroll: bool,
workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>,
) {
let (new_anchor, top_row) = if scroll_position.y <= 0. {
(
ScrollAnchor {
anchor: Anchor::min(),
offset: scroll_position.max(&gpui::Point::default()),
},
0,
)
} else {
let scroll_top_buffer_point =
DisplayPoint::new(scroll_position.y as u32, 0).to_point(&map);
let top_anchor = map
.buffer_snapshot
.anchor_at(scroll_top_buffer_point, Bias::Right);
(
ScrollAnchor {
anchor: top_anchor,
offset: point(
scroll_position.x,
scroll_position.y - top_anchor.to_display_point(&map).row() as f32,
),
},
scroll_top_buffer_point.row,
)
};
self.set_anchor(new_anchor, top_row, local, autoscroll, workspace_id, cx);
}
fn set_anchor(
&mut self,
anchor: ScrollAnchor,
top_row: u32,
local: bool,
autoscroll: bool,
workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>,
) {
self.anchor = anchor;
cx.emit(Event::ScrollPositionChanged { local, autoscroll });
self.show_scrollbar(cx);
self.autoscroll_request.take();
if let Some(workspace_id) = workspace_id {
let item_id = cx.view().entity_id().as_u64() as ItemId;
cx.foreground_executor()
.spawn(async move {
DB.save_scroll_position(
item_id,
workspace_id,
top_row,
anchor.offset.x,
anchor.offset.y,
)
.await
.log_err()
})
.detach()
}
cx.notify();
}
pub fn show_scrollbar(&mut self, cx: &mut ViewContext<Editor>) {
if !self.show_scrollbars {
self.show_scrollbars = true;
cx.notify();
}
if cx.default_global::<ScrollbarAutoHide>().0 {
self.hide_scrollbar_task = Some(cx.spawn(|editor, mut cx| async move {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
editor
.update(&mut cx, |editor, cx| {
editor.scroll_manager.show_scrollbars = false;
cx.notify();
})
.log_err();
}));
} else {
self.hide_scrollbar_task = None;
}
}
pub fn scrollbars_visible(&self) -> bool {
self.show_scrollbars
}
pub fn has_autoscroll_request(&self) -> bool {
self.autoscroll_request.is_some()
}
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
if max < self.anchor.offset.x {
self.anchor.offset.x = max;
true
} else {
false
}
}
}
// todo!()
impl Editor {
// pub fn vertical_scroll_margin(&mut self) -> usize {
// self.scroll_manager.vertical_scroll_margin as usize
// }
// pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext<Self>) {
// self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
// cx.notify();
// }
pub fn visible_line_count(&self) -> Option<f32> {
self.scroll_manager.visible_line_count
}
// pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
// let opened_first_time = self.scroll_manager.visible_line_count.is_none();
// self.scroll_manager.visible_line_count = Some(lines);
// if opened_first_time {
// cx.spawn(|editor, mut cx| async move {
// editor
// .update(&mut cx, |editor, cx| {
// editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
// })
// .ok()
// })
// .detach()
// }
// }
pub fn set_scroll_position(
&mut self,
scroll_position: gpui::Point<f32>,
cx: &mut ViewContext<Self>,
) {
self.set_scroll_position_internal(scroll_position, true, false, cx);
}
pub(crate) fn set_scroll_position_internal(
&mut self,
scroll_position: gpui::Point<f32>,
local: bool,
autoscroll: bool,
cx: &mut ViewContext<Self>,
) {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
self.scroll_manager.set_scroll_position(
scroll_position,
&map,
local,
autoscroll,
workspace_id,
cx,
);
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
}
// pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> gpui::Point<Pixels> {
// let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
// self.scroll_manager.anchor.scroll_position(&display_map)
// }
pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor
.anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, true, false, workspace_id, cx);
}
pub(crate) fn set_scroll_anchor_remote(
&mut self,
scroll_anchor: ScrollAnchor,
cx: &mut ViewContext<Self>,
) {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor
.anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
}
// pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
// if matches!(self.mode, EditorMode::SingleLine) {
// cx.propagate_action();
// return;
// }
// if self.take_rename(true, cx).is_some() {
// return;
// }
// let cur_position = self.scroll_position(cx);
// let new_pos = cur_position + point(0., amount.lines(self));
// self.set_scroll_position(new_pos, cx);
// }
// /// Returns an ordering. The newest selection is:
// /// Ordering::Equal => on screen
// /// Ordering::Less => above the screen
// /// Ordering::Greater => below the screen
// pub fn newest_selection_on_screen(&self, cx: &mut AppContext) -> Ordering {
// let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
// let newest_head = self
// .selections
// .newest_anchor()
// .head()
// .to_display_point(&snapshot);
// let screen_top = self
// .scroll_manager
// .anchor
// .anchor
// .to_display_point(&snapshot);
// if screen_top > newest_head {
// return Ordering::Less;
// }
// if let Some(visible_lines) = self.visible_line_count() {
// if newest_head.row() < screen_top.row() + visible_lines as u32 {
// return Ordering::Equal;
// }
// }
// Ordering::Greater
// }
pub fn read_scroll_position_from_db(
&mut self,
item_id: usize,
workspace_id: WorkspaceId,
cx: &mut ViewContext<Editor>,
) {
let scroll_position = DB.get_scroll_position(item_id, workspace_id);
if let Ok(Some((top_row, x, y))) = scroll_position {
let top_anchor = self
.buffer()
.read(cx)
.snapshot(cx)
.anchor_at(Point::new(top_row as u32, 0), Bias::Left);
let scroll_anchor = ScrollAnchor {
offset: gpui::Point::new(x, y),
anchor: top_anchor,
};
self.set_scroll_anchor(scroll_anchor, cx);
}
}
}

View file

@ -0,0 +1,148 @@
use gpui::AppContext;
// actions!(
// editor,
// [
// LineDown,
// LineUp,
// HalfPageDown,
// HalfPageUp,
// PageDown,
// PageUp,
// NextScreen,
// ScrollCursorTop,
// ScrollCursorCenter,
// ScrollCursorBottom,
// ]
// );
pub fn init(cx: &mut AppContext) {
// todo!()
// cx.add_action(Editor::next_screen);
// cx.add_action(Editor::scroll_cursor_top);
// cx.add_action(Editor::scroll_cursor_center);
// cx.add_action(Editor::scroll_cursor_bottom);
// cx.add_action(|this: &mut Editor, _: &LineDown, cx| {
// this.scroll_screen(&ScrollAmount::Line(1.), cx)
// });
// cx.add_action(|this: &mut Editor, _: &LineUp, cx| {
// this.scroll_screen(&ScrollAmount::Line(-1.), cx)
// });
// cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| {
// this.scroll_screen(&ScrollAmount::Page(0.5), cx)
// });
// cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| {
// this.scroll_screen(&ScrollAmount::Page(-0.5), cx)
// });
// cx.add_action(|this: &mut Editor, _: &PageDown, cx| {
// this.scroll_screen(&ScrollAmount::Page(1.), cx)
// });
// cx.add_action(|this: &mut Editor, _: &PageUp, cx| {
// this.scroll_screen(&ScrollAmount::Page(-1.), cx)
// });
}
// impl Editor {
// pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) -> Option<()> {
// if self.take_rename(true, cx).is_some() {
// return None;
// }
// if self.mouse_context_menu.read(cx).visible() {
// return None;
// }
// if matches!(self.mode, EditorMode::SingleLine) {
// cx.propagate_action();
// return None;
// }
// self.request_autoscroll(Autoscroll::Next, cx);
// Some(())
// }
// pub fn scroll(
// &mut self,
// scroll_position: Vector2F,
// axis: Option<Axis>,
// cx: &mut ViewContext<Self>,
// ) {
// self.scroll_manager.update_ongoing_scroll(axis);
// self.set_scroll_position(scroll_position, cx);
// }
// fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {
// let snapshot = editor.snapshot(cx).display_snapshot;
// let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
// let mut new_screen_top = editor.selections.newest_display(cx).head();
// *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(scroll_margin_rows);
// *new_screen_top.column_mut() = 0;
// let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
// let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
// editor.set_scroll_anchor(
// ScrollAnchor {
// anchor: new_anchor,
// offset: Default::default(),
// },
// cx,
// )
// }
// fn scroll_cursor_center(
// editor: &mut Editor,
// _: &ScrollCursorCenter,
// cx: &mut ViewContext<Editor>,
// ) {
// let snapshot = editor.snapshot(cx).display_snapshot;
// let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
// visible_rows as u32
// } else {
// return;
// };
// let mut new_screen_top = editor.selections.newest_display(cx).head();
// *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(visible_rows / 2);
// *new_screen_top.column_mut() = 0;
// let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
// let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
// editor.set_scroll_anchor(
// ScrollAnchor {
// anchor: new_anchor,
// offset: Default::default(),
// },
// cx,
// )
// }
// fn scroll_cursor_bottom(
// editor: &mut Editor,
// _: &ScrollCursorBottom,
// cx: &mut ViewContext<Editor>,
// ) {
// let snapshot = editor.snapshot(cx).display_snapshot;
// let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
// let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
// visible_rows as u32
// } else {
// return;
// };
// let mut new_screen_top = editor.selections.newest_display(cx).head();
// *new_screen_top.row_mut() = new_screen_top
// .row()
// .saturating_sub(visible_rows.saturating_sub(scroll_margin_rows));
// *new_screen_top.column_mut() = 0;
// let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
// let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
// editor.set_scroll_anchor(
// ScrollAnchor {
// anchor: new_anchor,
// offset: Default::default(),
// },
// cx,
// )
// }
// }

View file

@ -0,0 +1,253 @@
use std::{cmp, f32};
use gpui::{px, Pixels, ViewContext};
use language::Point;
use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
#[derive(PartialEq, Eq)]
pub enum Autoscroll {
Next,
Strategy(AutoscrollStrategy),
}
impl Autoscroll {
pub fn fit() -> Self {
Self::Strategy(AutoscrollStrategy::Fit)
}
pub fn newest() -> Self {
Self::Strategy(AutoscrollStrategy::Newest)
}
pub fn center() -> Self {
Self::Strategy(AutoscrollStrategy::Center)
}
}
#[derive(PartialEq, Eq, Default)]
pub enum AutoscrollStrategy {
Fit,
Newest,
#[default]
Center,
Top,
Bottom,
}
impl AutoscrollStrategy {
fn next(&self) -> Self {
match self {
AutoscrollStrategy::Center => AutoscrollStrategy::Top,
AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
_ => AutoscrollStrategy::Center,
}
}
}
impl Editor {
pub fn autoscroll_vertically(
&mut self,
viewport_height: f32,
line_height: f32,
cx: &mut ViewContext<Editor>,
) -> bool {
let visible_lines = viewport_height / line_height;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
(display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
} else {
display_map.max_point().row() as f32
};
if scroll_position.y > max_scroll_top {
scroll_position.y = (max_scroll_top);
self.set_scroll_position(scroll_position, cx);
}
let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
return false;
};
let mut target_top;
let mut target_bottom;
if let Some(highlighted_rows) = &self.highlighted_rows {
target_top = highlighted_rows.start as f32;
target_bottom = target_top + 1.;
} else {
let selections = self.selections.all::<Point>(cx);
target_top = selections
.first()
.unwrap()
.head()
.to_display_point(&display_map)
.row() as f32;
target_bottom = selections
.last()
.unwrap()
.head()
.to_display_point(&display_map)
.row() as f32
+ 1.0;
// If the selections can't all fit on screen, scroll to the newest.
if autoscroll == Autoscroll::newest()
|| autoscroll == Autoscroll::fit() && target_bottom - target_top > visible_lines
{
let newest_selection_top = selections
.iter()
.max_by_key(|s| s.id)
.unwrap()
.head()
.to_display_point(&display_map)
.row() as f32;
target_top = newest_selection_top;
target_bottom = newest_selection_top + 1.;
}
}
let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
0.
} else {
((visible_lines - (target_bottom - target_top)) / 2.0).floor()
};
let strategy = match autoscroll {
Autoscroll::Strategy(strategy) => strategy,
Autoscroll::Next => {
let last_autoscroll = &self.scroll_manager.last_autoscroll;
if let Some(last_autoscroll) = last_autoscroll {
if self.scroll_manager.anchor.offset == last_autoscroll.0
&& target_top == last_autoscroll.1
&& target_bottom == last_autoscroll.2
{
last_autoscroll.3.next()
} else {
AutoscrollStrategy::default()
}
} else {
AutoscrollStrategy::default()
}
}
};
match strategy {
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
let target_top = (target_top - margin).max(0.0);
let target_bottom = target_bottom + margin;
let start_row = scroll_position.y;
let end_row = start_row + visible_lines;
let needs_scroll_up = target_top < start_row;
let needs_scroll_down = target_bottom >= end_row;
if needs_scroll_up && !needs_scroll_down {
scroll_position.y = (target_top);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
if !needs_scroll_up && needs_scroll_down {
scroll_position.y = (target_bottom - visible_lines);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
}
AutoscrollStrategy::Center => {
scroll_position.y = ((target_top - margin).max(0.0));
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Top => {
scroll_position.y = ((target_top).max(0.0));
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Bottom => {
scroll_position.y = ((target_bottom - visible_lines).max(0.0));
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
}
self.scroll_manager.last_autoscroll = Some((
self.scroll_manager.anchor.offset,
target_top,
target_bottom,
strategy,
));
true
}
pub fn autoscroll_horizontally(
&mut self,
start_row: u32,
viewport_width: Pixels,
scroll_width: Pixels,
max_glyph_width: Pixels,
layouts: &[LineWithInvisibles],
cx: &mut ViewContext<Self>,
) -> bool {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx);
let mut target_left;
let mut target_right;
if self.highlighted_rows.is_some() {
target_left = px(0.);
target_right = px(0.);
} else {
target_left = px(f32::INFINITY);
target_right = px(0.);
for selection in selections {
let head = selection.head().to_display_point(&display_map);
if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 {
let start_column = head.column().saturating_sub(3);
let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
target_left = target_left.min(
layouts[(head.row() - start_row) as usize]
.line
.x_for_index(start_column as usize),
);
target_right = target_right.max(
layouts[(head.row() - start_row) as usize]
.line
.x_for_index(end_column as usize)
+ max_glyph_width,
);
}
}
}
target_right = target_right.min(scroll_width);
if target_right - target_left > viewport_width {
return false;
}
let scroll_left = self.scroll_manager.anchor.offset.x * max_glyph_width;
let scroll_right = scroll_left + viewport_width;
if target_left < scroll_left {
self.scroll_manager.anchor.offset.x = (target_left / max_glyph_width).into();
true
} else if target_right > scroll_right {
self.scroll_manager.anchor.offset.x =
((target_right - viewport_width) / max_glyph_width).into();
true
} else {
false
}
}
pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
self.scroll_manager.autoscroll_request = Some((autoscroll, true));
cx.notify();
}
pub(crate) fn request_autoscroll_remotely(
&mut self,
autoscroll: Autoscroll,
cx: &mut ViewContext<Self>,
) {
self.scroll_manager.autoscroll_request = Some((autoscroll, false));
cx.notify();
}
}

View file

@ -0,0 +1,29 @@
use crate::Editor;
use serde::Deserialize;
#[derive(Clone, PartialEq, Deserialize)]
pub enum ScrollAmount {
// Scroll N lines (positive is towards the end of the document)
Line(f32),
// Scroll N pages (positive is towards the end of the document)
Page(f32),
}
impl ScrollAmount {
pub fn lines(&self, editor: &mut Editor) -> f32 {
todo!()
// match self {
// Self::Line(count) => *count,
// Self::Page(count) => editor
// .visible_line_count()
// .map(|mut l| {
// // for full pages subtract one to leave an anchor line
// if count.abs() == 1.0 {
// l -= 1.0
// }
// (l * count).trunc()
// })
// .unwrap_or(0.),
// }
}
}

View file

@ -0,0 +1,887 @@
use std::{
cell::Ref,
iter, mem,
ops::{Deref, DerefMut, Range, Sub},
sync::Arc,
};
use collections::HashMap;
use gpui::{AppContext, Model, Pixels};
use itertools::Itertools;
use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint};
use util::post_inc;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
movement::TextLayoutDetails,
Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
};
#[derive(Debug, Clone)]
pub struct PendingSelection {
pub selection: Selection<Anchor>,
pub mode: SelectMode,
}
#[derive(Debug, Clone)]
pub struct SelectionsCollection {
display_map: Model<DisplayMap>,
buffer: Model<MultiBuffer>,
pub next_selection_id: usize,
pub line_mode: bool,
disjoint: Arc<[Selection<Anchor>]>,
pending: Option<PendingSelection>,
}
impl SelectionsCollection {
pub fn new(display_map: Model<DisplayMap>, buffer: Model<MultiBuffer>) -> Self {
Self {
display_map,
buffer,
next_selection_id: 1,
line_mode: false,
disjoint: Arc::from([]),
pending: Some(PendingSelection {
selection: Selection {
id: 0,
start: Anchor::min(),
end: Anchor::min(),
reversed: false,
goal: SelectionGoal::None,
},
mode: SelectMode::Character,
}),
}
}
pub fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot {
self.display_map.update(cx, |map, cx| map.snapshot(cx))
}
fn buffer<'a>(&self, cx: &'a AppContext) -> Ref<'a, MultiBufferSnapshot> {
self.buffer.read(cx).read(cx)
}
pub fn clone_state(&mut self, other: &SelectionsCollection) {
self.next_selection_id = other.next_selection_id;
self.line_mode = other.line_mode;
self.disjoint = other.disjoint.clone();
self.pending = other.pending.clone();
}
pub fn count(&self) -> usize {
let mut count = self.disjoint.len();
if self.pending.is_some() {
count += 1;
}
count
}
/// The non-pending, non-overlapping selections. There could still be a pending
/// selection that overlaps these if the mouse is being dragged, etc. Returned as
/// selections over Anchors.
pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> {
self.disjoint.clone()
}
pub fn pending_anchor(&self) -> Option<Selection<Anchor>> {
self.pending
.as_ref()
.map(|pending| pending.selection.clone())
}
pub fn pending<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Option<Selection<D>> {
self.pending_anchor()
.as_ref()
.map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx))))
}
pub fn pending_mode(&self) -> Option<SelectMode> {
self.pending.as_ref().map(|pending| pending.mode.clone())
}
pub fn all<'a, D>(&self, cx: &AppContext) -> Vec<Selection<D>>
where
D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
{
let disjoint_anchors = &self.disjoint;
let mut disjoint =
resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
let mut pending_opt = self.pending::<D>(cx);
iter::from_fn(move || {
if let Some(pending) = pending_opt.as_mut() {
while let Some(next_selection) = disjoint.peek() {
if pending.start <= next_selection.end && pending.end >= next_selection.start {
let next_selection = disjoint.next().unwrap();
if next_selection.start < pending.start {
pending.start = next_selection.start;
}
if next_selection.end > pending.end {
pending.end = next_selection.end;
}
} else if next_selection.end < pending.start {
return disjoint.next();
} else {
break;
}
}
pending_opt.take()
} else {
disjoint.next()
}
})
.collect()
}
/// Returns all of the selections, adjusted to take into account the selection line_mode
pub fn all_adjusted(&self, cx: &mut AppContext) -> Vec<Selection<Point>> {
let mut selections = self.all::<Point>(cx);
if self.line_mode {
let map = self.display_map(cx);
for selection in &mut selections {
let new_range = map.expand_to_line(selection.range());
selection.start = new_range.start;
selection.end = new_range.end;
}
}
selections
}
pub fn all_adjusted_display(
&self,
cx: &mut AppContext,
) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
if self.line_mode {
let selections = self.all::<Point>(cx);
let map = self.display_map(cx);
let result = selections
.into_iter()
.map(|mut selection| {
let new_range = map.expand_to_line(selection.range());
selection.start = new_range.start;
selection.end = new_range.end;
selection.map(|point| point.to_display_point(&map))
})
.collect();
(map, result)
} else {
self.all_display(cx)
}
}
pub fn disjoint_in_range<'a, D>(
&self,
range: Range<Anchor>,
cx: &AppContext,
) -> Vec<Selection<D>>
where
D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
{
let buffer = self.buffer(cx);
let start_ix = match self
.disjoint
.binary_search_by(|probe| probe.end.cmp(&range.start, &buffer))
{
Ok(ix) | Err(ix) => ix,
};
let end_ix = match self
.disjoint
.binary_search_by(|probe| probe.start.cmp(&range.end, &buffer))
{
Ok(ix) => ix + 1,
Err(ix) => ix,
};
resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect()
}
pub fn all_display(
&self,
cx: &mut AppContext,
) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
let display_map = self.display_map(cx);
let selections = self
.all::<Point>(cx)
.into_iter()
.map(|selection| selection.map(|point| point.to_display_point(&display_map)))
.collect();
(display_map, selections)
}
pub fn newest_anchor(&self) -> &Selection<Anchor> {
self.pending
.as_ref()
.map(|s| &s.selection)
.or_else(|| self.disjoint.iter().max_by_key(|s| s.id))
.unwrap()
}
pub fn newest<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
resolve(self.newest_anchor(), &self.buffer(cx))
}
pub fn newest_display(&self, cx: &mut AppContext) -> Selection<DisplayPoint> {
let display_map = self.display_map(cx);
let selection = self
.newest_anchor()
.map(|point| point.to_display_point(&display_map));
selection
}
pub fn oldest_anchor(&self) -> &Selection<Anchor> {
self.disjoint
.iter()
.min_by_key(|s| s.id)
.or_else(|| self.pending.as_ref().map(|p| &p.selection))
.unwrap()
}
pub fn oldest<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
resolve(self.oldest_anchor(), &self.buffer(cx))
}
pub fn first_anchor(&self) -> Selection<Anchor> {
self.disjoint[0].clone()
}
pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
self.all(cx).first().unwrap().clone()
}
pub fn last<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
self.all(cx).last().unwrap().clone()
}
#[cfg(any(test, feature = "test-support"))]
pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
&self,
cx: &AppContext,
) -> Vec<Range<D>> {
self.all::<D>(cx)
.iter()
.map(|s| {
if s.reversed {
s.end.clone()..s.start.clone()
} else {
s.start.clone()..s.end.clone()
}
})
.collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn display_ranges(&self, cx: &mut AppContext) -> Vec<Range<DisplayPoint>> {
let display_map = self.display_map(cx);
self.disjoint_anchors()
.iter()
.chain(self.pending_anchor().as_ref())
.map(|s| {
if s.reversed {
s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
} else {
s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
}
})
.collect()
}
// pub fn build_columnar_selection(
// &mut self,
// display_map: &DisplaySnapshot,
// row: u32,
// positions: &Range<Pixels>,
// reversed: bool,
// text_layout_details: &TextLayoutDetails,
// ) -> Option<Selection<Point>> {
// let is_empty = positions.start == positions.end;
// let line_len = display_map.line_len(row);
// let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
// let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
// if start_col < line_len || (is_empty && positions.start == layed_out_line.width()) {
// let start = DisplayPoint::new(row, start_col);
// let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
// let end = DisplayPoint::new(row, end_col);
// Some(Selection {
// id: post_inc(&mut self.next_selection_id),
// start: start.to_point(display_map),
// end: end.to_point(display_map),
// reversed,
// goal: SelectionGoal::HorizontalRange {
// start: positions.start,
// end: positions.end,
// },
// })
// } else {
// None
// }
// }
pub(crate) fn change_with<R>(
&mut self,
cx: &mut AppContext,
change: impl FnOnce(&mut MutableSelectionsCollection) -> R,
) -> (bool, R) {
let mut mutable_collection = MutableSelectionsCollection {
collection: self,
selections_changed: false,
cx,
};
let result = change(&mut mutable_collection);
assert!(
!mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(),
"There must be at least one selection"
);
(mutable_collection.selections_changed, result)
}
}
pub struct MutableSelectionsCollection<'a> {
collection: &'a mut SelectionsCollection,
selections_changed: bool,
cx: &'a mut AppContext,
}
impl<'a> MutableSelectionsCollection<'a> {
pub fn display_map(&mut self) -> DisplaySnapshot {
self.collection.display_map(self.cx)
}
fn buffer(&self) -> Ref<MultiBufferSnapshot> {
self.collection.buffer(self.cx)
}
pub fn clear_disjoint(&mut self) {
self.collection.disjoint = Arc::from([]);
}
pub fn delete(&mut self, selection_id: usize) {
let mut changed = false;
self.collection.disjoint = self
.disjoint
.iter()
.filter(|selection| {
let found = selection.id == selection_id;
changed |= found;
!found
})
.cloned()
.collect();
self.selections_changed |= changed;
}
pub fn clear_pending(&mut self) {
if self.collection.pending.is_some() {
self.collection.pending = None;
self.selections_changed = true;
}
}
pub fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection {
selection: Selection {
id: post_inc(&mut self.collection.next_selection_id),
start: range.start,
end: range.end,
reversed: false,
goal: SelectionGoal::None,
},
mode,
});
self.selections_changed = true;
}
pub fn set_pending_display_range(&mut self, range: Range<DisplayPoint>, mode: SelectMode) {
let (start, end, reversed) = {
let display_map = self.display_map();
let buffer = self.buffer();
let mut start = range.start;
let mut end = range.end;
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
let end_bias = if end > start { Bias::Left } else { Bias::Right };
(
buffer.anchor_before(start.to_point(&display_map)),
buffer.anchor_at(end.to_point(&display_map), end_bias),
reversed,
)
};
let new_pending = PendingSelection {
selection: Selection {
id: post_inc(&mut self.collection.next_selection_id),
start,
end,
reversed,
goal: SelectionGoal::None,
},
mode,
};
self.collection.pending = Some(new_pending);
self.selections_changed = true;
}
pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection { selection, mode });
self.selections_changed = true;
}
pub fn try_cancel(&mut self) -> bool {
if let Some(pending) = self.collection.pending.take() {
if self.disjoint.is_empty() {
self.collection.disjoint = Arc::from([pending.selection]);
}
self.selections_changed = true;
return true;
}
let mut oldest = self.oldest_anchor().clone();
if self.count() > 1 {
self.collection.disjoint = Arc::from([oldest]);
self.selections_changed = true;
return true;
}
if !oldest.start.cmp(&oldest.end, &self.buffer()).is_eq() {
let head = oldest.head();
oldest.start = head.clone();
oldest.end = head;
self.collection.disjoint = Arc::from([oldest]);
self.selections_changed = true;
return true;
}
false
}
pub fn insert_range<T>(&mut self, range: Range<T>)
where
T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
{
let mut selections = self.all(self.cx);
let mut start = range.start.to_offset(&self.buffer());
let mut end = range.end.to_offset(&self.buffer());
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
selections.push(Selection {
id: post_inc(&mut self.collection.next_selection_id),
start,
end,
reversed,
goal: SelectionGoal::None,
});
self.select(selections);
}
pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
where
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
selections.sort_unstable_by_key(|s| s.start);
// Merge overlapping selections.
let mut i = 1;
while i < selections.len() {
if selections[i - 1].end >= selections[i].start {
let removed = selections.remove(i);
if removed.start < selections[i - 1].start {
selections[i - 1].start = removed.start;
}
if removed.end > selections[i - 1].end {
selections[i - 1].end = removed.end;
}
} else {
i += 1;
}
}
self.collection.disjoint = Arc::from_iter(selections.into_iter().map(|selection| {
let end_bias = if selection.end > selection.start {
Bias::Left
} else {
Bias::Right
};
Selection {
id: selection.id,
start: buffer.anchor_after(selection.start),
end: buffer.anchor_at(selection.end, end_bias),
reversed: selection.reversed,
goal: selection.goal,
}
}));
self.collection.pending = None;
self.selections_changed = true;
}
pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let resolved_selections =
resolve_multiple::<usize, _>(&selections, &buffer).collect::<Vec<_>>();
self.select(resolved_selections);
}
pub fn select_ranges<I, T>(&mut self, ranges: I)
where
I: IntoIterator<Item = Range<T>>,
T: ToOffset,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let ranges = ranges
.into_iter()
.map(|range| range.start.to_offset(&buffer)..range.end.to_offset(&buffer));
self.select_offset_ranges(ranges);
}
fn select_offset_ranges<I>(&mut self, ranges: I)
where
I: IntoIterator<Item = Range<usize>>,
{
let selections = ranges
.into_iter()
.map(|range| {
let mut start = range.start;
let mut end = range.end;
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
start,
end,
reversed,
goal: SelectionGoal::None,
}
})
.collect::<Vec<_>>();
self.select(selections)
}
pub fn select_anchor_ranges<I: IntoIterator<Item = Range<Anchor>>>(&mut self, ranges: I) {
todo!()
// let buffer = self.buffer.read(self.cx).snapshot(self.cx);
// let selections = ranges
// .into_iter()
// .map(|range| {
// let mut start = range.start;
// let mut end = range.end;
// let reversed = if start.cmp(&end, &buffer).is_gt() {
// mem::swap(&mut start, &mut end);
// true
// } else {
// false
// };
// Selection {
// id: post_inc(&mut self.collection.next_selection_id),
// start,
// end,
// reversed,
// goal: SelectionGoal::None,
// }
// })
// .collect::<Vec<_>>();
// self.select_anchors(selections)
}
pub fn new_selection_id(&mut self) -> usize {
post_inc(&mut self.next_selection_id)
}
pub fn select_display_ranges<T>(&mut self, ranges: T)
where
T: IntoIterator<Item = Range<DisplayPoint>>,
{
let display_map = self.display_map();
let selections = ranges
.into_iter()
.map(|range| {
let mut start = range.start;
let mut end = range.end;
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
start: start.to_point(&display_map),
end: end.to_point(&display_map),
reversed,
goal: SelectionGoal::None,
}
})
.collect();
self.select(selections);
}
pub fn move_with(
&mut self,
mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
) {
let mut changed = false;
let display_map = self.display_map();
let selections = self
.all::<Point>(self.cx)
.into_iter()
.map(|selection| {
let mut moved_selection =
selection.map(|point| point.to_display_point(&display_map));
move_selection(&display_map, &mut moved_selection);
let moved_selection =
moved_selection.map(|display_point| display_point.to_point(&display_map));
if selection != moved_selection {
changed = true;
}
moved_selection
})
.collect();
if changed {
self.select(selections)
}
}
pub fn move_offsets_with(
&mut self,
mut move_selection: impl FnMut(&MultiBufferSnapshot, &mut Selection<usize>),
) {
let mut changed = false;
let snapshot = self.buffer().clone();
let selections = self
.all::<usize>(self.cx)
.into_iter()
.map(|selection| {
let mut moved_selection = selection.clone();
move_selection(&snapshot, &mut moved_selection);
if selection != moved_selection {
changed = true;
}
moved_selection
})
.collect();
drop(snapshot);
if changed {
self.select(selections)
}
}
pub fn move_heads_with(
&mut self,
mut update_head: impl FnMut(
&DisplaySnapshot,
DisplayPoint,
SelectionGoal,
) -> (DisplayPoint, SelectionGoal),
) {
self.move_with(|map, selection| {
let (new_head, new_goal) = update_head(map, selection.head(), selection.goal);
selection.set_head(new_head, new_goal);
});
}
pub fn move_cursors_with(
&mut self,
mut update_cursor_position: impl FnMut(
&DisplaySnapshot,
DisplayPoint,
SelectionGoal,
) -> (DisplayPoint, SelectionGoal),
) {
self.move_with(|map, selection| {
let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal);
selection.collapse_to(cursor, new_goal)
});
}
pub fn maybe_move_cursors_with(
&mut self,
mut update_cursor_position: impl FnMut(
&DisplaySnapshot,
DisplayPoint,
SelectionGoal,
) -> Option<(DisplayPoint, SelectionGoal)>,
) {
self.move_cursors_with(|map, point, goal| {
update_cursor_position(map, point, goal).unwrap_or((point, goal))
})
}
pub fn replace_cursors_with(
&mut self,
mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,
) {
let display_map = self.display_map();
let new_selections = find_replacement_cursors(&display_map)
.into_iter()
.map(|cursor| {
let cursor_point = cursor.to_point(&display_map);
Selection {
id: post_inc(&mut self.collection.next_selection_id),
start: cursor_point,
end: cursor_point,
reversed: false,
goal: SelectionGoal::None,
}
})
.collect();
self.select(new_selections);
}
/// Compute new ranges for any selections that were located in excerpts that have
/// since been removed.
///
/// Returns a `HashMap` indicating which selections whose former head position
/// was no longer present. The keys of the map are selection ids. The values are
/// the id of the new excerpt where the head of the selection has been moved.
pub fn refresh(&mut self) -> HashMap<usize, ExcerptId> {
let mut pending = self.collection.pending.take();
let mut selections_with_lost_position = HashMap::default();
let anchors_with_status = {
let buffer = self.buffer();
let disjoint_anchors = self
.disjoint
.iter()
.flat_map(|selection| [&selection.start, &selection.end]);
buffer.refresh_anchors(disjoint_anchors)
};
let adjusted_disjoint: Vec<_> = anchors_with_status
.chunks(2)
.map(|selection_anchors| {
let (anchor_ix, start, kept_start) = selection_anchors[0].clone();
let (_, end, kept_end) = selection_anchors[1].clone();
let selection = &self.disjoint[anchor_ix / 2];
let kept_head = if selection.reversed {
kept_start
} else {
kept_end
};
if !kept_head {
selections_with_lost_position.insert(selection.id, selection.head().excerpt_id);
}
Selection {
id: selection.id,
start,
end,
reversed: selection.reversed,
goal: selection.goal,
}
})
.collect();
if !adjusted_disjoint.is_empty() {
let resolved_selections =
resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect();
self.select::<usize>(resolved_selections);
}
if let Some(pending) = pending.as_mut() {
let buffer = self.buffer();
let anchors =
buffer.refresh_anchors([&pending.selection.start, &pending.selection.end]);
let (_, start, kept_start) = anchors[0].clone();
let (_, end, kept_end) = anchors[1].clone();
let kept_head = if pending.selection.reversed {
kept_start
} else {
kept_end
};
if !kept_head {
selections_with_lost_position
.insert(pending.selection.id, pending.selection.head().excerpt_id);
}
pending.selection.start = start;
pending.selection.end = end;
}
self.collection.pending = pending;
self.selections_changed = true;
selections_with_lost_position
}
}
impl<'a> Deref for MutableSelectionsCollection<'a> {
type Target = SelectionsCollection;
fn deref(&self) -> &Self::Target {
self.collection
}
}
impl<'a> DerefMut for MutableSelectionsCollection<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.collection
}
}
// Panics if passed selections are not in order
pub fn resolve_multiple<'a, D, I>(
selections: I,
snapshot: &MultiBufferSnapshot,
) -> impl 'a + Iterator<Item = Selection<D>>
where
D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
{
let (to_summarize, selections) = selections.into_iter().tee();
let mut summaries = snapshot
.summaries_for_anchors::<D, _>(
to_summarize
.flat_map(|s| [&s.start, &s.end])
.collect::<Vec<_>>(),
)
.into_iter();
selections.map(move |s| Selection {
id: s.id,
start: summaries.next().unwrap(),
end: summaries.next().unwrap(),
reversed: s.reversed,
goal: s.goal,
})
}
fn resolve<D: TextDimension + Ord + Sub<D, Output = D>>(
selection: &Selection<Anchor>,
buffer: &MultiBufferSnapshot,
) -> Selection<D> {
selection.map(|p| p.summary::<D>(buffer))
}

View file

@ -0,0 +1,81 @@
pub mod editor_lsp_test_context;
pub mod editor_test_context;
// todo!()
// use crate::{
// display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
// DisplayPoint, Editor, EditorMode, MultiBuffer,
// };
// use gpui::{Model, ViewContext};
// use project::Project;
// use util::test::{marked_text_offsets, marked_text_ranges};
// #[cfg(test)]
// #[ctor::ctor]
// fn init_logger() {
// if std::env::var("RUST_LOG").is_ok() {
// env_logger::init();
// }
// }
// // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
// pub fn marked_display_snapshot(
// text: &str,
// cx: &mut gpui::AppContext,
// ) -> (DisplaySnapshot, Vec<DisplayPoint>) {
// let (unmarked_text, markers) = marked_text_offsets(text);
// let family_id = cx
// .font_cache()
// .load_family(&["Helvetica"], &Default::default())
// .unwrap();
// let font_id = cx
// .font_cache()
// .select_font(family_id, &Default::default())
// .unwrap();
// let font_size = 14.0;
// let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
// let display_map =
// cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
// let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
// let markers = markers
// .into_iter()
// .map(|offset| offset.to_display_point(&snapshot))
// .collect();
// (snapshot, markers)
// }
// pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
// let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
// assert_eq!(editor.text(cx), unmarked_text);
// editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
// }
// pub fn assert_text_with_selections(
// editor: &mut Editor,
// marked_text: &str,
// cx: &mut ViewContext<Editor>,
// ) {
// let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
// assert_eq!(editor.text(cx), unmarked_text);
// assert_eq!(editor.selections.ranges(cx), text_ranges);
// }
// // RA thinks this is dead code even though it is used in a whole lot of tests
// #[allow(dead_code)]
// #[cfg(any(test, feature = "test-support"))]
// pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
// Editor::new(EditorMode::Full, buffer, None, None, cx)
// }
// pub(crate) fn build_editor_with_project(
// project: Model<Project>,
// buffer: Model<MultiBuffer>,
// cx: &mut ViewContext<Editor>,
// ) -> Editor {
// Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
// }

View file

@ -0,0 +1,297 @@
// use std::{
// borrow::Cow,
// ops::{Deref, DerefMut, Range},
// sync::Arc,
// };
// use anyhow::Result;
// use crate::{Editor, ToPoint};
// use collections::HashSet;
// use futures::Future;
// use gpui::{json, View, ViewContext};
// use indoc::indoc;
// use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
// use lsp::{notification, request};
// use multi_buffer::ToPointUtf16;
// use project::Project;
// use smol::stream::StreamExt;
// use workspace::{AppState, Workspace, WorkspaceHandle};
// use super::editor_test_context::EditorTestContext;
// pub struct EditorLspTestContext<'a> {
// pub cx: EditorTestContext<'a>,
// pub lsp: lsp::FakeLanguageServer,
// pub workspace: View<Workspace>,
// pub buffer_lsp_url: lsp::Url,
// }
// impl<'a> EditorLspTestContext<'a> {
// pub async fn new(
// mut language: Language,
// capabilities: lsp::ServerCapabilities,
// cx: &'a mut gpui::TestAppContext,
// ) -> EditorLspTestContext<'a> {
// use json::json;
// let app_state = cx.update(AppState::test);
// cx.update(|cx| {
// language::init(cx);
// crate::init(cx);
// workspace::init(app_state.clone(), cx);
// Project::init_settings(cx);
// });
// let file_name = format!(
// "file.{}",
// language
// .path_suffixes()
// .first()
// .expect("language must have a path suffix for EditorLspTestContext")
// );
// let mut fake_servers = language
// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
// capabilities,
// ..Default::default()
// }))
// .await;
// let project = Project::test(app_state.fs.clone(), [], cx).await;
// project.update(cx, |project, _| project.languages().add(Arc::new(language)));
// app_state
// .fs
// .as_fake()
// .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
// .await;
// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// let workspace = window.root(cx);
// project
// .update(cx, |project, cx| {
// project.find_or_create_local_worktree("/root", true, cx)
// })
// .await
// .unwrap();
// cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
// .await;
// let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
// let item = workspace
// .update(cx, |workspace, cx| {
// workspace.open_path(file, None, true, cx)
// })
// .await
// .expect("Could not open test file");
// let editor = cx.update(|cx| {
// item.act_as::<Editor>(cx)
// .expect("Opened test file wasn't an editor")
// });
// editor.update(cx, |_, cx| cx.focus_self());
// let lsp = fake_servers.next().await.unwrap();
// Self {
// cx: EditorTestContext {
// cx,
// window: window.into(),
// editor,
// },
// lsp,
// workspace,
// buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
// }
// }
// pub async fn new_rust(
// capabilities: lsp::ServerCapabilities,
// cx: &'a mut gpui::TestAppContext,
// ) -> EditorLspTestContext<'a> {
// let language = Language::new(
// LanguageConfig {
// name: "Rust".into(),
// path_suffixes: vec!["rs".to_string()],
// ..Default::default()
// },
// Some(tree_sitter_rust::language()),
// )
// .with_queries(LanguageQueries {
// indents: Some(Cow::from(indoc! {r#"
// [
// ((where_clause) _ @end)
// (field_expression)
// (call_expression)
// (assignment_expression)
// (let_declaration)
// (let_chain)
// (await_expression)
// ] @indent
// (_ "[" "]" @end) @indent
// (_ "<" ">" @end) @indent
// (_ "{" "}" @end) @indent
// (_ "(" ")" @end) @indent"#})),
// brackets: Some(Cow::from(indoc! {r#"
// ("(" @open ")" @close)
// ("[" @open "]" @close)
// ("{" @open "}" @close)
// ("<" @open ">" @close)
// ("\"" @open "\"" @close)
// (closure_parameters "|" @open "|" @close)"#})),
// ..Default::default()
// })
// .expect("Could not parse queries");
// Self::new(language, capabilities, cx).await
// }
// pub async fn new_typescript(
// capabilities: lsp::ServerCapabilities,
// cx: &'a mut gpui::TestAppContext,
// ) -> EditorLspTestContext<'a> {
// let mut word_characters: HashSet<char> = Default::default();
// word_characters.insert('$');
// word_characters.insert('#');
// let language = Language::new(
// LanguageConfig {
// name: "Typescript".into(),
// path_suffixes: vec!["ts".to_string()],
// brackets: language::BracketPairConfig {
// pairs: vec![language::BracketPair {
// start: "{".to_string(),
// end: "}".to_string(),
// close: true,
// newline: true,
// }],
// disabled_scopes_by_bracket_ix: Default::default(),
// },
// word_characters,
// ..Default::default()
// },
// Some(tree_sitter_typescript::language_typescript()),
// )
// .with_queries(LanguageQueries {
// brackets: Some(Cow::from(indoc! {r#"
// ("(" @open ")" @close)
// ("[" @open "]" @close)
// ("{" @open "}" @close)
// ("<" @open ">" @close)
// ("\"" @open "\"" @close)"#})),
// indents: Some(Cow::from(indoc! {r#"
// [
// (call_expression)
// (assignment_expression)
// (member_expression)
// (lexical_declaration)
// (variable_declaration)
// (assignment_expression)
// (if_statement)
// (for_statement)
// ] @indent
// (_ "[" "]" @end) @indent
// (_ "<" ">" @end) @indent
// (_ "{" "}" @end) @indent
// (_ "(" ")" @end) @indent
// "#})),
// ..Default::default()
// })
// .expect("Could not parse queries");
// Self::new(language, capabilities, cx).await
// }
// // Constructs lsp range using a marked string with '[', ']' range delimiters
// pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
// let ranges = self.ranges(marked_text);
// self.to_lsp_range(ranges[0].clone())
// }
// pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
// let start_point = range.start.to_point(&snapshot.buffer_snapshot);
// let end_point = range.end.to_point(&snapshot.buffer_snapshot);
// self.editor(|editor, cx| {
// let buffer = editor.buffer().read(cx);
// let start = point_to_lsp(
// buffer
// .point_to_buffer_offset(start_point, cx)
// .unwrap()
// .1
// .to_point_utf16(&buffer.read(cx)),
// );
// let end = point_to_lsp(
// buffer
// .point_to_buffer_offset(end_point, cx)
// .unwrap()
// .1
// .to_point_utf16(&buffer.read(cx)),
// );
// lsp::Range { start, end }
// })
// }
// pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
// let point = offset.to_point(&snapshot.buffer_snapshot);
// self.editor(|editor, cx| {
// let buffer = editor.buffer().read(cx);
// point_to_lsp(
// buffer
// .point_to_buffer_offset(point, cx)
// .unwrap()
// .1
// .to_point_utf16(&buffer.read(cx)),
// )
// })
// }
// pub fn update_workspace<F, T>(&mut self, update: F) -> T
// where
// F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
// {
// self.workspace.update(self.cx.cx, update)
// }
// pub fn handle_request<T, F, Fut>(
// &self,
// mut handler: F,
// ) -> futures::channel::mpsc::UnboundedReceiver<()>
// where
// T: 'static + request::Request,
// T::Params: 'static + Send,
// F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
// Fut: 'static + Send + Future<Output = Result<T::Result>>,
// {
// let url = self.buffer_lsp_url.clone();
// self.lsp.handle_request::<T, _, _>(move |params, cx| {
// let url = url.clone();
// handler(url, params, cx)
// })
// }
// pub fn notify<T: notification::Notification>(&self, params: T::Params) {
// self.lsp.notify::<T>(params);
// }
// }
// impl<'a> Deref for EditorLspTestContext<'a> {
// type Target = EditorTestContext<'a>;
// fn deref(&self) -> &Self::Target {
// &self.cx
// }
// }
// impl<'a> DerefMut for EditorLspTestContext<'a> {
// fn deref_mut(&mut self) -> &mut Self::Target {
// &mut self.cx
// }
// }

View file

@ -0,0 +1,331 @@
use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
};
use futures::Future;
use gpui::{
AnyWindowHandle, AppContext, ForegroundExecutor, Keystroke, ModelContext, View, ViewContext,
};
use indoc::indoc;
use language::{Buffer, BufferSnapshot};
use project::{FakeFs, Project};
use std::{
any::TypeId,
ops::{Deref, DerefMut, Range},
};
use util::{
assert_set_eq,
test::{generate_marked_text, marked_text_ranges},
};
// use super::build_editor_with_project;
// pub struct EditorTestContext<'a> {
// pub cx: &'a mut gpui::TestAppContext,
// pub window: AnyWindowHandle,
// pub editor: View<Editor>,
// }
// impl<'a> EditorTestContext<'a> {
// pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
// let fs = FakeFs::new(cx.background());
// // fs.insert_file("/file", "".to_owned()).await;
// fs.insert_tree(
// "/root",
// gpui::serde_json::json!({
// "file": "",
// }),
// )
// .await;
// let project = Project::test(fs, ["/root".as_ref()], cx).await;
// let buffer = project
// .update(cx, |project, cx| {
// project.open_local_buffer("/root/file", cx)
// })
// .await
// .unwrap();
// let window = cx.add_window(|cx| {
// cx.focus_self();
// build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx)
// });
// let editor = window.root(cx);
// Self {
// cx,
// window: window.into(),
// editor,
// }
// }
// pub fn condition(
// &self,
// predicate: impl FnMut(&Editor, &AppContext) -> bool,
// ) -> impl Future<Output = ()> {
// self.editor.condition(self.cx, predicate)
// }
// pub fn editor<F, T>(&self, read: F) -> T
// where
// F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
// {
// self.editor.update(self.cx, read)
// }
// pub fn update_editor<F, T>(&mut self, update: F) -> T
// where
// F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
// {
// self.editor.update(self.cx, update)
// }
// pub fn multibuffer<F, T>(&self, read: F) -> T
// where
// F: FnOnce(&MultiBuffer, &AppContext) -> T,
// {
// self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
// }
// pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
// where
// F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
// {
// self.update_editor(|editor, cx| editor.buffer().update(cx, update))
// }
// pub fn buffer_text(&self) -> String {
// self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
// }
// pub fn buffer<F, T>(&self, read: F) -> T
// where
// F: FnOnce(&Buffer, &AppContext) -> T,
// {
// self.multibuffer(|multibuffer, cx| {
// let buffer = multibuffer.as_singleton().unwrap().read(cx);
// read(buffer, cx)
// })
// }
// pub fn update_buffer<F, T>(&mut self, update: F) -> T
// where
// F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
// {
// self.update_multibuffer(|multibuffer, cx| {
// let buffer = multibuffer.as_singleton().unwrap();
// buffer.update(cx, update)
// })
// }
// pub fn buffer_snapshot(&self) -> BufferSnapshot {
// self.buffer(|buffer, _| buffer.snapshot())
// }
// pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
// let keystroke_under_test_handle =
// self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
// let keystroke = Keystroke::parse(keystroke_text).unwrap();
// self.cx.dispatch_keystroke(self.window, keystroke, false);
// keystroke_under_test_handle
// }
// pub fn simulate_keystrokes<const COUNT: usize>(
// &mut self,
// keystroke_texts: [&str; COUNT],
// ) -> ContextHandle {
// let keystrokes_under_test_handle =
// self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
// for keystroke_text in keystroke_texts.into_iter() {
// self.simulate_keystroke(keystroke_text);
// }
// // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
// // before returning.
// // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
// // quickly races with async actions.
// if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() {
// executor.run_until_parked();
// } else {
// unreachable!();
// }
// keystrokes_under_test_handle
// }
// pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
// let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
// assert_eq!(self.buffer_text(), unmarked_text);
// ranges
// }
// pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
// let ranges = self.ranges(marked_text);
// let snapshot = self
// .editor
// .update(self.cx, |editor, cx| editor.snapshot(cx));
// ranges[0].start.to_display_point(&snapshot)
// }
// // Returns anchors for the current buffer using `«` and `»`
// pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
// let ranges = self.ranges(marked_text);
// let snapshot = self.buffer_snapshot();
// snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
// }
// pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
// let diff_base = diff_base.map(String::from);
// self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
// }
// /// Change the editor's text and selections using a string containing
// /// embedded range markers that represent the ranges and directions of
// /// each selection.
// ///
// /// Returns a context handle so that assertion failures can print what
// /// editor state was needed to cause the failure.
// ///
// /// See the `util::test::marked_text_ranges` function for more information.
// pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
// let state_context = self.add_assertion_context(format!(
// "Initial Editor State: \"{}\"",
// marked_text.escape_debug().to_string()
// ));
// let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
// self.editor.update(self.cx, |editor, cx| {
// editor.set_text(unmarked_text, cx);
// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
// s.select_ranges(selection_ranges)
// })
// });
// state_context
// }
// /// Only change the editor's selections
// pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
// let state_context = self.add_assertion_context(format!(
// "Initial Editor State: \"{}\"",
// marked_text.escape_debug().to_string()
// ));
// let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
// self.editor.update(self.cx, |editor, cx| {
// assert_eq!(editor.text(cx), unmarked_text);
// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
// s.select_ranges(selection_ranges)
// })
// });
// state_context
// }
// /// Make an assertion about the editor's text and the ranges and directions
// /// of its selections using a string containing embedded range markers.
// ///
// /// See the `util::test::marked_text_ranges` function for more information.
// #[track_caller]
// pub fn assert_editor_state(&mut self, marked_text: &str) {
// let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
// let buffer_text = self.buffer_text();
// if buffer_text != unmarked_text {
// panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
// }
// self.assert_selections(expected_selections, marked_text.to_string())
// }
// pub fn editor_state(&mut self) -> String {
// generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
// }
// #[track_caller]
// pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
// let expected_ranges = self.ranges(marked_text);
// let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
// let snapshot = editor.snapshot(cx);
// editor
// .background_highlights
// .get(&TypeId::of::<Tag>())
// .map(|h| h.1.clone())
// .unwrap_or_default()
// .into_iter()
// .map(|range| range.to_offset(&snapshot.buffer_snapshot))
// .collect()
// });
// assert_set_eq!(actual_ranges, expected_ranges);
// }
// #[track_caller]
// pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
// let expected_ranges = self.ranges(marked_text);
// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
// let actual_ranges: Vec<Range<usize>> = snapshot
// .text_highlight_ranges::<Tag>()
// .map(|ranges| ranges.as_ref().clone().1)
// .unwrap_or_default()
// .into_iter()
// .map(|range| range.to_offset(&snapshot.buffer_snapshot))
// .collect();
// assert_set_eq!(actual_ranges, expected_ranges);
// }
// #[track_caller]
// pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
// let expected_marked_text =
// generate_marked_text(&self.buffer_text(), &expected_selections, true);
// self.assert_selections(expected_selections, expected_marked_text)
// }
// fn editor_selections(&self) -> Vec<Range<usize>> {
// self.editor
// .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
// .into_iter()
// .map(|s| {
// if s.reversed {
// s.end..s.start
// } else {
// s.start..s.end
// }
// })
// .collect::<Vec<_>>()
// }
// #[track_caller]
// fn assert_selections(
// &mut self,
// expected_selections: Vec<Range<usize>>,
// expected_marked_text: String,
// ) {
// let actual_selections = self.editor_selections();
// let actual_marked_text =
// generate_marked_text(&self.buffer_text(), &actual_selections, true);
// if expected_selections != actual_selections {
// panic!(
// indoc! {"
// {}Editor has unexpected selections.
// Expected selections:
// {}
// Actual selections:
// {}
// "},
// self.assertion_context(),
// expected_marked_text,
// actual_marked_text,
// );
// }
// }
// }
//
// impl<'a> Deref for EditorTestContext<'a> {
// type Target = gpui::TestAppContext;
// fn deref(&self) -> &Self::Target {
// self.cx
// }
// }
// impl<'a> DerefMut for EditorTestContext<'a> {
// fn deref_mut(&mut self) -> &mut Self::Target {
// &mut self.cx
// }
// }

View file

@ -15,7 +15,6 @@ doctest = false
[dependencies]
collections = { path = "../collections" }
gpui_macros = { path = "../gpui_macros" }
gpui2_macros = { path = "../gpui2_macros" }
util = { path = "../util" }
sum_tree = { path = "../sum_tree" }

View file

@ -373,6 +373,10 @@ impl AppContext {
self.platform.reveal_path(path)
}
pub fn should_auto_hide_scrollbars(&self) -> bool {
self.platform.should_auto_hide_scrollbars()
}
pub(crate) fn push_effect(&mut self, effect: Effect) {
match &effect {
Effect::Notify { emitter } => {

View file

@ -29,6 +29,7 @@ pub struct ForegroundExecutor {
}
#[must_use]
#[derive(Debug)]
pub enum Task<T> {
Ready(Option<T>),
Spawned(async_task::Task<T>),
@ -49,11 +50,11 @@ impl<T> Task<T> {
impl<E, T> Task<Result<T, E>>
where
T: 'static + Send,
E: 'static + Send + Debug,
T: 'static,
E: 'static + Debug,
{
pub fn detach_and_log_err(self, cx: &mut AppContext) {
cx.background_executor().spawn(self.log_err()).detach();
cx.foreground_executor().spawn(self.log_err()).detach();
}
}

View file

@ -755,6 +755,10 @@ impl Pixels {
pub fn pow(&self, exponent: f32) -> Self {
Self(self.0.powf(exponent))
}
pub fn abs(&self) -> Self {
Self(self.0.abs())
}
}
impl Mul<Pixels> for Pixels {
@ -815,6 +819,18 @@ impl From<Pixels> for f64 {
}
}
impl From<Pixels> for u32 {
fn from(pixels: Pixels) -> Self {
pixels.0 as u32
}
}
impl From<Pixels> for usize {
fn from(pixels: Pixels) -> Self {
pixels.0 as usize
}
}
#[derive(
Add, AddAssign, Clone, Copy, Default, Div, Eq, Hash, Ord, PartialEq, PartialOrd, Sub, SubAssign,
)]

View file

@ -411,6 +411,7 @@ impl MacTextSystemState {
descent: typographic_bounds.descent.into(),
runs,
font_size,
len: text.len(),
}
}

View file

@ -167,6 +167,15 @@ impl TextStyle {
Ok(self)
}
pub fn font(&self) -> Font {
Font {
family: self.font_family.clone(),
features: self.font_features.clone(),
weight: self.font_weight,
style: self.font_style,
}
}
pub fn to_run(&self, len: usize) -> TextRun {
TextRun {
len,

View file

@ -7,7 +7,7 @@ use anyhow::anyhow;
pub use font_features::*;
pub use line::*;
pub use line_layout::*;
use line_wrapper::*;
pub use line_wrapper::*;
use smallvec::SmallVec;
use crate::{
@ -151,7 +151,7 @@ impl TextSystem {
pub fn layout_text(
&self,
text: &SharedString,
text: &str,
font_size: Pixels,
runs: &[TextRun],
wrap_width: Option<Pixels>,

View file

@ -2,6 +2,7 @@ use crate::{
black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size,
UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
};
use derive_more::{Deref, DerefMut};
use smallvec::SmallVec;
use std::sync::Arc;
@ -12,8 +13,10 @@ pub struct DecorationRun {
pub underline: Option<UnderlineStyle>,
}
#[derive(Clone, Default, Debug)]
#[derive(Clone, Default, Debug, Deref, DerefMut)]
pub struct Line {
#[deref]
#[deref_mut]
pub(crate) layout: Arc<WrappedLineLayout>,
pub(crate) decorations: SmallVec<[DecorationRun; 32]>,
}
@ -26,6 +29,10 @@ impl Line {
)
}
pub fn width(&self) -> Pixels {
self.layout.width
}
pub fn wrap_count(&self) -> usize {
self.layout.wrap_boundaries.len()
}

View file

@ -16,6 +16,7 @@ pub struct LineLayout {
pub ascent: Pixels,
pub descent: Pixels,
pub runs: Vec<ShapedRun>,
pub len: usize,
}
#[derive(Debug)]
@ -48,6 +49,28 @@ impl LineLayout {
}
}
/// closest_index_for_x returns the character boundary closest to the given x coordinate
/// (e.g. to handle aligning up/down arrow keys)
pub fn closest_index_for_x(&self, x: Pixels) -> usize {
let mut prev_index = 0;
let mut prev_x = px(0.);
for run in self.runs.iter() {
for glyph in run.glyphs.iter() {
if glyph.position.x >= x {
if glyph.position.x - x < x - prev_x {
return glyph.index;
} else {
return prev_index;
}
}
prev_index = glyph.index;
prev_x = glyph.position.x;
}
}
prev_index
}
pub fn x_for_index(&self, index: usize) -> Pixels {
for run in &self.runs {
for glyph in &run.glyphs {

View file

@ -1,7 +1,7 @@
use crate::{
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId,
Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext,
Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, Model, Pixels,
Size, ViewContext, VisualContext, WeakModel, WindowContext,
};
use anyhow::{Context, Result};
use std::{
@ -196,31 +196,9 @@ impl<V: Render> From<View<V>> for AnyView {
fn from(value: View<V>) -> Self {
AnyView {
model: value.model.into_any(),
initialize: |view, cx| {
cx.with_element_id(view.model.entity_id, |_, cx| {
let view = view.clone().downcast::<V>().unwrap();
let element = view.update(cx, |view, cx| {
let mut element = AnyElement::new(view.render(cx));
element.initialize(view, cx);
element
});
Box::new(element)
})
},
layout: |view, element, cx| {
cx.with_element_id(view.model.entity_id, |_, cx| {
let view = view.clone().downcast::<V>().unwrap();
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
view.update(cx, |view, cx| element.layout(view, cx))
})
},
paint: |view, element, cx| {
cx.with_element_id(view.model.entity_id, |_, cx| {
let view = view.clone().downcast::<V>().unwrap();
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
view.update(cx, |view, cx| element.paint(view, cx))
})
},
initialize: any_view::initialize::<V>,
layout: any_view::layout::<V>,
paint: any_view::paint::<V>,
}
}
}
@ -280,6 +258,17 @@ impl AnyWeakView {
}
}
impl<V: Render> From<WeakView<V>> for AnyWeakView {
fn from(view: WeakView<V>) -> Self {
Self {
model: view.model.into(),
initialize: any_view::initialize::<V>,
layout: any_view::layout::<V>,
paint: any_view::paint::<V>,
}
}
}
impl<T, E> Render for T
where
T: 'static + FnMut(&mut WindowContext) -> E,
@ -291,3 +280,44 @@ where
(self)(cx)
}
}
mod any_view {
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
use std::any::Any;
pub(crate) fn initialize<V: Render>(view: &AnyView, cx: &mut WindowContext) -> Box<dyn Any> {
cx.with_element_id(view.model.entity_id, |_, cx| {
let view = view.clone().downcast::<V>().unwrap();
let element = view.update(cx, |view, cx| {
let mut element = AnyElement::new(view.render(cx));
element.initialize(view, cx);
element
});
Box::new(element)
})
}
pub(crate) fn layout<V: Render>(
view: &AnyView,
element: &mut Box<dyn Any>,
cx: &mut WindowContext,
) -> LayoutId {
cx.with_element_id(view.model.entity_id, |_, cx| {
let view = view.clone().downcast::<V>().unwrap();
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
view.update(cx, |view, cx| element.layout(view, cx))
})
}
pub(crate) fn paint<V: Render>(
view: &AnyView,
element: &mut Box<dyn Any>,
cx: &mut WindowContext,
) {
cx.with_element_id(view.model.entity_id, |_, cx| {
let view = view.clone().downcast::<V>().unwrap();
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
view.update(cx, |view, cx| element.paint(view, cx))
})
}
}

View file

@ -1655,6 +1655,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
}
// todo!("change this to return a reference");
pub fn view(&self) -> View<V> {
self.view.clone()
}
@ -1663,6 +1664,11 @@ impl<'a, V: 'static> ViewContext<'a, V> {
self.view.model.clone()
}
/// Access the underlying window context.
pub fn window_context(&mut self) -> &mut WindowContext<'a> {
&mut self.window_cx
}
pub fn stack<R>(&mut self, order: u32, f: impl FnOnce(&mut Self) -> R) -> R {
self.window.z_index_stack.push(order);
let result = f(self);

View file

@ -58,6 +58,7 @@ unicase = "2.6"
rand = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
pulldown-cmark = { version = "0.9.2", default-features = false }
[dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] }

View file

@ -1,6 +1,7 @@
pub use crate::{
diagnostic_set::DiagnosticSet,
highlight_map::{HighlightId, HighlightMap},
markdown::ParsedMarkdown,
proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
};
use crate::{

View file

@ -8,6 +8,7 @@ mod syntax_map;
#[cfg(test)]
mod buffer_tests;
pub mod markdown;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;

View file

@ -0,0 +1,301 @@
use std::sync::Arc;
use std::{ops::Range, path::PathBuf};
use crate::{HighlightId, Language, LanguageRegistry};
use gpui::{px, FontStyle, FontWeight, HighlightStyle, UnderlineStyle};
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
#[derive(Debug, Clone)]
pub struct ParsedMarkdown {
pub text: String,
pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
pub region_ranges: Vec<Range<usize>>,
pub regions: Vec<ParsedRegion>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MarkdownHighlight {
Style(MarkdownHighlightStyle),
Code(HighlightId),
}
impl MarkdownHighlight {
pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
match self {
MarkdownHighlight::Style(style) => {
let mut highlight = HighlightStyle::default();
if style.italic {
highlight.font_style = Some(FontStyle::Italic);
}
if style.underline {
highlight.underline = Some(UnderlineStyle {
thickness: px(1.),
..Default::default()
});
}
if style.weight != FontWeight::default() {
highlight.font_weight = Some(style.weight);
}
Some(highlight)
}
MarkdownHighlight::Code(id) => id.style(theme),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MarkdownHighlightStyle {
pub italic: bool,
pub underline: bool,
pub weight: FontWeight,
}
#[derive(Debug, Clone)]
pub struct ParsedRegion {
pub code: bool,
pub link: Option<Link>,
}
#[derive(Debug, Clone)]
pub enum Link {
Web { url: String },
Path { path: PathBuf },
}
impl Link {
fn identify(text: String) -> Option<Link> {
if text.starts_with("http") {
return Some(Link::Web { url: text });
}
let path = PathBuf::from(text);
if path.is_absolute() {
return Some(Link::Path { path });
}
None
}
}
pub async fn parse_markdown(
markdown: &str,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
) -> ParsedMarkdown {
let mut text = String::new();
let mut highlights = Vec::new();
let mut region_ranges = Vec::new();
let mut regions = Vec::new();
parse_markdown_block(
markdown,
language_registry,
language,
&mut text,
&mut highlights,
&mut region_ranges,
&mut regions,
)
.await;
ParsedMarkdown {
text,
highlights,
region_ranges,
regions,
}
}
pub async fn parse_markdown_block(
markdown: &str,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
text: &mut String,
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
region_ranges: &mut Vec<Range<usize>>,
regions: &mut Vec<ParsedRegion>,
) {
let mut bold_depth = 0;
let mut italic_depth = 0;
let mut link_url = None;
let mut current_language = None;
let mut list_stack = Vec::new();
for event in Parser::new_ext(&markdown, Options::all()) {
let prev_len = text.len();
match event {
Event::Text(t) => {
if let Some(language) = &current_language {
highlight_code(text, highlights, t.as_ref(), language);
} else {
text.push_str(t.as_ref());
let mut style = MarkdownHighlightStyle::default();
if bold_depth > 0 {
style.weight = FontWeight::BOLD;
}
if italic_depth > 0 {
style.italic = true;
}
if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) {
region_ranges.push(prev_len..text.len());
regions.push(ParsedRegion {
code: false,
link: Some(link),
});
style.underline = true;
}
if style != MarkdownHighlightStyle::default() {
let mut new_highlight = true;
if let Some((last_range, MarkdownHighlight::Style(last_style))) =
highlights.last_mut()
{
if last_range.end == prev_len && last_style == &style {
last_range.end = text.len();
new_highlight = false;
}
}
if new_highlight {
let range = prev_len..text.len();
highlights.push((range, MarkdownHighlight::Style(style)));
}
}
}
}
Event::Code(t) => {
text.push_str(t.as_ref());
region_ranges.push(prev_len..text.len());
let link = link_url.clone().and_then(|u| Link::identify(u));
if link.is_some() {
highlights.push((
prev_len..text.len(),
MarkdownHighlight::Style(MarkdownHighlightStyle {
underline: true,
..Default::default()
}),
));
}
regions.push(ParsedRegion { code: true, link });
}
Event::Start(tag) => match tag {
Tag::Paragraph => new_paragraph(text, &mut list_stack),
Tag::Heading(_, _, _) => {
new_paragraph(text, &mut list_stack);
bold_depth += 1;
}
Tag::CodeBlock(kind) => {
new_paragraph(text, &mut list_stack);
current_language = if let CodeBlockKind::Fenced(language) = kind {
language_registry
.language_for_name(language.as_ref())
.await
.ok()
} else {
language.clone()
}
}
Tag::Emphasis => italic_depth += 1,
Tag::Strong => bold_depth += 1,
Tag::Link(_, url, _) => link_url = Some(url.to_string()),
Tag::List(number) => {
list_stack.push((number, false));
}
Tag::Item => {
let len = list_stack.len();
if let Some((list_number, has_content)) = list_stack.last_mut() {
*has_content = false;
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
for _ in 0..len - 1 {
text.push_str(" ");
}
if let Some(number) = list_number {
text.push_str(&format!("{}. ", number));
*number += 1;
*has_content = false;
} else {
text.push_str("- ");
}
}
}
_ => {}
},
Event::End(tag) => match tag {
Tag::Heading(_, _, _) => bold_depth -= 1,
Tag::CodeBlock(_) => current_language = None,
Tag::Emphasis => italic_depth -= 1,
Tag::Strong => bold_depth -= 1,
Tag::Link(_, _, _) => link_url = None,
Tag::List(_) => drop(list_stack.pop()),
_ => {}
},
Event::HardBreak => text.push('\n'),
Event::SoftBreak => text.push(' '),
_ => {}
}
}
}
pub fn highlight_code(
text: &mut String,
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
content: &str,
language: &Arc<Language>,
) {
let prev_len = text.len();
text.push_str(content);
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
let highlight = MarkdownHighlight::Code(highlight_id);
highlights.push((prev_len + range.start..prev_len + range.end, highlight));
}
}
pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
let mut is_subsequent_paragraph_of_list = false;
if let Some((_, has_content)) = list_stack.last_mut() {
if *has_content {
is_subsequent_paragraph_of_list = true;
} else {
*has_content = true;
return;
}
}
if !text.is_empty() {
if !text.ends_with('\n') {
text.push('\n');
}
text.push('\n');
}
for _ in 0..list_stack.len().saturating_sub(1) {
text.push_str(" ");
}
if is_subsequent_paragraph_of_list {
text.push_str(" ");
}
}

View file

@ -5,7 +5,7 @@ use std::ops::Range;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum SelectionGoal {
None,
HorizontalPosition(f32),
HorizontalPosition(f32), // todo!("Can we use pixels here without adding a runtime gpui dependency?")
HorizontalRange { start: f32, end: f32 },
WrappedHorizontalPosition((u32, f32)),
}

View file

@ -12,10 +12,9 @@ use client2::{
Client,
};
use gpui::{
AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, HighlightStyle, Model, Pixels,
Point, Render, SharedString, Task, View, ViewContext, WeakView, WindowContext,
AnyElement, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, HighlightStyle,
Model, Pixels, Point, Render, SharedString, Task, View, ViewContext, WeakView, WindowContext,
};
use parking_lot::Mutex;
use project2::{Project, ProjectEntryId, ProjectPath};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -23,8 +22,10 @@ use settings2::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
cell::RefCell,
ops::Range,
path::PathBuf,
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
@ -90,7 +91,8 @@ pub struct BreadcrumbText {
pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
}
pub trait Item: Render + EventEmitter + Send {
pub trait Item: Render + EventEmitter {
fn focus_handle(&self) -> FocusHandle;
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
@ -104,7 +106,11 @@ pub trait Item: Render + EventEmitter + Send {
}
fn tab_content<V: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<V>;
fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project2::Item)) {
fn for_each_project_item(
&self,
_: &AppContext,
_: &mut dyn FnMut(EntityId, &dyn project2::Item),
) {
} // (model id, Item)
fn is_singleton(&self, _cx: &AppContext) -> bool {
false
@ -208,6 +214,7 @@ pub trait Item: Render + EventEmitter + Send {
}
pub trait ItemHandle: 'static + Send {
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
fn subscribe_to_item_events(
&self,
cx: &mut WindowContext,
@ -219,8 +226,12 @@ pub trait ItemHandle: 'static + Send {
fn dragged_tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Workspace>;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]>;
fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project2::Item));
fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]>;
fn for_each_project_item(
&self,
_: &AppContext,
_: &mut dyn FnMut(EntityId, &dyn project2::Item),
);
fn is_singleton(&self, cx: &AppContext) -> bool;
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
fn clone_on_split(
@ -253,7 +264,7 @@ pub trait ItemHandle: 'static + Send {
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyView>;
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
fn on_release(
&mut self,
&self,
cx: &mut AppContext,
callback: Box<dyn FnOnce(&mut AppContext) + Send>,
) -> gpui::Subscription;
@ -282,6 +293,10 @@ impl dyn ItemHandle {
}
impl<T: Item> ItemHandle for View<T> {
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
self.read(cx).focus_handle()
}
fn subscribe_to_item_events(
&self,
cx: &mut WindowContext,
@ -331,7 +346,7 @@ impl<T: Item> ItemHandle for View<T> {
result
}
fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]> {
fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]> {
let mut result = SmallVec::new();
self.read(cx).for_each_project_item(cx, &mut |id, _| {
result.push(id);
@ -342,7 +357,7 @@ impl<T: Item> ItemHandle for View<T> {
fn for_each_project_item(
&self,
cx: &AppContext,
f: &mut dyn FnMut(usize, &dyn project2::Item),
f: &mut dyn FnMut(EntityId, &dyn project2::Item),
) {
self.read(cx).for_each_project_item(cx, f)
}
@ -398,91 +413,94 @@ impl<T: Item> ItemHandle for View<T> {
.is_none()
{
let mut pending_autosave = DelayedDebouncedEditAction::new();
let pending_update = Arc::new(Mutex::new(None));
let pending_update = Rc::new(RefCell::new(None));
let pending_update_scheduled = Arc::new(AtomicBool::new(false));
let event_subscription = Some(cx.subscribe(self, move |workspace, item, event, cx| {
let pane = if let Some(pane) = workspace
.panes_by_item
.get(&item.id())
.and_then(|pane| pane.upgrade())
{
pane
} else {
log::error!("unexpected item event after pane was dropped");
return;
};
if let Some(item) = item.to_followable_item_handle(cx) {
let _is_project_item = item.is_project_item(cx);
let leader_id = workspace.leader_for_pane(&pane);
if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
workspace.unfollow(&pane, cx);
}
if item.add_event_to_update_proto(event, &mut *pending_update.lock(), cx)
&& !pending_update_scheduled.load(Ordering::SeqCst)
let mut event_subscription =
Some(cx.subscribe(self, move |workspace, item, event, cx| {
let pane = if let Some(pane) = workspace
.panes_by_item
.get(&item.id())
.and_then(|pane| pane.upgrade())
{
pending_update_scheduled.store(true, Ordering::SeqCst);
todo!("replace with on_next_frame?");
// cx.after_window_update({
// let pending_update = pending_update.clone();
// let pending_update_scheduled = pending_update_scheduled.clone();
// move |this, cx| {
// pending_update_scheduled.store(false, Ordering::SeqCst);
// this.update_followers(
// is_project_item,
// proto::update_followers::Variant::UpdateView(
// proto::UpdateView {
// id: item
// .remote_id(&this.app_state.client, cx)
// .map(|id| id.to_proto()),
// variant: pending_update.borrow_mut().take(),
// leader_id,
// },
// ),
// cx,
// );
// }
// });
}
}
pane
} else {
log::error!("unexpected item event after pane was dropped");
return;
};
for item_event in T::to_item_events(event).into_iter() {
match item_event {
ItemEvent::CloseItem => {
pane.update(cx, |pane, cx| {
pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx)
})
.detach_and_log_err(cx);
return;
if let Some(item) = item.to_followable_item_handle(cx) {
let is_project_item = item.is_project_item(cx);
let leader_id = workspace.leader_for_pane(&pane);
if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
workspace.unfollow(&pane, cx);
}
ItemEvent::UpdateTab => {
pane.update(cx, |_, cx| {
cx.emit(pane::Event::ChangeItemTitle);
cx.notify();
if item.add_event_to_update_proto(
event,
&mut *pending_update.borrow_mut(),
cx,
) && !pending_update_scheduled.load(Ordering::SeqCst)
{
pending_update_scheduled.store(true, Ordering::SeqCst);
cx.on_next_frame({
let pending_update = pending_update.clone();
let pending_update_scheduled = pending_update_scheduled.clone();
move |this, cx| {
pending_update_scheduled.store(false, Ordering::SeqCst);
this.update_followers(
is_project_item,
proto::update_followers::Variant::UpdateView(
proto::UpdateView {
id: item
.remote_id(&this.app_state.client, cx)
.map(|id| id.to_proto()),
variant: pending_update.borrow_mut().take(),
leader_id,
},
),
cx,
);
}
});
}
}
ItemEvent::Edit => {
let autosave = WorkspaceSettings::get_global(cx).autosave;
if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
let delay = Duration::from_millis(milliseconds);
let item = item.clone();
pending_autosave.fire_new(delay, cx, move |workspace, cx| {
Pane::autosave_item(&item, workspace.project().clone(), cx)
for item_event in T::to_item_events(event).into_iter() {
match item_event {
ItemEvent::CloseItem => {
pane.update(cx, |pane, cx| {
pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx)
})
.detach_and_log_err(cx);
return;
}
ItemEvent::UpdateTab => {
pane.update(cx, |_, cx| {
cx.emit(pane::Event::ChangeItemTitle);
cx.notify();
});
}
ItemEvent::Edit => {
let autosave = WorkspaceSettings::get_global(cx).autosave;
if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
let delay = Duration::from_millis(milliseconds);
let item = item.clone();
pending_autosave.fire_new(delay, cx, move |workspace, cx| {
Pane::autosave_item(&item, workspace.project().clone(), cx)
});
}
}
_ => {}
}
_ => {}
}
}
}));
}));
todo!("observe focus");
// todo!()
// cx.observe_focus(self, move |workspace, item, focused, cx| {
// if !focused
// && WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange
@ -493,12 +511,12 @@ impl<T: Item> ItemHandle for View<T> {
// })
// .detach();
// let item_id = self.id();
// cx.observe_release(self, move |workspace, _, _| {
// workspace.panes_by_item.remove(&item_id);
// event_subscription.take();
// })
// .detach();
let item_id = self.id();
cx.observe_release(self, move |workspace, _, _| {
workspace.panes_by_item.remove(&item_id);
event_subscription.take();
})
.detach();
}
cx.defer(|workspace, cx| {
@ -570,7 +588,7 @@ impl<T: Item> ItemHandle for View<T> {
}
fn on_release(
&mut self,
&self,
cx: &mut AppContext,
callback: Box<dyn FnOnce(&mut AppContext) + Send>,
) -> gpui::Subscription {

View file

@ -9,8 +9,8 @@ use crate::{
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
use gpui::{
AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, Model, PromptLevel,
Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, Model,
PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use parking_lot::Mutex;
use project2::{Project, ProjectEntryId, ProjectPath};
@ -171,6 +171,7 @@ impl fmt::Debug for Event {
}
pub struct Pane {
focus_handle: FocusHandle,
items: Vec<Box<dyn ItemHandle>>,
activation_history: Vec<EntityId>,
zoomed: bool,
@ -183,7 +184,6 @@ pub struct Pane {
// tab_context_menu: ViewHandle<ContextMenu>,
workspace: WeakView<Workspace>,
project: Model<Project>,
has_focus: bool,
// can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
// can_split: bool,
// render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
@ -330,6 +330,7 @@ impl Pane {
let handle = cx.view().downgrade();
Self {
focus_handle: cx.focus_handle(),
items: Vec::new(),
activation_history: Vec::new(),
zoomed: false,
@ -353,7 +354,6 @@ impl Pane {
// tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)),
workspace,
project,
has_focus: false,
// can_drop: Rc::new(|_, _| true),
// can_split: true,
// render_tab_bar_buttons: Rc::new(move |pane, cx| {
@ -420,8 +420,8 @@ impl Pane {
&self.workspace
}
pub fn has_focus(&self) -> bool {
self.has_focus
pub fn has_focus(&self, cx: &WindowContext) -> bool {
self.focus_handle.contains_focused(cx)
}
pub fn active_item_index(&self) -> usize {
@ -639,19 +639,19 @@ impl Pane {
// .pixel_position_of_cursor(cx)
// }
// pub fn item_for_entry(
// &self,
// entry_id: ProjectEntryId,
// cx: &AppContext,
// ) -> Option<Box<dyn ItemHandle>> {
// self.items.iter().find_map(|item| {
// if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
// Some(item.boxed_clone())
// } else {
// None
// }
// })
// }
pub fn item_for_entry(
&self,
entry_id: ProjectEntryId,
cx: &AppContext,
) -> Option<Box<dyn ItemHandle>> {
self.items.iter().find_map(|item| {
if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
Some(item.boxed_clone())
} else {
None
}
})
}
pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
self.items.iter().position(|i| i.id() == item.id())
@ -1020,7 +1020,7 @@ impl Pane {
// to activating the item to the left
.unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
let should_activate = activate_pane || self.has_focus;
let should_activate = activate_pane || self.has_focus(cx);
self.activate_item(index_to_activate, should_activate, should_activate, cx);
}
@ -1184,11 +1184,15 @@ impl Pane {
}
}
pub fn focus(&mut self, cx: &mut ViewContext<Pane>) {
cx.focus(&self.focus_handle);
}
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
todo!();
// if let Some(active_item) = self.active_item() {
// cx.focus(active_item.as_any());
// }
if let Some(active_item) = self.active_item() {
let focus_handle = active_item.focus_handle(cx);
cx.focus(&focus_handle);
}
}
// pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {

View file

@ -14,9 +14,12 @@ mod status_bar;
mod toolbar;
mod workspace_settings;
use crate::persistence::model::{
DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup,
SerializedWorkspace,
pub use crate::persistence::{
model::{
DockData, DockStructure, ItemId, SerializedItem, SerializedPane, SerializedPaneGroup,
SerializedWorkspace,
},
WorkspaceDb,
};
use anyhow::{anyhow, Context as _, Result};
use call2::ActiveCall;
@ -33,10 +36,10 @@ use futures::{
};
use gpui::{
div, point, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
AsyncWindowContext, Bounds, Component, Div, EntityId, EventEmitter, GlobalPixels, Model,
ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, Styled, Subscription,
Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle,
WindowOptions,
AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle,
GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, StatefulInteractive,
Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds,
WindowContext, WindowHandle, WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
@ -46,15 +49,13 @@ use node_runtime::NodeRuntime;
use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
pub use pane::*;
pub use pane_group::*;
use persistence::{
model::{ItemId, WorkspaceLocation},
DB,
};
use persistence::{model::WorkspaceLocation, DB};
use postage::stream::Stream;
use project2::{Project, ProjectEntryId, ProjectPath, Worktree};
use serde::Deserialize;
use settings2::Settings;
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
use std::{
any::TypeId,
borrow::Cow,
@ -415,18 +416,17 @@ type ItemDeserializers = HashMap<
) -> Task<Result<Box<dyn ItemHandle>>>,
>;
pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
cx.update_global(|deserializers: &mut ItemDeserializers, _cx| {
if let Some(serialized_item_kind) = I::serialized_item_kind() {
deserializers.insert(
Arc::from(serialized_item_kind),
|project, workspace, workspace_id, item_id, cx| {
let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
cx.foreground_executor()
.spawn(async { Ok(Box::new(task.await?) as Box<_>) })
},
);
}
});
if let Some(serialized_item_kind) = I::serialized_item_kind() {
let deserializers = cx.default_global::<ItemDeserializers>();
deserializers.insert(
Arc::from(serialized_item_kind),
|project, workspace, workspace_id, item_id, cx| {
let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
cx.foreground_executor()
.spawn(async { Ok(Box::new(task.await?) as Box<_>) })
},
);
}
}
pub struct AppState {
@ -548,6 +548,7 @@ pub enum Event {
pub struct Workspace {
weak_self: WeakView<Self>,
focus_handle: FocusHandle,
// modal: Option<ActiveModal>,
zoomed: Option<AnyWeakView>,
zoomed_position: Option<DockPosition>,
@ -767,6 +768,7 @@ impl Workspace {
cx.defer(|this, cx| this.update_window_title(cx));
Workspace {
weak_self: weak_handle.clone(),
focus_handle: cx.focus_handle(),
// modal: None,
zoomed: None,
zoomed_position: None,
@ -1924,44 +1926,44 @@ impl Workspace {
// self.zoomed.and_then(|view| view.upgrade(cx))
// }
// fn dismiss_zoomed_items_to_reveal(
// &mut self,
// dock_to_reveal: Option<DockPosition>,
// cx: &mut ViewContext<Self>,
// ) {
// // If a center pane is zoomed, unzoom it.
// for pane in &self.panes {
// if pane != &self.active_pane || dock_to_reveal.is_some() {
// pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
// }
// }
fn dismiss_zoomed_items_to_reveal(
&mut self,
dock_to_reveal: Option<DockPosition>,
cx: &mut ViewContext<Self>,
) {
// If a center pane is zoomed, unzoom it.
for pane in &self.panes {
if pane != &self.active_pane || dock_to_reveal.is_some() {
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
}
}
// // If another dock is zoomed, hide it.
// let mut focus_center = false;
// for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
// dock.update(cx, |dock, cx| {
// if Some(dock.position()) != dock_to_reveal {
// if let Some(panel) = dock.active_panel() {
// if panel.is_zoomed(cx) {
// focus_center |= panel.has_focus(cx);
// dock.set_open(false, cx);
// }
// }
// }
// });
// }
// If another dock is zoomed, hide it.
let mut focus_center = false;
for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
dock.update(cx, |dock, cx| {
if Some(dock.position()) != dock_to_reveal {
if let Some(panel) = dock.active_panel() {
if panel.is_zoomed(cx) {
focus_center |= panel.has_focus(cx);
dock.set_open(false, cx);
}
}
}
});
}
// if focus_center {
// cx.focus_self();
// }
if focus_center {
cx.focus(&self.focus_handle);
}
// if self.zoomed_position != dock_to_reveal {
// self.zoomed = None;
// self.zoomed_position = None;
// }
if self.zoomed_position != dock_to_reveal {
self.zoomed = None;
self.zoomed_position = None;
}
// cx.notify();
// }
cx.notify();
}
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
let pane = cx.build_view(|cx| {
@ -1997,22 +1999,22 @@ impl Workspace {
// }
// }
// pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
// self.active_pane
// .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
// }
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
self.active_pane
.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
}
// pub fn split_item(
// &mut self,
// split_direction: SplitDirection,
// item: Box<dyn ItemHandle>,
// cx: &mut ViewContext<Self>,
// ) {
// let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
// new_pane.update(cx, move |new_pane, cx| {
// new_pane.add_item(item, true, true, None, cx)
// })
// }
pub fn split_item(
&mut self,
split_direction: SplitDirection,
item: Box<dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
new_pane.update(cx, move |new_pane, cx| {
new_pane.add_item(item, true, true, None, cx)
})
}
// pub fn open_abs_path(
// &mut self,
@ -2142,53 +2144,55 @@ impl Workspace {
})
}
// pub fn open_project_item<T>(
// &mut self,
// project_item: ModelHandle<T::Item>,
// cx: &mut ViewContext<Self>,
// ) -> View<T>
// where
// T: ProjectItem,
// {
// use project::Item as _;
pub fn open_project_item<T>(
&mut self,
project_item: Model<T::Item>,
cx: &mut ViewContext<Self>,
) -> View<T>
where
T: ProjectItem,
{
use project2::Item as _;
// let entry_id = project_item.read(cx).entry_id(cx);
// if let Some(item) = entry_id
// .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
// .and_then(|item| item.downcast())
// {
// self.activate_item(&item, cx);
// return item;
// }
let entry_id = project_item.read(cx).entry_id(cx);
if let Some(item) = entry_id
.and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
.and_then(|item| item.downcast())
{
self.activate_item(&item, cx);
return item;
}
// let item = cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
// self.add_item(Box::new(item.clone()), cx);
// item
// }
let item =
cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
self.add_item(Box::new(item.clone()), cx);
item
}
// pub fn split_project_item<T>(
// &mut self,
// project_item: ModelHandle<T::Item>,
// cx: &mut ViewContext<Self>,
// ) -> View<T>
// where
// T: ProjectItem,
// {
// use project::Item as _;
pub fn split_project_item<T>(
&mut self,
project_item: Model<T::Item>,
cx: &mut ViewContext<Self>,
) -> View<T>
where
T: ProjectItem,
{
use project2::Item as _;
// let entry_id = project_item.read(cx).entry_id(cx);
// if let Some(item) = entry_id
// .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
// .and_then(|item| item.downcast())
// {
// self.activate_item(&item, cx);
// return item;
// }
let entry_id = project_item.read(cx).entry_id(cx);
if let Some(item) = entry_id
.and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
.and_then(|item| item.downcast())
{
self.activate_item(&item, cx);
return item;
}
// let item = cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
// self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
// item
// }
let item =
cx.build_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
item
}
// pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
// if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
@ -2198,19 +2202,19 @@ impl Workspace {
// }
// }
// pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
// let result = self.panes.iter().find_map(|pane| {
// pane.read(cx)
// .index_for_item(item)
// .map(|ix| (pane.clone(), ix))
// });
// if let Some((pane, ix)) = result {
// pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
// true
// } else {
// false
// }
// }
pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
let result = self.panes.iter().find_map(|pane| {
pane.read(cx)
.index_for_item(item)
.map(|ix| (pane.clone(), ix))
});
if let Some((pane, ix)) = result {
pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
true
} else {
false
}
}
// fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
// let panes = self.center.panes();
@ -2288,214 +2292,213 @@ impl Workspace {
// self.center.pane_at_pixel_position(target)
// }
// fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
// if self.active_pane != pane {
// self.active_pane = pane.clone();
// self.status_bar.update(cx, |status_bar, cx| {
// status_bar.set_active_pane(&self.active_pane, cx);
// });
// self.active_item_path_changed(cx);
// self.last_active_center_pane = Some(pane.downgrade());
// }
fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
if self.active_pane != pane {
self.active_pane = pane.clone();
self.status_bar.update(cx, |status_bar, cx| {
status_bar.set_active_pane(&self.active_pane, cx);
});
self.active_item_path_changed(cx);
self.last_active_center_pane = Some(pane.downgrade());
}
// self.dismiss_zoomed_items_to_reveal(None, cx);
// if pane.read(cx).is_zoomed() {
// self.zoomed = Some(pane.downgrade().into_any());
// } else {
// self.zoomed = None;
// }
// self.zoomed_position = None;
// self.update_active_view_for_followers(cx);
self.dismiss_zoomed_items_to_reveal(None, cx);
if pane.read(cx).is_zoomed() {
self.zoomed = Some(pane.downgrade().into());
} else {
self.zoomed = None;
}
self.zoomed_position = None;
self.update_active_view_for_followers(cx);
// cx.notify();
// }
cx.notify();
}
fn handle_pane_event(
&mut self,
_pane: View<Pane>,
_event: &pane::Event,
_cx: &mut ViewContext<Self>,
pane: View<Pane>,
event: &pane::Event,
cx: &mut ViewContext<Self>,
) {
todo!()
// match event {
// pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
// pane::Event::Split(direction) => {
// self.split_and_clone(pane, *direction, cx);
// }
// pane::Event::Remove => self.remove_pane(pane, cx),
// pane::Event::ActivateItem { local } => {
// if *local {
// self.unfollow(&pane, cx);
// }
// if &pane == self.active_pane() {
// self.active_item_path_changed(cx);
// }
// }
// pane::Event::ChangeItemTitle => {
// if pane == self.active_pane {
// self.active_item_path_changed(cx);
// }
// self.update_window_edited(cx);
// }
// pane::Event::RemoveItem { item_id } => {
// self.update_window_edited(cx);
// if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
// if entry.get().id() == pane.id() {
// entry.remove();
// }
// }
// }
// pane::Event::Focus => {
// self.handle_pane_focused(pane.clone(), cx);
// }
// pane::Event::ZoomIn => {
// if pane == self.active_pane {
// pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
// if pane.read(cx).has_focus() {
// self.zoomed = Some(pane.downgrade().into_any());
// self.zoomed_position = None;
// }
// cx.notify();
// }
// }
// pane::Event::ZoomOut => {
// pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
// if self.zoomed_position.is_none() {
// self.zoomed = None;
// }
// cx.notify();
// }
// }
match event {
pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
pane::Event::Split(direction) => {
self.split_and_clone(pane, *direction, cx);
}
pane::Event::Remove => self.remove_pane(pane, cx),
pane::Event::ActivateItem { local } => {
if *local {
self.unfollow(&pane, cx);
}
if &pane == self.active_pane() {
self.active_item_path_changed(cx);
}
}
pane::Event::ChangeItemTitle => {
if pane == self.active_pane {
self.active_item_path_changed(cx);
}
self.update_window_edited(cx);
}
pane::Event::RemoveItem { item_id } => {
self.update_window_edited(cx);
if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
if entry.get().entity_id() == pane.entity_id() {
entry.remove();
}
}
}
pane::Event::Focus => {
self.handle_pane_focused(pane.clone(), cx);
}
pane::Event::ZoomIn => {
if pane == self.active_pane {
pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
if pane.read(cx).has_focus(cx) {
self.zoomed = Some(pane.downgrade().into());
self.zoomed_position = None;
}
cx.notify();
}
}
pane::Event::ZoomOut => {
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
if self.zoomed_position.is_none() {
self.zoomed = None;
}
cx.notify();
}
}
// self.serialize_workspace(cx);
self.serialize_workspace(cx);
}
// pub fn split_pane(
// &mut self,
// pane_to_split: View<Pane>,
// split_direction: SplitDirection,
// cx: &mut ViewContext<Self>,
// ) -> View<Pane> {
// let new_pane = self.add_pane(cx);
// self.center
// .split(&pane_to_split, &new_pane, split_direction)
// .unwrap();
// cx.notify();
// new_pane
// }
pub fn split_pane(
&mut self,
pane_to_split: View<Pane>,
split_direction: SplitDirection,
cx: &mut ViewContext<Self>,
) -> View<Pane> {
let new_pane = self.add_pane(cx);
self.center
.split(&pane_to_split, &new_pane, split_direction)
.unwrap();
cx.notify();
new_pane
}
// pub fn split_and_clone(
// &mut self,
// pane: View<Pane>,
// direction: SplitDirection,
// cx: &mut ViewContext<Self>,
// ) -> Option<View<Pane>> {
// let item = pane.read(cx).active_item()?;
// let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
// let new_pane = self.add_pane(cx);
// new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
// self.center.split(&pane, &new_pane, direction).unwrap();
// Some(new_pane)
// } else {
// None
// };
// cx.notify();
// maybe_pane_handle
// }
pub fn split_and_clone(
&mut self,
pane: View<Pane>,
direction: SplitDirection,
cx: &mut ViewContext<Self>,
) -> Option<View<Pane>> {
let item = pane.read(cx).active_item()?;
let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
let new_pane = self.add_pane(cx);
new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
self.center.split(&pane, &new_pane, direction).unwrap();
Some(new_pane)
} else {
None
};
cx.notify();
maybe_pane_handle
}
// pub fn split_pane_with_item(
// &mut self,
// pane_to_split: WeakView<Pane>,
// split_direction: SplitDirection,
// from: WeakView<Pane>,
// item_id_to_move: usize,
// cx: &mut ViewContext<Self>,
// ) {
// let Some(pane_to_split) = pane_to_split.upgrade(cx) else {
// return;
// };
// let Some(from) = from.upgrade(cx) else {
// return;
// };
pub fn split_pane_with_item(
&mut self,
pane_to_split: WeakView<Pane>,
split_direction: SplitDirection,
from: WeakView<Pane>,
item_id_to_move: EntityId,
cx: &mut ViewContext<Self>,
) {
let Some(pane_to_split) = pane_to_split.upgrade() else {
return;
};
let Some(from) = from.upgrade() else {
return;
};
// let new_pane = self.add_pane(cx);
// self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
// self.center
// .split(&pane_to_split, &new_pane, split_direction)
// .unwrap();
// cx.notify();
// }
let new_pane = self.add_pane(cx);
self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
self.center
.split(&pane_to_split, &new_pane, split_direction)
.unwrap();
cx.notify();
}
// pub fn split_pane_with_project_entry(
// &mut self,
// pane_to_split: WeakView<Pane>,
// split_direction: SplitDirection,
// project_entry: ProjectEntryId,
// cx: &mut ViewContext<Self>,
// ) -> Option<Task<Result<()>>> {
// let pane_to_split = pane_to_split.upgrade(cx)?;
// let new_pane = self.add_pane(cx);
// self.center
// .split(&pane_to_split, &new_pane, split_direction)
// .unwrap();
pub fn split_pane_with_project_entry(
&mut self,
pane_to_split: WeakView<Pane>,
split_direction: SplitDirection,
project_entry: ProjectEntryId,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let pane_to_split = pane_to_split.upgrade()?;
let new_pane = self.add_pane(cx);
self.center
.split(&pane_to_split, &new_pane, split_direction)
.unwrap();
// let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
// let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
// Some(cx.foreground().spawn(async move {
// task.await?;
// Ok(())
// }))
// }
let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
Some(cx.foreground_executor().spawn(async move {
task.await?;
Ok(())
}))
}
// pub fn move_item(
// &mut self,
// source: View<Pane>,
// destination: View<Pane>,
// item_id_to_move: usize,
// destination_index: usize,
// cx: &mut ViewContext<Self>,
// ) {
// let item_to_move = source
// .read(cx)
// .items()
// .enumerate()
// .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
pub fn move_item(
&mut self,
source: View<Pane>,
destination: View<Pane>,
item_id_to_move: EntityId,
destination_index: usize,
cx: &mut ViewContext<Self>,
) {
let item_to_move = source
.read(cx)
.items()
.enumerate()
.find(|(_, item_handle)| item_handle.id() == item_id_to_move);
// if item_to_move.is_none() {
// log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
// return;
// }
// let (item_ix, item_handle) = item_to_move.unwrap();
// let item_handle = item_handle.clone();
if item_to_move.is_none() {
log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
return;
}
let (item_ix, item_handle) = item_to_move.unwrap();
let item_handle = item_handle.clone();
// if source != destination {
// // Close item from previous pane
// source.update(cx, |source, cx| {
// source.remove_item(item_ix, false, cx);
// });
// }
if source != destination {
// Close item from previous pane
source.update(cx, |source, cx| {
source.remove_item(item_ix, false, cx);
});
}
// // This automatically removes duplicate items in the pane
// destination.update(cx, |destination, cx| {
// destination.add_item(item_handle, true, true, Some(destination_index), cx);
// cx.focus_self();
// });
// }
// This automatically removes duplicate items in the pane
destination.update(cx, |destination, cx| {
destination.add_item(item_handle, true, true, Some(destination_index), cx);
destination.focus(cx)
});
}
// fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
// if self.center.remove(&pane).unwrap() {
// self.force_remove_pane(&pane, cx);
// self.unfollow(&pane, cx);
// self.last_leaders_by_pane.remove(&pane.downgrade());
// for removed_item in pane.read(cx).items() {
// self.panes_by_item.remove(&removed_item.id());
// }
fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
if self.center.remove(&pane).unwrap() {
self.force_remove_pane(&pane, cx);
self.unfollow(&pane, cx);
self.last_leaders_by_pane.remove(&pane.downgrade());
for removed_item in pane.read(cx).items() {
self.panes_by_item.remove(&removed_item.id());
}
// cx.notify();
// } else {
// self.active_item_path_changed(cx);
// }
// }
cx.notify();
} else {
self.active_item_path_changed(cx);
}
}
pub fn panes(&self) -> &[View<Pane>] {
&self.panes
@ -2708,12 +2711,12 @@ impl Workspace {
.child("Collab title bar Item") // self.titlebar_item
}
// fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
// let active_entry = self.active_project_path(cx);
// self.project
// .update(cx, |project, cx| project.set_active_path(active_entry, cx));
// self.update_window_title(cx);
// }
fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
let active_entry = self.active_project_path(cx);
self.project
.update(cx, |project, cx| project.set_active_path(active_entry, cx));
self.update_window_title(cx);
}
fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
let project = self.project().read(cx);
@ -3010,7 +3013,7 @@ impl Workspace {
fn update_active_view_for_followers(&mut self, cx: &mut ViewContext<Self>) {
let mut is_project_item = true;
let mut update = proto::UpdateActiveView::default();
if self.active_pane.read(cx).has_focus() {
if self.active_pane.read(cx).has_focus(cx) {
let item = self
.active_item(cx)
.and_then(|item| item.to_followable_item_handle(cx));
@ -3105,7 +3108,7 @@ impl Workspace {
}
for (pane, item) in items_to_activate {
let pane_was_focused = pane.read(cx).has_focus();
let pane_was_focused = pane.read(cx).has_focus(cx);
if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
} else {
@ -3243,7 +3246,7 @@ impl Workspace {
// }
fn serialize_workspace(&self, cx: &mut ViewContext<Self>) {
fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &AppContext) -> SerializedPane {
fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
let (items, active) = {
let pane = pane_handle.read(cx);
let active_item_id = pane.active_item().map(|item| item.id());
@ -3257,7 +3260,7 @@ impl Workspace {
})
})
.collect::<Vec<_>>(),
pane.has_focus(),
pane.has_focus(cx),
)
};
@ -3266,7 +3269,7 @@ impl Workspace {
fn build_serialized_pane_group(
pane_group: &Member,
cx: &AppContext,
cx: &WindowContext,
) -> SerializedPaneGroup {
match pane_group {
Member::Axis(PaneAxis {

View file

@ -34,7 +34,7 @@ copilot = { package = "copilot2", path = "../copilot2" }
# copilot_button = { path = "../copilot_button" }
# diagnostics = { path = "../diagnostics" }
db = { package = "db2", path = "../db2" }
# editor = { path = "../editor" }
editor = { package="editor2", path = "../editor2" }
# feedback = { path = "../feedback" }
# file_finder = { path = "../file_finder" }
# search = { path = "../search" }
@ -70,7 +70,7 @@ theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
# semantic_index = { path = "../semantic_index" }
# vim = { path = "../vim" }
workspace2 = { path = "../workspace2" }
workspace = { package = "workspace2", path = "../workspace2" }
# welcome = { path = "../welcome" }
# zed-actions = {path = "../zed-actions"}
anyhow.workspace = true

View file

@ -4,18 +4,13 @@
// Allow binary to be called Zed for a nice application menu when running executable directly
#![allow(non_snake_case)]
use crate::open_listener::{OpenListener, OpenRequest};
use anyhow::{anyhow, Context as _, Result};
use backtrace::Backtrace;
use cli::{
ipc::{self, IpcSender},
CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use client::UserStore;
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use fs::RealFs;
use futures::{channel::mpsc, SinkExt, StreamExt};
use futures::StreamExt;
use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
use isahc::{prelude::Configurable, Request};
use language::LanguageRegistry;
@ -35,7 +30,7 @@ use std::{
fs::OpenOptions,
io::{IsTerminal, Write},
panic,
path::Path,
path::{Path, PathBuf},
sync::{
atomic::{AtomicU32, Ordering},
Arc,
@ -43,18 +38,18 @@ use std::{
thread,
time::{SystemTime, UNIX_EPOCH},
};
use text::Point;
use util::{
async_maybe,
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
http::{self, HttpClient},
paths::{self, PathLikeWithPosition},
ResultExt,
paths, ResultExt,
};
use uuid::Uuid;
use workspace2::{AppState, WorkspaceStore};
use zed2::{build_window_options, initialize_workspace, languages};
use zed2::{ensure_only_instance, Assets, IsOnlyInstance};
use workspace::{AppState, WorkspaceStore};
use zed2::{
build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
};
mod open_listener;
@ -143,7 +138,7 @@ fn main() {
client::init(&client, cx);
// command_palette::init(cx);
language::init(cx);
// editor::init(cx);
editor::init(cx);
// go_to_line::init(cx);
// file_finder::init(cx);
// outline::init(cx);
@ -194,7 +189,7 @@ fn main() {
// audio::init(Assets, cx);
// auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
workspace2::init(app_state.clone(), cx);
workspace::init(app_state.clone(), cx);
// recent_projects::init(cx);
// journal2::init(app_state.clone(), cx);
@ -213,6 +208,7 @@ fn main() {
if stdout_is_a_pty() {
cx.activate(true);
let urls = collect_url_args();
dbg!(&urls);
if !urls.is_empty() {
listener.open_urls(urls)
}
@ -230,9 +226,27 @@ fn main() {
let mut _triggered_authentication = false;
fn open_paths_and_log_errs(
paths: &[PathBuf],
app_state: &Arc<AppState>,
cx: &mut AppContext,
) {
let task = workspace::open_paths(&paths, &app_state, None, cx);
cx.spawn(|cx| async move {
if let Some((_window, results)) = task.await.log_err() {
for result in results {
if let Some(Err(e)) = result {
log::error!("Error opening path: {}", e);
}
}
}
})
.detach();
}
match open_rx.try_next() {
Ok(Some(OpenRequest::Paths { paths })) => {
workspace2::open_paths(&paths, &app_state, None, cx).detach();
open_paths_and_log_errs(&paths, &app_state, cx)
}
Ok(Some(OpenRequest::CliConnection { connection })) => {
let app_state = app_state.clone();
@ -240,6 +254,7 @@ fn main() {
.detach();
}
Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => {
todo!()
// triggered_authentication = true;
// let app_state = app_state.clone();
// let client = client.clone();
@ -251,6 +266,9 @@ fn main() {
// })
// .detach_and_log_err(cx)
}
Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => {
todo!()
}
Ok(None) | Err(_) => cx
.spawn({
let app_state = app_state.clone();
@ -260,29 +278,25 @@ fn main() {
}
let app_state = app_state.clone();
cx.spawn(|cx| {
async move {
while let Some(request) = open_rx.next().await {
match request {
OpenRequest::Paths { paths } => {
cx.update(|cx| workspace2::open_paths(&paths, &app_state, None, cx))
.ok()
.map(|t| t.detach());
}
OpenRequest::CliConnection { connection } => {
let app_state = app_state.clone();
cx.spawn(move |cx| {
handle_cli_connection(connection, app_state.clone(), cx)
})
.detach();
}
OpenRequest::JoinChannel { channel_id: _ } => {
// cx
// .update(|cx| {
// workspace::join_channel(channel_id, app_state.clone(), None, cx)
// })
// .detach()
}
cx.spawn(|cx| async move {
while let Some(request) = open_rx.next().await {
match request {
OpenRequest::Paths { paths } => {
cx.update(|cx| open_paths_and_log_errs(&paths, &app_state, cx))
.ok();
}
OpenRequest::CliConnection { connection } => {
let app_state = app_state.clone();
cx.spawn(move |cx| {
handle_cli_connection(connection, app_state.clone(), cx)
})
.detach();
}
OpenRequest::JoinChannel { channel_id: _ } => {
todo!()
}
OpenRequest::OpenChannelNotes { channel_id: _ } => {
todo!()
}
}
}
@ -325,8 +339,8 @@ async fn installation_id() -> Result<String> {
async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
async_maybe!({
if let Some(location) = workspace2::last_opened_workspace_paths().await {
cx.update(|cx| workspace2::open_paths(location.paths().as_ref(), app_state, None, cx))?
if let Some(location) = workspace::last_opened_workspace_paths().await {
cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
.await
.log_err();
} else if matches!(KEY_VALUE_STORE.read_kvp("******* THIS IS A BAD KEY PLEASE UNCOMMENT BELOW TO FIX THIS VERY LONG LINE *******"), Ok(None)) {
@ -336,7 +350,7 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp
// cx.update(|cx| show_welcome_experience(app_state, cx));
} else {
cx.update(|cx| {
workspace2::open_new(app_state, cx, |workspace, cx| {
workspace::open_new(app_state, cx, |workspace, cx| {
// todo!(editor)
// Editor::new_file(workspace, &Default::default(), cx)
})
@ -746,190 +760,6 @@ fn load_embedded_fonts(cx: &AppContext) {
// #[cfg(not(debug_assertions))]
// fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
fn connect_to_cli(
server_name: &str,
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
.context("error connecting to cli")?;
let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
handshake_tx
.send(IpcHandshake {
requests: request_tx,
responses: response_rx,
})
.context("error sending ipc handshake")?;
let (mut async_request_tx, async_request_rx) =
futures::channel::mpsc::channel::<CliRequest>(16);
thread::spawn(move || {
while let Ok(cli_request) = request_rx.recv() {
if smol::block_on(async_request_tx.send(cli_request)).is_err() {
break;
}
}
Ok::<_, anyhow::Error>(())
});
Ok((async_request_rx, response_tx))
}
async fn handle_cli_connection(
(mut requests, _responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
_app_state: Arc<AppState>,
mut _cx: AsyncAppContext,
) {
if let Some(request) = requests.next().await {
match request {
CliRequest::Open { paths, wait } => {
let mut caret_positions = HashMap::default();
let paths = if paths.is_empty() {
workspace2::last_opened_workspace_paths()
.await
.map(|location| location.paths().to_vec())
.unwrap_or_default()
} else {
paths
.into_iter()
.filter_map(|path_with_position_string| {
let path_with_position = PathLikeWithPosition::parse_str(
&path_with_position_string,
|path_str| {
Ok::<_, std::convert::Infallible>(
Path::new(path_str).to_path_buf(),
)
},
)
.expect("Infallible");
let path = path_with_position.path_like;
if let Some(row) = path_with_position.row {
if path.is_file() {
let row = row.saturating_sub(1);
let col =
path_with_position.column.unwrap_or(0).saturating_sub(1);
caret_positions.insert(path.clone(), Point::new(row, col));
}
}
Some(path)
})
.collect()
};
// todo!("editor")
// let mut errored = false;
// match cx
// .update(|cx| workspace2::open_paths(&paths, &app_state, None, cx))
// .await
// {
// Ok((workspace, items)) => {
// let mut item_release_futures = Vec::new();
// for (item, path) in items.into_iter().zip(&paths) {
// match item {
// Some(Ok(item)) => {
// if let Some(point) = caret_positions.remove(path) {
// if let Some(active_editor) = item.downcast::<Editor>() {
// active_editor
// .downgrade()
// .update(&mut cx, |editor, cx| {
// let snapshot =
// editor.snapshot(cx).display_snapshot;
// let point = snapshot
// .buffer_snapshot
// .clip_point(point, Bias::Left);
// editor.change_selections(
// Some(Autoscroll::center()),
// cx,
// |s| s.select_ranges([point..point]),
// );
// })
// .log_err();
// }
// }
// let released = oneshot::channel();
// cx.update(|cx| {
// item.on_release(
// cx,
// Box::new(move |_| {
// let _ = released.0.send(());
// }),
// )
// .detach();
// });
// item_release_futures.push(released.1);
// }
// Some(Err(err)) => {
// responses
// .send(CliResponse::Stderr {
// message: format!("error opening {:?}: {}", path, err),
// })
// .log_err();
// errored = true;
// }
// None => {}
// }
// }
// if wait {
// let background = cx.background();
// let wait = async move {
// if paths.is_empty() {
// let (done_tx, done_rx) = oneshot::channel();
// if let Some(workspace) = workspace.upgrade(&cx) {
// let _subscription = cx.update(|cx| {
// cx.observe_release(&workspace, move |_, _| {
// let _ = done_tx.send(());
// })
// });
// drop(workspace);
// let _ = done_rx.await;
// }
// } else {
// let _ =
// futures::future::try_join_all(item_release_futures).await;
// };
// }
// .fuse();
// futures::pin_mut!(wait);
// loop {
// // Repeatedly check if CLI is still open to avoid wasting resources
// // waiting for files or workspaces to close.
// let mut timer = background.timer(Duration::from_secs(1)).fuse();
// futures::select_biased! {
// _ = wait => break,
// _ = timer => {
// if responses.send(CliResponse::Ping).is_err() {
// break;
// }
// }
// }
// }
// }
// }
// Err(error) => {
// errored = true;
// responses
// .send(CliResponse::Stderr {
// message: format!("error opening {:?}: {}", paths, error),
// })
// .log_err();
// }
// }
// responses
// .send(CliResponse::Exit {
// status: i32::from(errored),
// })
// .log_err();
}
}
}
}
pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
// &[
// ("Go to file", &file_finder::Toggle),

View file

@ -1,15 +1,26 @@
use anyhow::anyhow;
use anyhow::{anyhow, Context, Result};
use cli::{ipc, IpcHandshake};
use cli::{ipc::IpcSender, CliRequest, CliResponse};
use futures::channel::mpsc;
use editor::scroll::autoscroll::Autoscroll;
use editor::Editor;
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures::channel::{mpsc, oneshot};
use futures::{FutureExt, SinkExt, StreamExt};
use gpui::AsyncAppContext;
use language::{Bias, Point};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::os::unix::prelude::OsStrExt;
use std::path::Path;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use std::{path::PathBuf, sync::atomic::AtomicBool};
use util::channel::parse_zed_link;
use util::paths::PathLikeWithPosition;
use util::ResultExt;
use crate::connect_to_cli;
use workspace::AppState;
pub enum OpenRequest {
Paths {
@ -21,6 +32,9 @@ pub enum OpenRequest {
JoinChannel {
channel_id: u64,
},
OpenChannelNotes {
channel_id: u64,
},
}
pub struct OpenListener {
@ -74,7 +88,11 @@ impl OpenListener {
if let Some(slug) = parts.next() {
if let Some(id_str) = slug.split("-").last() {
if let Ok(channel_id) = id_str.parse::<u64>() {
return Some(OpenRequest::JoinChannel { channel_id });
if Some("notes") == parts.next() {
return Some(OpenRequest::OpenChannelNotes { channel_id });
} else {
return Some(OpenRequest::JoinChannel { channel_id });
}
}
}
}
@ -96,3 +114,191 @@ impl OpenListener {
Some(OpenRequest::Paths { paths })
}
}
fn connect_to_cli(
server_name: &str,
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
.context("error connecting to cli")?;
let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
handshake_tx
.send(IpcHandshake {
requests: request_tx,
responses: response_rx,
})
.context("error sending ipc handshake")?;
let (mut async_request_tx, async_request_rx) =
futures::channel::mpsc::channel::<CliRequest>(16);
thread::spawn(move || {
while let Ok(cli_request) = request_rx.recv() {
if smol::block_on(async_request_tx.send(cli_request)).is_err() {
break;
}
}
Ok::<_, anyhow::Error>(())
});
Ok((async_request_rx, response_tx))
}
pub async fn handle_cli_connection(
(mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
app_state: Arc<AppState>,
mut cx: AsyncAppContext,
) {
if let Some(request) = requests.next().await {
match request {
CliRequest::Open { paths, wait } => {
let mut caret_positions = HashMap::new();
let paths = if paths.is_empty() {
workspace::last_opened_workspace_paths()
.await
.map(|location| location.paths().to_vec())
.unwrap_or_default()
} else {
paths
.into_iter()
.filter_map(|path_with_position_string| {
let path_with_position = PathLikeWithPosition::parse_str(
&path_with_position_string,
|path_str| {
Ok::<_, std::convert::Infallible>(
Path::new(path_str).to_path_buf(),
)
},
)
.expect("Infallible");
let path = path_with_position.path_like;
if let Some(row) = path_with_position.row {
if path.is_file() {
let row = row.saturating_sub(1);
let col =
path_with_position.column.unwrap_or(0).saturating_sub(1);
caret_positions.insert(path.clone(), Point::new(row, col));
}
}
Some(path)
})
.collect()
};
let mut errored = false;
match cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) {
Ok(task) => match task.await {
Ok((workspace, items)) => {
let mut item_release_futures = Vec::new();
for (item, path) in items.into_iter().zip(&paths) {
match item {
Some(Ok(item)) => {
if let Some(point) = caret_positions.remove(path) {
if let Some(active_editor) = item.downcast::<Editor>() {
workspace
.update(&mut cx, |_, cx| {
active_editor.update(cx, |editor, cx| {
let snapshot = editor
.snapshot(cx)
.display_snapshot;
let point = snapshot
.buffer_snapshot
.clip_point(point, Bias::Left);
editor.change_selections(
Some(Autoscroll::center()),
cx,
|s| s.select_ranges([point..point]),
);
});
})
.log_err();
}
}
cx.update(|cx| {
let released = oneshot::channel();
item.on_release(
cx,
Box::new(move |_| {
let _ = released.0.send(());
}),
)
.detach();
item_release_futures.push(released.1);
})
.log_err();
}
Some(Err(err)) => {
responses
.send(CliResponse::Stderr {
message: format!(
"error opening {:?}: {}",
path, err
),
})
.log_err();
errored = true;
}
None => {}
}
}
if wait {
let background = cx.background_executor().clone();
let wait = async move {
if paths.is_empty() {
let (done_tx, done_rx) = oneshot::channel();
let _subscription =
workspace.update(&mut cx, |workspace, cx| {
cx.on_release(move |_, _| {
let _ = done_tx.send(());
})
});
let _ = done_rx.await;
} else {
let _ = futures::future::try_join_all(item_release_futures)
.await;
};
}
.fuse();
futures::pin_mut!(wait);
loop {
// Repeatedly check if CLI is still open to avoid wasting resources
// waiting for files or workspaces to close.
let mut timer = background.timer(Duration::from_secs(1)).fuse();
futures::select_biased! {
_ = wait => break,
_ = timer => {
if responses.send(CliResponse::Ping).is_err() {
break;
}
}
}
}
}
}
Err(error) => {
errored = true;
responses
.send(CliResponse::Stderr {
message: format!("error opening {:?}: {}", paths, error),
})
.log_err();
}
},
Err(_) => errored = true,
}
responses
.send(CliResponse::Exit {
status: i32::from(errored),
})
.log_err();
}
}
}
}

View file

@ -7,216 +7,17 @@ mod only_instance;
mod open_listener;
pub use assets::*;
use collections::HashMap;
use gpui::{
point, px, AppContext, AsyncAppContext, AsyncWindowContext, Point, Task, TitlebarOptions,
WeakView, WindowBounds, WindowKind, WindowOptions,
point, px, AppContext, AsyncWindowContext, Task, TitlebarOptions, WeakView, WindowBounds,
WindowKind, WindowOptions,
};
pub use only_instance::*;
pub use open_listener::*;
use anyhow::{Context, Result};
use cli::{
ipc::{self, IpcSender},
CliRequest, CliResponse, IpcHandshake,
};
use futures::{
channel::{mpsc, oneshot},
FutureExt, SinkExt, StreamExt,
};
use std::{path::Path, sync::Arc, thread, time::Duration};
use util::{paths::PathLikeWithPosition, ResultExt};
use anyhow::Result;
use std::sync::Arc;
use uuid::Uuid;
use workspace2::{AppState, Workspace};
pub fn connect_to_cli(
server_name: &str,
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
.context("error connecting to cli")?;
let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
handshake_tx
.send(IpcHandshake {
requests: request_tx,
responses: response_rx,
})
.context("error sending ipc handshake")?;
let (mut async_request_tx, async_request_rx) =
futures::channel::mpsc::channel::<CliRequest>(16);
thread::spawn(move || {
while let Ok(cli_request) = request_rx.recv() {
if smol::block_on(async_request_tx.send(cli_request)).is_err() {
break;
}
}
Ok::<_, anyhow::Error>(())
});
Ok((async_request_rx, response_tx))
}
pub async fn handle_cli_connection(
(mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
app_state: Arc<AppState>,
mut cx: AsyncAppContext,
) {
if let Some(request) = requests.next().await {
match request {
CliRequest::Open { paths, wait } => {
let mut caret_positions = HashMap::default();
let paths = if paths.is_empty() {
workspace2::last_opened_workspace_paths()
.await
.map(|location| location.paths().to_vec())
.unwrap_or_default()
} else {
paths
.into_iter()
.filter_map(|path_with_position_string| {
let path_with_position = PathLikeWithPosition::parse_str(
&path_with_position_string,
|path_str| {
Ok::<_, std::convert::Infallible>(
Path::new(path_str).to_path_buf(),
)
},
)
.expect("Infallible");
let path = path_with_position.path_like;
if let Some(row) = path_with_position.row {
if path.is_file() {
let row = row.saturating_sub(1);
let col =
path_with_position.column.unwrap_or(0).saturating_sub(1);
caret_positions.insert(path.clone(), Point::new(row, col));
}
}
Some(path)
})
.collect::<Vec<_>>()
};
let mut errored = false;
if let Some(open_paths_task) = cx
.update(|cx| workspace2::open_paths(&paths, &app_state, None, cx))
.log_err()
{
match open_paths_task.await {
Ok((workspace, items)) => {
let mut item_release_futures = Vec::new();
for (item, path) in items.into_iter().zip(&paths) {
match item {
Some(Ok(mut item)) => {
if let Some(point) = caret_positions.remove(path) {
todo!("editor")
// if let Some(active_editor) = item.downcast::<Editor>() {
// active_editor
// .downgrade()
// .update(&mut cx, |editor, cx| {
// let snapshot =
// editor.snapshot(cx).display_snapshot;
// let point = snapshot
// .buffer_snapshot
// .clip_point(point, Bias::Left);
// editor.change_selections(
// Some(Autoscroll::center()),
// cx,
// |s| s.select_ranges([point..point]),
// );
// })
// .log_err();
// }
}
let released = oneshot::channel();
cx.update(move |cx| {
item.on_release(
cx,
Box::new(move |_| {
let _ = released.0.send(());
}),
)
.detach();
})
.ok();
item_release_futures.push(released.1);
}
Some(Err(err)) => {
responses
.send(CliResponse::Stderr {
message: format!(
"error opening {:?}: {}",
path, err
),
})
.log_err();
errored = true;
}
None => {}
}
}
if wait {
let executor = cx.background_executor().clone();
let wait = async move {
if paths.is_empty() {
let (done_tx, done_rx) = oneshot::channel();
let _subscription =
workspace.update(&mut cx, move |_, cx| {
cx.on_release(|_, _| {
let _ = done_tx.send(());
})
});
let _ = done_rx.await;
} else {
let _ = futures::future::try_join_all(item_release_futures)
.await;
};
}
.fuse();
futures::pin_mut!(wait);
loop {
// Repeatedly check if CLI is still open to avoid wasting resources
// waiting for files or workspaces to close.
let mut timer = executor.timer(Duration::from_secs(1)).fuse();
futures::select_biased! {
_ = wait => break,
_ = timer => {
if responses.send(CliResponse::Ping).is_err() {
break;
}
}
}
}
}
}
Err(error) => {
errored = true;
responses
.send(CliResponse::Stderr {
message: format!("error opening {:?}: {}", paths, error),
})
.log_err();
}
}
responses
.send(CliResponse::Exit {
status: i32::from(errored),
})
.log_err();
}
}
}
}
}
use workspace::{AppState, Workspace};
pub fn build_window_options(
bounds: Option<WindowBounds>,
@ -257,7 +58,7 @@ pub fn initialize_workspace(
let workspace_handle = cx.view();
cx.subscribe(&workspace_handle, {
move |workspace, _, event, cx| {
if let workspace2::Event::PaneAdded(pane) = event {
if let workspace::Event::PaneAdded(pane) = event {
pane.update(cx, |pane, cx| {
pane.toolbar().update(cx, |toolbar, cx| {
// todo!()