Merge pull request #218 from zed-industries/lsp
Integrate rust-analyzer and highlight diagnostics
This commit is contained in:
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -32,6 +32,11 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Download rust-analyzer
|
||||
run: |
|
||||
script/download-rust-analyzer
|
||||
echo "$PWD/vendor/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --no-fail-fast
|
||||
|
||||
@@ -63,6 +68,9 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Download rust-analyzer
|
||||
run: script/download-rust-analyzer
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
|
||||
61
Cargo.lock
generated
61
Cargo.lock
generated
@@ -328,6 +328,15 @@ dependencies = [
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-pipe"
|
||||
version = "0.1.3"
|
||||
source = "git+https://github.com/routerify/async-pipe-rs?rev=feeb77e83142a9ff837d0767652ae41bfc5d8e47#feeb77e83142a9ff837d0767652ae41bfc5d8e47"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "1.0.2"
|
||||
@@ -752,7 +761,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"log",
|
||||
"rand 0.8.3",
|
||||
"rpc",
|
||||
"seahash",
|
||||
"smallvec",
|
||||
"sum_tree",
|
||||
@@ -2300,6 +2308,7 @@ dependencies = [
|
||||
"etagere",
|
||||
"font-kit",
|
||||
"foreign-types",
|
||||
"futures",
|
||||
"gpui_macros",
|
||||
"image 0.23.14",
|
||||
"lazy_static",
|
||||
@@ -2819,7 +2828,9 @@ dependencies = [
|
||||
"gpui",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"lsp",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"rand 0.8.3",
|
||||
"rpc",
|
||||
"serde 1.0.125",
|
||||
@@ -2966,6 +2977,39 @@ dependencies = [
|
||||
"scoped-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lsp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-pipe",
|
||||
"futures",
|
||||
"gpui",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"serde 1.0.125",
|
||||
"serde_json 1.0.64",
|
||||
"simplelog",
|
||||
"smol",
|
||||
"unindent",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.91.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be7801b458592d0998af808d97f6a85a6057af3aaf2a2a5c3c677702bbeb4ed7"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"serde 1.0.125",
|
||||
"serde_json 1.0.64",
|
||||
"serde_repr",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzw"
|
||||
version = "0.10.0"
|
||||
@@ -3780,16 +3824,19 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"lsp",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"rand 0.8.3",
|
||||
"rpc",
|
||||
"serde 1.0.125",
|
||||
"serde_json 1.0.64",
|
||||
"simplelog",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempdir",
|
||||
"toml 0.5.8",
|
||||
"unindent",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -4589,6 +4636,17 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.0"
|
||||
@@ -6199,6 +6257,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"log-panics",
|
||||
"lsp",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"people_panel",
|
||||
|
||||
@@ -6,6 +6,14 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
|
||||
|
||||
## Development tips
|
||||
|
||||
### Compiling on macOS Monterey
|
||||
|
||||
The Zed server uses libcurl, which currently triggers [a bug](https://github.com/rust-lang/rust/issues/90342) in `rustc`. To work around this bug, export the following environment variable:
|
||||
|
||||
```
|
||||
export MACOSX_DEPLOYMENT_TARGET=10.7
|
||||
```
|
||||
|
||||
### Dump element JSON
|
||||
|
||||
If you trigger `cmd-shift-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way.
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
[package]
|
||||
name = "buffer"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
test-support = ["rand", "seahash"]
|
||||
|
||||
[dependencies]
|
||||
clock = { path = "../clock" }
|
||||
rpc = { path = "../rpc" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
anyhow = "1.0.38"
|
||||
arrayvec = "0.7.1"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use super::{Buffer, Content, Point};
|
||||
use super::{Buffer, Content, FromAnchor, FullOffset, Point, ToOffset};
|
||||
use anyhow::Result;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt::{Debug, Formatter},
|
||||
ops::Range,
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
use sum_tree::{Bias, SumTree};
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
|
||||
pub struct Anchor {
|
||||
pub offset: usize,
|
||||
pub full_offset: FullOffset,
|
||||
pub bias: Bias,
|
||||
pub version: clock::Global,
|
||||
}
|
||||
@@ -17,7 +17,7 @@ pub struct Anchor {
|
||||
#[derive(Clone)]
|
||||
pub struct AnchorMap<T> {
|
||||
pub(crate) version: clock::Global,
|
||||
pub(crate) entries: Vec<((usize, Bias), T)>,
|
||||
pub(crate) entries: Vec<((FullOffset, Bias), T)>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -26,16 +26,45 @@ pub struct AnchorSet(pub(crate) AnchorMap<()>);
|
||||
#[derive(Clone)]
|
||||
pub struct AnchorRangeMap<T> {
|
||||
pub(crate) version: clock::Global,
|
||||
pub(crate) entries: Vec<(Range<(usize, Bias)>, T)>,
|
||||
pub(crate) entries: Vec<(Range<(FullOffset, Bias)>, T)>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnchorRangeSet(pub(crate) AnchorRangeMap<()>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnchorRangeMultimap<T: Clone> {
|
||||
pub(crate) entries: SumTree<AnchorRangeMultimapEntry<T>>,
|
||||
pub(crate) version: clock::Global,
|
||||
pub(crate) start_bias: Bias,
|
||||
pub(crate) end_bias: Bias,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AnchorRangeMultimapEntry<T> {
|
||||
pub(crate) range: FullOffsetRange,
|
||||
pub(crate) value: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct FullOffsetRange {
|
||||
pub(crate) start: FullOffset,
|
||||
pub(crate) end: FullOffset,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct AnchorRangeMultimapSummary {
|
||||
start: FullOffset,
|
||||
end: FullOffset,
|
||||
min_start: FullOffset,
|
||||
max_end: FullOffset,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Anchor {
|
||||
pub fn min() -> Self {
|
||||
Self {
|
||||
offset: 0,
|
||||
full_offset: FullOffset(0),
|
||||
bias: Bias::Left,
|
||||
version: Default::default(),
|
||||
}
|
||||
@@ -43,7 +72,7 @@ impl Anchor {
|
||||
|
||||
pub fn max() -> Self {
|
||||
Self {
|
||||
offset: usize::MAX,
|
||||
full_offset: FullOffset::MAX,
|
||||
bias: Bias::Right,
|
||||
version: Default::default(),
|
||||
}
|
||||
@@ -57,7 +86,7 @@ impl Anchor {
|
||||
}
|
||||
|
||||
let offset_comparison = if self.version == other.version {
|
||||
self.offset.cmp(&other.offset)
|
||||
self.full_offset.cmp(&other.full_offset)
|
||||
} else {
|
||||
buffer
|
||||
.full_offset_for_anchor(self)
|
||||
@@ -147,12 +176,17 @@ impl<T> AnchorRangeMap<T> {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
pub fn from_raw(version: clock::Global, entries: Vec<(Range<(usize, Bias)>, T)>) -> Self {
|
||||
pub fn from_full_offset_ranges(
|
||||
version: clock::Global,
|
||||
entries: Vec<(Range<(FullOffset, Bias)>, T)>,
|
||||
) -> Self {
|
||||
Self { version, entries }
|
||||
}
|
||||
|
||||
pub fn raw_entries(&self) -> &[(Range<(usize, Bias)>, T)] {
|
||||
&self.entries
|
||||
pub fn full_offset_ranges(&self) -> impl Iterator<Item = (Range<FullOffset>, &T)> {
|
||||
self.entries
|
||||
.iter()
|
||||
.map(|(range, value)| (range.start.0..range.end.0, value))
|
||||
}
|
||||
|
||||
pub fn point_ranges<'a>(
|
||||
@@ -229,6 +263,196 @@ impl AnchorRangeSet {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Default for AnchorRangeMultimap<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
entries: Default::default(),
|
||||
version: Default::default(),
|
||||
start_bias: Bias::Left,
|
||||
end_bias: Bias::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> AnchorRangeMultimap<T> {
|
||||
pub fn version(&self) -> &clock::Global {
|
||||
&self.version
|
||||
}
|
||||
|
||||
pub fn intersecting_ranges<'a, I, O>(
|
||||
&'a self,
|
||||
range: Range<I>,
|
||||
content: Content<'a>,
|
||||
inclusive: bool,
|
||||
) -> impl Iterator<Item = (usize, Range<O>, &T)> + 'a
|
||||
where
|
||||
I: ToOffset,
|
||||
O: FromAnchor,
|
||||
{
|
||||
let end_bias = if inclusive { Bias::Right } else { Bias::Left };
|
||||
let range = range.start.to_full_offset(&content, Bias::Left)
|
||||
..range.end.to_full_offset(&content, end_bias);
|
||||
let mut cursor = self.entries.filter::<_, usize>(
|
||||
{
|
||||
let content = content.clone();
|
||||
let mut endpoint = Anchor {
|
||||
full_offset: FullOffset(0),
|
||||
bias: Bias::Right,
|
||||
version: self.version.clone(),
|
||||
};
|
||||
move |summary: &AnchorRangeMultimapSummary| {
|
||||
endpoint.full_offset = summary.max_end;
|
||||
endpoint.bias = self.end_bias;
|
||||
let max_end = endpoint.to_full_offset(&content, self.end_bias);
|
||||
let start_cmp = range.start.cmp(&max_end);
|
||||
|
||||
endpoint.full_offset = summary.min_start;
|
||||
endpoint.bias = self.start_bias;
|
||||
let min_start = endpoint.to_full_offset(&content, self.start_bias);
|
||||
let end_cmp = range.end.cmp(&min_start);
|
||||
|
||||
if inclusive {
|
||||
start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
|
||||
} else {
|
||||
start_cmp == Ordering::Less && end_cmp == Ordering::Greater
|
||||
}
|
||||
}
|
||||
},
|
||||
&(),
|
||||
);
|
||||
|
||||
std::iter::from_fn({
|
||||
let mut endpoint = Anchor {
|
||||
full_offset: FullOffset(0),
|
||||
bias: Bias::Left,
|
||||
version: self.version.clone(),
|
||||
};
|
||||
move || {
|
||||
if let Some(item) = cursor.item() {
|
||||
let ix = *cursor.start();
|
||||
endpoint.full_offset = item.range.start;
|
||||
endpoint.bias = self.start_bias;
|
||||
let start = O::from_anchor(&endpoint, &content);
|
||||
endpoint.full_offset = item.range.end;
|
||||
endpoint.bias = self.end_bias;
|
||||
let end = O::from_anchor(&endpoint, &content);
|
||||
let value = &item.value;
|
||||
cursor.next(&());
|
||||
Some((ix, start..end, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_full_offset_ranges(
|
||||
version: clock::Global,
|
||||
start_bias: Bias,
|
||||
end_bias: Bias,
|
||||
entries: impl Iterator<Item = (Range<FullOffset>, T)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
version,
|
||||
start_bias,
|
||||
end_bias,
|
||||
entries: SumTree::from_iter(
|
||||
entries.map(|(range, value)| AnchorRangeMultimapEntry {
|
||||
range: FullOffsetRange {
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
},
|
||||
value,
|
||||
}),
|
||||
&(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn full_offset_ranges(&self) -> impl Iterator<Item = (Range<FullOffset>, &T)> {
|
||||
self.entries
|
||||
.cursor::<()>()
|
||||
.map(|entry| (entry.range.start..entry.range.end, &entry.value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> sum_tree::Item for AnchorRangeMultimapEntry<T> {
|
||||
type Summary = AnchorRangeMultimapSummary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
AnchorRangeMultimapSummary {
|
||||
start: self.range.start,
|
||||
end: self.range.end,
|
||||
min_start: self.range.start,
|
||||
max_end: self.range.end,
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AnchorRangeMultimapSummary {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
start: FullOffset(0),
|
||||
end: FullOffset::MAX,
|
||||
min_start: FullOffset::MAX,
|
||||
max_end: FullOffset(0),
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for AnchorRangeMultimapSummary {
|
||||
type Context = ();
|
||||
|
||||
fn add_summary(&mut self, other: &Self, _: &Self::Context) {
|
||||
self.min_start = self.min_start.min(other.min_start);
|
||||
self.max_end = self.max_end.max(other.max_end);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let start_comparison = self.start.cmp(&other.start);
|
||||
assert!(start_comparison <= Ordering::Equal);
|
||||
if start_comparison == Ordering::Equal {
|
||||
assert!(self.end.cmp(&other.end) >= Ordering::Equal);
|
||||
}
|
||||
}
|
||||
|
||||
self.start = other.start;
|
||||
self.end = other.end;
|
||||
self.count += other.count;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FullOffsetRange {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
start: FullOffset(0),
|
||||
end: FullOffset::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, AnchorRangeMultimapSummary> for usize {
|
||||
fn add_summary(&mut self, summary: &'a AnchorRangeMultimapSummary, _: &()) {
|
||||
*self += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, AnchorRangeMultimapSummary> for FullOffsetRange {
|
||||
fn add_summary(&mut self, summary: &'a AnchorRangeMultimapSummary, _: &()) {
|
||||
self.start = summary.start;
|
||||
self.end = summary.end;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, AnchorRangeMultimapSummary, FullOffsetRange> for FullOffsetRange {
|
||||
fn cmp(&self, cursor_location: &FullOffsetRange, _: &()) -> Ordering {
|
||||
Ord::cmp(&self.start, &cursor_location.start)
|
||||
.then_with(|| Ord::cmp(&cursor_location.end, &self.end))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnchorRangeExt {
|
||||
fn cmp<'a>(&self, b: &Range<Anchor>, buffer: impl Into<Content<'a>>) -> Result<Ordering>;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,14 @@ impl<'a> Add<&'a Self> for Point {
|
||||
type Output = Point;
|
||||
|
||||
fn add(self, other: &'a Self) -> Self::Output {
|
||||
self + *other
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Point {
|
||||
type Output = Point;
|
||||
|
||||
fn add(self, other: Self) -> Self::Output {
|
||||
if other.row == 0 {
|
||||
Point::new(self.row, self.column + other.column)
|
||||
} else {
|
||||
@@ -40,25 +48,11 @@ impl<'a> Add<&'a Self> for Point {
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Point {
|
||||
type Output = Point;
|
||||
|
||||
fn add(self, other: Self) -> Self::Output {
|
||||
self + &other
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Sub<&'a Self> for Point {
|
||||
type Output = Point;
|
||||
|
||||
fn sub(self, other: &'a Self) -> Self::Output {
|
||||
debug_assert!(*other <= self);
|
||||
|
||||
if self.row == other.row {
|
||||
Point::new(0, self.column - other.column)
|
||||
} else {
|
||||
Point::new(self.row - other.row, self.column)
|
||||
}
|
||||
self - *other
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +60,13 @@ impl Sub for Point {
|
||||
type Output = Point;
|
||||
|
||||
fn sub(self, other: Self) -> Self::Output {
|
||||
self - &other
|
||||
debug_assert!(other <= self);
|
||||
|
||||
if self.row == other.row {
|
||||
Point::new(0, self.column - other.column)
|
||||
} else {
|
||||
Point::new(self.row - other.row, self.column)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
111
crates/buffer/src/point_utf16.rs
Normal file
111
crates/buffer/src/point_utf16.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
ops::{Add, AddAssign, Sub},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash)]
|
||||
pub struct PointUtf16 {
|
||||
pub row: u32,
|
||||
pub column: u32,
|
||||
}
|
||||
|
||||
impl PointUtf16 {
|
||||
pub const MAX: Self = Self {
|
||||
row: u32::MAX,
|
||||
column: u32::MAX,
|
||||
};
|
||||
|
||||
pub fn new(row: u32, column: u32) -> Self {
|
||||
PointUtf16 { row, column }
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
PointUtf16::new(0, 0)
|
||||
}
|
||||
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.row == 0 && self.column == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Add<&'a Self> for PointUtf16 {
|
||||
type Output = PointUtf16;
|
||||
|
||||
fn add(self, other: &'a Self) -> Self::Output {
|
||||
self + *other
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for PointUtf16 {
|
||||
type Output = PointUtf16;
|
||||
|
||||
fn add(self, other: Self) -> Self::Output {
|
||||
if other.row == 0 {
|
||||
PointUtf16::new(self.row, self.column + other.column)
|
||||
} else {
|
||||
PointUtf16::new(self.row + other.row, other.column)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Sub<&'a Self> for PointUtf16 {
|
||||
type Output = PointUtf16;
|
||||
|
||||
fn sub(self, other: &'a Self) -> Self::Output {
|
||||
self - *other
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for PointUtf16 {
|
||||
type Output = PointUtf16;
|
||||
|
||||
fn sub(self, other: Self) -> Self::Output {
|
||||
debug_assert!(other <= self);
|
||||
|
||||
if self.row == other.row {
|
||||
PointUtf16::new(0, self.column - other.column)
|
||||
} else {
|
||||
PointUtf16::new(self.row - other.row, self.column)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AddAssign<&'a Self> for PointUtf16 {
|
||||
fn add_assign(&mut self, other: &'a Self) {
|
||||
*self += *other;
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign<Self> for PointUtf16 {
|
||||
fn add_assign(&mut self, other: Self) {
|
||||
if other.row == 0 {
|
||||
self.column += other.column;
|
||||
} else {
|
||||
self.row += other.row;
|
||||
self.column = other.column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for PointUtf16 {
|
||||
fn partial_cmp(&self, other: &PointUtf16) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for PointUtf16 {
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn cmp(&self, other: &PointUtf16) -> Ordering {
|
||||
let a = (self.row as usize) << 32 | self.column as usize;
|
||||
let b = (other.row as usize) << 32 | other.column as usize;
|
||||
a.cmp(&b)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
fn cmp(&self, other: &PointUtf16) -> Ordering {
|
||||
match self.row.cmp(&other.row) {
|
||||
Ordering::Equal => self.column.cmp(&other.column),
|
||||
comparison @ _ => comparison,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::PointUtf16;
|
||||
|
||||
use super::Point;
|
||||
use arrayvec::ArrayString;
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp, ops::Range, str};
|
||||
use sum_tree::{Bias, SumTree};
|
||||
use sum_tree::{Bias, Dimension, SumTree};
|
||||
|
||||
#[cfg(test)]
|
||||
const CHUNK_BASE: usize = 6;
|
||||
@@ -136,7 +138,7 @@ impl Rope {
|
||||
Chunks::new(self, range, true)
|
||||
}
|
||||
|
||||
pub fn to_point(&self, offset: usize) -> Point {
|
||||
pub fn offset_to_point(&self, offset: usize) -> Point {
|
||||
assert!(offset <= self.summary().bytes);
|
||||
let mut cursor = self.chunks.cursor::<(usize, Point)>();
|
||||
cursor.seek(&offset, Bias::Left, &());
|
||||
@@ -144,15 +146,40 @@ impl Rope {
|
||||
cursor.start().1
|
||||
+ cursor
|
||||
.item()
|
||||
.map_or(Point::zero(), |chunk| chunk.to_point(overshoot))
|
||||
.map_or(Point::zero(), |chunk| chunk.offset_to_point(overshoot))
|
||||
}
|
||||
|
||||
pub fn to_offset(&self, point: Point) -> usize {
|
||||
pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 {
|
||||
assert!(offset <= self.summary().bytes);
|
||||
let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>();
|
||||
cursor.seek(&offset, Bias::Left, &());
|
||||
let overshoot = offset - cursor.start().0;
|
||||
cursor.start().1
|
||||
+ cursor.item().map_or(PointUtf16::zero(), |chunk| {
|
||||
chunk.offset_to_point_utf16(overshoot)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn point_to_offset(&self, point: Point) -> usize {
|
||||
assert!(point <= self.summary().lines);
|
||||
let mut cursor = self.chunks.cursor::<(Point, usize)>();
|
||||
cursor.seek(&point, Bias::Left, &());
|
||||
let overshoot = point - cursor.start().0;
|
||||
cursor.start().1 + cursor.item().map_or(0, |chunk| chunk.to_offset(overshoot))
|
||||
cursor.start().1
|
||||
+ cursor
|
||||
.item()
|
||||
.map_or(0, |chunk| chunk.point_to_offset(overshoot))
|
||||
}
|
||||
|
||||
pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize {
|
||||
assert!(point <= self.summary().lines_utf16);
|
||||
let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>();
|
||||
cursor.seek(&point, Bias::Left, &());
|
||||
let overshoot = point - cursor.start().0;
|
||||
cursor.start().1
|
||||
+ cursor
|
||||
.item()
|
||||
.map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot))
|
||||
}
|
||||
|
||||
pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize {
|
||||
@@ -188,6 +215,17 @@ impl Rope {
|
||||
self.summary().lines
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
|
||||
let mut cursor = self.chunks.cursor::<PointUtf16>();
|
||||
cursor.seek(&point, Bias::Right, &());
|
||||
if let Some(chunk) = cursor.item() {
|
||||
let overshoot = point - cursor.start();
|
||||
*cursor.start() + chunk.clip_point_utf16(overshoot, bias)
|
||||
} else {
|
||||
self.summary().lines_utf16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Rope {
|
||||
@@ -258,22 +296,24 @@ impl<'a> Cursor<'a> {
|
||||
slice
|
||||
}
|
||||
|
||||
pub fn summary(&mut self, end_offset: usize) -> TextSummary {
|
||||
pub fn summary<D: TextDimension<'a>>(&mut self, end_offset: usize) -> D {
|
||||
debug_assert!(end_offset >= self.offset);
|
||||
|
||||
let mut summary = TextSummary::default();
|
||||
let mut summary = D::default();
|
||||
if let Some(start_chunk) = self.chunks.item() {
|
||||
let start_ix = self.offset - self.chunks.start();
|
||||
let end_ix = cmp::min(end_offset, self.chunks.end(&())) - self.chunks.start();
|
||||
summary = TextSummary::from(&start_chunk.0[start_ix..end_ix]);
|
||||
summary.add_assign(&D::from_summary(&TextSummary::from(
|
||||
&start_chunk.0[start_ix..end_ix],
|
||||
)));
|
||||
}
|
||||
|
||||
if end_offset > self.chunks.end(&()) {
|
||||
self.chunks.next(&());
|
||||
summary += &self.chunks.summary(&end_offset, Bias::Right, &());
|
||||
summary.add_assign(&self.chunks.summary(&end_offset, Bias::Right, &()));
|
||||
if let Some(end_chunk) = self.chunks.item() {
|
||||
let end_ix = end_offset - self.chunks.start();
|
||||
summary += TextSummary::from(&end_chunk.0[..end_ix]);
|
||||
summary.add_assign(&D::from_summary(&TextSummary::from(&end_chunk.0[..end_ix])));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +415,7 @@ impl<'a> Iterator for Chunks<'a> {
|
||||
struct Chunk(ArrayString<{ 2 * CHUNK_BASE }>);
|
||||
|
||||
impl Chunk {
|
||||
fn to_point(&self, target: usize) -> Point {
|
||||
fn offset_to_point(&self, target: usize) -> Point {
|
||||
let mut offset = 0;
|
||||
let mut point = Point::new(0, 0);
|
||||
for ch in self.0.chars() {
|
||||
@@ -394,7 +434,26 @@ impl Chunk {
|
||||
point
|
||||
}
|
||||
|
||||
fn to_offset(&self, target: Point) -> usize {
|
||||
fn offset_to_point_utf16(&self, target: usize) -> PointUtf16 {
|
||||
let mut offset = 0;
|
||||
let mut point = PointUtf16::new(0, 0);
|
||||
for ch in self.0.chars() {
|
||||
if offset >= target {
|
||||
break;
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
point.row += 1;
|
||||
point.column = 0;
|
||||
} else {
|
||||
point.column += ch.len_utf16() as u32;
|
||||
}
|
||||
offset += ch.len_utf8();
|
||||
}
|
||||
point
|
||||
}
|
||||
|
||||
fn point_to_offset(&self, target: Point) -> usize {
|
||||
let mut offset = 0;
|
||||
let mut point = Point::new(0, 0);
|
||||
for ch in self.0.chars() {
|
||||
@@ -416,6 +475,28 @@ impl Chunk {
|
||||
offset
|
||||
}
|
||||
|
||||
fn point_utf16_to_offset(&self, target: PointUtf16) -> usize {
|
||||
let mut offset = 0;
|
||||
let mut point = PointUtf16::new(0, 0);
|
||||
for ch in self.0.chars() {
|
||||
if point >= target {
|
||||
if point > target {
|
||||
panic!("point {:?} is inside of character {:?}", target, ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
point.row += 1;
|
||||
point.column = 0;
|
||||
} else {
|
||||
point.column += ch.len_utf16() as u32;
|
||||
}
|
||||
offset += ch.len_utf8();
|
||||
}
|
||||
offset
|
||||
}
|
||||
|
||||
fn clip_point(&self, target: Point, bias: Bias) -> Point {
|
||||
for (row, line) in self.0.split('\n').enumerate() {
|
||||
if row == target.row as usize {
|
||||
@@ -431,6 +512,23 @@ impl Chunk {
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn clip_point_utf16(&self, target: PointUtf16, bias: Bias) -> PointUtf16 {
|
||||
for (row, line) in self.0.split('\n').enumerate() {
|
||||
if row == target.row as usize {
|
||||
let mut code_units = line.encode_utf16();
|
||||
let mut column = code_units.by_ref().take(target.column as usize).count();
|
||||
if char::decode_utf16(code_units).next().transpose().is_err() {
|
||||
match bias {
|
||||
Bias::Left => column -= 1,
|
||||
Bias::Right => column += 1,
|
||||
}
|
||||
}
|
||||
return PointUtf16::new(row as u32, column as u32);
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for Chunk {
|
||||
@@ -445,6 +543,7 @@ impl sum_tree::Item for Chunk {
|
||||
pub struct TextSummary {
|
||||
pub bytes: usize,
|
||||
pub lines: Point,
|
||||
pub lines_utf16: PointUtf16,
|
||||
pub first_line_chars: u32,
|
||||
pub last_line_chars: u32,
|
||||
pub longest_row: u32,
|
||||
@@ -454,17 +553,19 @@ pub struct TextSummary {
|
||||
impl<'a> From<&'a str> for TextSummary {
|
||||
fn from(text: &'a str) -> Self {
|
||||
let mut lines = Point::new(0, 0);
|
||||
let mut lines_utf16 = PointUtf16::new(0, 0);
|
||||
let mut first_line_chars = 0;
|
||||
let mut last_line_chars = 0;
|
||||
let mut longest_row = 0;
|
||||
let mut longest_row_chars = 0;
|
||||
for c in text.chars() {
|
||||
if c == '\n' {
|
||||
lines.row += 1;
|
||||
lines.column = 0;
|
||||
lines += Point::new(1, 0);
|
||||
lines_utf16 += PointUtf16::new(1, 0);
|
||||
last_line_chars = 0;
|
||||
} else {
|
||||
lines.column += c.len_utf8() as u32;
|
||||
lines_utf16.column += c.len_utf16() as u32;
|
||||
last_line_chars += 1;
|
||||
}
|
||||
|
||||
@@ -481,6 +582,7 @@ impl<'a> From<&'a str> for TextSummary {
|
||||
TextSummary {
|
||||
bytes: text.len(),
|
||||
lines,
|
||||
lines_utf16,
|
||||
first_line_chars,
|
||||
last_line_chars,
|
||||
longest_row,
|
||||
@@ -520,7 +622,8 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
|
||||
}
|
||||
|
||||
self.bytes += other.bytes;
|
||||
self.lines += &other.lines;
|
||||
self.lines += other.lines;
|
||||
self.lines_utf16 += other.lines_utf16;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,15 +633,77 @@ impl std::ops::AddAssign<Self> for TextSummary {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TextDimension<'a>: Dimension<'a, TextSummary> {
|
||||
fn from_summary(summary: &TextSummary) -> Self;
|
||||
fn add_assign(&mut self, other: &Self);
|
||||
}
|
||||
|
||||
impl<'a, D1: TextDimension<'a>, D2: TextDimension<'a>> TextDimension<'a> for (D1, D2) {
|
||||
fn from_summary(summary: &TextSummary) -> Self {
|
||||
(D1::from_summary(summary), D2::from_summary(summary))
|
||||
}
|
||||
|
||||
fn add_assign(&mut self, other: &Self) {
|
||||
self.0.add_assign(&other.0);
|
||||
self.1.add_assign(&other.1);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TextDimension<'a> for TextSummary {
|
||||
fn from_summary(summary: &TextSummary) -> Self {
|
||||
summary.clone()
|
||||
}
|
||||
|
||||
fn add_assign(&mut self, other: &Self) {
|
||||
*self += other;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, TextSummary> for usize {
|
||||
fn add_summary(&mut self, summary: &'a TextSummary, _: &()) {
|
||||
*self += summary.bytes;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TextDimension<'a> for usize {
|
||||
fn from_summary(summary: &TextSummary) -> Self {
|
||||
summary.bytes
|
||||
}
|
||||
|
||||
fn add_assign(&mut self, other: &Self) {
|
||||
*self += other;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, TextSummary> for Point {
|
||||
fn add_summary(&mut self, summary: &'a TextSummary, _: &()) {
|
||||
*self += &summary.lines;
|
||||
*self += summary.lines;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TextDimension<'a> for Point {
|
||||
fn from_summary(summary: &TextSummary) -> Self {
|
||||
summary.lines
|
||||
}
|
||||
|
||||
fn add_assign(&mut self, other: &Self) {
|
||||
*self += other;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, TextSummary> for PointUtf16 {
|
||||
fn add_summary(&mut self, summary: &'a TextSummary, _: &()) {
|
||||
*self += summary.lines_utf16;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TextDimension<'a> for PointUtf16 {
|
||||
fn from_summary(summary: &TextSummary) -> Self {
|
||||
summary.lines_utf16
|
||||
}
|
||||
|
||||
fn add_assign(&mut self, other: &Self) {
|
||||
*self += other;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,6 +742,41 @@ mod tests {
|
||||
assert_eq!(rope.text(), text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clip() {
|
||||
let rope = Rope::from("🧘");
|
||||
|
||||
assert_eq!(rope.clip_offset(1, Bias::Left), 0);
|
||||
assert_eq!(rope.clip_offset(1, Bias::Right), 4);
|
||||
assert_eq!(rope.clip_offset(5, Bias::Right), 4);
|
||||
|
||||
assert_eq!(
|
||||
rope.clip_point(Point::new(0, 1), Bias::Left),
|
||||
Point::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
rope.clip_point(Point::new(0, 1), Bias::Right),
|
||||
Point::new(0, 4)
|
||||
);
|
||||
assert_eq!(
|
||||
rope.clip_point(Point::new(0, 5), Bias::Right),
|
||||
Point::new(0, 4)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rope.clip_point_utf16(PointUtf16::new(0, 1), Bias::Left),
|
||||
PointUtf16::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
rope.clip_point_utf16(PointUtf16::new(0, 1), Bias::Right),
|
||||
PointUtf16::new(0, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
rope.clip_point_utf16(PointUtf16::new(0, 3), Bias::Right),
|
||||
PointUtf16::new(0, 2)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random(mut rng: StdRng) {
|
||||
let operations = env::var("OPERATIONS")
|
||||
@@ -624,14 +824,33 @@ mod tests {
|
||||
}
|
||||
|
||||
let mut point = Point::new(0, 0);
|
||||
let mut point_utf16 = PointUtf16::new(0, 0);
|
||||
for (ix, ch) in expected.char_indices().chain(Some((expected.len(), '\0'))) {
|
||||
assert_eq!(actual.to_point(ix), point, "to_point({})", ix);
|
||||
assert_eq!(actual.to_offset(point), ix, "to_offset({:?})", point);
|
||||
assert_eq!(actual.offset_to_point(ix), point, "offset_to_point({})", ix);
|
||||
assert_eq!(
|
||||
actual.offset_to_point_utf16(ix),
|
||||
point_utf16,
|
||||
"offset_to_point_utf16({})",
|
||||
ix
|
||||
);
|
||||
assert_eq!(
|
||||
actual.point_to_offset(point),
|
||||
ix,
|
||||
"point_to_offset({:?})",
|
||||
point
|
||||
);
|
||||
assert_eq!(
|
||||
actual.point_utf16_to_offset(point_utf16),
|
||||
ix,
|
||||
"point_utf16_to_offset({:?})",
|
||||
point_utf16
|
||||
);
|
||||
if ch == '\n' {
|
||||
point.row += 1;
|
||||
point.column = 0
|
||||
point += Point::new(1, 0);
|
||||
point_utf16 += PointUtf16::new(1, 0);
|
||||
} else {
|
||||
point.column += ch.len_utf8() as u32;
|
||||
point_utf16.column += ch.len_utf16() as u32;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,7 +858,7 @@ mod tests {
|
||||
let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right);
|
||||
let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left);
|
||||
assert_eq!(
|
||||
actual.cursor(start_ix).summary(end_ix),
|
||||
actual.cursor(start_ix).summary::<TextSummary>(end_ix),
|
||||
TextSummary::from(&expected[start_ix..end_ix])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::{AnchorRangeMap, Buffer, Content, Point, ToOffset, ToPoint};
|
||||
use rpc::proto;
|
||||
use super::{AnchorRangeMap, Buffer, Content, Point, ToOffset, ToPoint};
|
||||
use std::{cmp::Ordering, ops::Range, sync::Arc};
|
||||
use sum_tree::Bias;
|
||||
|
||||
pub type SelectionSetId = clock::Lamport;
|
||||
pub type SelectionsVersion = usize;
|
||||
@@ -129,53 +127,3 @@ impl SelectionSet {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<proto::SelectionSet> for &'a SelectionSet {
|
||||
fn into(self) -> proto::SelectionSet {
|
||||
let version = self.selections.version();
|
||||
let entries = self.selections.raw_entries();
|
||||
proto::SelectionSet {
|
||||
replica_id: self.id.replica_id as u32,
|
||||
lamport_timestamp: self.id.value as u32,
|
||||
is_active: self.active,
|
||||
version: version.into(),
|
||||
selections: entries
|
||||
.iter()
|
||||
.map(|(range, state)| proto::Selection {
|
||||
id: state.id as u64,
|
||||
start: range.start.0 as u64,
|
||||
end: range.end.0 as u64,
|
||||
reversed: state.reversed,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::SelectionSet> for SelectionSet {
|
||||
fn from(set: proto::SelectionSet) -> Self {
|
||||
Self {
|
||||
id: clock::Lamport {
|
||||
replica_id: set.replica_id as u16,
|
||||
value: set.lamport_timestamp,
|
||||
},
|
||||
active: set.is_active,
|
||||
selections: Arc::new(AnchorRangeMap::from_raw(
|
||||
set.version.into(),
|
||||
set.selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let range = (selection.start as usize, Bias::Left)
|
||||
..(selection.end as usize, Bias::Right);
|
||||
let state = SelectionState {
|
||||
id: selection.id as usize,
|
||||
reversed: selection.reversed,
|
||||
goal: SelectionGoal::None,
|
||||
};
|
||||
(range, state)
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ fn test_random_edits(mut rng: StdRng) {
|
||||
|
||||
for mut old_buffer in buffer_versions {
|
||||
let edits = buffer
|
||||
.edits_since(old_buffer.version.clone())
|
||||
.edits_since::<usize>(&old_buffer.version)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
log::info!(
|
||||
@@ -88,12 +88,12 @@ fn test_random_edits(mut rng: StdRng) {
|
||||
edits,
|
||||
);
|
||||
|
||||
let mut delta = 0_isize;
|
||||
for edit in edits {
|
||||
let old_start = (edit.old_bytes.start as isize + delta) as usize;
|
||||
let new_text: String = buffer.text_for_range(edit.new_bytes.clone()).collect();
|
||||
old_buffer.edit(Some(old_start..old_start + edit.deleted_bytes()), new_text);
|
||||
delta += edit.delta();
|
||||
let new_text: String = buffer.text_for_range(edit.new.clone()).collect();
|
||||
old_buffer.edit(
|
||||
Some(edit.new.start..edit.new.start + edit.old.len()),
|
||||
new_text,
|
||||
);
|
||||
}
|
||||
assert_eq!(old_buffer.text(), buffer.text());
|
||||
}
|
||||
@@ -123,6 +123,7 @@ fn test_text_summary_for_range() {
|
||||
TextSummary {
|
||||
bytes: 2,
|
||||
lines: Point::new(1, 0),
|
||||
lines_utf16: PointUtf16::new(1, 0),
|
||||
first_line_chars: 1,
|
||||
last_line_chars: 0,
|
||||
longest_row: 0,
|
||||
@@ -134,6 +135,7 @@ fn test_text_summary_for_range() {
|
||||
TextSummary {
|
||||
bytes: 11,
|
||||
lines: Point::new(3, 0),
|
||||
lines_utf16: PointUtf16::new(3, 0),
|
||||
first_line_chars: 1,
|
||||
last_line_chars: 0,
|
||||
longest_row: 2,
|
||||
@@ -145,6 +147,7 @@ fn test_text_summary_for_range() {
|
||||
TextSummary {
|
||||
bytes: 20,
|
||||
lines: Point::new(4, 1),
|
||||
lines_utf16: PointUtf16::new(4, 1),
|
||||
first_line_chars: 2,
|
||||
last_line_chars: 1,
|
||||
longest_row: 3,
|
||||
@@ -156,6 +159,7 @@ fn test_text_summary_for_range() {
|
||||
TextSummary {
|
||||
bytes: 22,
|
||||
lines: Point::new(4, 3),
|
||||
lines_utf16: PointUtf16::new(4, 3),
|
||||
first_line_chars: 2,
|
||||
last_line_chars: 3,
|
||||
longest_row: 3,
|
||||
@@ -167,6 +171,7 @@ fn test_text_summary_for_range() {
|
||||
TextSummary {
|
||||
bytes: 15,
|
||||
lines: Point::new(2, 3),
|
||||
lines_utf16: PointUtf16::new(2, 3),
|
||||
first_line_chars: 4,
|
||||
last_line_chars: 3,
|
||||
longest_row: 1,
|
||||
|
||||
@@ -359,7 +359,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{movement, test::*};
|
||||
use gpui::{color::Color, MutableAppContext};
|
||||
use language::{History, Language, LanguageConfig, RandomCharIter, SelectionGoal};
|
||||
use language::{Language, LanguageConfig, RandomCharIter, SelectionGoal};
|
||||
use rand::{prelude::StdRng, Rng};
|
||||
use std::{env, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
@@ -701,9 +701,8 @@ mod tests {
|
||||
);
|
||||
lang.set_theme(&theme);
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
Buffer::from_history(0, History::new(text.into()), None, Some(lang), cx)
|
||||
});
|
||||
let buffer =
|
||||
cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx));
|
||||
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
|
||||
|
||||
let tab_size = 2;
|
||||
@@ -789,9 +788,8 @@ mod tests {
|
||||
);
|
||||
lang.set_theme(&theme);
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
Buffer::from_history(0, History::new(text.into()), None, Some(lang), cx)
|
||||
});
|
||||
let buffer =
|
||||
cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx));
|
||||
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
|
||||
|
||||
let font_cache = cx.font_cache();
|
||||
@@ -974,16 +972,16 @@ mod tests {
|
||||
) -> Vec<(String, Option<&'a str>)> {
|
||||
let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let mut chunks: Vec<(String, Option<&str>)> = Vec::new();
|
||||
for (chunk, style_id) in snapshot.highlighted_chunks_for_rows(rows) {
|
||||
let style_name = style_id.name(theme);
|
||||
for chunk in snapshot.highlighted_chunks_for_rows(rows) {
|
||||
let style_name = chunk.highlight_id.name(theme);
|
||||
if let Some((last_chunk, last_style_name)) = chunks.last_mut() {
|
||||
if style_name == *last_style_name {
|
||||
last_chunk.push_str(chunk);
|
||||
last_chunk.push_str(chunk.text);
|
||||
} else {
|
||||
chunks.push((chunk.to_string(), style_name));
|
||||
chunks.push((chunk.text.to_string(), style_name));
|
||||
}
|
||||
} else {
|
||||
chunks.push((chunk.to_string(), style_name));
|
||||
chunks.push((chunk.text.to_string(), style_name));
|
||||
}
|
||||
}
|
||||
chunks
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use gpui::{AppContext, ModelHandle};
|
||||
use language::{Anchor, AnchorRangeExt, Buffer, HighlightId, Point, TextSummary, ToOffset};
|
||||
use language::{
|
||||
Anchor, AnchorRangeExt, Buffer, HighlightId, HighlightedChunk, Point, PointUtf16, TextSummary,
|
||||
ToOffset,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
@@ -110,9 +113,8 @@ impl<'a> FoldMapWriter<'a> {
|
||||
let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
|
||||
folds.push(fold);
|
||||
edits.push(buffer::Edit {
|
||||
old_bytes: range.clone(),
|
||||
new_bytes: range.clone(),
|
||||
..Default::default()
|
||||
old: range.clone(),
|
||||
new: range,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -155,9 +157,8 @@ impl<'a> FoldMapWriter<'a> {
|
||||
while let Some(fold) = folds_cursor.item() {
|
||||
let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer);
|
||||
edits.push(buffer::Edit {
|
||||
old_bytes: offset_range.clone(),
|
||||
new_bytes: offset_range,
|
||||
..Default::default()
|
||||
old: offset_range.clone(),
|
||||
new: offset_range,
|
||||
});
|
||||
fold_ixs_to_delete.push(*folds_cursor.start());
|
||||
folds_cursor.next(&buffer);
|
||||
@@ -202,6 +203,7 @@ pub struct FoldMap {
|
||||
struct SyncState {
|
||||
version: clock::Global,
|
||||
parse_count: usize,
|
||||
diagnostics_update_count: usize,
|
||||
}
|
||||
|
||||
impl FoldMap {
|
||||
@@ -223,6 +225,7 @@ impl FoldMap {
|
||||
last_sync: Mutex::new(SyncState {
|
||||
version: buffer.version(),
|
||||
parse_count: buffer.parse_count(),
|
||||
diagnostics_update_count: buffer.diagnostics_update_count(),
|
||||
}),
|
||||
version: AtomicUsize::new(0),
|
||||
};
|
||||
@@ -254,14 +257,17 @@ impl FoldMap {
|
||||
SyncState {
|
||||
version: buffer.version(),
|
||||
parse_count: buffer.parse_count(),
|
||||
diagnostics_update_count: buffer.diagnostics_update_count(),
|
||||
},
|
||||
);
|
||||
let edits = buffer
|
||||
.edits_since(last_sync.version)
|
||||
.edits_since(&last_sync.version)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>();
|
||||
if edits.is_empty() {
|
||||
if last_sync.parse_count != buffer.parse_count() {
|
||||
if last_sync.parse_count != buffer.parse_count()
|
||||
|| last_sync.diagnostics_update_count != buffer.diagnostics_update_count()
|
||||
{
|
||||
self.version.fetch_add(1, SeqCst);
|
||||
}
|
||||
Vec::new()
|
||||
@@ -281,7 +287,11 @@ impl FoldMap {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_edits(&self, buffer_edits: Vec<buffer::Edit>, cx: &AppContext) -> Vec<FoldEdit> {
|
||||
fn apply_edits(
|
||||
&self,
|
||||
buffer_edits: Vec<buffer::Edit<usize>>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<FoldEdit> {
|
||||
let buffer = self.buffer.read(cx).snapshot();
|
||||
let mut buffer_edits_iter = buffer_edits.iter().cloned().peekable();
|
||||
|
||||
@@ -291,28 +301,28 @@ impl FoldMap {
|
||||
cursor.seek(&0, Bias::Right, &());
|
||||
|
||||
while let Some(mut edit) = buffer_edits_iter.next() {
|
||||
new_transforms.push_tree(cursor.slice(&edit.old_bytes.start, Bias::Left, &()), &());
|
||||
edit.new_bytes.start -= edit.old_bytes.start - cursor.start();
|
||||
edit.old_bytes.start = *cursor.start();
|
||||
new_transforms.push_tree(cursor.slice(&edit.old.start, Bias::Left, &()), &());
|
||||
edit.new.start -= edit.old.start - cursor.start();
|
||||
edit.old.start = *cursor.start();
|
||||
|
||||
cursor.seek(&edit.old_bytes.end, Bias::Right, &());
|
||||
cursor.seek(&edit.old.end, Bias::Right, &());
|
||||
cursor.next(&());
|
||||
|
||||
let mut delta = edit.delta();
|
||||
let mut delta = edit.new.len() as isize - edit.old.len() as isize;
|
||||
loop {
|
||||
edit.old_bytes.end = *cursor.start();
|
||||
edit.old.end = *cursor.start();
|
||||
|
||||
if let Some(next_edit) = buffer_edits_iter.peek() {
|
||||
if next_edit.old_bytes.start > edit.old_bytes.end {
|
||||
if next_edit.old.start > edit.old.end {
|
||||
break;
|
||||
}
|
||||
|
||||
let next_edit = buffer_edits_iter.next().unwrap();
|
||||
delta += next_edit.delta();
|
||||
delta += next_edit.new.len() as isize - next_edit.old.len() as isize;
|
||||
|
||||
if next_edit.old_bytes.end >= edit.old_bytes.end {
|
||||
edit.old_bytes.end = next_edit.old_bytes.end;
|
||||
cursor.seek(&edit.old_bytes.end, Bias::Right, &());
|
||||
if next_edit.old.end >= edit.old.end {
|
||||
edit.old.end = next_edit.old.end;
|
||||
cursor.seek(&edit.old.end, Bias::Right, &());
|
||||
cursor.next(&());
|
||||
}
|
||||
} else {
|
||||
@@ -320,10 +330,9 @@ impl FoldMap {
|
||||
}
|
||||
}
|
||||
|
||||
edit.new_bytes.end =
|
||||
((edit.new_bytes.start + edit.deleted_bytes()) as isize + delta) as usize;
|
||||
edit.new.end = ((edit.new.start + edit.old.len()) as isize + delta) as usize;
|
||||
|
||||
let anchor = buffer.anchor_before(edit.new_bytes.start);
|
||||
let anchor = buffer.anchor_before(edit.new.start);
|
||||
let mut folds_cursor = self.folds.cursor::<Fold>();
|
||||
folds_cursor.seek(&Fold(anchor..Anchor::max()), Bias::Left, &buffer);
|
||||
|
||||
@@ -339,10 +348,7 @@ impl FoldMap {
|
||||
})
|
||||
.peekable();
|
||||
|
||||
while folds
|
||||
.peek()
|
||||
.map_or(false, |fold| fold.start < edit.new_bytes.end)
|
||||
{
|
||||
while folds.peek().map_or(false, |fold| fold.start < edit.new.end) {
|
||||
let mut fold = folds.next().unwrap();
|
||||
let sum = new_transforms.summary();
|
||||
|
||||
@@ -375,13 +381,15 @@ impl FoldMap {
|
||||
if fold.end > fold.start {
|
||||
let output_text = "…";
|
||||
let chars = output_text.chars().count() as u32;
|
||||
let lines = super::Point::new(0, output_text.len() as u32);
|
||||
let lines = Point::new(0, output_text.len() as u32);
|
||||
let lines_utf16 = PointUtf16::new(0, output_text.encode_utf16().count() as u32);
|
||||
new_transforms.push(
|
||||
Transform {
|
||||
summary: TransformSummary {
|
||||
output: TextSummary {
|
||||
bytes: output_text.len(),
|
||||
lines,
|
||||
lines_utf16,
|
||||
first_line_chars: chars,
|
||||
last_line_chars: chars,
|
||||
longest_row: 0,
|
||||
@@ -397,9 +405,8 @@ impl FoldMap {
|
||||
}
|
||||
|
||||
let sum = new_transforms.summary();
|
||||
if sum.input.bytes < edit.new_bytes.end {
|
||||
let text_summary =
|
||||
buffer.text_summary_for_range(sum.input.bytes..edit.new_bytes.end);
|
||||
if sum.input.bytes < edit.new.end {
|
||||
let text_summary = buffer.text_summary_for_range(sum.input.bytes..edit.new.end);
|
||||
new_transforms.push(
|
||||
Transform {
|
||||
summary: TransformSummary {
|
||||
@@ -436,35 +443,35 @@ impl FoldMap {
|
||||
let mut new_transforms = new_transforms.cursor::<(usize, FoldOffset)>();
|
||||
|
||||
for mut edit in buffer_edits {
|
||||
old_transforms.seek(&edit.old_bytes.start, Bias::Left, &());
|
||||
old_transforms.seek(&edit.old.start, Bias::Left, &());
|
||||
if old_transforms.item().map_or(false, |t| t.is_fold()) {
|
||||
edit.old_bytes.start = old_transforms.start().0;
|
||||
edit.old.start = old_transforms.start().0;
|
||||
}
|
||||
let old_start =
|
||||
old_transforms.start().1 .0 + (edit.old_bytes.start - old_transforms.start().0);
|
||||
old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0);
|
||||
|
||||
old_transforms.seek_forward(&edit.old_bytes.end, Bias::Right, &());
|
||||
old_transforms.seek_forward(&edit.old.end, Bias::Right, &());
|
||||
if old_transforms.item().map_or(false, |t| t.is_fold()) {
|
||||
old_transforms.next(&());
|
||||
edit.old_bytes.end = old_transforms.start().0;
|
||||
edit.old.end = old_transforms.start().0;
|
||||
}
|
||||
let old_end =
|
||||
old_transforms.start().1 .0 + (edit.old_bytes.end - old_transforms.start().0);
|
||||
old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0);
|
||||
|
||||
new_transforms.seek(&edit.new_bytes.start, Bias::Left, &());
|
||||
new_transforms.seek(&edit.new.start, Bias::Left, &());
|
||||
if new_transforms.item().map_or(false, |t| t.is_fold()) {
|
||||
edit.new_bytes.start = new_transforms.start().0;
|
||||
edit.new.start = new_transforms.start().0;
|
||||
}
|
||||
let new_start =
|
||||
new_transforms.start().1 .0 + (edit.new_bytes.start - new_transforms.start().0);
|
||||
new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0);
|
||||
|
||||
new_transforms.seek_forward(&edit.new_bytes.end, Bias::Right, &());
|
||||
new_transforms.seek_forward(&edit.new.end, Bias::Right, &());
|
||||
if new_transforms.item().map_or(false, |t| t.is_fold()) {
|
||||
new_transforms.next(&());
|
||||
edit.new_bytes.end = new_transforms.start().0;
|
||||
edit.new.end = new_transforms.start().0;
|
||||
}
|
||||
let new_end =
|
||||
new_transforms.start().1 .0 + (edit.new_bytes.end - new_transforms.start().0);
|
||||
new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0);
|
||||
|
||||
fold_edits.push(FoldEdit {
|
||||
old_bytes: FoldOffset(old_start)..FoldOffset(old_end),
|
||||
@@ -720,7 +727,7 @@ fn intersecting_folds<'a, T>(
|
||||
folds: &'a SumTree<Fold>,
|
||||
range: Range<T>,
|
||||
inclusive: bool,
|
||||
) -> FilterCursor<'a, impl 'a + Fn(&FoldSummary) -> bool, Fold, usize>
|
||||
) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize>
|
||||
where
|
||||
T: ToOffset,
|
||||
{
|
||||
@@ -741,22 +748,22 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
fn consolidate_buffer_edits(edits: &mut Vec<buffer::Edit>) {
|
||||
fn consolidate_buffer_edits(edits: &mut Vec<buffer::Edit<usize>>) {
|
||||
edits.sort_unstable_by(|a, b| {
|
||||
a.old_bytes
|
||||
a.old
|
||||
.start
|
||||
.cmp(&b.old_bytes.start)
|
||||
.then_with(|| b.old_bytes.end.cmp(&a.old_bytes.end))
|
||||
.cmp(&b.old.start)
|
||||
.then_with(|| b.old.end.cmp(&a.old.end))
|
||||
});
|
||||
|
||||
let mut i = 1;
|
||||
while i < edits.len() {
|
||||
let edit = edits[i].clone();
|
||||
let prev_edit = &mut edits[i - 1];
|
||||
if prev_edit.old_bytes.end >= edit.old_bytes.start {
|
||||
prev_edit.old_bytes.end = prev_edit.old_bytes.end.max(edit.old_bytes.end);
|
||||
prev_edit.new_bytes.start = prev_edit.new_bytes.start.min(edit.new_bytes.start);
|
||||
prev_edit.new_bytes.end = prev_edit.new_bytes.end.max(edit.new_bytes.end);
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
|
||||
prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
|
||||
prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
|
||||
edits.remove(i);
|
||||
continue;
|
||||
}
|
||||
@@ -995,12 +1002,12 @@ impl<'a> Iterator for Chunks<'a> {
|
||||
pub struct HighlightedChunks<'a> {
|
||||
transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>,
|
||||
buffer_chunks: language::HighlightedChunks<'a>,
|
||||
buffer_chunk: Option<(usize, &'a str, HighlightId)>,
|
||||
buffer_chunk: Option<(usize, HighlightedChunk<'a>)>,
|
||||
buffer_offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
type Item = (&'a str, HighlightId);
|
||||
type Item = HighlightedChunk<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let transform = if let Some(item) = self.transform_cursor.item() {
|
||||
@@ -1022,34 +1029,35 @@ impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
self.transform_cursor.next(&());
|
||||
}
|
||||
|
||||
return Some((output_text, HighlightId::default()));
|
||||
return Some(HighlightedChunk {
|
||||
text: output_text,
|
||||
highlight_id: HighlightId::default(),
|
||||
diagnostic: None,
|
||||
});
|
||||
}
|
||||
|
||||
// Retrieve a chunk from the current location in the buffer.
|
||||
if self.buffer_chunk.is_none() {
|
||||
let chunk_offset = self.buffer_chunks.offset();
|
||||
self.buffer_chunk = self
|
||||
.buffer_chunks
|
||||
.next()
|
||||
.map(|(chunk, capture_ix)| (chunk_offset, chunk, capture_ix));
|
||||
self.buffer_chunk = self.buffer_chunks.next().map(|chunk| (chunk_offset, chunk));
|
||||
}
|
||||
|
||||
// Otherwise, take a chunk from the buffer's text.
|
||||
if let Some((chunk_offset, mut chunk, capture_ix)) = self.buffer_chunk {
|
||||
if let Some((chunk_offset, mut chunk)) = self.buffer_chunk {
|
||||
let offset_in_chunk = self.buffer_offset - chunk_offset;
|
||||
chunk = &chunk[offset_in_chunk..];
|
||||
chunk.text = &chunk.text[offset_in_chunk..];
|
||||
|
||||
// Truncate the chunk so that it ends at the next fold.
|
||||
let region_end = self.transform_cursor.end(&()).1 - self.buffer_offset;
|
||||
if chunk.len() >= region_end {
|
||||
chunk = &chunk[0..region_end];
|
||||
if chunk.text.len() >= region_end {
|
||||
chunk.text = &chunk.text[0..region_end];
|
||||
self.transform_cursor.next(&());
|
||||
} else {
|
||||
self.buffer_chunk.take();
|
||||
}
|
||||
|
||||
self.buffer_offset += chunk.len();
|
||||
return Some((chunk, capture_ix));
|
||||
self.buffer_offset += chunk.text.len();
|
||||
return Some(chunk);
|
||||
}
|
||||
|
||||
None
|
||||
@@ -1335,7 +1343,9 @@ mod tests {
|
||||
let start_version = buffer.version.clone();
|
||||
let edit_count = rng.gen_range(1..=5);
|
||||
buffer.randomly_edit(&mut rng, edit_count);
|
||||
buffer.edits_since(start_version).collect::<Vec<_>>()
|
||||
buffer
|
||||
.edits_since::<Point>(&start_version)
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
log::info!("editing {:?}", edits);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot};
|
||||
use language::{rope, HighlightId};
|
||||
use language::{rope, HighlightedChunk};
|
||||
use parking_lot::Mutex;
|
||||
use std::{mem, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
@@ -173,9 +173,11 @@ impl Snapshot {
|
||||
.highlighted_chunks(input_start..input_end),
|
||||
column: expanded_char_column,
|
||||
tab_size: self.tab_size,
|
||||
chunk: &SPACES[0..to_next_stop],
|
||||
chunk: HighlightedChunk {
|
||||
text: &SPACES[0..to_next_stop],
|
||||
..Default::default()
|
||||
},
|
||||
skip_leading_tab: to_next_stop > 0,
|
||||
style_id: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,23 +417,21 @@ impl<'a> Iterator for Chunks<'a> {
|
||||
|
||||
pub struct HighlightedChunks<'a> {
|
||||
fold_chunks: fold_map::HighlightedChunks<'a>,
|
||||
chunk: &'a str,
|
||||
style_id: HighlightId,
|
||||
chunk: HighlightedChunk<'a>,
|
||||
column: usize,
|
||||
tab_size: usize,
|
||||
skip_leading_tab: bool,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
type Item = (&'a str, HighlightId);
|
||||
type Item = HighlightedChunk<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.chunk.is_empty() {
|
||||
if let Some((chunk, style_id)) = self.fold_chunks.next() {
|
||||
if self.chunk.text.is_empty() {
|
||||
if let Some(chunk) = self.fold_chunks.next() {
|
||||
self.chunk = chunk;
|
||||
self.style_id = style_id;
|
||||
if self.skip_leading_tab {
|
||||
self.chunk = &self.chunk[1..];
|
||||
self.chunk.text = &self.chunk.text[1..];
|
||||
self.skip_leading_tab = false;
|
||||
}
|
||||
} else {
|
||||
@@ -439,18 +439,24 @@ impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
for (ix, c) in self.chunk.char_indices() {
|
||||
for (ix, c) in self.chunk.text.char_indices() {
|
||||
match c {
|
||||
'\t' => {
|
||||
if ix > 0 {
|
||||
let (prefix, suffix) = self.chunk.split_at(ix);
|
||||
self.chunk = suffix;
|
||||
return Some((prefix, self.style_id));
|
||||
let (prefix, suffix) = self.chunk.text.split_at(ix);
|
||||
self.chunk.text = suffix;
|
||||
return Some(HighlightedChunk {
|
||||
text: prefix,
|
||||
..self.chunk
|
||||
});
|
||||
} else {
|
||||
self.chunk = &self.chunk[1..];
|
||||
self.chunk.text = &self.chunk.text[1..];
|
||||
let len = self.tab_size - self.column % self.tab_size;
|
||||
self.column += len;
|
||||
return Some((&SPACES[0..len], self.style_id));
|
||||
return Some(HighlightedChunk {
|
||||
text: &SPACES[0..len],
|
||||
..self.chunk
|
||||
});
|
||||
}
|
||||
}
|
||||
'\n' => self.column = 0,
|
||||
@@ -458,7 +464,7 @@ impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
Some((mem::take(&mut self.chunk), mem::take(&mut self.style_id)))
|
||||
Some(mem::take(&mut self.chunk))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::{
|
||||
tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary},
|
||||
};
|
||||
use gpui::{fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, Task};
|
||||
use language::{HighlightId, Point};
|
||||
use language::{HighlightedChunk, Point};
|
||||
use lazy_static::lazy_static;
|
||||
use smol::future::yield_now;
|
||||
use std::{collections::VecDeque, ops::Range, time::Duration};
|
||||
@@ -52,8 +52,7 @@ pub struct Chunks<'a> {
|
||||
|
||||
pub struct HighlightedChunks<'a> {
|
||||
input_chunks: tab_map::HighlightedChunks<'a>,
|
||||
input_chunk: &'a str,
|
||||
style_id: HighlightId,
|
||||
input_chunk: HighlightedChunk<'a>,
|
||||
output_position: WrapPoint,
|
||||
max_output_row: u32,
|
||||
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
|
||||
@@ -490,8 +489,7 @@ impl Snapshot {
|
||||
.min(self.tab_snapshot.max_point());
|
||||
HighlightedChunks {
|
||||
input_chunks: self.tab_snapshot.highlighted_chunks(input_start..input_end),
|
||||
input_chunk: "",
|
||||
style_id: HighlightId::default(),
|
||||
input_chunk: Default::default(),
|
||||
output_position: output_start,
|
||||
max_output_row: rows.end,
|
||||
transforms,
|
||||
@@ -674,7 +672,7 @@ impl<'a> Iterator for Chunks<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
type Item = (&'a str, HighlightId);
|
||||
type Item = HighlightedChunk<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.output_position.row() >= self.max_output_row {
|
||||
@@ -699,18 +697,19 @@ impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
|
||||
self.output_position.0 += summary;
|
||||
self.transforms.next(&());
|
||||
return Some((&display_text[start_ix..end_ix], self.style_id));
|
||||
return Some(HighlightedChunk {
|
||||
text: &display_text[start_ix..end_ix],
|
||||
..self.input_chunk
|
||||
});
|
||||
}
|
||||
|
||||
if self.input_chunk.is_empty() {
|
||||
let (chunk, style_id) = self.input_chunks.next().unwrap();
|
||||
self.input_chunk = chunk;
|
||||
self.style_id = style_id;
|
||||
if self.input_chunk.text.is_empty() {
|
||||
self.input_chunk = self.input_chunks.next().unwrap();
|
||||
}
|
||||
|
||||
let mut input_len = 0;
|
||||
let transform_end = self.transforms.end(&()).0;
|
||||
for c in self.input_chunk.chars() {
|
||||
for c in self.input_chunk.text.chars() {
|
||||
let char_len = c.len_utf8();
|
||||
input_len += char_len;
|
||||
if c == '\n' {
|
||||
@@ -726,9 +725,12 @@ impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
let (prefix, suffix) = self.input_chunk.split_at(input_len);
|
||||
self.input_chunk = suffix;
|
||||
Some((prefix, self.style_id))
|
||||
let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
|
||||
self.input_chunk.text = suffix;
|
||||
Some(HighlightedChunk {
|
||||
text: prefix,
|
||||
..self.input_chunk
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1090,7 +1092,7 @@ mod tests {
|
||||
|
||||
let actual_text = self
|
||||
.highlighted_chunks_for_rows(start_row..end_row)
|
||||
.map(|c| c.0)
|
||||
.map(|c| c.text)
|
||||
.collect::<String>();
|
||||
assert_eq!(
|
||||
expected_text,
|
||||
|
||||
@@ -17,7 +17,7 @@ use gpui::{
|
||||
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use json::json;
|
||||
use language::HighlightId;
|
||||
use language::{DiagnosticSeverity, HighlightedChunk};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
@@ -394,7 +394,7 @@ impl EditorElement {
|
||||
RunStyle {
|
||||
font_id: style.text.font_id,
|
||||
color: Color::black(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
},
|
||||
)],
|
||||
)
|
||||
@@ -435,7 +435,7 @@ impl EditorElement {
|
||||
RunStyle {
|
||||
font_id: style.text.font_id,
|
||||
color,
|
||||
underline: false,
|
||||
underline: None,
|
||||
},
|
||||
)],
|
||||
)));
|
||||
@@ -476,7 +476,7 @@ impl EditorElement {
|
||||
RunStyle {
|
||||
font_id: placeholder_style.font_id,
|
||||
color: placeholder_style.color,
|
||||
underline: false,
|
||||
underline: None,
|
||||
},
|
||||
)],
|
||||
)
|
||||
@@ -495,8 +495,12 @@ impl EditorElement {
|
||||
let mut line_exceeded_max_len = false;
|
||||
let chunks = snapshot.highlighted_chunks_for_rows(rows.clone());
|
||||
|
||||
'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
|
||||
for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
|
||||
let newline_chunk = HighlightedChunk {
|
||||
text: "\n",
|
||||
..Default::default()
|
||||
};
|
||||
'outer: for chunk in chunks.chain([newline_chunk]) {
|
||||
for (ix, mut line_chunk) in chunk.text.split('\n').enumerate() {
|
||||
if ix > 0 {
|
||||
layouts.push(cx.text_layout_cache.layout_str(
|
||||
&line,
|
||||
@@ -513,7 +517,8 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
if !line_chunk.is_empty() && !line_exceeded_max_len {
|
||||
let highlight_style = style_ix
|
||||
let highlight_style = chunk
|
||||
.highlight_id
|
||||
.style(&style.syntax)
|
||||
.unwrap_or(style.text.clone().into());
|
||||
// Avoid a lookup if the font properties match the previous ones.
|
||||
@@ -537,13 +542,25 @@ impl EditorElement {
|
||||
line_exceeded_max_len = true;
|
||||
}
|
||||
|
||||
let underline = if let Some(severity) = chunk.diagnostic {
|
||||
match severity {
|
||||
DiagnosticSeverity::ERROR => Some(style.error_underline),
|
||||
DiagnosticSeverity::WARNING => Some(style.warning_underline),
|
||||
DiagnosticSeverity::INFORMATION => Some(style.information_underline),
|
||||
DiagnosticSeverity::HINT => Some(style.hint_underline),
|
||||
_ => highlight_style.underline,
|
||||
}
|
||||
} else {
|
||||
highlight_style.underline
|
||||
};
|
||||
|
||||
line.push_str(line_chunk);
|
||||
styles.push((
|
||||
line_chunk.len(),
|
||||
RunStyle {
|
||||
font_id,
|
||||
color: highlight_style.color,
|
||||
underline: highlight_style.underline,
|
||||
underline,
|
||||
},
|
||||
));
|
||||
prev_font_id = font_id;
|
||||
@@ -859,7 +876,7 @@ impl LayoutState {
|
||||
RunStyle {
|
||||
font_id: self.style.text.font_id,
|
||||
color: Color::black(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
},
|
||||
)],
|
||||
)
|
||||
|
||||
@@ -1527,10 +1527,12 @@ impl Editor {
|
||||
|
||||
pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
|
||||
self.buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
self.request_autoscroll(cx);
|
||||
}
|
||||
|
||||
pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
|
||||
self.buffer.update(cx, |buffer, cx| buffer.redo(cx));
|
||||
self.request_autoscroll(cx);
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
|
||||
@@ -2344,10 +2346,8 @@ impl Editor {
|
||||
}
|
||||
|
||||
if autoscroll {
|
||||
self.autoscroll_requested = true;
|
||||
cx.notify();
|
||||
self.request_autoscroll(cx);
|
||||
}
|
||||
|
||||
self.pause_cursor_blinking(cx);
|
||||
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
@@ -2357,6 +2357,11 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
fn request_autoscroll(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.autoscroll_requested = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn start_transaction(&self, cx: &mut ViewContext<Self>) {
|
||||
self.buffer.update(cx, |buffer, _| {
|
||||
buffer
|
||||
@@ -2682,7 +2687,7 @@ impl EditorSettings {
|
||||
font_size: 14.,
|
||||
color: gpui::color::Color::from_u32(0xff0000ff),
|
||||
font_properties,
|
||||
underline: false,
|
||||
underline: None,
|
||||
},
|
||||
placeholder_text: None,
|
||||
background: Default::default(),
|
||||
@@ -2693,6 +2698,10 @@ impl EditorSettings {
|
||||
selection: Default::default(),
|
||||
guest_selections: Default::default(),
|
||||
syntax: Default::default(),
|
||||
error_underline: Default::default(),
|
||||
warning_underline: Default::default(),
|
||||
information_underline: Default::default(),
|
||||
hint_underline: Default::default(),
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -2822,7 +2831,7 @@ impl SelectionExt for Selection<Point> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::sample_text;
|
||||
use buffer::{History, Point};
|
||||
use buffer::Point;
|
||||
use unindent::Unindent;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -4325,10 +4334,10 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) {
|
||||
let settings = cx.read(EditorSettings::test);
|
||||
let language = Arc::new(Language::new(
|
||||
let language = Some(Arc::new(Language::new(
|
||||
LanguageConfig::default(),
|
||||
tree_sitter_rust::language(),
|
||||
));
|
||||
)));
|
||||
|
||||
let text = r#"
|
||||
use mod1::mod2::{mod3, mod4};
|
||||
@@ -4339,10 +4348,7 @@ mod tests {
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let history = History::new(text.into());
|
||||
Buffer::from_history(0, history, None, Some(language), cx)
|
||||
});
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx));
|
||||
let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx));
|
||||
view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing())
|
||||
.await;
|
||||
@@ -4469,7 +4475,7 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_autoclose_pairs(mut cx: gpui::TestAppContext) {
|
||||
let settings = cx.read(EditorSettings::test);
|
||||
let language = Arc::new(Language::new(
|
||||
let language = Some(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
brackets: vec![
|
||||
BracketPair {
|
||||
@@ -4488,7 +4494,7 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
tree_sitter_rust::language(),
|
||||
));
|
||||
)));
|
||||
|
||||
let text = r#"
|
||||
a
|
||||
@@ -4498,10 +4504,7 @@ mod tests {
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let history = History::new(text.into());
|
||||
Buffer::from_history(0, history, None, Some(language), cx)
|
||||
});
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx));
|
||||
let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx));
|
||||
view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing())
|
||||
.await;
|
||||
@@ -4584,7 +4587,7 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_extra_newline_insertion(mut cx: gpui::TestAppContext) {
|
||||
let settings = cx.read(EditorSettings::test);
|
||||
let language = Arc::new(Language::new(
|
||||
let language = Some(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
brackets: vec![
|
||||
BracketPair {
|
||||
@@ -4603,7 +4606,7 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
tree_sitter_rust::language(),
|
||||
));
|
||||
)));
|
||||
|
||||
let text = concat!(
|
||||
"{ }\n", // Suppress rustfmt
|
||||
@@ -4613,10 +4616,7 @@ mod tests {
|
||||
"{{} }\n", //
|
||||
);
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let history = History::new(text.into());
|
||||
Buffer::from_history(0, history, None, Some(language), cx)
|
||||
});
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx));
|
||||
let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx));
|
||||
view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing())
|
||||
.await;
|
||||
|
||||
@@ -15,6 +15,7 @@ backtrace = "0.3"
|
||||
ctor = "0.1"
|
||||
env_logger = { version = "0.8", optional = true }
|
||||
etagere = "0.2"
|
||||
futures = "0.3"
|
||||
image = "0.23"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4"
|
||||
|
||||
@@ -62,7 +62,7 @@ impl gpui::Element for TextElement {
|
||||
.select_font(family, &Default::default())
|
||||
.unwrap(),
|
||||
color: Color::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
let bold = RunStyle {
|
||||
font_id: cx
|
||||
@@ -76,7 +76,7 @@ impl gpui::Element for TextElement {
|
||||
)
|
||||
.unwrap(),
|
||||
color: Color::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
|
||||
let text = "Hello world!";
|
||||
|
||||
@@ -23,6 +23,7 @@ use std::{
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
rc::{self, Rc},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
@@ -35,6 +36,12 @@ pub trait Entity: 'static {
|
||||
type Event;
|
||||
|
||||
fn release(&mut self, _: &mut MutableAppContext) {}
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
_: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub trait View: Entity + Sized {
|
||||
@@ -198,8 +205,6 @@ pub struct App(Rc<RefCell<MutableAppContext>>);
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
|
||||
|
||||
pub struct BackgroundAppContext(*const RefCell<MutableAppContext>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestAppContext {
|
||||
cx: Rc<RefCell<MutableAppContext>>,
|
||||
@@ -220,20 +225,29 @@ impl App {
|
||||
asset_source,
|
||||
))));
|
||||
|
||||
let cx = app.0.clone();
|
||||
foreground_platform.on_menu_command(Box::new(move |action| {
|
||||
let mut cx = cx.borrow_mut();
|
||||
if let Some(key_window_id) = cx.cx.platform.key_window_id() {
|
||||
if let Some((presenter, _)) = cx.presenters_and_platform_windows.get(&key_window_id)
|
||||
{
|
||||
let presenter = presenter.clone();
|
||||
let path = presenter.borrow().dispatch_path(cx.as_ref());
|
||||
cx.dispatch_action_any(key_window_id, &path, action);
|
||||
foreground_platform.on_quit(Box::new({
|
||||
let cx = app.0.clone();
|
||||
move || {
|
||||
cx.borrow_mut().quit();
|
||||
}
|
||||
}));
|
||||
foreground_platform.on_menu_command(Box::new({
|
||||
let cx = app.0.clone();
|
||||
move |action| {
|
||||
let mut cx = cx.borrow_mut();
|
||||
if let Some(key_window_id) = cx.cx.platform.key_window_id() {
|
||||
if let Some((presenter, _)) =
|
||||
cx.presenters_and_platform_windows.get(&key_window_id)
|
||||
{
|
||||
let presenter = presenter.clone();
|
||||
let path = presenter.borrow().dispatch_path(cx.as_ref());
|
||||
cx.dispatch_action_any(key_window_id, &path, action);
|
||||
} else {
|
||||
cx.dispatch_global_action_any(action);
|
||||
}
|
||||
} else {
|
||||
cx.dispatch_global_action_any(action);
|
||||
}
|
||||
} else {
|
||||
cx.dispatch_global_action_any(action);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -265,6 +279,18 @@ impl App {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_quit<F>(self, mut callback: F) -> Self
|
||||
where
|
||||
F: 'static + FnMut(&mut MutableAppContext),
|
||||
{
|
||||
let cx = self.0.clone();
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.foreground_platform
|
||||
.on_quit(Box::new(move || callback(&mut *cx.borrow_mut())));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_event<F>(self, mut callback: F) -> Self
|
||||
where
|
||||
F: 'static + FnMut(Event, &mut MutableAppContext) -> bool,
|
||||
@@ -739,6 +765,39 @@ impl MutableAppContext {
|
||||
App(self.weak_self.as_ref().unwrap().upgrade().unwrap())
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
let mut futures = Vec::new();
|
||||
for model_id in self.cx.models.keys().copied().collect::<Vec<_>>() {
|
||||
let mut model = self.cx.models.remove(&model_id).unwrap();
|
||||
futures.extend(model.app_will_quit(self));
|
||||
self.cx.models.insert(model_id, model);
|
||||
}
|
||||
|
||||
for view_id in self.cx.views.keys().copied().collect::<Vec<_>>() {
|
||||
let mut view = self.cx.views.remove(&view_id).unwrap();
|
||||
futures.extend(view.app_will_quit(self));
|
||||
self.cx.views.insert(view_id, view);
|
||||
}
|
||||
|
||||
self.remove_all_windows();
|
||||
|
||||
let futures = futures::future::join_all(futures);
|
||||
if self
|
||||
.background
|
||||
.block_with_timeout(Duration::from_millis(100), futures)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("timed out waiting on app_will_quit");
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_all_windows(&mut self) {
|
||||
for (window_id, _) in self.cx.windows.drain() {
|
||||
self.presenters_and_platform_windows.remove(&window_id);
|
||||
}
|
||||
self.remove_dropped_entities();
|
||||
}
|
||||
|
||||
pub fn platform(&self) -> Arc<dyn platform::Platform> {
|
||||
self.cx.platform.clone()
|
||||
}
|
||||
@@ -1879,6 +1938,10 @@ pub trait AnyModel {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn release(&mut self, cx: &mut MutableAppContext);
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>>;
|
||||
}
|
||||
|
||||
impl<T> AnyModel for T
|
||||
@@ -1896,12 +1959,23 @@ where
|
||||
fn release(&mut self, cx: &mut MutableAppContext) {
|
||||
self.release(cx);
|
||||
}
|
||||
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>> {
|
||||
self.app_will_quit(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnyView {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn release(&mut self, cx: &mut MutableAppContext);
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>>;
|
||||
fn ui_name(&self) -> &'static str;
|
||||
fn render<'a>(
|
||||
&mut self,
|
||||
@@ -1932,6 +2006,13 @@ where
|
||||
self.release(cx);
|
||||
}
|
||||
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>> {
|
||||
self.app_will_quit(cx)
|
||||
}
|
||||
|
||||
fn ui_name(&self) -> &'static str {
|
||||
T::ui_name()
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ mod tests {
|
||||
"Menlo",
|
||||
12.,
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
Color::black(),
|
||||
cx.font_cache(),
|
||||
)
|
||||
@@ -216,7 +216,7 @@ mod tests {
|
||||
"Menlo",
|
||||
12.,
|
||||
*FontProperties::new().weight(Weight::BOLD),
|
||||
false,
|
||||
None,
|
||||
Color::new(255, 0, 0, 255),
|
||||
cx.font_cache(),
|
||||
)
|
||||
|
||||
@@ -38,7 +38,9 @@ pub enum Foreground {
|
||||
}
|
||||
|
||||
pub enum Background {
|
||||
Deterministic(Arc<Deterministic>),
|
||||
Deterministic {
|
||||
executor: Arc<Deterministic>,
|
||||
},
|
||||
Production {
|
||||
executor: Arc<smol::Executor<'static>>,
|
||||
_stop: channel::Sender<()>,
|
||||
@@ -50,6 +52,7 @@ type AnyFuture = Pin<Box<dyn 'static + Send + Future<Output = Box<dyn Any + Send
|
||||
type AnyTask = async_task::Task<Box<dyn Any + Send + 'static>>;
|
||||
type AnyLocalTask = async_task::Task<Box<dyn Any + 'static>>;
|
||||
|
||||
#[must_use]
|
||||
pub enum Task<T> {
|
||||
Local {
|
||||
any_task: AnyLocalTask,
|
||||
@@ -515,7 +518,7 @@ impl Background {
|
||||
let future = any_future(future);
|
||||
let any_task = match self {
|
||||
Self::Production { executor, .. } => executor.spawn(future),
|
||||
Self::Deterministic(executor) => executor.spawn(future),
|
||||
Self::Deterministic { executor, .. } => executor.spawn(future),
|
||||
};
|
||||
Task::send(any_task)
|
||||
}
|
||||
@@ -533,7 +536,7 @@ impl Background {
|
||||
if !timeout.is_zero() {
|
||||
let output = match self {
|
||||
Self::Production { .. } => smol::block_on(util::timeout(timeout, &mut future)).ok(),
|
||||
Self::Deterministic(executor) => executor.block_on(&mut future),
|
||||
Self::Deterministic { executor, .. } => executor.block_on(&mut future),
|
||||
};
|
||||
if let Some(output) = output {
|
||||
return Ok(*output.downcast().unwrap());
|
||||
@@ -586,7 +589,7 @@ pub fn deterministic(seed: u64) -> (Rc<Foreground>, Arc<Background>) {
|
||||
let executor = Arc::new(Deterministic::new(seed));
|
||||
(
|
||||
Rc::new(Foreground::Deterministic(executor.clone())),
|
||||
Arc::new(Background::Deterministic(executor)),
|
||||
Arc::new(Background::Deterministic { executor }),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,14 +27,14 @@ pub struct TextStyle {
|
||||
pub font_id: FontId,
|
||||
pub font_size: f32,
|
||||
pub font_properties: Properties,
|
||||
pub underline: bool,
|
||||
pub underline: Option<Color>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct HighlightStyle {
|
||||
pub color: Color,
|
||||
pub font_properties: Properties,
|
||||
pub underline: bool,
|
||||
pub underline: Option<Color>,
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
@@ -64,7 +64,7 @@ struct TextStyleJson {
|
||||
#[serde(default)]
|
||||
italic: bool,
|
||||
#[serde(default)]
|
||||
underline: bool,
|
||||
underline: UnderlineStyleJson,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -74,7 +74,14 @@ struct HighlightStyleJson {
|
||||
#[serde(default)]
|
||||
italic: bool,
|
||||
#[serde(default)]
|
||||
underline: bool,
|
||||
underline: UnderlineStyleJson,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum UnderlineStyleJson {
|
||||
Underlined(bool),
|
||||
UnderlinedWithColor(Color),
|
||||
}
|
||||
|
||||
impl TextStyle {
|
||||
@@ -82,7 +89,7 @@ impl TextStyle {
|
||||
font_family_name: impl Into<Arc<str>>,
|
||||
font_size: f32,
|
||||
font_properties: Properties,
|
||||
underline: bool,
|
||||
underline: Option<Color>,
|
||||
color: Color,
|
||||
font_cache: &FontCache,
|
||||
) -> anyhow::Result<Self> {
|
||||
@@ -116,7 +123,7 @@ impl TextStyle {
|
||||
json.family,
|
||||
json.size,
|
||||
font_properties,
|
||||
json.underline,
|
||||
underline_from_json(json.underline, json.color),
|
||||
json.color,
|
||||
font_cache,
|
||||
)
|
||||
@@ -167,6 +174,12 @@ impl From<TextStyle> for HighlightStyle {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UnderlineStyleJson {
|
||||
fn default() -> Self {
|
||||
Self::Underlined(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TextStyle {
|
||||
fn default() -> Self {
|
||||
FONT_CACHE.with(|font_cache| {
|
||||
@@ -199,7 +212,7 @@ impl HighlightStyle {
|
||||
Self {
|
||||
color: json.color,
|
||||
font_properties,
|
||||
underline: json.underline,
|
||||
underline: underline_from_json(json.underline, json.color),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,7 +222,7 @@ impl From<Color> for HighlightStyle {
|
||||
Self {
|
||||
color,
|
||||
font_properties: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,12 +261,20 @@ impl<'de> Deserialize<'de> for HighlightStyle {
|
||||
Ok(Self {
|
||||
color: serde_json::from_value(json).map_err(de::Error::custom)?,
|
||||
font_properties: Properties::new(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn underline_from_json(json: UnderlineStyleJson, text_color: Color) -> Option<Color> {
|
||||
match json {
|
||||
UnderlineStyleJson::Underlined(false) => None,
|
||||
UnderlineStyleJson::Underlined(true) => Some(text_color),
|
||||
UnderlineStyleJson::UnderlinedWithColor(color) => Some(color),
|
||||
}
|
||||
}
|
||||
|
||||
fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
|
||||
let weight = match weight.unwrap_or(WeightJson::normal) {
|
||||
WeightJson::thin => Weight::THIN,
|
||||
|
||||
@@ -53,11 +53,14 @@ pub trait Platform: Send + Sync {
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
|
||||
fn local_timezone(&self) -> UtcOffset;
|
||||
|
||||
fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result<PathBuf>;
|
||||
}
|
||||
|
||||
pub(crate) trait ForegroundPlatform {
|
||||
fn on_become_active(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_resign_active(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
|
||||
fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>);
|
||||
fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
|
||||
|
||||
@@ -417,21 +417,21 @@ mod tests {
|
||||
let menlo_regular = RunStyle {
|
||||
font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
let menlo_italic = RunStyle {
|
||||
font_id: fonts
|
||||
.select_font(&menlo, &Properties::new().style(Style::Italic))
|
||||
.unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
let menlo_bold = RunStyle {
|
||||
font_id: fonts
|
||||
.select_font(&menlo, &Properties::new().weight(Weight::BOLD))
|
||||
.unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
assert_ne!(menlo_regular, menlo_italic);
|
||||
assert_ne!(menlo_regular, menlo_bold);
|
||||
@@ -458,13 +458,13 @@ mod tests {
|
||||
let zapfino_regular = RunStyle {
|
||||
font_id: fonts.select_font(&zapfino, &Properties::new())?,
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
let menlo = fonts.load_family("Menlo")?;
|
||||
let menlo_regular = RunStyle {
|
||||
font_id: fonts.select_font(&menlo, &Properties::new())?,
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
|
||||
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
|
||||
@@ -543,7 +543,7 @@ mod tests {
|
||||
let style = RunStyle {
|
||||
font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
|
||||
let line = "\u{feff}";
|
||||
|
||||
@@ -14,7 +14,9 @@ use cocoa::{
|
||||
NSPasteboardTypeString, NSSavePanel, NSWindow,
|
||||
},
|
||||
base::{id, nil, selector, YES},
|
||||
foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSURL},
|
||||
foundation::{
|
||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSString, NSUInteger, NSURL,
|
||||
},
|
||||
};
|
||||
use core_foundation::{
|
||||
base::{CFType, CFTypeRef, OSStatus, TCFType as _},
|
||||
@@ -45,6 +47,9 @@ use std::{
|
||||
};
|
||||
use time::UtcOffset;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSUTF8StringEncoding: NSUInteger = 4;
|
||||
|
||||
const MAC_PLATFORM_IVAR: &'static str = "platform";
|
||||
static mut APP_CLASS: *const Class = ptr::null();
|
||||
static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
|
||||
@@ -76,6 +81,10 @@ unsafe fn build_classes() {
|
||||
sel!(applicationDidResignActive:),
|
||||
did_resign_active as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(applicationWillTerminate:),
|
||||
will_terminate as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(handleGPUIMenuItem:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
@@ -95,6 +104,7 @@ pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>);
|
||||
pub struct MacForegroundPlatformState {
|
||||
become_active: Option<Box<dyn FnMut()>>,
|
||||
resign_active: Option<Box<dyn FnMut()>>,
|
||||
quit: Option<Box<dyn FnMut()>>,
|
||||
event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
|
||||
menu_command: Option<Box<dyn FnMut(&dyn AnyAction)>>,
|
||||
open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
|
||||
@@ -191,6 +201,10 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
|
||||
self.0.borrow_mut().resign_active = Some(callback);
|
||||
}
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.borrow_mut().quit = Some(callback);
|
||||
}
|
||||
|
||||
fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
|
||||
self.0.borrow_mut().event = Some(callback);
|
||||
}
|
||||
@@ -588,6 +602,27 @@ impl platform::Platform for MacPlatform {
|
||||
UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result<PathBuf> {
|
||||
unsafe {
|
||||
let bundle: id = NSBundle::mainBundle();
|
||||
if bundle.is_null() {
|
||||
Err(anyhow!("app is not running inside a bundle"))
|
||||
} else {
|
||||
let name = name.map_or(nil, |name| ns_string(name));
|
||||
let extension = extension.map_or(nil, |extension| ns_string(extension));
|
||||
let path: id = msg_send![bundle, pathForResource: name ofType: extension];
|
||||
if path.is_null() {
|
||||
Err(anyhow!("resource could not be found"))
|
||||
} else {
|
||||
let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
|
||||
let bytes = path.UTF8String() as *const u8;
|
||||
let path = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap();
|
||||
Ok(PathBuf::from(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform {
|
||||
@@ -638,6 +673,13 @@ extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
if let Some(callback) = platform.0.borrow_mut().quit.as_mut() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
|
||||
let paths = unsafe {
|
||||
(0..paths.count())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::CursorStyle;
|
||||
use crate::{AnyAction, ClipboardItem};
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use parking_lot::Mutex;
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use std::{
|
||||
@@ -58,6 +58,8 @@ impl super::ForegroundPlatform for ForegroundPlatform {
|
||||
|
||||
fn on_resign_active(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_quit(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_event(&self, _: Box<dyn FnMut(crate::Event) -> bool>) {}
|
||||
|
||||
fn on_open_files(&self, _: Box<dyn FnMut(Vec<std::path::PathBuf>)>) {}
|
||||
@@ -148,6 +150,10 @@ impl super::Platform for Platform {
|
||||
fn local_timezone(&self) -> UtcOffset {
|
||||
UtcOffset::UTC
|
||||
}
|
||||
|
||||
fn path_for_resource(&self, _name: Option<&str>, _extension: Option<&str>) -> Result<PathBuf> {
|
||||
Err(anyhow!("app not running inside a bundle"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct TextLayoutCache {
|
||||
pub struct RunStyle {
|
||||
pub color: Color,
|
||||
pub font_id: FontId,
|
||||
pub underline: bool,
|
||||
pub underline: Option<Color>,
|
||||
}
|
||||
|
||||
impl TextLayoutCache {
|
||||
@@ -167,7 +167,7 @@ impl<'a> Hash for CacheKeyRef<'a> {
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Line {
|
||||
layout: Arc<LineLayout>,
|
||||
style_runs: SmallVec<[(u32, Color, bool); 32]>,
|
||||
style_runs: SmallVec<[(u32, Color, Option<Color>); 32]>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -249,7 +249,7 @@ impl Line {
|
||||
let mut style_runs = self.style_runs.iter();
|
||||
let mut run_end = 0;
|
||||
let mut color = Color::black();
|
||||
let mut underline_start = None;
|
||||
let mut underline = None;
|
||||
|
||||
for run in &self.layout.runs {
|
||||
let max_glyph_width = cx
|
||||
@@ -268,24 +268,24 @@ impl Line {
|
||||
}
|
||||
|
||||
if glyph.index >= run_end {
|
||||
if let Some((run_len, run_color, run_underlined)) = style_runs.next() {
|
||||
if let Some(underline_origin) = underline_start {
|
||||
if !*run_underlined || *run_color != color {
|
||||
if let Some((run_len, run_color, run_underline_color)) = style_runs.next() {
|
||||
if let Some((underline_origin, underline_color)) = underline {
|
||||
if *run_underline_color != Some(underline_color) {
|
||||
cx.scene.push_underline(scene::Quad {
|
||||
bounds: RectF::from_points(
|
||||
underline_origin,
|
||||
glyph_origin + vec2f(0., 1.),
|
||||
),
|
||||
background: Some(color),
|
||||
background: Some(underline_color),
|
||||
border: Default::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
underline_start = None;
|
||||
underline = None;
|
||||
}
|
||||
}
|
||||
|
||||
if *run_underlined {
|
||||
underline_start.get_or_insert(glyph_origin);
|
||||
if let Some(run_underline_color) = run_underline_color {
|
||||
underline.get_or_insert((glyph_origin, *run_underline_color));
|
||||
}
|
||||
|
||||
run_end += *run_len as usize;
|
||||
@@ -293,13 +293,13 @@ impl Line {
|
||||
} else {
|
||||
run_end = self.layout.len;
|
||||
color = Color::black();
|
||||
if let Some(underline_origin) = underline_start.take() {
|
||||
if let Some((underline_origin, underline_color)) = underline.take() {
|
||||
cx.scene.push_underline(scene::Quad {
|
||||
bounds: RectF::from_points(
|
||||
underline_origin,
|
||||
glyph_origin + vec2f(0., 1.),
|
||||
),
|
||||
background: Some(color),
|
||||
background: Some(underline_color),
|
||||
border: Default::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
@@ -317,12 +317,12 @@ impl Line {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(underline_start) = underline_start.take() {
|
||||
if let Some((underline_start, underline_color)) = underline.take() {
|
||||
let line_end = origin + baseline_offset + vec2f(self.layout.width, 0.);
|
||||
|
||||
cx.scene.push_underline(scene::Quad {
|
||||
bounds: RectF::from_points(underline_start, line_end + vec2f(0., 1.)),
|
||||
background: Some(color),
|
||||
background: Some(underline_color),
|
||||
border: Default::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
@@ -597,7 +597,7 @@ impl LineWrapper {
|
||||
RunStyle {
|
||||
font_id: self.font_id,
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
},
|
||||
)],
|
||||
)
|
||||
@@ -681,7 +681,7 @@ mod tests {
|
||||
let normal = RunStyle {
|
||||
font_id,
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
let bold = RunStyle {
|
||||
font_id: font_cache
|
||||
@@ -694,7 +694,7 @@ mod tests {
|
||||
)
|
||||
.unwrap(),
|
||||
color: Default::default(),
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
|
||||
let text = "aa bbb cccc ddddd eeee";
|
||||
|
||||
@@ -4,12 +4,18 @@ version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
test-support = ["rand", "buffer/test-support"]
|
||||
test-support = [
|
||||
"rand",
|
||||
"buffer/test-support",
|
||||
"lsp/test-support",
|
||||
"tree-sitter-rust",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
buffer = { path = "../buffer" }
|
||||
clock = { path = "../clock" }
|
||||
gpui = { path = "../gpui" }
|
||||
lsp = { path = "../lsp" }
|
||||
rpc = { path = "../rpc" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
@@ -18,15 +24,18 @@ futures = "0.3"
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
parking_lot = "0.11.1"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
rand = { version = "0.8.3", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
similar = "1.3"
|
||||
smol = "1.2"
|
||||
tree-sitter = "0.19.5"
|
||||
tree-sitter-rust = { version = "0.19.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
buffer = { path = "../buffer", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
rand = "0.8.3"
|
||||
tree-sitter-rust = "0.19.0"
|
||||
unindent = "0.1.7"
|
||||
|
||||
6
crates/language/build.rs
Normal file
6
crates/language/build.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
fn main() {
|
||||
if let Ok(bundled) = std::env::var("ZED_BUNDLE") {
|
||||
println!("cargo:rustc-env=ZED_BUNDLE={}", bundled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::HighlightMap;
|
||||
use anyhow::Result;
|
||||
use gpui::{executor::Background, AppContext};
|
||||
use lsp::LanguageServer;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use std::{path::Path, str, sync::Arc};
|
||||
use std::{collections::HashSet, path::Path, str, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use tree_sitter::{Language as Grammar, Query};
|
||||
pub use tree_sitter::{Parser, Tree};
|
||||
@@ -12,6 +14,16 @@ pub struct LanguageConfig {
|
||||
pub name: String,
|
||||
pub path_suffixes: Vec<String>,
|
||||
pub brackets: Vec<BracketPair>,
|
||||
pub language_server: Option<LanguageServerConfig>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct LanguageServerConfig {
|
||||
pub binary: String,
|
||||
pub disk_based_diagnostic_sources: HashSet<String>,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[serde(skip)]
|
||||
pub fake_server: Option<(Arc<LanguageServer>, Arc<std::sync::atomic::AtomicBool>)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
@@ -51,6 +63,12 @@ impl LanguageRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_language(&self, name: &str) -> Option<&Arc<Language>> {
|
||||
self.languages
|
||||
.iter()
|
||||
.find(|language| language.name() == name)
|
||||
}
|
||||
|
||||
pub fn select_language(&self, path: impl AsRef<Path>) -> Option<&Arc<Language>> {
|
||||
let path = path.as_ref();
|
||||
let filename = path.file_name().and_then(|name| name.to_str());
|
||||
@@ -97,6 +115,38 @@ impl Language {
|
||||
self.config.name.as_str()
|
||||
}
|
||||
|
||||
pub fn start_server(
|
||||
&self,
|
||||
root_path: &Path,
|
||||
cx: &AppContext,
|
||||
) -> Result<Option<Arc<lsp::LanguageServer>>> {
|
||||
if let Some(config) = &self.config.language_server {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if let Some((server, started)) = &config.fake_server {
|
||||
started.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
return Ok(Some(server.clone()));
|
||||
}
|
||||
|
||||
const ZED_BUNDLE: Option<&'static str> = option_env!("ZED_BUNDLE");
|
||||
let binary_path = if ZED_BUNDLE.map_or(Ok(false), |b| b.parse())? {
|
||||
cx.platform()
|
||||
.path_for_resource(Some(&config.binary), None)?
|
||||
} else {
|
||||
Path::new(&config.binary).to_path_buf()
|
||||
};
|
||||
lsp::LanguageServer::new(&binary_path, root_path, cx.background().clone()).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet<String>> {
|
||||
self.config
|
||||
.language_server
|
||||
.as_ref()
|
||||
.map(|config| &config.disk_based_diagnostic_sources)
|
||||
}
|
||||
|
||||
pub fn brackets(&self) -> &[BracketPair] {
|
||||
&self.config.brackets
|
||||
}
|
||||
@@ -111,6 +161,23 @@ impl Language {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LanguageServerConfig {
|
||||
pub async fn fake(executor: Arc<Background>) -> (Self, lsp::FakeLanguageServer) {
|
||||
let (server, fake) = lsp::LanguageServer::fake(executor).await;
|
||||
fake.started
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
let started = fake.started.clone();
|
||||
(
|
||||
Self {
|
||||
fake_server: Some((server, started)),
|
||||
..Default::default()
|
||||
},
|
||||
fake,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
mod highlight_map;
|
||||
mod language;
|
||||
pub mod proto;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use self::{
|
||||
highlight_map::{HighlightId, HighlightMap},
|
||||
language::{BracketPair, Language, LanguageConfig, LanguageRegistry},
|
||||
language::{BracketPair, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
pub use buffer::{Buffer as TextBuffer, *};
|
||||
pub use buffer::{Buffer as TextBuffer, Operation as _, *};
|
||||
use clock::ReplicaId;
|
||||
use futures::FutureExt as _;
|
||||
use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
|
||||
use lazy_static::lazy_static;
|
||||
use lsp::LanguageServer;
|
||||
use parking_lot::Mutex;
|
||||
use rpc::proto;
|
||||
use postage::{prelude::Stream, sink::Sink, watch};
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use smol::future::yield_now;
|
||||
use std::{
|
||||
@@ -24,15 +26,21 @@ use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
ffi::OsString,
|
||||
future::Future,
|
||||
iter::Iterator,
|
||||
iter::{Iterator, Peekable},
|
||||
ops::{Deref, DerefMut, Range},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
vec,
|
||||
};
|
||||
use tree_sitter::{InputEdit, Parser, QueryCursor, Tree};
|
||||
use util::TryFutureExt as _;
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use tree_sitter_rust;
|
||||
|
||||
pub use lsp::DiagnosticSeverity;
|
||||
|
||||
thread_local! {
|
||||
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
|
||||
@@ -57,6 +65,9 @@ pub struct Buffer {
|
||||
syntax_tree: Mutex<Option<SyntaxTree>>,
|
||||
parsing_in_background: bool,
|
||||
parse_count: usize,
|
||||
diagnostics: AnchorRangeMultimap<Diagnostic>,
|
||||
diagnostics_update_count: usize,
|
||||
language_server: Option<LanguageServerState>,
|
||||
#[cfg(test)]
|
||||
operations: Vec<Operation>,
|
||||
}
|
||||
@@ -64,11 +75,39 @@ pub struct Buffer {
|
||||
pub struct Snapshot {
|
||||
text: buffer::Snapshot,
|
||||
tree: Option<Tree>,
|
||||
diagnostics: AnchorRangeMultimap<Diagnostic>,
|
||||
is_parsing: bool,
|
||||
language: Option<Arc<Language>>,
|
||||
query_cursor: QueryCursorHandle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Diagnostic {
|
||||
pub severity: DiagnosticSeverity,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
struct LanguageServerState {
|
||||
server: Arc<LanguageServer>,
|
||||
latest_snapshot: watch::Sender<Option<LanguageServerSnapshot>>,
|
||||
pending_snapshots: BTreeMap<usize, LanguageServerSnapshot>,
|
||||
next_version: usize,
|
||||
_maintain_server: Task<Option<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LanguageServerSnapshot {
|
||||
buffer_snapshot: buffer::Snapshot,
|
||||
version: usize,
|
||||
path: Arc<Path>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Operation {
|
||||
Buffer(buffer::Operation),
|
||||
UpdateDiagnostics(AnchorRangeMultimap<Diagnostic>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Event {
|
||||
Edited,
|
||||
@@ -87,13 +126,19 @@ pub trait File {
|
||||
|
||||
fn mtime(&self) -> SystemTime;
|
||||
|
||||
/// Returns the path of this file relative to the worktree's root directory.
|
||||
fn path(&self) -> &Arc<Path>;
|
||||
|
||||
fn full_path(&self, cx: &AppContext) -> PathBuf;
|
||||
/// Returns the absolute path of this file.
|
||||
fn abs_path(&self) -> Option<PathBuf>;
|
||||
|
||||
/// Returns the path of this file relative to the worktree's parent directory (this means it
|
||||
/// includes the name of the worktree's root folder).
|
||||
fn full_path(&self) -> PathBuf;
|
||||
|
||||
/// Returns the last component of this handle's absolute path. If this handle refers to the root
|
||||
/// of its worktree, then this method will return the name of the worktree itself.
|
||||
fn file_name<'a>(&'a self, cx: &'a AppContext) -> Option<OsString>;
|
||||
fn file_name(&self) -> Option<OsString>;
|
||||
|
||||
fn is_deleted(&self) -> bool;
|
||||
|
||||
@@ -150,15 +195,34 @@ struct Highlights<'a> {
|
||||
pub struct HighlightedChunks<'a> {
|
||||
range: Range<usize>,
|
||||
chunks: Chunks<'a>,
|
||||
diagnostic_endpoints: Peekable<vec::IntoIter<DiagnosticEndpoint>>,
|
||||
error_depth: usize,
|
||||
warning_depth: usize,
|
||||
information_depth: usize,
|
||||
hint_depth: usize,
|
||||
highlights: Option<Highlights<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct HighlightedChunk<'a> {
|
||||
pub text: &'a str,
|
||||
pub highlight_id: HighlightId,
|
||||
pub diagnostic: Option<DiagnosticSeverity>,
|
||||
}
|
||||
|
||||
struct Diff {
|
||||
base_version: clock::Global,
|
||||
new_text: Arc<str>,
|
||||
changes: Vec<(ChangeTag, usize)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct DiagnosticEndpoint {
|
||||
offset: usize,
|
||||
is_start: bool,
|
||||
severity: DiagnosticSeverity,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn new<T: Into<Arc<str>>>(
|
||||
replica_id: ReplicaId,
|
||||
@@ -172,23 +236,22 @@ impl Buffer {
|
||||
History::new(base_text.into()),
|
||||
),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_history(
|
||||
pub fn from_file<T: Into<Arc<str>>>(
|
||||
replica_id: ReplicaId,
|
||||
history: History,
|
||||
file: Option<Box<dyn File>>,
|
||||
language: Option<Arc<Language>>,
|
||||
base_text: T,
|
||||
file: Box<dyn File>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
Self::build(
|
||||
TextBuffer::new(replica_id, cx.model_id() as u64, history),
|
||||
file,
|
||||
language,
|
||||
cx,
|
||||
TextBuffer::new(
|
||||
replica_id,
|
||||
cx.model_id() as u64,
|
||||
History::new(base_text.into()),
|
||||
),
|
||||
Some(file),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -196,23 +259,54 @@ impl Buffer {
|
||||
replica_id: ReplicaId,
|
||||
message: proto::Buffer,
|
||||
file: Option<Box<dyn File>>,
|
||||
language: Option<Arc<Language>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self::build(
|
||||
TextBuffer::from_proto(replica_id, message)?,
|
||||
file,
|
||||
language,
|
||||
cx,
|
||||
))
|
||||
let mut buffer =
|
||||
buffer::Buffer::new(replica_id, message.id, History::new(message.content.into()));
|
||||
let ops = message
|
||||
.history
|
||||
.into_iter()
|
||||
.map(|op| buffer::Operation::Edit(proto::deserialize_edit_operation(op)));
|
||||
buffer.apply_ops(ops)?;
|
||||
for set in message.selections {
|
||||
let set = proto::deserialize_selection_set(set);
|
||||
buffer.add_raw_selection_set(set.id, set);
|
||||
}
|
||||
let mut this = Self::build(buffer, file);
|
||||
if let Some(diagnostics) = message.diagnostics {
|
||||
this.apply_diagnostic_update(proto::deserialize_diagnostics(diagnostics), cx);
|
||||
}
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
fn build(
|
||||
buffer: TextBuffer,
|
||||
file: Option<Box<dyn File>>,
|
||||
pub fn to_proto(&self) -> proto::Buffer {
|
||||
proto::Buffer {
|
||||
id: self.remote_id(),
|
||||
content: self.text.base_text().to_string(),
|
||||
history: self
|
||||
.text
|
||||
.history()
|
||||
.map(proto::serialize_edit_operation)
|
||||
.collect(),
|
||||
selections: self
|
||||
.selection_sets()
|
||||
.map(|(_, set)| proto::serialize_selection_set(set))
|
||||
.collect(),
|
||||
diagnostics: Some(proto::serialize_diagnostics(&self.diagnostics)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_language(
|
||||
mut self,
|
||||
language: Option<Arc<Language>>,
|
||||
language_server: Option<Arc<LanguageServer>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
self.set_language(language, language_server, cx);
|
||||
self
|
||||
}
|
||||
|
||||
fn build(buffer: TextBuffer, file: Option<Box<dyn File>>) -> Self {
|
||||
let saved_mtime;
|
||||
if let Some(file) = file.as_ref() {
|
||||
saved_mtime = file.mtime();
|
||||
@@ -220,7 +314,7 @@ impl Buffer {
|
||||
saved_mtime = UNIX_EPOCH;
|
||||
}
|
||||
|
||||
let mut result = Self {
|
||||
Self {
|
||||
text: buffer,
|
||||
saved_mtime,
|
||||
saved_version: clock::Global::new(),
|
||||
@@ -231,19 +325,20 @@ impl Buffer {
|
||||
sync_parse_timeout: Duration::from_millis(1),
|
||||
autoindent_requests: Default::default(),
|
||||
pending_autoindent: Default::default(),
|
||||
language,
|
||||
|
||||
language: None,
|
||||
diagnostics: Default::default(),
|
||||
diagnostics_update_count: 0,
|
||||
language_server: None,
|
||||
#[cfg(test)]
|
||||
operations: Default::default(),
|
||||
};
|
||||
result.reparse(cx);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Snapshot {
|
||||
Snapshot {
|
||||
text: self.text.snapshot(),
|
||||
tree: self.syntax_tree(),
|
||||
diagnostics: self.diagnostics.clone(),
|
||||
is_parsing: self.parsing_in_background,
|
||||
language: self.language.clone(),
|
||||
query_cursor: QueryCursorHandle::new(),
|
||||
@@ -263,7 +358,7 @@ impl Buffer {
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("buffer has no file"))?;
|
||||
let text = self.as_rope().clone();
|
||||
let version = self.version.clone();
|
||||
let version = self.version();
|
||||
let save = file.save(self.remote_id(), text, version, cx.as_mut());
|
||||
Ok(cx.spawn(|this, mut cx| async move {
|
||||
let (version, mtime) = save.await?;
|
||||
@@ -274,9 +369,96 @@ impl Buffer {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn set_language(&mut self, language: Option<Arc<Language>>, cx: &mut ModelContext<Self>) {
|
||||
pub fn set_language(
|
||||
&mut self,
|
||||
language: Option<Arc<Language>>,
|
||||
language_server: Option<Arc<lsp::LanguageServer>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.language = language;
|
||||
self.language_server = if let Some(server) = language_server {
|
||||
let (latest_snapshot_tx, mut latest_snapshot_rx) = watch::channel();
|
||||
Some(LanguageServerState {
|
||||
latest_snapshot: latest_snapshot_tx,
|
||||
pending_snapshots: Default::default(),
|
||||
next_version: 0,
|
||||
server: server.clone(),
|
||||
_maintain_server: cx.background().spawn(
|
||||
async move {
|
||||
let mut prev_snapshot: Option<LanguageServerSnapshot> = None;
|
||||
while let Some(snapshot) = latest_snapshot_rx.recv().await {
|
||||
if let Some(snapshot) = snapshot {
|
||||
let uri = lsp::Url::from_file_path(&snapshot.path).unwrap();
|
||||
if let Some(prev_snapshot) = prev_snapshot {
|
||||
let changes = lsp::DidChangeTextDocumentParams {
|
||||
text_document: lsp::VersionedTextDocumentIdentifier::new(
|
||||
uri,
|
||||
snapshot.version as i32,
|
||||
),
|
||||
content_changes: snapshot
|
||||
.buffer_snapshot
|
||||
.edits_since::<(PointUtf16, usize)>(
|
||||
prev_snapshot.buffer_snapshot.version(),
|
||||
)
|
||||
.map(|edit| {
|
||||
let edit_start = edit.new.start.0;
|
||||
let edit_end = edit_start
|
||||
+ (edit.old.end.0 - edit.old.start.0);
|
||||
let new_text = snapshot
|
||||
.buffer_snapshot
|
||||
.text_for_range(
|
||||
edit.new.start.1..edit.new.end.1,
|
||||
)
|
||||
.collect();
|
||||
lsp::TextDocumentContentChangeEvent {
|
||||
range: Some(lsp::Range::new(
|
||||
lsp::Position::new(
|
||||
edit_start.row,
|
||||
edit_start.column,
|
||||
),
|
||||
lsp::Position::new(
|
||||
edit_end.row,
|
||||
edit_end.column,
|
||||
),
|
||||
)),
|
||||
range_length: None,
|
||||
text: new_text,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
server
|
||||
.notify::<lsp::notification::DidChangeTextDocument>(changes)
|
||||
.await?;
|
||||
} else {
|
||||
server
|
||||
.notify::<lsp::notification::DidOpenTextDocument>(
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
uri,
|
||||
Default::default(),
|
||||
snapshot.version as i32,
|
||||
snapshot.buffer_snapshot.text().into(),
|
||||
),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
prev_snapshot = Some(snapshot);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.reparse(cx);
|
||||
self.update_language_server();
|
||||
}
|
||||
|
||||
pub fn did_save(
|
||||
@@ -291,6 +473,25 @@ impl Buffer {
|
||||
if let Some(new_file) = new_file {
|
||||
self.file = Some(new_file);
|
||||
}
|
||||
if let Some(state) = &self.language_server {
|
||||
cx.background()
|
||||
.spawn(
|
||||
state
|
||||
.server
|
||||
.notify::<lsp::notification::DidSaveTextDocument>(
|
||||
lsp::DidSaveTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier {
|
||||
uri: lsp::Url::from_file_path(
|
||||
self.file.as_ref().unwrap().abs_path().unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
},
|
||||
text: None,
|
||||
},
|
||||
),
|
||||
)
|
||||
.detach()
|
||||
}
|
||||
cx.emit(Event::Saved);
|
||||
}
|
||||
|
||||
@@ -332,7 +533,7 @@ impl Buffer {
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.apply_diff(diff, cx) {
|
||||
this.saved_version = this.version.clone();
|
||||
this.saved_version = this.version();
|
||||
this.saved_mtime = new_mtime;
|
||||
cx.emit(Event::Reloaded);
|
||||
}
|
||||
@@ -453,22 +654,17 @@ impl Buffer {
|
||||
}
|
||||
|
||||
fn interpolate_tree(&self, tree: &mut SyntaxTree) {
|
||||
let mut delta = 0_isize;
|
||||
for edit in self.edits_since(tree.version.clone()) {
|
||||
let start_offset = (edit.old_bytes.start as isize + delta) as usize;
|
||||
let start_point = self.as_rope().to_point(start_offset);
|
||||
for edit in self.edits_since::<(usize, Point)>(&tree.version) {
|
||||
let (bytes, lines) = edit.flatten();
|
||||
tree.tree.edit(&InputEdit {
|
||||
start_byte: start_offset,
|
||||
old_end_byte: start_offset + edit.deleted_bytes(),
|
||||
new_end_byte: start_offset + edit.inserted_bytes(),
|
||||
start_position: start_point.to_ts_point(),
|
||||
old_end_position: (start_point + edit.deleted_lines()).to_ts_point(),
|
||||
new_end_position: self
|
||||
.as_rope()
|
||||
.to_point(start_offset + edit.inserted_bytes())
|
||||
start_byte: bytes.new.start,
|
||||
old_end_byte: bytes.new.start + bytes.old.len(),
|
||||
new_end_byte: bytes.new.end,
|
||||
start_position: lines.new.start.to_ts_point(),
|
||||
old_end_position: (lines.new.start + (lines.old.end - lines.old.start))
|
||||
.to_ts_point(),
|
||||
new_end_position: lines.new.end.to_ts_point(),
|
||||
});
|
||||
delta += edit.inserted_bytes() as isize - edit.deleted_bytes() as isize;
|
||||
}
|
||||
tree.version = self.version();
|
||||
}
|
||||
@@ -486,6 +682,118 @@ impl Buffer {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn update_diagnostics(
|
||||
&mut self,
|
||||
version: Option<i32>,
|
||||
mut diagnostics: Vec<lsp::Diagnostic>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<Operation> {
|
||||
let version = version.map(|version| version as usize);
|
||||
let content = if let Some(version) = version {
|
||||
let language_server = self.language_server.as_mut().unwrap();
|
||||
let snapshot = language_server
|
||||
.pending_snapshots
|
||||
.get(&version)
|
||||
.ok_or_else(|| anyhow!("missing snapshot"))?;
|
||||
snapshot.buffer_snapshot.content()
|
||||
} else {
|
||||
self.content()
|
||||
};
|
||||
|
||||
let empty_set = HashSet::new();
|
||||
let disk_based_sources = self
|
||||
.language
|
||||
.as_ref()
|
||||
.and_then(|language| language.disk_based_diagnostic_sources())
|
||||
.unwrap_or(&empty_set);
|
||||
|
||||
diagnostics.sort_unstable_by_key(|d| (d.range.start, d.range.end));
|
||||
self.diagnostics = {
|
||||
let mut edits_since_save = content
|
||||
.edits_since::<PointUtf16>(&self.saved_version)
|
||||
.peekable();
|
||||
let mut last_edit_old_end = PointUtf16::zero();
|
||||
let mut last_edit_new_end = PointUtf16::zero();
|
||||
|
||||
content.anchor_range_multimap(
|
||||
Bias::Left,
|
||||
Bias::Right,
|
||||
diagnostics.into_iter().filter_map(|diagnostic| {
|
||||
let mut start = PointUtf16::new(
|
||||
diagnostic.range.start.line,
|
||||
diagnostic.range.start.character,
|
||||
);
|
||||
let mut end =
|
||||
PointUtf16::new(diagnostic.range.end.line, diagnostic.range.end.character);
|
||||
if diagnostic
|
||||
.source
|
||||
.as_ref()
|
||||
.map_or(false, |source| disk_based_sources.contains(source))
|
||||
{
|
||||
while let Some(edit) = edits_since_save.peek() {
|
||||
if edit.old.end <= start {
|
||||
last_edit_old_end = edit.old.end;
|
||||
last_edit_new_end = edit.new.end;
|
||||
edits_since_save.next();
|
||||
} else if edit.old.start <= end && edit.old.end >= start {
|
||||
return None;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
start = last_edit_new_end + (start - last_edit_old_end);
|
||||
end = last_edit_new_end + (end - last_edit_old_end);
|
||||
}
|
||||
|
||||
let mut range = content.clip_point_utf16(start, Bias::Left)
|
||||
..content.clip_point_utf16(end, Bias::Right);
|
||||
if range.start == range.end {
|
||||
range.end.column += 1;
|
||||
range.end = content.clip_point_utf16(range.end, Bias::Right);
|
||||
}
|
||||
Some((
|
||||
range,
|
||||
Diagnostic {
|
||||
severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR),
|
||||
message: diagnostic.message,
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(version) = version {
|
||||
let language_server = self.language_server.as_mut().unwrap();
|
||||
let versions_to_delete = language_server
|
||||
.pending_snapshots
|
||||
.range(..version)
|
||||
.map(|(v, _)| *v)
|
||||
.collect::<Vec<_>>();
|
||||
for version in versions_to_delete {
|
||||
language_server.pending_snapshots.remove(&version);
|
||||
}
|
||||
}
|
||||
|
||||
self.diagnostics_update_count += 1;
|
||||
cx.notify();
|
||||
Ok(Operation::UpdateDiagnostics(self.diagnostics.clone()))
|
||||
}
|
||||
|
||||
pub fn diagnostics_in_range<'a, T: 'a + ToOffset>(
|
||||
&'a self,
|
||||
range: Range<T>,
|
||||
) -> impl Iterator<Item = (Range<Point>, &Diagnostic)> + 'a {
|
||||
let content = self.content();
|
||||
self.diagnostics
|
||||
.intersecting_ranges(range, content, true)
|
||||
.map(move |(_, range, diagnostic)| (range, diagnostic))
|
||||
}
|
||||
|
||||
pub fn diagnostics_update_count(&self) -> usize {
|
||||
self.diagnostics_update_count
|
||||
}
|
||||
|
||||
fn request_autoindent(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(indent_columns) = self.compute_autoindents() {
|
||||
let indent_columns = cx.background().spawn(indent_columns);
|
||||
@@ -810,17 +1118,39 @@ impl Buffer {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some(start_version) = self.text.end_transaction_at(selection_set_ids, now) {
|
||||
cx.notify();
|
||||
let was_dirty = start_version != self.saved_version;
|
||||
let edited = self.edits_since(start_version).next().is_some();
|
||||
if edited {
|
||||
self.did_edit(was_dirty, cx);
|
||||
self.reparse(cx);
|
||||
}
|
||||
self.did_edit(&start_version, was_dirty, cx);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_language_server(&mut self) {
|
||||
let language_server = if let Some(language_server) = self.language_server.as_mut() {
|
||||
language_server
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let abs_path = self
|
||||
.file
|
||||
.as_ref()
|
||||
.map_or(Path::new("/").to_path_buf(), |file| {
|
||||
file.abs_path().unwrap()
|
||||
});
|
||||
|
||||
let version = post_inc(&mut language_server.next_version);
|
||||
let snapshot = LanguageServerSnapshot {
|
||||
buffer_snapshot: self.text.snapshot(),
|
||||
version,
|
||||
path: Arc::from(abs_path),
|
||||
};
|
||||
language_server
|
||||
.pending_snapshots
|
||||
.insert(version, snapshot.clone());
|
||||
let _ = language_server
|
||||
.latest_snapshot
|
||||
.blocking_send(Some(snapshot));
|
||||
}
|
||||
|
||||
pub fn edit<I, S, T>(&mut self, ranges_iter: I, new_text: T, cx: &mut ModelContext<Self>)
|
||||
where
|
||||
I: IntoIterator<Item = Range<S>>,
|
||||
@@ -925,14 +1255,27 @@ impl Buffer {
|
||||
}
|
||||
|
||||
self.end_transaction(None, cx).unwrap();
|
||||
self.send_operation(Operation::Edit(edit), cx);
|
||||
self.send_operation(Operation::Buffer(buffer::Operation::Edit(edit)), cx);
|
||||
}
|
||||
|
||||
fn did_edit(&self, was_dirty: bool, cx: &mut ModelContext<Self>) {
|
||||
fn did_edit(
|
||||
&mut self,
|
||||
old_version: &clock::Global,
|
||||
was_dirty: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if self.edits_since::<usize>(old_version).next().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.reparse(cx);
|
||||
self.update_language_server();
|
||||
|
||||
cx.emit(Event::Edited);
|
||||
if !was_dirty {
|
||||
cx.emit(Event::Dirtied);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_selection_set<T: ToOffset>(
|
||||
@@ -941,10 +1284,10 @@ impl Buffer {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> SelectionSetId {
|
||||
let operation = self.text.add_selection_set(selections);
|
||||
if let Operation::UpdateSelections { set_id, .. } = &operation {
|
||||
if let buffer::Operation::UpdateSelections { set_id, .. } = &operation {
|
||||
let set_id = *set_id;
|
||||
cx.notify();
|
||||
self.send_operation(operation, cx);
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
set_id
|
||||
} else {
|
||||
unreachable!()
|
||||
@@ -959,7 +1302,7 @@ impl Buffer {
|
||||
) -> Result<()> {
|
||||
let operation = self.text.update_selection_set(set_id, selections)?;
|
||||
cx.notify();
|
||||
self.send_operation(operation, cx);
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -969,7 +1312,7 @@ impl Buffer {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
let operation = self.text.set_active_selection_set(set_id)?;
|
||||
self.send_operation(operation, cx);
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -980,7 +1323,7 @@ impl Buffer {
|
||||
) -> Result<()> {
|
||||
let operation = self.text.remove_selection_set(set_id)?;
|
||||
cx.notify();
|
||||
self.send_operation(operation, cx);
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -990,21 +1333,36 @@ impl Buffer {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
self.pending_autoindent.take();
|
||||
|
||||
let was_dirty = self.is_dirty();
|
||||
let old_version = self.version.clone();
|
||||
|
||||
self.text.apply_ops(ops)?;
|
||||
|
||||
let buffer_ops = ops
|
||||
.into_iter()
|
||||
.filter_map(|op| match op {
|
||||
Operation::Buffer(op) => Some(op),
|
||||
Operation::UpdateDiagnostics(diagnostics) => {
|
||||
self.apply_diagnostic_update(diagnostics, cx);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.text.apply_ops(buffer_ops)?;
|
||||
self.did_edit(&old_version, was_dirty, cx);
|
||||
// Notify independently of whether the buffer was edited as the operations could include a
|
||||
// selection update.
|
||||
cx.notify();
|
||||
if self.edits_since(old_version).next().is_some() {
|
||||
self.did_edit(was_dirty, cx);
|
||||
self.reparse(cx);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_diagnostic_update(
|
||||
&mut self,
|
||||
diagnostics: AnchorRangeMultimap<Diagnostic>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.diagnostics = diagnostics;
|
||||
self.diagnostics_update_count += 1;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub fn send_operation(&mut self, operation: Operation, cx: &mut ModelContext<Self>) {
|
||||
if let Some(file) = &self.file {
|
||||
@@ -1027,14 +1385,10 @@ impl Buffer {
|
||||
let old_version = self.version.clone();
|
||||
|
||||
for operation in self.text.undo() {
|
||||
self.send_operation(operation, cx);
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
if self.edits_since(old_version).next().is_some() {
|
||||
self.did_edit(was_dirty, cx);
|
||||
self.reparse(cx);
|
||||
}
|
||||
self.did_edit(&old_version, was_dirty, cx);
|
||||
}
|
||||
|
||||
pub fn redo(&mut self, cx: &mut ModelContext<Self>) {
|
||||
@@ -1042,14 +1396,10 @@ impl Buffer {
|
||||
let old_version = self.version.clone();
|
||||
|
||||
for operation in self.text.redo() {
|
||||
self.send_operation(operation, cx);
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
if self.edits_since(old_version).next().is_some() {
|
||||
self.did_edit(was_dirty, cx);
|
||||
self.reparse(cx);
|
||||
}
|
||||
self.did_edit(&old_version, was_dirty, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1080,6 +1430,7 @@ impl Entity for Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Do we need to clone a buffer?
|
||||
impl Clone for Buffer {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
@@ -1094,7 +1445,9 @@ impl Clone for Buffer {
|
||||
parse_count: self.parse_count,
|
||||
autoindent_requests: Default::default(),
|
||||
pending_autoindent: Default::default(),
|
||||
|
||||
diagnostics: self.diagnostics.clone(),
|
||||
diagnostics_update_count: self.diagnostics_update_count,
|
||||
language_server: None,
|
||||
#[cfg(test)]
|
||||
operations: self.operations.clone(),
|
||||
}
|
||||
@@ -1247,30 +1600,54 @@ impl Snapshot {
|
||||
range: Range<T>,
|
||||
) -> HighlightedChunks {
|
||||
let range = range.start.to_offset(&*self)..range.end.to_offset(&*self);
|
||||
let chunks = self.text.as_rope().chunks_in_range(range.clone());
|
||||
if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) {
|
||||
let captures = self.query_cursor.set_byte_range(range.clone()).captures(
|
||||
&language.highlights_query,
|
||||
tree.root_node(),
|
||||
TextProvider(self.text.as_rope()),
|
||||
);
|
||||
|
||||
HighlightedChunks {
|
||||
range,
|
||||
chunks,
|
||||
highlights: Some(Highlights {
|
||||
let mut diagnostic_endpoints = Vec::<DiagnosticEndpoint>::new();
|
||||
for (_, range, diagnostic) in
|
||||
self.diagnostics
|
||||
.intersecting_ranges(range.clone(), self.content(), true)
|
||||
{
|
||||
diagnostic_endpoints.push(DiagnosticEndpoint {
|
||||
offset: range.start,
|
||||
is_start: true,
|
||||
severity: diagnostic.severity,
|
||||
});
|
||||
diagnostic_endpoints.push(DiagnosticEndpoint {
|
||||
offset: range.end,
|
||||
is_start: false,
|
||||
severity: diagnostic.severity,
|
||||
});
|
||||
}
|
||||
diagnostic_endpoints.sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start));
|
||||
let diagnostic_endpoints = diagnostic_endpoints.into_iter().peekable();
|
||||
|
||||
let chunks = self.text.as_rope().chunks_in_range(range.clone());
|
||||
let highlights =
|
||||
if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) {
|
||||
let captures = self.query_cursor.set_byte_range(range.clone()).captures(
|
||||
&language.highlights_query,
|
||||
tree.root_node(),
|
||||
TextProvider(self.text.as_rope()),
|
||||
);
|
||||
|
||||
Some(Highlights {
|
||||
captures,
|
||||
next_capture: None,
|
||||
stack: Default::default(),
|
||||
highlight_map: language.highlight_map(),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
HighlightedChunks {
|
||||
range,
|
||||
chunks,
|
||||
highlights: None,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
HighlightedChunks {
|
||||
range,
|
||||
chunks,
|
||||
diagnostic_endpoints,
|
||||
error_depth: 0,
|
||||
warning_depth: 0,
|
||||
information_depth: 0,
|
||||
hint_depth: 0,
|
||||
highlights,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1280,6 +1657,7 @@ impl Clone for Snapshot {
|
||||
Self {
|
||||
text: self.text.clone(),
|
||||
tree: self.tree.clone(),
|
||||
diagnostics: self.diagnostics.clone(),
|
||||
is_parsing: self.is_parsing,
|
||||
language: self.language.clone(),
|
||||
query_cursor: QueryCursorHandle::new(),
|
||||
@@ -1341,13 +1719,43 @@ impl<'a> HighlightedChunks<'a> {
|
||||
pub fn offset(&self) -> usize {
|
||||
self.range.start
|
||||
}
|
||||
|
||||
fn update_diagnostic_depths(&mut self, endpoint: DiagnosticEndpoint) {
|
||||
let depth = match endpoint.severity {
|
||||
DiagnosticSeverity::ERROR => &mut self.error_depth,
|
||||
DiagnosticSeverity::WARNING => &mut self.warning_depth,
|
||||
DiagnosticSeverity::INFORMATION => &mut self.information_depth,
|
||||
DiagnosticSeverity::HINT => &mut self.hint_depth,
|
||||
_ => return,
|
||||
};
|
||||
if endpoint.is_start {
|
||||
*depth += 1;
|
||||
} else {
|
||||
*depth -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn current_diagnostic_severity(&mut self) -> Option<DiagnosticSeverity> {
|
||||
if self.error_depth > 0 {
|
||||
Some(DiagnosticSeverity::ERROR)
|
||||
} else if self.warning_depth > 0 {
|
||||
Some(DiagnosticSeverity::WARNING)
|
||||
} else if self.information_depth > 0 {
|
||||
Some(DiagnosticSeverity::INFORMATION)
|
||||
} else if self.hint_depth > 0 {
|
||||
Some(DiagnosticSeverity::HINT)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
type Item = (&'a str, HighlightId);
|
||||
type Item = HighlightedChunk<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut next_capture_start = usize::MAX;
|
||||
let mut next_diagnostic_endpoint = usize::MAX;
|
||||
|
||||
if let Some(highlights) = self.highlights.as_mut() {
|
||||
while let Some((parent_capture_end, _)) = highlights.stack.last() {
|
||||
@@ -1368,22 +1776,36 @@ impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
next_capture_start = capture.node.start_byte();
|
||||
break;
|
||||
} else {
|
||||
let style_id = highlights.highlight_map.get(capture.index);
|
||||
highlights.stack.push((capture.node.end_byte(), style_id));
|
||||
let highlight_id = highlights.highlight_map.get(capture.index);
|
||||
highlights
|
||||
.stack
|
||||
.push((capture.node.end_byte(), highlight_id));
|
||||
highlights.next_capture = highlights.captures.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(endpoint) = self.diagnostic_endpoints.peek().copied() {
|
||||
if endpoint.offset <= self.range.start {
|
||||
self.update_diagnostic_depths(endpoint);
|
||||
self.diagnostic_endpoints.next();
|
||||
} else {
|
||||
next_diagnostic_endpoint = endpoint.offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(chunk) = self.chunks.peek() {
|
||||
let chunk_start = self.range.start;
|
||||
let mut chunk_end = (self.chunks.offset() + chunk.len()).min(next_capture_start);
|
||||
let mut style_id = HighlightId::default();
|
||||
if let Some((parent_capture_end, parent_style_id)) =
|
||||
let mut chunk_end = (self.chunks.offset() + chunk.len())
|
||||
.min(next_capture_start)
|
||||
.min(next_diagnostic_endpoint);
|
||||
let mut highlight_id = HighlightId::default();
|
||||
if let Some((parent_capture_end, parent_highlight_id)) =
|
||||
self.highlights.as_ref().and_then(|h| h.stack.last())
|
||||
{
|
||||
chunk_end = chunk_end.min(*parent_capture_end);
|
||||
style_id = *parent_style_id;
|
||||
highlight_id = *parent_highlight_id;
|
||||
}
|
||||
|
||||
let slice =
|
||||
@@ -1393,7 +1815,11 @@ impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
self.chunks.next().unwrap();
|
||||
}
|
||||
|
||||
Some((slice, style_id))
|
||||
Some(HighlightedChunk {
|
||||
text: slice,
|
||||
highlight_id,
|
||||
diagnostic: self.current_diagnostic_severity(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
315
crates/language/src/proto.rs
Normal file
315
crates/language/src/proto.rs
Normal file
@@ -0,0 +1,315 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::Diagnostic;
|
||||
|
||||
use super::Operation;
|
||||
use anyhow::{anyhow, Result};
|
||||
use buffer::*;
|
||||
use clock::ReplicaId;
|
||||
use lsp::DiagnosticSeverity;
|
||||
use rpc::proto;
|
||||
|
||||
pub use proto::Buffer;
|
||||
|
||||
pub fn serialize_operation(operation: &Operation) -> proto::Operation {
|
||||
proto::Operation {
|
||||
variant: Some(match operation {
|
||||
Operation::Buffer(buffer::Operation::Edit(edit)) => {
|
||||
proto::operation::Variant::Edit(serialize_edit_operation(edit))
|
||||
}
|
||||
Operation::Buffer(buffer::Operation::Undo {
|
||||
undo,
|
||||
lamport_timestamp,
|
||||
}) => proto::operation::Variant::Undo(proto::operation::Undo {
|
||||
replica_id: undo.id.replica_id as u32,
|
||||
local_timestamp: undo.id.value,
|
||||
lamport_timestamp: lamport_timestamp.value,
|
||||
ranges: undo
|
||||
.ranges
|
||||
.iter()
|
||||
.map(|r| proto::Range {
|
||||
start: r.start.0 as u64,
|
||||
end: r.end.0 as u64,
|
||||
})
|
||||
.collect(),
|
||||
counts: undo
|
||||
.counts
|
||||
.iter()
|
||||
.map(|(edit_id, count)| proto::operation::UndoCount {
|
||||
replica_id: edit_id.replica_id as u32,
|
||||
local_timestamp: edit_id.value,
|
||||
count: *count,
|
||||
})
|
||||
.collect(),
|
||||
version: From::from(&undo.version),
|
||||
}),
|
||||
Operation::Buffer(buffer::Operation::UpdateSelections {
|
||||
set_id,
|
||||
selections,
|
||||
lamport_timestamp,
|
||||
}) => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
|
||||
replica_id: set_id.replica_id as u32,
|
||||
local_timestamp: set_id.value,
|
||||
lamport_timestamp: lamport_timestamp.value,
|
||||
version: selections.version().into(),
|
||||
selections: selections
|
||||
.full_offset_ranges()
|
||||
.map(|(range, state)| proto::Selection {
|
||||
id: state.id as u64,
|
||||
start: range.start.0 as u64,
|
||||
end: range.end.0 as u64,
|
||||
reversed: state.reversed,
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
Operation::Buffer(buffer::Operation::RemoveSelections {
|
||||
set_id,
|
||||
lamport_timestamp,
|
||||
}) => proto::operation::Variant::RemoveSelections(proto::operation::RemoveSelections {
|
||||
replica_id: set_id.replica_id as u32,
|
||||
local_timestamp: set_id.value,
|
||||
lamport_timestamp: lamport_timestamp.value,
|
||||
}),
|
||||
Operation::Buffer(buffer::Operation::SetActiveSelections {
|
||||
set_id,
|
||||
lamport_timestamp,
|
||||
}) => proto::operation::Variant::SetActiveSelections(
|
||||
proto::operation::SetActiveSelections {
|
||||
replica_id: lamport_timestamp.replica_id as u32,
|
||||
local_timestamp: set_id.map(|set_id| set_id.value),
|
||||
lamport_timestamp: lamport_timestamp.value,
|
||||
},
|
||||
),
|
||||
Operation::UpdateDiagnostics(diagnostic_set) => {
|
||||
proto::operation::Variant::UpdateDiagnostics(serialize_diagnostics(diagnostic_set))
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit {
|
||||
let ranges = operation
|
||||
.ranges
|
||||
.iter()
|
||||
.map(|range| proto::Range {
|
||||
start: range.start.0 as u64,
|
||||
end: range.end.0 as u64,
|
||||
})
|
||||
.collect();
|
||||
proto::operation::Edit {
|
||||
replica_id: operation.timestamp.replica_id as u32,
|
||||
local_timestamp: operation.timestamp.local,
|
||||
lamport_timestamp: operation.timestamp.lamport,
|
||||
version: From::from(&operation.version),
|
||||
ranges,
|
||||
new_text: operation.new_text.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_selection_set(set: &SelectionSet) -> proto::SelectionSet {
|
||||
let version = set.selections.version();
|
||||
let entries = set.selections.full_offset_ranges();
|
||||
proto::SelectionSet {
|
||||
replica_id: set.id.replica_id as u32,
|
||||
lamport_timestamp: set.id.value as u32,
|
||||
is_active: set.active,
|
||||
version: version.into(),
|
||||
selections: entries
|
||||
.map(|(range, state)| proto::Selection {
|
||||
id: state.id as u64,
|
||||
start: range.start.0 as u64,
|
||||
end: range.end.0 as u64,
|
||||
reversed: state.reversed,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_diagnostics(map: &AnchorRangeMultimap<Diagnostic>) -> proto::DiagnosticSet {
|
||||
proto::DiagnosticSet {
|
||||
version: map.version().into(),
|
||||
diagnostics: map
|
||||
.full_offset_ranges()
|
||||
.map(|(range, diagnostic)| proto::Diagnostic {
|
||||
start: range.start.0 as u64,
|
||||
end: range.end.0 as u64,
|
||||
message: diagnostic.message.clone(),
|
||||
severity: match diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error,
|
||||
DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning,
|
||||
DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information,
|
||||
DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint,
|
||||
_ => proto::diagnostic::Severity::None,
|
||||
} as i32,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
|
||||
Ok(
|
||||
match message
|
||||
.variant
|
||||
.ok_or_else(|| anyhow!("missing operation variant"))?
|
||||
{
|
||||
proto::operation::Variant::Edit(edit) => {
|
||||
Operation::Buffer(buffer::Operation::Edit(deserialize_edit_operation(edit)))
|
||||
}
|
||||
proto::operation::Variant::Undo(undo) => Operation::Buffer(buffer::Operation::Undo {
|
||||
lamport_timestamp: clock::Lamport {
|
||||
replica_id: undo.replica_id as ReplicaId,
|
||||
value: undo.lamport_timestamp,
|
||||
},
|
||||
undo: UndoOperation {
|
||||
id: clock::Local {
|
||||
replica_id: undo.replica_id as ReplicaId,
|
||||
value: undo.local_timestamp,
|
||||
},
|
||||
counts: undo
|
||||
.counts
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
(
|
||||
clock::Local {
|
||||
replica_id: c.replica_id as ReplicaId,
|
||||
value: c.local_timestamp,
|
||||
},
|
||||
c.count,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
ranges: undo
|
||||
.ranges
|
||||
.into_iter()
|
||||
.map(|r| FullOffset(r.start as usize)..FullOffset(r.end as usize))
|
||||
.collect(),
|
||||
version: undo.version.into(),
|
||||
},
|
||||
}),
|
||||
proto::operation::Variant::UpdateSelections(message) => {
|
||||
let version = message.version.into();
|
||||
let entries = message
|
||||
.selections
|
||||
.iter()
|
||||
.map(|selection| {
|
||||
let range = (FullOffset(selection.start as usize), Bias::Left)
|
||||
..(FullOffset(selection.end as usize), Bias::Right);
|
||||
let state = SelectionState {
|
||||
id: selection.id as usize,
|
||||
reversed: selection.reversed,
|
||||
goal: SelectionGoal::None,
|
||||
};
|
||||
(range, state)
|
||||
})
|
||||
.collect();
|
||||
let selections = AnchorRangeMap::from_full_offset_ranges(version, entries);
|
||||
|
||||
Operation::Buffer(buffer::Operation::UpdateSelections {
|
||||
set_id: clock::Lamport {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
value: message.local_timestamp,
|
||||
},
|
||||
lamport_timestamp: clock::Lamport {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
value: message.lamport_timestamp,
|
||||
},
|
||||
selections: Arc::from(selections),
|
||||
})
|
||||
}
|
||||
proto::operation::Variant::RemoveSelections(message) => {
|
||||
Operation::Buffer(buffer::Operation::RemoveSelections {
|
||||
set_id: clock::Lamport {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
value: message.local_timestamp,
|
||||
},
|
||||
lamport_timestamp: clock::Lamport {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
value: message.lamport_timestamp,
|
||||
},
|
||||
})
|
||||
}
|
||||
proto::operation::Variant::SetActiveSelections(message) => {
|
||||
Operation::Buffer(buffer::Operation::SetActiveSelections {
|
||||
set_id: message.local_timestamp.map(|value| clock::Lamport {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
value,
|
||||
}),
|
||||
lamport_timestamp: clock::Lamport {
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
value: message.lamport_timestamp,
|
||||
},
|
||||
})
|
||||
}
|
||||
proto::operation::Variant::UpdateDiagnostics(message) => {
|
||||
Operation::UpdateDiagnostics(deserialize_diagnostics(message))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation {
|
||||
let ranges = edit
|
||||
.ranges
|
||||
.into_iter()
|
||||
.map(|range| FullOffset(range.start as usize)..FullOffset(range.end as usize))
|
||||
.collect();
|
||||
EditOperation {
|
||||
timestamp: InsertionTimestamp {
|
||||
replica_id: edit.replica_id as ReplicaId,
|
||||
local: edit.local_timestamp,
|
||||
lamport: edit.lamport_timestamp,
|
||||
},
|
||||
version: edit.version.into(),
|
||||
ranges,
|
||||
new_text: edit.new_text,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_selection_set(set: proto::SelectionSet) -> SelectionSet {
|
||||
SelectionSet {
|
||||
id: clock::Lamport {
|
||||
replica_id: set.replica_id as u16,
|
||||
value: set.lamport_timestamp,
|
||||
},
|
||||
active: set.is_active,
|
||||
selections: Arc::new(AnchorRangeMap::from_full_offset_ranges(
|
||||
set.version.into(),
|
||||
set.selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let range = (FullOffset(selection.start as usize), Bias::Left)
|
||||
..(FullOffset(selection.end as usize), Bias::Right);
|
||||
let state = SelectionState {
|
||||
id: selection.id as usize,
|
||||
reversed: selection.reversed,
|
||||
goal: SelectionGoal::None,
|
||||
};
|
||||
(range, state)
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_diagnostics(message: proto::DiagnosticSet) -> AnchorRangeMultimap<Diagnostic> {
|
||||
AnchorRangeMultimap::from_full_offset_ranges(
|
||||
message.version.into(),
|
||||
Bias::Left,
|
||||
Bias::Right,
|
||||
message.diagnostics.into_iter().filter_map(|diagnostic| {
|
||||
Some((
|
||||
FullOffset(diagnostic.start as usize)..FullOffset(diagnostic.end as usize),
|
||||
Diagnostic {
|
||||
severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? {
|
||||
proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR,
|
||||
proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING,
|
||||
proto::diagnostic::Severity::Information => DiagnosticSeverity::INFORMATION,
|
||||
proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT,
|
||||
proto::diagnostic::Severity::None => return None,
|
||||
},
|
||||
message: diagnostic.message,
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use gpui::{ModelHandle, MutableAppContext};
|
||||
use std::rc::Rc;
|
||||
use std::{iter::FromIterator, rc::Rc};
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -78,9 +78,9 @@ async fn test_apply_diff(mut cx: gpui::TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reparse(mut cx: gpui::TestAppContext) {
|
||||
let text = "fn a() {}";
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let text = "fn a() {}".into();
|
||||
Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx)
|
||||
Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
|
||||
});
|
||||
|
||||
// Wait for the initial text to parse
|
||||
@@ -222,9 +222,8 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
}
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
.into();
|
||||
Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx)
|
||||
.unindent();
|
||||
Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
|
||||
});
|
||||
let buffer = buffer.read(cx);
|
||||
assert_eq!(
|
||||
@@ -253,8 +252,9 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
#[gpui::test]
|
||||
fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let text = "fn a() {}".into();
|
||||
let mut buffer = Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx);
|
||||
let text = "fn a() {}";
|
||||
let mut buffer =
|
||||
Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
|
||||
|
||||
buffer.edit_with_autoindent([8..8], "\n\n", cx);
|
||||
assert_eq!(buffer.text(), "fn a() {\n \n}");
|
||||
@@ -272,8 +272,10 @@ fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
|
||||
#[gpui::test]
|
||||
fn test_autoindent_moves_selections(cx: &mut MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let text = History::new("fn a() {}".into());
|
||||
let mut buffer = Buffer::from_history(0, text, None, Some(rust_lang()), cx);
|
||||
let text = "fn a() {}";
|
||||
|
||||
let mut buffer =
|
||||
Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
|
||||
|
||||
let selection_set_id = buffer.add_selection_set::<usize>(&[], cx);
|
||||
buffer.start_transaction(Some(selection_set_id)).unwrap();
|
||||
@@ -329,9 +331,10 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
|
||||
d;
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
.into();
|
||||
let mut buffer = Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx);
|
||||
.unindent();
|
||||
|
||||
let mut buffer =
|
||||
Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
|
||||
|
||||
// Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
|
||||
// their indentation is not adjusted.
|
||||
@@ -375,14 +378,13 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
|
||||
#[gpui::test]
|
||||
fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) {
|
||||
cx.add_model(|cx| {
|
||||
let text = History::new(
|
||||
"
|
||||
fn a() {}
|
||||
"
|
||||
.unindent()
|
||||
.into(),
|
||||
);
|
||||
let mut buffer = Buffer::from_history(0, text, None, Some(rust_lang()), cx);
|
||||
let text = "
|
||||
fn a() {}
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let mut buffer =
|
||||
Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
|
||||
|
||||
buffer.edit_with_autoindent([5..5], "\nb", cx);
|
||||
assert_eq!(
|
||||
@@ -410,6 +412,247 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diagnostics(mut cx: gpui::TestAppContext) {
|
||||
let (language_server, mut fake) = lsp::LanguageServer::fake(cx.background()).await;
|
||||
let mut rust_lang = rust_lang();
|
||||
rust_lang.config.language_server = Some(LanguageServerConfig {
|
||||
disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let text = "
|
||||
fn a() { A }
|
||||
fn b() { BB }
|
||||
fn c() { CCC }
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang)), Some(language_server), cx)
|
||||
});
|
||||
|
||||
let open_notification = fake
|
||||
.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await;
|
||||
|
||||
// Edit the buffer, moving the content down
|
||||
buffer.update(&mut cx, |buffer, cx| buffer.edit([0..0], "\n\n", cx));
|
||||
let change_notification_1 = fake
|
||||
.receive_notification::<lsp::notification::DidChangeTextDocument>()
|
||||
.await;
|
||||
assert!(change_notification_1.text_document.version > open_notification.text_document.version);
|
||||
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
// Receive diagnostics for an earlier version of the buffer.
|
||||
buffer
|
||||
.update_diagnostics(
|
||||
Some(open_notification.text_document.version),
|
||||
vec![
|
||||
lsp::Diagnostic {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
message: "undefined variable 'A'".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::Diagnostic {
|
||||
range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
message: "undefined variable 'BB'".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::Diagnostic {
|
||||
range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
message: "undefined variable 'CCC'".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The diagnostics have moved down since they were created.
|
||||
assert_eq!(
|
||||
buffer
|
||||
.diagnostics_in_range(Point::new(3, 0)..Point::new(5, 0))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(
|
||||
Point::new(3, 9)..Point::new(3, 11),
|
||||
&Diagnostic {
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
message: "undefined variable 'BB'".to_string()
|
||||
},
|
||||
),
|
||||
(
|
||||
Point::new(4, 9)..Point::new(4, 12),
|
||||
&Diagnostic {
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
message: "undefined variable 'CCC'".to_string()
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
chunks_with_diagnostics(buffer, 0..buffer.len()),
|
||||
[
|
||||
("\n\nfn a() { ".to_string(), None),
|
||||
("A".to_string(), Some(DiagnosticSeverity::ERROR)),
|
||||
(" }\nfn b() { ".to_string(), None),
|
||||
("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
|
||||
(" }\nfn c() { ".to_string(), None),
|
||||
("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
|
||||
(" }\n".to_string(), None),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
|
||||
[
|
||||
("B".to_string(), Some(DiagnosticSeverity::ERROR)),
|
||||
(" }\nfn c() { ".to_string(), None),
|
||||
("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
|
||||
]
|
||||
);
|
||||
|
||||
// Ensure overlapping diagnostics are highlighted correctly.
|
||||
buffer
|
||||
.update_diagnostics(
|
||||
Some(open_notification.text_document.version),
|
||||
vec![
|
||||
lsp::Diagnostic {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
message: "undefined variable 'A'".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::Diagnostic {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
|
||||
severity: Some(lsp::DiagnosticSeverity::WARNING),
|
||||
message: "unreachable statement".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
buffer
|
||||
.diagnostics_in_range(Point::new(2, 0)..Point::new(3, 0))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(
|
||||
Point::new(2, 9)..Point::new(2, 12),
|
||||
&Diagnostic {
|
||||
severity: DiagnosticSeverity::WARNING,
|
||||
message: "unreachable statement".to_string()
|
||||
}
|
||||
),
|
||||
(
|
||||
Point::new(2, 9)..Point::new(2, 10),
|
||||
&Diagnostic {
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
message: "undefined variable 'A'".to_string()
|
||||
},
|
||||
)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
|
||||
[
|
||||
("fn a() { ".to_string(), None),
|
||||
("A".to_string(), Some(DiagnosticSeverity::ERROR)),
|
||||
(" }".to_string(), Some(DiagnosticSeverity::WARNING)),
|
||||
("\n".to_string(), None),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
|
||||
[
|
||||
(" }".to_string(), Some(DiagnosticSeverity::WARNING)),
|
||||
("\n".to_string(), None),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Keep editing the buffer and ensure disk-based diagnostics get translated according to the
|
||||
// changes since the last save.
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx);
|
||||
buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
|
||||
});
|
||||
let change_notification_2 = fake
|
||||
.receive_notification::<lsp::notification::DidChangeTextDocument>()
|
||||
.await;
|
||||
assert!(
|
||||
change_notification_2.text_document.version > change_notification_1.text_document.version
|
||||
);
|
||||
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer
|
||||
.update_diagnostics(
|
||||
Some(change_notification_2.text_document.version),
|
||||
vec![
|
||||
lsp::Diagnostic {
|
||||
range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
message: "undefined variable 'BB'".to_string(),
|
||||
source: Some("disk".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::Diagnostic {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
message: "undefined variable 'A'".to_string(),
|
||||
source: Some("disk".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
buffer
|
||||
.diagnostics_in_range(0..buffer.len())
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(
|
||||
Point::new(2, 21)..Point::new(2, 22),
|
||||
&Diagnostic {
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
message: "undefined variable 'A'".to_string()
|
||||
}
|
||||
),
|
||||
(
|
||||
Point::new(3, 9)..Point::new(3, 11),
|
||||
&Diagnostic {
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
message: "undefined variable 'BB'".to_string()
|
||||
},
|
||||
)
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
fn chunks_with_diagnostics<T: ToOffset>(
|
||||
buffer: &Buffer,
|
||||
range: Range<T>,
|
||||
) -> Vec<(String, Option<DiagnosticSeverity>)> {
|
||||
let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
|
||||
for chunk in buffer.snapshot().highlighted_text_for_range(range) {
|
||||
if chunks
|
||||
.last()
|
||||
.map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
|
||||
{
|
||||
chunks.last_mut().unwrap().0.push_str(chunk.text);
|
||||
} else {
|
||||
chunks.push((chunk.text.to_string(), chunk.diagnostic));
|
||||
}
|
||||
}
|
||||
chunks
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contiguous_ranges() {
|
||||
assert_eq!(
|
||||
@@ -437,28 +680,27 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_lang() -> Arc<Language> {
|
||||
Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
tree_sitter_rust::language(),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
language_server: None,
|
||||
..Default::default()
|
||||
},
|
||||
tree_sitter_rust::language(),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.with_brackets_query(r#" ("{" @open "}" @close) "#)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.with_brackets_query(r#" ("{" @open "}" @close) "#)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn empty(point: Point) -> Range<Point> {
|
||||
|
||||
28
crates/lsp/Cargo.toml
Normal file
28
crates/lsp/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "lsp"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
test-support = ["async-pipe"]
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
anyhow = "1.0"
|
||||
async-pipe = { git = "https://github.com/routerify/async-pipe-rs", rev = "feeb77e83142a9ff837d0767652ae41bfc5d8e47", optional = true }
|
||||
futures = "0.3"
|
||||
log = "0.4"
|
||||
lsp-types = "0.91"
|
||||
parking_lot = "0.11"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
smol = "1.2"
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
async-pipe = { git = "https://github.com/routerify/async-pipe-rs", rev = "feeb77e83142a9ff837d0767652ae41bfc5d8e47" }
|
||||
simplelog = "0.9"
|
||||
unindent = "0.1.7"
|
||||
710
crates/lsp/src/lib.rs
Normal file
710
crates/lsp/src/lib.rs
Normal file
@@ -0,0 +1,710 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::{io::BufWriter, AsyncRead, AsyncWrite};
|
||||
use gpui::{executor, Task};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use postage::{barrier, oneshot, prelude::Stream, sink::Sink};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, value::RawValue, Value};
|
||||
use smol::{
|
||||
channel,
|
||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
||||
process::Command,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
io::Write,
|
||||
str::FromStr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use std::{path::Path, process::Stdio};
|
||||
use util::TryFutureExt;
|
||||
|
||||
pub use lsp_types::*;
|
||||
|
||||
const JSON_RPC_VERSION: &'static str = "2.0";
|
||||
const CONTENT_LEN_HEADER: &'static str = "Content-Length: ";
|
||||
|
||||
type NotificationHandler = Box<dyn Send + Sync + Fn(&str)>;
|
||||
type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
|
||||
|
||||
pub struct LanguageServer {
|
||||
next_id: AtomicUsize,
|
||||
outbound_tx: RwLock<Option<channel::Sender<Vec<u8>>>>,
|
||||
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
|
||||
response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
|
||||
executor: Arc<executor::Background>,
|
||||
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
|
||||
initialized: barrier::Receiver,
|
||||
output_done_rx: Mutex<Option<barrier::Receiver>>,
|
||||
}
|
||||
|
||||
pub struct Subscription {
|
||||
method: &'static str,
|
||||
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Request<'a, T> {
|
||||
jsonrpc: &'a str,
|
||||
id: usize,
|
||||
method: &'a str,
|
||||
params: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AnyResponse<'a> {
|
||||
id: usize,
|
||||
#[serde(default)]
|
||||
error: Option<Error>,
|
||||
#[serde(borrow)]
|
||||
result: Option<&'a RawValue>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Notification<'a, T> {
|
||||
#[serde(borrow)]
|
||||
jsonrpc: &'a str,
|
||||
#[serde(borrow)]
|
||||
method: &'a str,
|
||||
params: T,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AnyNotification<'a> {
|
||||
#[serde(borrow)]
|
||||
method: &'a str,
|
||||
#[serde(borrow)]
|
||||
params: &'a RawValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Error {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl LanguageServer {
|
||||
pub fn new(
|
||||
binary_path: &Path,
|
||||
root_path: &Path,
|
||||
background: Arc<executor::Background>,
|
||||
) -> Result<Arc<Self>> {
|
||||
let mut server = Command::new(binary_path)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()?;
|
||||
let stdin = server.stdin.take().unwrap();
|
||||
let stdout = server.stdout.take().unwrap();
|
||||
Self::new_internal(stdin, stdout, root_path, background)
|
||||
}
|
||||
|
||||
fn new_internal<Stdin, Stdout>(
|
||||
stdin: Stdin,
|
||||
stdout: Stdout,
|
||||
root_path: &Path,
|
||||
executor: Arc<executor::Background>,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
Stdin: AsyncWrite + Unpin + Send + 'static,
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
let mut stdin = BufWriter::new(stdin);
|
||||
let mut stdout = BufReader::new(stdout);
|
||||
let (outbound_tx, outbound_rx) = channel::unbounded::<Vec<u8>>();
|
||||
let notification_handlers = Arc::new(RwLock::new(HashMap::<_, NotificationHandler>::new()));
|
||||
let response_handlers = Arc::new(Mutex::new(HashMap::<_, ResponseHandler>::new()));
|
||||
let input_task = executor.spawn(
|
||||
{
|
||||
let notification_handlers = notification_handlers.clone();
|
||||
let response_handlers = response_handlers.clone();
|
||||
async move {
|
||||
let mut buffer = Vec::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
let message_len: usize = std::str::from_utf8(&buffer)?
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.ok_or_else(|| anyhow!("invalid header"))?
|
||||
.trim_end()
|
||||
.parse()?;
|
||||
|
||||
buffer.resize(message_len, 0);
|
||||
stdout.read_exact(&mut buffer).await?;
|
||||
|
||||
if let Ok(AnyNotification { method, params }) =
|
||||
serde_json::from_slice(&buffer)
|
||||
{
|
||||
if let Some(handler) = notification_handlers.read().get(method) {
|
||||
handler(params.get());
|
||||
} else {
|
||||
log::info!(
|
||||
"unhandled notification {}:\n{}",
|
||||
method,
|
||||
serde_json::to_string_pretty(
|
||||
&Value::from_str(params.get()).unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
} else if let Ok(AnyResponse { id, error, result }) =
|
||||
serde_json::from_slice(&buffer)
|
||||
{
|
||||
if let Some(handler) = response_handlers.lock().remove(&id) {
|
||||
if let Some(error) = error {
|
||||
handler(Err(error));
|
||||
} else if let Some(result) = result {
|
||||
handler(Ok(result.get()));
|
||||
} else {
|
||||
handler(Ok("null"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"failed to deserialize message:\n{}",
|
||||
std::str::from_utf8(&buffer)?
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.log_err(),
|
||||
);
|
||||
let (output_done_tx, output_done_rx) = barrier::channel();
|
||||
let output_task = executor.spawn(
|
||||
async move {
|
||||
let mut content_len_buffer = Vec::new();
|
||||
while let Ok(message) = outbound_rx.recv().await {
|
||||
content_len_buffer.clear();
|
||||
write!(content_len_buffer, "{}", message.len()).unwrap();
|
||||
stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
|
||||
stdin.write_all(&content_len_buffer).await?;
|
||||
stdin.write_all("\r\n\r\n".as_bytes()).await?;
|
||||
stdin.write_all(&message).await?;
|
||||
stdin.flush().await?;
|
||||
}
|
||||
drop(output_done_tx);
|
||||
Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
);
|
||||
|
||||
let (initialized_tx, initialized_rx) = barrier::channel();
|
||||
let this = Arc::new(Self {
|
||||
notification_handlers,
|
||||
response_handlers,
|
||||
next_id: Default::default(),
|
||||
outbound_tx: RwLock::new(Some(outbound_tx)),
|
||||
executor: executor.clone(),
|
||||
io_tasks: Mutex::new(Some((input_task, output_task))),
|
||||
initialized: initialized_rx,
|
||||
output_done_rx: Mutex::new(Some(output_done_rx)),
|
||||
});
|
||||
|
||||
let root_uri =
|
||||
lsp_types::Url::from_file_path(root_path).map_err(|_| anyhow!("invalid root path"))?;
|
||||
executor
|
||||
.spawn({
|
||||
let this = this.clone();
|
||||
async move {
|
||||
this.init(root_uri).log_err().await;
|
||||
drop(initialized_tx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
async fn init(self: Arc<Self>, root_uri: lsp_types::Url) -> Result<()> {
|
||||
#[allow(deprecated)]
|
||||
let params = lsp_types::InitializeParams {
|
||||
process_id: Default::default(),
|
||||
root_path: Default::default(),
|
||||
root_uri: Some(root_uri),
|
||||
initialization_options: Default::default(),
|
||||
capabilities: lsp_types::ClientCapabilities {
|
||||
experimental: Some(json!({
|
||||
"serverStatusNotification": true,
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
trace: Default::default(),
|
||||
workspace_folders: Default::default(),
|
||||
client_info: Default::default(),
|
||||
locale: Default::default(),
|
||||
};
|
||||
|
||||
let this = self.clone();
|
||||
let request = Self::request_internal::<lsp_types::request::Initialize>(
|
||||
&this.next_id,
|
||||
&this.response_handlers,
|
||||
this.outbound_tx.read().as_ref(),
|
||||
params,
|
||||
);
|
||||
request.await?;
|
||||
Self::notify_internal::<lsp_types::notification::Initialized>(
|
||||
this.outbound_tx.read().as_ref(),
|
||||
lsp_types::InitializedParams {},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) -> Option<impl 'static + Send + Future<Output = Result<()>>> {
|
||||
if let Some(tasks) = self.io_tasks.lock().take() {
|
||||
let response_handlers = self.response_handlers.clone();
|
||||
let outbound_tx = self.outbound_tx.write().take();
|
||||
let next_id = AtomicUsize::new(self.next_id.load(SeqCst));
|
||||
let mut output_done = self.output_done_rx.lock().take().unwrap();
|
||||
Some(async move {
|
||||
Self::request_internal::<lsp_types::request::Shutdown>(
|
||||
&next_id,
|
||||
&response_handlers,
|
||||
outbound_tx.as_ref(),
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
Self::notify_internal::<lsp_types::notification::Exit>(outbound_tx.as_ref(), ())?;
|
||||
drop(outbound_tx);
|
||||
output_done.recv().await;
|
||||
drop(tasks);
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_notification<T, F>(&self, f: F) -> Subscription
|
||||
where
|
||||
T: lsp_types::notification::Notification,
|
||||
F: 'static + Send + Sync + Fn(T::Params),
|
||||
{
|
||||
let prev_handler = self.notification_handlers.write().insert(
|
||||
T::METHOD,
|
||||
Box::new(
|
||||
move |notification| match serde_json::from_str(notification) {
|
||||
Ok(notification) => f(notification),
|
||||
Err(err) => log::error!("error parsing notification {}: {}", T::METHOD, err),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
assert!(
|
||||
prev_handler.is_none(),
|
||||
"registered multiple handlers for the same notification"
|
||||
);
|
||||
|
||||
Subscription {
|
||||
method: T::METHOD,
|
||||
notification_handlers: self.notification_handlers.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request<T: lsp_types::request::Request>(
|
||||
self: Arc<Self>,
|
||||
params: T::Params,
|
||||
) -> impl Future<Output = Result<T::Result>>
|
||||
where
|
||||
T::Result: 'static + Send,
|
||||
{
|
||||
let this = self.clone();
|
||||
async move {
|
||||
this.initialized.clone().recv().await;
|
||||
Self::request_internal::<T>(
|
||||
&this.next_id,
|
||||
&this.response_handlers,
|
||||
this.outbound_tx.read().as_ref(),
|
||||
params,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn request_internal<T: lsp_types::request::Request>(
|
||||
next_id: &AtomicUsize,
|
||||
response_handlers: &Mutex<HashMap<usize, ResponseHandler>>,
|
||||
outbound_tx: Option<&channel::Sender<Vec<u8>>>,
|
||||
params: T::Params,
|
||||
) -> impl 'static + Future<Output = Result<T::Result>>
|
||||
where
|
||||
T::Result: 'static + Send,
|
||||
{
|
||||
let id = next_id.fetch_add(1, SeqCst);
|
||||
let message = serde_json::to_vec(&Request {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
id,
|
||||
method: T::METHOD,
|
||||
params,
|
||||
})
|
||||
.unwrap();
|
||||
let mut response_handlers = response_handlers.lock();
|
||||
let (mut tx, mut rx) = oneshot::channel();
|
||||
response_handlers.insert(
|
||||
id,
|
||||
Box::new(move |result| {
|
||||
let response = match result {
|
||||
Ok(response) => {
|
||||
serde_json::from_str(response).context("failed to deserialize response")
|
||||
}
|
||||
Err(error) => Err(anyhow!("{}", error.message)),
|
||||
};
|
||||
let _ = tx.try_send(response);
|
||||
}),
|
||||
);
|
||||
|
||||
let send = outbound_tx
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
anyhow!("tried to send a request to a language server that has been shut down")
|
||||
})
|
||||
.and_then(|outbound_tx| {
|
||||
outbound_tx.try_send(message)?;
|
||||
Ok(())
|
||||
});
|
||||
async move {
|
||||
send?;
|
||||
rx.recv().await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify<T: lsp_types::notification::Notification>(
|
||||
self: &Arc<Self>,
|
||||
params: T::Params,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
let this = self.clone();
|
||||
async move {
|
||||
this.initialized.clone().recv().await;
|
||||
Self::notify_internal::<T>(this.outbound_tx.read().as_ref(), params)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_internal<T: lsp_types::notification::Notification>(
|
||||
outbound_tx: Option<&channel::Sender<Vec<u8>>>,
|
||||
params: T::Params,
|
||||
) -> Result<()> {
|
||||
let message = serde_json::to_vec(&Notification {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
method: T::METHOD,
|
||||
params,
|
||||
})
|
||||
.unwrap();
|
||||
let outbound_tx = outbound_tx
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("tried to notify a language server that has been shut down"))?;
|
||||
outbound_tx.try_send(message)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LanguageServer {
|
||||
fn drop(&mut self) {
|
||||
if let Some(shutdown) = self.shutdown() {
|
||||
self.executor.spawn(shutdown).detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
pub fn detach(mut self) {
|
||||
self.method = "";
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
self.notification_handlers.write().remove(self.method);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeLanguageServer {
|
||||
buffer: Vec<u8>,
|
||||
stdin: smol::io::BufReader<async_pipe::PipeReader>,
|
||||
stdout: smol::io::BufWriter<async_pipe::PipeWriter>,
|
||||
pub started: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct RequestId<T> {
|
||||
id: usize,
|
||||
_type: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LanguageServer {
|
||||
pub async fn fake(executor: Arc<executor::Background>) -> (Arc<Self>, FakeLanguageServer) {
|
||||
let stdin = async_pipe::pipe();
|
||||
let stdout = async_pipe::pipe();
|
||||
let mut fake = FakeLanguageServer {
|
||||
stdin: smol::io::BufReader::new(stdin.1),
|
||||
stdout: smol::io::BufWriter::new(stdout.0),
|
||||
buffer: Vec::new(),
|
||||
started: Arc::new(AtomicBool::new(true)),
|
||||
};
|
||||
|
||||
let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap();
|
||||
|
||||
let (init_id, _) = fake.receive_request::<request::Initialize>().await;
|
||||
fake.respond(init_id, InitializeResult::default()).await;
|
||||
fake.receive_notification::<notification::Initialized>()
|
||||
.await;
|
||||
|
||||
(server, fake)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeLanguageServer {
|
||||
pub async fn notify<T: notification::Notification>(&mut self, params: T::Params) {
|
||||
if !self.started.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
panic!("can't simulate an LSP notification before the server has been started");
|
||||
}
|
||||
let message = serde_json::to_vec(&Notification {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
method: T::METHOD,
|
||||
params,
|
||||
})
|
||||
.unwrap();
|
||||
self.send(message).await;
|
||||
}
|
||||
|
||||
pub async fn respond<'a, T: request::Request>(
|
||||
&mut self,
|
||||
request_id: RequestId<T>,
|
||||
result: T::Result,
|
||||
) {
|
||||
let result = serde_json::to_string(&result).unwrap();
|
||||
let message = serde_json::to_vec(&AnyResponse {
|
||||
id: request_id.id,
|
||||
error: None,
|
||||
result: Some(&RawValue::from_string(result).unwrap()),
|
||||
})
|
||||
.unwrap();
|
||||
self.send(message).await;
|
||||
}
|
||||
|
||||
pub async fn receive_request<T: request::Request>(&mut self) -> (RequestId<T>, T::Params) {
|
||||
self.receive().await;
|
||||
let request = serde_json::from_slice::<Request<T::Params>>(&self.buffer).unwrap();
|
||||
assert_eq!(request.method, T::METHOD);
|
||||
assert_eq!(request.jsonrpc, JSON_RPC_VERSION);
|
||||
(
|
||||
RequestId {
|
||||
id: request.id,
|
||||
_type: std::marker::PhantomData,
|
||||
},
|
||||
request.params,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
|
||||
self.receive().await;
|
||||
let notification = serde_json::from_slice::<Notification<T::Params>>(&self.buffer).unwrap();
|
||||
assert_eq!(notification.method, T::METHOD);
|
||||
notification.params
|
||||
}
|
||||
|
||||
async fn send(&mut self, message: Vec<u8>) {
|
||||
self.stdout
|
||||
.write_all(CONTENT_LEN_HEADER.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
self.stdout
|
||||
.write_all((format!("{}", message.len())).as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
self.stdout.write_all("\r\n\r\n".as_bytes()).await.unwrap();
|
||||
self.stdout.write_all(&message).await.unwrap();
|
||||
self.stdout.flush().await.unwrap();
|
||||
}
|
||||
|
||||
async fn receive(&mut self) {
|
||||
self.buffer.clear();
|
||||
self.stdin
|
||||
.read_until(b'\n', &mut self.buffer)
|
||||
.await
|
||||
.unwrap();
|
||||
self.stdin
|
||||
.read_until(b'\n', &mut self.buffer)
|
||||
.await
|
||||
.unwrap();
|
||||
let message_len: usize = std::str::from_utf8(&self.buffer)
|
||||
.unwrap()
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.unwrap()
|
||||
.trim_end()
|
||||
.parse()
|
||||
.unwrap();
|
||||
self.buffer.resize(message_len, 0);
|
||||
self.stdin.read_exact(&mut self.buffer).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use simplelog::SimpleLogger;
|
||||
use unindent::Unindent;
|
||||
use util::test::temp_tree;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic(cx: TestAppContext) {
|
||||
let lib_source = r#"
|
||||
fn fun() {
|
||||
let hello = "world";
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
let root_dir = temp_tree(json!({
|
||||
"Cargo.toml": r#"
|
||||
[package]
|
||||
name = "temp"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
"#.unindent(),
|
||||
"src": {
|
||||
"lib.rs": &lib_source
|
||||
}
|
||||
}));
|
||||
let lib_file_uri =
|
||||
lsp_types::Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap();
|
||||
|
||||
let server = cx.read(|cx| {
|
||||
LanguageServer::new(
|
||||
Path::new("rust-analyzer"),
|
||||
root_dir.path(),
|
||||
cx.background().clone(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
server.next_idle_notification().await;
|
||||
|
||||
server
|
||||
.notify::<lsp_types::notification::DidOpenTextDocument>(
|
||||
lsp_types::DidOpenTextDocumentParams {
|
||||
text_document: lsp_types::TextDocumentItem::new(
|
||||
lib_file_uri.clone(),
|
||||
"rust".to_string(),
|
||||
0,
|
||||
lib_source,
|
||||
),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let hover = server
|
||||
.request::<lsp_types::request::HoverRequest>(lsp_types::HoverParams {
|
||||
text_document_position_params: lsp_types::TextDocumentPositionParams {
|
||||
text_document: lsp_types::TextDocumentIdentifier::new(lib_file_uri),
|
||||
position: lsp_types::Position::new(1, 21),
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
hover.contents,
|
||||
lsp_types::HoverContents::Markup(lsp_types::MarkupContent {
|
||||
kind: lsp_types::MarkupKind::Markdown,
|
||||
value: "&str".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fake(cx: TestAppContext) {
|
||||
SimpleLogger::init(log::LevelFilter::Info, Default::default()).unwrap();
|
||||
|
||||
let (server, mut fake) = LanguageServer::fake(cx.background()).await;
|
||||
|
||||
let (message_tx, message_rx) = channel::unbounded();
|
||||
let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
|
||||
server
|
||||
.on_notification::<notification::ShowMessage, _>(move |params| {
|
||||
message_tx.try_send(params).unwrap()
|
||||
})
|
||||
.detach();
|
||||
server
|
||||
.on_notification::<notification::PublishDiagnostics, _>(move |params| {
|
||||
diagnostics_tx.try_send(params).unwrap()
|
||||
})
|
||||
.detach();
|
||||
|
||||
server
|
||||
.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
|
||||
text_document: TextDocumentItem::new(
|
||||
Url::from_str("file://a/b").unwrap(),
|
||||
"rust".to_string(),
|
||||
0,
|
||||
"".to_string(),
|
||||
),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
fake.receive_notification::<notification::DidOpenTextDocument>()
|
||||
.await
|
||||
.text_document
|
||||
.uri
|
||||
.as_str(),
|
||||
"file://a/b"
|
||||
);
|
||||
|
||||
fake.notify::<notification::ShowMessage>(ShowMessageParams {
|
||||
typ: MessageType::ERROR,
|
||||
message: "ok".to_string(),
|
||||
})
|
||||
.await;
|
||||
fake.notify::<notification::PublishDiagnostics>(PublishDiagnosticsParams {
|
||||
uri: Url::from_str("file://b/c").unwrap(),
|
||||
version: Some(5),
|
||||
diagnostics: vec![],
|
||||
})
|
||||
.await;
|
||||
assert_eq!(message_rx.recv().await.unwrap().message, "ok");
|
||||
assert_eq!(
|
||||
diagnostics_rx.recv().await.unwrap().uri.as_str(),
|
||||
"file://b/c"
|
||||
);
|
||||
|
||||
drop(server);
|
||||
let (shutdown_request, _) = fake.receive_request::<lsp_types::request::Shutdown>().await;
|
||||
fake.respond(shutdown_request, ()).await;
|
||||
fake.receive_notification::<lsp_types::notification::Exit>()
|
||||
.await;
|
||||
}
|
||||
|
||||
impl LanguageServer {
|
||||
async fn next_idle_notification(self: &Arc<Self>) {
|
||||
let (tx, rx) = channel::unbounded();
|
||||
let _subscription =
|
||||
self.on_notification::<ServerStatusNotification, _>(move |params| {
|
||||
if params.quiescent {
|
||||
tx.try_send(()).unwrap();
|
||||
}
|
||||
});
|
||||
let _ = rx.recv().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ServerStatusNotification {}
|
||||
|
||||
impl lsp_types::notification::Notification for ServerStatusNotification {
|
||||
type Params = ServerStatusParams;
|
||||
const METHOD: &'static str = "experimental/serverStatus";
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
pub struct ServerStatusParams {
|
||||
pub quiescent: bool,
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,16 @@ test-support = ["language/test-support", "buffer/test-support"]
|
||||
|
||||
[dependencies]
|
||||
buffer = { path = "../buffer" }
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
fsevent = { path = "../fsevent" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
client = { path = "../client" }
|
||||
lsp = { path = "../lsp" }
|
||||
rpc = { path = "../rpc" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
util = { path = "../util" }
|
||||
rpc = { path = "../rpc" }
|
||||
|
||||
anyhow = "1.0.38"
|
||||
async-trait = "0.1"
|
||||
futures = "0.3"
|
||||
@@ -36,8 +36,10 @@ toml = "0.5"
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
|
||||
rand = "0.8.3"
|
||||
simplelog = "0.9"
|
||||
tempdir = { version = "0.3.7" }
|
||||
unindent = "0.1.7"
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::{
|
||||
ignore::IgnoreStack,
|
||||
};
|
||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{proto, Client, PeerId, TypedEnvelope};
|
||||
use clock::ReplicaId;
|
||||
use futures::{Stream, StreamExt};
|
||||
@@ -12,13 +12,15 @@ use gpui::{
|
||||
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||
Task, UpgradeModelHandle, WeakModelHandle,
|
||||
};
|
||||
use language::{Buffer, History, LanguageRegistry, Operation, Rope};
|
||||
use language::{Buffer, Language, LanguageRegistry, Operation, Rope};
|
||||
use lazy_static::lazy_static;
|
||||
use lsp::LanguageServer;
|
||||
use parking_lot::Mutex;
|
||||
use postage::{
|
||||
prelude::{Sink as _, Stream as _},
|
||||
watch,
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
use smol::channel::{self, Sender};
|
||||
use std::{
|
||||
@@ -39,7 +41,7 @@ use std::{
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
use sum_tree::{Edit, SeekTarget, SumTree};
|
||||
use util::TryFutureExt;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
lazy_static! {
|
||||
static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
|
||||
@@ -89,6 +91,29 @@ impl Entity for Worktree {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
_: &mut MutableAppContext,
|
||||
) -> Option<std::pin::Pin<Box<dyn 'static + Future<Output = ()>>>> {
|
||||
use futures::FutureExt;
|
||||
|
||||
if let Self::Local(worktree) = self {
|
||||
let shutdown_futures = worktree
|
||||
.language_servers
|
||||
.drain()
|
||||
.filter_map(|(_, server)| server.shutdown())
|
||||
.collect::<Vec<_>>();
|
||||
Some(
|
||||
async move {
|
||||
futures::future::join_all(shutdown_futures).await;
|
||||
}
|
||||
.boxed(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Worktree {
|
||||
@@ -421,8 +446,8 @@ impl Worktree {
|
||||
let ops = payload
|
||||
.operations
|
||||
.into_iter()
|
||||
.map(|op| op.try_into())
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
.map(|op| language::proto::deserialize_operation(op))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
match self {
|
||||
Worktree::Local(worktree) => {
|
||||
@@ -587,6 +612,8 @@ impl Worktree {
|
||||
}
|
||||
};
|
||||
|
||||
let local = self.as_local().is_some();
|
||||
let worktree_path = self.abs_path.clone();
|
||||
let worktree_handle = cx.handle();
|
||||
let mut buffers_to_delete = Vec::new();
|
||||
for (buffer_id, buffer) in open_buffers {
|
||||
@@ -598,6 +625,8 @@ impl Worktree {
|
||||
.and_then(|entry_id| self.entry_for_id(entry_id))
|
||||
{
|
||||
File {
|
||||
is_local: local,
|
||||
worktree_path: worktree_path.clone(),
|
||||
entry_id: Some(entry.id),
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
@@ -605,6 +634,8 @@ impl Worktree {
|
||||
}
|
||||
} else if let Some(entry) = self.entry_for_path(old_file.path().as_ref()) {
|
||||
File {
|
||||
is_local: local,
|
||||
worktree_path: worktree_path.clone(),
|
||||
entry_id: Some(entry.id),
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
@@ -612,6 +643,8 @@ impl Worktree {
|
||||
}
|
||||
} else {
|
||||
File {
|
||||
is_local: local,
|
||||
worktree_path: worktree_path.clone(),
|
||||
entry_id: None,
|
||||
path: old_file.path().clone(),
|
||||
mtime: old_file.mtime(),
|
||||
@@ -640,6 +673,79 @@ impl Worktree {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_diagnostics(
|
||||
&mut self,
|
||||
params: lsp::PublishDiagnosticsParams,
|
||||
cx: &mut ModelContext<Worktree>,
|
||||
) -> Result<()> {
|
||||
let this = self.as_local_mut().ok_or_else(|| anyhow!("not local"))?;
|
||||
let file_path = params
|
||||
.uri
|
||||
.to_file_path()
|
||||
.map_err(|_| anyhow!("URI is not a file"))?
|
||||
.strip_prefix(&this.abs_path)
|
||||
.context("path is not within worktree")?
|
||||
.to_owned();
|
||||
|
||||
for buffer in this.open_buffers.values() {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map_or(false, |file| file.path().as_ref() == file_path)
|
||||
{
|
||||
let (remote_id, operation) = buffer.update(cx, |buffer, cx| {
|
||||
(
|
||||
buffer.remote_id(),
|
||||
buffer.update_diagnostics(params.version, params.diagnostics, cx),
|
||||
)
|
||||
});
|
||||
self.send_buffer_update(remote_id, operation?, cx);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.diagnostics.insert(file_path, params.diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_buffer_update(
|
||||
&mut self,
|
||||
buffer_id: u64,
|
||||
operation: Operation,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some((rpc, remote_id)) = match self {
|
||||
Worktree::Local(worktree) => worktree
|
||||
.remote_id
|
||||
.borrow()
|
||||
.map(|id| (worktree.rpc.clone(), id)),
|
||||
Worktree::Remote(worktree) => Some((worktree.client.clone(), worktree.remote_id)),
|
||||
} {
|
||||
cx.spawn(|worktree, mut cx| async move {
|
||||
if let Err(error) = rpc
|
||||
.request(proto::UpdateBuffer {
|
||||
worktree_id: remote_id,
|
||||
buffer_id,
|
||||
operations: vec![language::proto::serialize_operation(&operation)],
|
||||
})
|
||||
.await
|
||||
{
|
||||
worktree.update(&mut cx, |worktree, _| {
|
||||
log::error!("error sending buffer operation: {}", error);
|
||||
match worktree {
|
||||
Worktree::Local(t) => &mut t.queued_operations,
|
||||
Worktree::Remote(t) => &mut t.queued_operations,
|
||||
}
|
||||
.push((buffer_id, operation));
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Worktree {
|
||||
@@ -665,11 +771,13 @@ pub struct LocalWorktree {
|
||||
share: Option<ShareState>,
|
||||
open_buffers: HashMap<usize, WeakModelHandle<Buffer>>,
|
||||
shared_buffers: HashMap<PeerId, HashMap<u64, ModelHandle<Buffer>>>,
|
||||
diagnostics: HashMap<PathBuf, Vec<lsp::Diagnostic>>,
|
||||
peers: HashMap<PeerId, ReplicaId>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
queued_operations: Vec<(u64, Operation)>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
rpc: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
language_servers: HashMap<String, Arc<LanguageServer>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
@@ -777,11 +885,13 @@ impl LocalWorktree {
|
||||
poll_task: None,
|
||||
open_buffers: Default::default(),
|
||||
shared_buffers: Default::default(),
|
||||
diagnostics: Default::default(),
|
||||
queued_operations: Default::default(),
|
||||
peers: Default::default(),
|
||||
languages,
|
||||
rpc,
|
||||
fs,
|
||||
language_servers: Default::default(),
|
||||
};
|
||||
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
@@ -817,6 +927,51 @@ impl LocalWorktree {
|
||||
Ok((tree, scan_states_tx))
|
||||
}
|
||||
|
||||
pub fn languages(&self) -> &LanguageRegistry {
|
||||
&self.languages
|
||||
}
|
||||
|
||||
pub fn ensure_language_server(
|
||||
&mut self,
|
||||
language: &Language,
|
||||
cx: &mut ModelContext<Worktree>,
|
||||
) -> Option<Arc<LanguageServer>> {
|
||||
if let Some(server) = self.language_servers.get(language.name()) {
|
||||
return Some(server.clone());
|
||||
}
|
||||
|
||||
if let Some(language_server) = language
|
||||
.start_server(self.abs_path(), cx)
|
||||
.log_err()
|
||||
.flatten()
|
||||
{
|
||||
let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
|
||||
language_server
|
||||
.on_notification::<lsp::notification::PublishDiagnostics, _>(move |params| {
|
||||
smol::block_on(diagnostics_tx.send(params)).ok();
|
||||
})
|
||||
.detach();
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Ok(diagnostics) = diagnostics_rx.recv().await {
|
||||
if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
|
||||
handle.update(&mut cx, |this, cx| {
|
||||
this.update_diagnostics(diagnostics, cx).log_err();
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.language_servers
|
||||
.insert(language.name().to_string(), language_server.clone());
|
||||
Some(language_server.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_buffer(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
@@ -847,26 +1002,32 @@ impl LocalWorktree {
|
||||
let (file, contents) = this
|
||||
.update(&mut cx, |this, cx| this.as_local().unwrap().load(&path, cx))
|
||||
.await?;
|
||||
let language = this.read_with(&cx, |this, cx| {
|
||||
let language = this.read_with(&cx, |this, _| {
|
||||
use language::File;
|
||||
|
||||
this.languages()
|
||||
.select_language(file.full_path(cx))
|
||||
.cloned()
|
||||
this.languages().select_language(file.full_path()).cloned()
|
||||
});
|
||||
let (diagnostics, language_server) = this.update(&mut cx, |this, cx| {
|
||||
let this = this.as_local_mut().unwrap();
|
||||
(
|
||||
this.diagnostics.remove(path.as_ref()),
|
||||
language
|
||||
.as_ref()
|
||||
.and_then(|language| this.ensure_language_server(language, cx)),
|
||||
)
|
||||
});
|
||||
let buffer = cx.add_model(|cx| {
|
||||
Buffer::from_history(
|
||||
0,
|
||||
History::new(contents.into()),
|
||||
Some(Box::new(file)),
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
let mut buffer = Buffer::from_file(0, contents, Box::new(file), cx);
|
||||
buffer.set_language(language, language_server, cx);
|
||||
if let Some(diagnostics) = diagnostics {
|
||||
buffer.update_diagnostics(None, diagnostics, cx).unwrap();
|
||||
}
|
||||
buffer
|
||||
});
|
||||
this.update(&mut cx, |this, _| {
|
||||
let this = this
|
||||
.as_local_mut()
|
||||
.ok_or_else(|| anyhow!("must be a local worktree"))?;
|
||||
|
||||
this.open_buffers.insert(buffer.id(), buffer.downgrade());
|
||||
Ok(buffer)
|
||||
})
|
||||
@@ -1009,6 +1170,7 @@ impl LocalWorktree {
|
||||
fn load(&self, path: &Path, cx: &mut ModelContext<Worktree>) -> Task<Result<(File, String)>> {
|
||||
let handle = cx.handle();
|
||||
let path = Arc::from(path);
|
||||
let worktree_path = self.abs_path.clone();
|
||||
let abs_path = self.absolutize(&path);
|
||||
let background_snapshot = self.background_snapshot.clone();
|
||||
let fs = self.fs.clone();
|
||||
@@ -1017,7 +1179,17 @@ impl LocalWorktree {
|
||||
// Eagerly populate the snapshot with an updated entry for the loaded file
|
||||
let entry = refresh_entry(fs.as_ref(), &background_snapshot, path, &abs_path).await?;
|
||||
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
||||
Ok((File::new(entry.id, handle, entry.path, entry.mtime), text))
|
||||
Ok((
|
||||
File {
|
||||
entry_id: Some(entry.id),
|
||||
worktree: handle,
|
||||
worktree_path,
|
||||
path: entry.path,
|
||||
mtime: entry.mtime,
|
||||
is_local: true,
|
||||
},
|
||||
text,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1032,11 +1204,16 @@ impl LocalWorktree {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let entry = save.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.as_local_mut()
|
||||
.unwrap()
|
||||
.open_buffers
|
||||
.insert(buffer.id(), buffer.downgrade());
|
||||
Ok(File::new(entry.id, cx.handle(), entry.path, entry.mtime))
|
||||
let this = this.as_local_mut().unwrap();
|
||||
this.open_buffers.insert(buffer.id(), buffer.downgrade());
|
||||
Ok(File {
|
||||
entry_id: Some(entry.id),
|
||||
worktree: cx.handle(),
|
||||
worktree_path: this.abs_path.clone(),
|
||||
path: entry.path,
|
||||
mtime: entry.mtime,
|
||||
is_local: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1225,6 +1402,7 @@ impl RemoteWorktree {
|
||||
let rpc = self.client.clone();
|
||||
let replica_id = self.replica_id;
|
||||
let remote_worktree_id = self.remote_id;
|
||||
let root_path = self.snapshot.abs_path.clone();
|
||||
let path = path.to_string_lossy().to_string();
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
if let Some(existing_buffer) = existing_buffer {
|
||||
@@ -1245,25 +1423,24 @@ impl RemoteWorktree {
|
||||
let this = this
|
||||
.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("worktree was closed"))?;
|
||||
let file = File::new(entry.id, this.clone(), entry.path, entry.mtime);
|
||||
let language = this.read_with(&cx, |this, cx| {
|
||||
let file = File {
|
||||
entry_id: Some(entry.id),
|
||||
worktree: this.clone(),
|
||||
worktree_path: root_path,
|
||||
path: entry.path,
|
||||
mtime: entry.mtime,
|
||||
is_local: false,
|
||||
};
|
||||
let language = this.read_with(&cx, |this, _| {
|
||||
use language::File;
|
||||
|
||||
this.languages()
|
||||
.select_language(file.full_path(cx))
|
||||
.cloned()
|
||||
this.languages().select_language(file.full_path()).cloned()
|
||||
});
|
||||
let remote_buffer = response.buffer.ok_or_else(|| anyhow!("empty buffer"))?;
|
||||
let buffer_id = remote_buffer.id as usize;
|
||||
let buffer = cx.add_model(|cx| {
|
||||
Buffer::from_proto(
|
||||
replica_id,
|
||||
remote_buffer,
|
||||
Some(Box::new(file)),
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
Buffer::from_proto(replica_id, remote_buffer, Some(Box::new(file)), cx)
|
||||
.unwrap()
|
||||
.with_language(language, None, cx)
|
||||
});
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let this = this.as_remote_mut().unwrap();
|
||||
@@ -1738,24 +1915,10 @@ impl fmt::Debug for Snapshot {
|
||||
pub struct File {
|
||||
entry_id: Option<usize>,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
worktree_path: Arc<Path>,
|
||||
pub path: Arc<Path>,
|
||||
pub mtime: SystemTime,
|
||||
}
|
||||
|
||||
impl File {
|
||||
pub fn new(
|
||||
entry_id: usize,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
path: Arc<Path>,
|
||||
mtime: SystemTime,
|
||||
) -> Self {
|
||||
Self {
|
||||
entry_id: Some(entry_id),
|
||||
worktree,
|
||||
path,
|
||||
mtime,
|
||||
}
|
||||
}
|
||||
is_local: bool,
|
||||
}
|
||||
|
||||
impl language::File for File {
|
||||
@@ -1775,20 +1938,29 @@ impl language::File for File {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn full_path(&self, cx: &AppContext) -> PathBuf {
|
||||
let worktree = self.worktree.read(cx);
|
||||
fn abs_path(&self) -> Option<PathBuf> {
|
||||
if self.is_local {
|
||||
Some(self.worktree_path.join(&self.path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn full_path(&self) -> PathBuf {
|
||||
let mut full_path = PathBuf::new();
|
||||
full_path.push(worktree.root_name());
|
||||
if let Some(worktree_name) = self.worktree_path.file_name() {
|
||||
full_path.push(worktree_name);
|
||||
}
|
||||
full_path.push(&self.path);
|
||||
full_path
|
||||
}
|
||||
|
||||
/// Returns the last component of this handle's absolute path. If this handle refers to the root
|
||||
/// of its worktree, then this method will return the name of the worktree itself.
|
||||
fn file_name<'a>(&'a self, cx: &'a AppContext) -> Option<OsString> {
|
||||
fn file_name<'a>(&'a self) -> Option<OsString> {
|
||||
self.path
|
||||
.file_name()
|
||||
.or_else(|| Some(OsStr::new(self.worktree.read(cx).root_name())))
|
||||
.or_else(|| self.worktree_path.file_name())
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
@@ -1855,34 +2027,7 @@ impl language::File for File {
|
||||
|
||||
fn buffer_updated(&self, buffer_id: u64, operation: Operation, cx: &mut MutableAppContext) {
|
||||
self.worktree.update(cx, |worktree, cx| {
|
||||
if let Some((rpc, remote_id)) = match worktree {
|
||||
Worktree::Local(worktree) => worktree
|
||||
.remote_id
|
||||
.borrow()
|
||||
.map(|id| (worktree.rpc.clone(), id)),
|
||||
Worktree::Remote(worktree) => Some((worktree.client.clone(), worktree.remote_id)),
|
||||
} {
|
||||
cx.spawn(|worktree, mut cx| async move {
|
||||
if let Err(error) = rpc
|
||||
.request(proto::UpdateBuffer {
|
||||
worktree_id: remote_id,
|
||||
buffer_id,
|
||||
operations: vec![(&operation).into()],
|
||||
})
|
||||
.await
|
||||
{
|
||||
worktree.update(&mut cx, |worktree, _| {
|
||||
log::error!("error sending buffer operation: {}", error);
|
||||
match worktree {
|
||||
Worktree::Local(t) => &mut t.queued_operations,
|
||||
Worktree::Remote(t) => &mut t.queued_operations,
|
||||
}
|
||||
.push((buffer_id, operation));
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
worktree.send_buffer_update(buffer_id, operation, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2798,8 +2943,12 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::fs::FakeFs;
|
||||
use anyhow::Result;
|
||||
use buffer::Point;
|
||||
use client::test::FakeServer;
|
||||
use fs::RealFs;
|
||||
use language::{tree_sitter_rust, LanguageServerConfig};
|
||||
use language::{Diagnostic, LanguageConfig};
|
||||
use lsp::Url;
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
@@ -3418,6 +3567,81 @@ mod tests {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_language_server_diagnostics(mut cx: gpui::TestAppContext) {
|
||||
simplelog::SimpleLogger::init(log::LevelFilter::Info, Default::default()).unwrap();
|
||||
|
||||
let (language_server_config, mut fake_server) =
|
||||
LanguageServerConfig::fake(cx.background()).await;
|
||||
let mut languages = LanguageRegistry::new();
|
||||
languages.add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
language_server: Some(language_server_config),
|
||||
..Default::default()
|
||||
},
|
||||
tree_sitter_rust::language(),
|
||||
)));
|
||||
|
||||
let dir = temp_tree(json!({
|
||||
"a.rs": "fn a() { A }",
|
||||
"b.rs": "const y: i32 = 1",
|
||||
}));
|
||||
|
||||
let tree = Worktree::open_local(
|
||||
Client::new(),
|
||||
dir.path(),
|
||||
Arc::new(RealFs),
|
||||
Arc::new(languages),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
// Cause worktree to start the fake language server
|
||||
let _buffer = tree
|
||||
.update(&mut cx, |tree, cx| tree.open_buffer("b.rs", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
fake_server
|
||||
.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
|
||||
uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
message: "undefined variable 'A'".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
})
|
||||
.await;
|
||||
|
||||
let buffer = tree
|
||||
.update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
buffer.read_with(&cx, |buffer, _| {
|
||||
let diagnostics = buffer
|
||||
.diagnostics_in_range(0..buffer.len())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
diagnostics,
|
||||
&[(
|
||||
Point::new(0, 9)..Point::new(0, 10),
|
||||
&Diagnostic {
|
||||
severity: lsp::DiagnosticSeverity::ERROR,
|
||||
message: "undefined variable 'A'".to_string()
|
||||
}
|
||||
)]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random(mut rng: StdRng) {
|
||||
let operations = env::var("OPERATIONS")
|
||||
|
||||
@@ -228,6 +228,7 @@ message Buffer {
|
||||
string content = 2;
|
||||
repeated Operation.Edit history = 3;
|
||||
repeated SelectionSet selections = 4;
|
||||
DiagnosticSet diagnostics = 5;
|
||||
}
|
||||
|
||||
message SelectionSet {
|
||||
@@ -245,6 +246,27 @@ message Selection {
|
||||
bool reversed = 4;
|
||||
}
|
||||
|
||||
message DiagnosticSet {
|
||||
repeated VectorClockEntry version = 1;
|
||||
repeated Diagnostic diagnostics = 2;
|
||||
}
|
||||
|
||||
message Diagnostic {
|
||||
uint64 start = 1;
|
||||
uint64 end = 2;
|
||||
Severity severity = 3;
|
||||
string message = 4;
|
||||
enum Severity {
|
||||
None = 0;
|
||||
Error = 1;
|
||||
Warning = 2;
|
||||
Information = 3;
|
||||
Hint = 4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
message Operation {
|
||||
oneof variant {
|
||||
Edit edit = 1;
|
||||
@@ -252,6 +274,7 @@ message Operation {
|
||||
UpdateSelections update_selections = 3;
|
||||
RemoveSelections remove_selections = 4;
|
||||
SetActiveSelections set_active_selections = 5;
|
||||
DiagnosticSet update_diagnostics = 6;
|
||||
}
|
||||
|
||||
message Edit {
|
||||
|
||||
@@ -398,6 +398,7 @@ mod tests {
|
||||
content: "path/one content".to_string(),
|
||||
history: vec![],
|
||||
selections: vec![],
|
||||
diagnostics: None,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -419,6 +420,7 @@ mod tests {
|
||||
content: "path/two content".to_string(),
|
||||
history: vec![],
|
||||
selections: vec![],
|
||||
diagnostics: None,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -449,6 +451,7 @@ mod tests {
|
||||
content: "path/one content".to_string(),
|
||||
history: vec![],
|
||||
selections: vec![],
|
||||
diagnostics: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -460,6 +463,7 @@ mod tests {
|
||||
content: "path/two content".to_string(),
|
||||
history: vec![],
|
||||
selections: vec![],
|
||||
diagnostics: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -982,7 +982,11 @@ mod tests {
|
||||
},
|
||||
editor::{Editor, EditorSettings, Input},
|
||||
fs::{FakeFs, Fs as _},
|
||||
language::LanguageRegistry,
|
||||
language::{
|
||||
tree_sitter_rust, Diagnostic, Language, LanguageConfig, LanguageRegistry,
|
||||
LanguageServerConfig, Point,
|
||||
},
|
||||
lsp,
|
||||
people_panel::JoinWorktree,
|
||||
project::{ProjectPath, Worktree},
|
||||
workspace::{Workspace, WorkspaceParams},
|
||||
@@ -1595,6 +1599,136 @@ mod tests {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_collaborating_with_diagnostics(
|
||||
mut cx_a: TestAppContext,
|
||||
mut cx_b: TestAppContext,
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let (language_server_config, mut fake_language_server) =
|
||||
LanguageServerConfig::fake(cx_a.background()).await;
|
||||
let mut lang_registry = LanguageRegistry::new();
|
||||
lang_registry.add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
language_server: Some(language_server_config),
|
||||
..Default::default()
|
||||
},
|
||||
tree_sitter_rust::language(),
|
||||
)));
|
||||
|
||||
let lang_registry = Arc::new(lang_registry);
|
||||
|
||||
// Connect to a server as 2 clients.
|
||||
let mut server = TestServer::start().await;
|
||||
let (client_a, _) = server.create_client(&mut cx_a, "user_a").await;
|
||||
let (client_b, _) = server.create_client(&mut cx_a, "user_b").await;
|
||||
|
||||
// Share a local worktree as client A
|
||||
let fs = Arc::new(FakeFs::new());
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
".zed.toml": r#"collaborators = ["user_b"]"#,
|
||||
"a.rs": "let one = two",
|
||||
"other.rs": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let worktree_a = Worktree::open_local(
|
||||
client_a.clone(),
|
||||
"/a".as_ref(),
|
||||
fs,
|
||||
lang_registry.clone(),
|
||||
&mut cx_a.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
worktree_a
|
||||
.read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
let worktree_id = worktree_a
|
||||
.update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Cause language server to start.
|
||||
let _ = cx_a
|
||||
.background()
|
||||
.spawn(worktree_a.update(&mut cx_a, |worktree, cx| {
|
||||
worktree.open_buffer("other.rs", cx)
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Simulate a language server reporting errors for a file.
|
||||
fake_language_server
|
||||
.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![
|
||||
lsp::Diagnostic {
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
|
||||
message: "message 1".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::Diagnostic {
|
||||
severity: Some(lsp::DiagnosticSeverity::WARNING),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(0, 10),
|
||||
lsp::Position::new(0, 13),
|
||||
),
|
||||
message: "message 2".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
})
|
||||
.await;
|
||||
|
||||
// Join the worktree as client B.
|
||||
let worktree_b = Worktree::open_remote(
|
||||
client_b.clone(),
|
||||
worktree_id,
|
||||
lang_registry.clone(),
|
||||
&mut cx_b.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Open the file with the errors.
|
||||
let buffer_b = cx_b
|
||||
.background()
|
||||
.spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
buffer_b.read_with(&cx_b, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer
|
||||
.diagnostics_in_range(0..buffer.len())
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(
|
||||
Point::new(0, 4)..Point::new(0, 7),
|
||||
&Diagnostic {
|
||||
message: "message 1".to_string(),
|
||||
severity: lsp::DiagnosticSeverity::ERROR,
|
||||
}
|
||||
),
|
||||
(
|
||||
Point::new(0, 10)..Point::new(0, 13),
|
||||
&Diagnostic {
|
||||
severity: lsp::DiagnosticSeverity::WARNING,
|
||||
message: "message 2".to_string()
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
|
||||
@@ -184,9 +184,9 @@ where
|
||||
self.next_internal(|_| true, cx)
|
||||
}
|
||||
|
||||
fn next_internal<F>(&mut self, filter_node: F, cx: &<T::Summary as Summary>::Context)
|
||||
fn next_internal<F>(&mut self, mut filter_node: F, cx: &<T::Summary as Summary>::Context)
|
||||
where
|
||||
F: Fn(&T::Summary) -> bool,
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
{
|
||||
let mut descend = false;
|
||||
|
||||
@@ -509,24 +509,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FilterCursor<'a, F: Fn(&T::Summary) -> bool, T: Item, D> {
|
||||
pub struct FilterCursor<'a, F, T: Item, D> {
|
||||
cursor: Cursor<'a, T, D>,
|
||||
filter_node: F,
|
||||
}
|
||||
|
||||
impl<'a, F, T, D> FilterCursor<'a, F, T, D>
|
||||
where
|
||||
F: Fn(&T::Summary) -> bool,
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
T: Item,
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
pub fn new(
|
||||
tree: &'a SumTree<T>,
|
||||
filter_node: F,
|
||||
mut filter_node: F,
|
||||
cx: &<T::Summary as Summary>::Context,
|
||||
) -> Self {
|
||||
let mut cursor = tree.cursor::<D>();
|
||||
cursor.next_internal(&filter_node, cx);
|
||||
cursor.next_internal(&mut filter_node, cx);
|
||||
Self {
|
||||
cursor,
|
||||
filter_node,
|
||||
@@ -537,12 +537,16 @@ where
|
||||
self.cursor.start()
|
||||
}
|
||||
|
||||
pub fn end(&self, cx: &<T::Summary as Summary>::Context) -> D {
|
||||
self.cursor.end(cx)
|
||||
}
|
||||
|
||||
pub fn item(&self) -> Option<&'a T> {
|
||||
self.cursor.item()
|
||||
}
|
||||
|
||||
pub fn next(&mut self, cx: &<T::Summary as Summary>::Context) {
|
||||
self.cursor.next_internal(&self.filter_node, cx);
|
||||
self.cursor.next_internal(&mut self.filter_node, cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ impl<T: Item> SumTree<T> {
|
||||
cx: &<T::Summary as Summary>::Context,
|
||||
) -> FilterCursor<F, T, U>
|
||||
where
|
||||
F: Fn(&T::Summary) -> bool,
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
U: Dimension<'a, T::Summary>,
|
||||
{
|
||||
FilterCursor::new(self, filter_node, cx)
|
||||
|
||||
@@ -214,6 +214,12 @@ pub struct EditorStyle {
|
||||
pub line_number_active: Color,
|
||||
pub guest_selections: Vec<SelectionStyle>,
|
||||
pub syntax: Arc<SyntaxTheme>,
|
||||
pub error_underline: Color,
|
||||
pub warning_underline: Color,
|
||||
#[serde(default)]
|
||||
pub information_underline: Color,
|
||||
#[serde(default)]
|
||||
pub hint_underline: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Deserialize)]
|
||||
@@ -254,6 +260,10 @@ impl InputEditorStyle {
|
||||
line_number_active: Default::default(),
|
||||
guest_selections: Default::default(),
|
||||
syntax: Default::default(),
|
||||
error_underline: Default::default(),
|
||||
warning_underline: Default::default(),
|
||||
information_underline: Default::default(),
|
||||
hint_underline: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ impl Item for Buffer {
|
||||
font_id,
|
||||
font_size,
|
||||
font_properties,
|
||||
underline: false,
|
||||
underline: None,
|
||||
};
|
||||
EditorSettings {
|
||||
tab_size: settings.tab_size,
|
||||
@@ -77,7 +77,7 @@ impl ItemView for Editor {
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| file.file_name(cx));
|
||||
.and_then(|file| file.file_name());
|
||||
if let Some(name) = filename {
|
||||
name.to_string_lossy().into()
|
||||
} else {
|
||||
@@ -127,16 +127,21 @@ impl ItemView for Editor {
|
||||
|
||||
cx.spawn(|buffer, mut cx| async move {
|
||||
save_as.await.map(|new_file| {
|
||||
let language = worktree.read_with(&cx, |worktree, cx| {
|
||||
worktree
|
||||
let (language, language_server) = worktree.update(&mut cx, |worktree, cx| {
|
||||
let worktree = worktree.as_local_mut().unwrap();
|
||||
let language = worktree
|
||||
.languages()
|
||||
.select_language(new_file.full_path(cx))
|
||||
.cloned()
|
||||
.select_language(new_file.full_path())
|
||||
.cloned();
|
||||
let language_server = language
|
||||
.as_ref()
|
||||
.and_then(|language| worktree.ensure_language_server(language, cx));
|
||||
(language, language_server.clone())
|
||||
});
|
||||
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx);
|
||||
buffer.set_language(language, cx);
|
||||
buffer.set_language(language, language_server, cx);
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,11 +17,14 @@ path = "src/main.rs"
|
||||
test-support = [
|
||||
"buffer/test-support",
|
||||
"client/test-support",
|
||||
"editor/test-support",
|
||||
"gpui/test-support",
|
||||
"language/test-support",
|
||||
"lsp/test-support",
|
||||
"project/test-support",
|
||||
"rpc/test-support",
|
||||
"tempdir",
|
||||
"workspace/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -35,6 +38,7 @@ editor = { path = "../editor" }
|
||||
file_finder = { path = "../file_finder" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
people_panel = { path = "../people_panel" }
|
||||
project = { path = "../project" }
|
||||
project_panel = { path = "../project_panel" }
|
||||
@@ -88,6 +92,7 @@ buffer = { path = "../buffer", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
|
||||
@@ -226,3 +226,8 @@ line_number = "$text.2.color"
|
||||
line_number_active = "$text.0.color"
|
||||
selection = "$selection.host"
|
||||
guest_selections = "$selection.guests"
|
||||
|
||||
error_underline = "$status.bad"
|
||||
warning_underline = "$status.warn"
|
||||
info_underline = "$status.info"
|
||||
hint_underline = "$status.info"
|
||||
|
||||
@@ -26,7 +26,7 @@ guests = [
|
||||
{ selection = "#EE823133", cursor = "#EE8231" },
|
||||
{ selection = "#5A2B9233", cursor = "#5A2B92" },
|
||||
{ selection = "#FDF35133", cursor = "#FDF351" },
|
||||
{ selection = "#4EACAD33", cursor = "#4EACAD" }
|
||||
{ selection = "#4EACAD33", cursor = "#4EACAD" },
|
||||
]
|
||||
|
||||
[status]
|
||||
|
||||
@@ -8,3 +8,7 @@ brackets = [
|
||||
{ start = "\"", end = "\"", close = true, newline = false },
|
||||
{ start = "/*", end = " */", close = true, newline = false },
|
||||
]
|
||||
|
||||
[language_server]
|
||||
binary = "rust-analyzer"
|
||||
disk_based_diagnostic_sources = ["rustc"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub use language::{Language, LanguageRegistry};
|
||||
pub use language::*;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::borrow::Cow;
|
||||
use std::{str, sync::Arc};
|
||||
|
||||
@@ -15,6 +15,7 @@ use gpui::{
|
||||
platform::WindowOptions,
|
||||
ModelHandle, MutableAppContext, PathPromptOptions, Task, ViewContext,
|
||||
};
|
||||
pub use lsp;
|
||||
use parking_lot::Mutex;
|
||||
pub use people_panel;
|
||||
use people_panel::PeoplePanel;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
export ZED_BUNDLE=true
|
||||
|
||||
# Install cargo-bundle 0.5.0 if it's not already installed
|
||||
cargo install cargo-bundle --version 0.5.0
|
||||
|
||||
@@ -16,6 +18,9 @@ cargo build --release --target aarch64-apple-darwin
|
||||
# Replace the bundle's binary with a "fat binary" that combines the two architecture-specific binaries
|
||||
lipo -create target/x86_64-apple-darwin/release/Zed target/aarch64-apple-darwin/release/Zed -output target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/zed
|
||||
|
||||
# Bundle rust-analyzer
|
||||
cp vendor/bin/rust-analyzer target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/
|
||||
|
||||
# Sign the app bundle with an ad-hoc signature so it runs on the M1. We need a real certificate but this works for now.
|
||||
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
|
||||
echo "Signing bundle with Apple-issued certificate"
|
||||
@@ -26,6 +31,7 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR
|
||||
security import /tmp/zed-certificate.p12 -k zed.keychain -P $MACOS_CERTIFICATE_PASSWORD -T /usr/bin/codesign
|
||||
rm /tmp/zed-certificate.p12
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $MACOS_CERTIFICATE_PASSWORD zed.keychain
|
||||
/usr/bin/codesign --force --deep --timestamp --options runtime --sign "Zed Industries, Inc." target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/rust-analyzer -v
|
||||
/usr/bin/codesign --force --deep --timestamp --options runtime --sign "Zed Industries, Inc." target/x86_64-apple-darwin/release/bundle/osx/Zed.app -v
|
||||
security default-keychain -s login.keychain
|
||||
else
|
||||
|
||||
19
script/download-rust-analyzer
Executable file
19
script/download-rust-analyzer
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
export RUST_ANALYZER_URL="https://github.com/rust-analyzer/rust-analyzer/releases/download/2021-10-18/"
|
||||
|
||||
function download {
|
||||
local filename="rust-analyzer-$1"
|
||||
curl -L $RUST_ANALYZER_URL/$filename.gz | gunzip > vendor/bin/$filename
|
||||
chmod +x vendor/bin/$filename
|
||||
}
|
||||
|
||||
mkdir -p vendor/bin
|
||||
download "x86_64-apple-darwin"
|
||||
download "aarch64-apple-darwin"
|
||||
|
||||
cd vendor/bin
|
||||
lipo -create rust-analyzer-* -output rust-analyzer
|
||||
rm rust-analyzer-*
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
set -e
|
||||
|
||||
cd server
|
||||
cd crates/server
|
||||
cargo run $@
|
||||
|
||||
@@ -5,7 +5,7 @@ set -e
|
||||
# Install sqlx-cli if needed
|
||||
[[ "$(sqlx --version)" == "sqlx-cli 0.5.7" ]] || cargo install sqlx-cli --version 0.5.7
|
||||
|
||||
cd server
|
||||
cd crates/server
|
||||
|
||||
# Export contents of .env.toml
|
||||
eval "$(cargo run --bin dotenv)"
|
||||
|
||||
Reference in New Issue
Block a user