Merge branch 'main' into vim-softwrap-word
This commit is contained in:
commit
aa7b65bbaf
111 changed files with 4173 additions and 1585 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -102,14 +102,20 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"collections",
|
"collections",
|
||||||
|
"ctor",
|
||||||
"editor",
|
"editor",
|
||||||
|
"env_logger 0.9.3",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"indoc",
|
||||||
"isahc",
|
"isahc",
|
||||||
"language",
|
"language",
|
||||||
|
"log",
|
||||||
"menu",
|
"menu",
|
||||||
|
"ordered-float",
|
||||||
"project",
|
"project",
|
||||||
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
"schemars",
|
"schemars",
|
||||||
"search",
|
"search",
|
||||||
|
@ -1447,7 +1453,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.18.0"
|
version = "0.19.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
|
@ -2762,6 +2768,7 @@ dependencies = [
|
||||||
"smol",
|
"smol",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"text",
|
||||||
"time 0.3.27",
|
"time 0.3.27",
|
||||||
"util",
|
"util",
|
||||||
]
|
]
|
||||||
|
@ -4170,8 +4177,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lsp-types"
|
name = "lsp-types"
|
||||||
version = "0.94.1"
|
version = "0.94.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/zed-industries/lsp-types?branch=updated-completion-list-item-defaults#90a040a1d195687bd19e1df47463320a44e93d7a"
|
||||||
checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -5649,6 +5655,7 @@ dependencies = [
|
||||||
name = "quick_action_bar"
|
name = "quick_action_bar"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ai",
|
||||||
"editor",
|
"editor",
|
||||||
"gpui",
|
"gpui",
|
||||||
"search",
|
"search",
|
||||||
|
@ -7629,7 +7636,6 @@ dependencies = [
|
||||||
"ctor",
|
"ctor",
|
||||||
"digest 0.9.0",
|
"digest 0.9.0",
|
||||||
"env_logger 0.9.3",
|
"env_logger 0.9.3",
|
||||||
"fs",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
@ -9695,7 +9701,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.102.0"
|
version = "0.103.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"ai",
|
"ai",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M5 8L6.5 9L9 5.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M5 8L6.5 9L9 5.5" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<circle cx="7" cy="7" r="4.875" stroke="black" stroke-width="1.25"/>
|
<circle cx="7" cy="7" r="4.875" stroke="#11181C" stroke-width="1.25"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 287 B |
|
@ -1,4 +1,4 @@
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M8.86396 2C8.99657 2 9.12375 2.05268 9.21751 2.14645L11.8536 4.78249C11.9473 4.87625 12 5.00343 12 5.13604L12 8.86396C12 8.99657 11.9473 9.12375 11.8536 9.21751L9.21751 11.8536C9.12375 11.9473 8.99657 12 8.86396 12L5.13604 12C5.00343 12 4.87625 11.9473 4.78249 11.8536L2.14645 9.21751C2.05268 9.12375 2 8.99657 2 8.86396L2 5.13604C2 5.00343 2.05268 4.87625 2.14645 4.78249L4.78249 2.14645C4.87625 2.05268 5.00343 2 5.13604 2L8.86396 2Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
|
<path d="M8.86396 2C8.99657 2 9.12375 2.05268 9.21751 2.14645L11.8536 4.78249C11.9473 4.87625 12 5.00343 12 5.13604L12 8.86396C12 8.99657 11.9473 9.12375 11.8536 9.21751L9.21751 11.8536C9.12375 11.9473 8.99657 12 8.86396 12L5.13604 12C5.00343 12 4.87625 11.9473 4.78249 11.8536L2.14645 9.21751C2.05268 9.12375 2 8.99657 2 8.86396L2 5.13604C2 5.00343 2.05268 4.87625 2.14645 4.78249L4.78249 2.14645C4.87625 2.05268 5.00343 2 5.13604 2L8.86396 2Z" fill="#001A33" fill-opacity="0.157" stroke="#11181C" stroke-width="1.25" stroke-linejoin="round"/>
|
||||||
<path d="M8.89063 5.10938L5.10937 8.89063M8.89063 8.89063L5.10937 5.10938" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
|
<path d="M8.89063 5.10938L5.10937 8.89063M8.89063 8.89063L5.10937 5.10938" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 785 B |
|
@ -1,5 +1,6 @@
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M9.5 6.5L11.994 11.625C12.1556 11.9571 11.9137 12.3438 11.5444 12.3438H2.45563C2.08628 12.3438 1.84442 11.9571 2.00603 11.625L4.5 6.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M2.45563 12.3438H11.5444C11.9137 12.3438 12.1556 11.9571 11.994 11.625L10.2346 8.00952C9.77174 7.05841 8.89104 6.37821 7.85383 6.17077C7.29019 6.05804 6.70981 6.05804 6.14617 6.17077C5.10896 6.37821 4.22826 7.05841 3.76542 8.00952L2.00603 11.625C1.84442 11.9571 2.08628 12.3438 2.45563 12.3438Z" fill="#001A33" fill-opacity="0.157"/>
|
||||||
<path d="M7 7L7 2" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M9.5 6.5L11.994 11.625C12.1556 11.9571 11.9137 12.3438 11.5444 12.3438H2.45563C2.08628 12.3438 1.84442 11.9571 2.00603 11.625L4.5 6.5" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<circle cx="7" cy="9.24219" r="0.75" fill="black"/>
|
<path d="M7 7L7 2" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="7" cy="9.24219" r="0.75" fill="#11181C"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 835 B |
|
@ -522,7 +522,7 @@
|
||||||
// TODO: Move this to a dock open action
|
// TODO: Move this to a dock open action
|
||||||
"cmd-shift-c": "collab_panel::ToggleFocus",
|
"cmd-shift-c": "collab_panel::ToggleFocus",
|
||||||
"cmd-alt-i": "zed::DebugElements",
|
"cmd-alt-i": "zed::DebugElements",
|
||||||
"ctrl-shift-:": "editor::ToggleInlayHints",
|
"ctrl-:": "editor::ToggleInlayHints",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -530,7 +530,8 @@
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"alt-enter": "editor::OpenExcerpts",
|
"alt-enter": "editor::OpenExcerpts",
|
||||||
"cmd-f8": "editor::GoToHunk",
|
"cmd-f8": "editor::GoToHunk",
|
||||||
"cmd-shift-f8": "editor::GoToPrevHunk"
|
"cmd-shift-f8": "editor::GoToPrevHunk",
|
||||||
|
"ctrl-enter": "assistant::InlineAssist"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,7 +24,9 @@ workspace = { path = "../workspace" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
indoc.workspace = true
|
||||||
isahc.workspace = true
|
isahc.workspace = true
|
||||||
|
ordered-float.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
@ -35,3 +37,8 @@ tiktoken-rs = "0.4"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
|
||||||
|
ctor.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
|
|
@ -1,28 +1,33 @@
|
||||||
pub mod assistant;
|
pub mod assistant;
|
||||||
mod assistant_settings;
|
mod assistant_settings;
|
||||||
|
mod streaming_diff;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
pub use assistant::AssistantPanel;
|
pub use assistant::AssistantPanel;
|
||||||
use assistant_settings::OpenAIModel;
|
use assistant_settings::OpenAIModel;
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::StreamExt;
|
use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
|
||||||
use gpui::AppContext;
|
use gpui::{executor::Background, AppContext};
|
||||||
|
use isahc::{http::StatusCode, Request, RequestExt};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Reverse,
|
cmp::Reverse,
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
|
io,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use util::paths::CONVERSATIONS_DIR;
|
use util::paths::CONVERSATIONS_DIR;
|
||||||
|
|
||||||
|
const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
|
||||||
|
|
||||||
// Data types for chat completion requests
|
// Data types for chat completion requests
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct OpenAIRequest {
|
pub struct OpenAIRequest {
|
||||||
model: String,
|
model: String,
|
||||||
messages: Vec<RequestMessage>,
|
messages: Vec<RequestMessage>,
|
||||||
stream: bool,
|
stream: bool,
|
||||||
|
@ -116,7 +121,7 @@ struct RequestMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||||
struct ResponseMessage {
|
pub struct ResponseMessage {
|
||||||
role: Option<Role>,
|
role: Option<Role>,
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -150,7 +155,7 @@ impl Display for Role {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct OpenAIResponseStreamEvent {
|
pub struct OpenAIResponseStreamEvent {
|
||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
pub object: String,
|
pub object: String,
|
||||||
pub created: u32,
|
pub created: u32,
|
||||||
|
@ -160,14 +165,14 @@ struct OpenAIResponseStreamEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct Usage {
|
pub struct Usage {
|
||||||
pub prompt_tokens: u32,
|
pub prompt_tokens: u32,
|
||||||
pub completion_tokens: u32,
|
pub completion_tokens: u32,
|
||||||
pub total_tokens: u32,
|
pub total_tokens: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct ChatChoiceDelta {
|
pub struct ChatChoiceDelta {
|
||||||
pub index: u32,
|
pub index: u32,
|
||||||
pub delta: ResponseMessage,
|
pub delta: ResponseMessage,
|
||||||
pub finish_reason: Option<String>,
|
pub finish_reason: Option<String>,
|
||||||
|
@ -191,3 +196,97 @@ struct OpenAIChoice {
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
assistant::init(cx);
|
assistant::init(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn stream_completion(
|
||||||
|
api_key: String,
|
||||||
|
executor: Arc<Background>,
|
||||||
|
mut request: OpenAIRequest,
|
||||||
|
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||||
|
request.stream = true;
|
||||||
|
|
||||||
|
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
|
||||||
|
|
||||||
|
let json_data = serde_json::to_string(&request)?;
|
||||||
|
let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", format!("Bearer {}", api_key))
|
||||||
|
.body(json_data)?
|
||||||
|
.send_async()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
if status == StatusCode::OK {
|
||||||
|
executor
|
||||||
|
.spawn(async move {
|
||||||
|
let mut lines = BufReader::new(response.body_mut()).lines();
|
||||||
|
|
||||||
|
fn parse_line(
|
||||||
|
line: Result<String, io::Error>,
|
||||||
|
) -> Result<Option<OpenAIResponseStreamEvent>> {
|
||||||
|
if let Some(data) = line?.strip_prefix("data: ") {
|
||||||
|
let event = serde_json::from_str(&data)?;
|
||||||
|
Ok(Some(event))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(line) = lines.next().await {
|
||||||
|
if let Some(event) = parse_line(line).transpose() {
|
||||||
|
let done = event.as_ref().map_or(false, |event| {
|
||||||
|
event
|
||||||
|
.choices
|
||||||
|
.last()
|
||||||
|
.map_or(false, |choice| choice.finish_reason.is_some())
|
||||||
|
});
|
||||||
|
if tx.unbounded_send(event).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if done {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Ok(rx)
|
||||||
|
} else {
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct OpenAIResponse {
|
||||||
|
error: OpenAIError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct OpenAIError {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
match serde_json::from_str::<OpenAIResponse>(&body) {
|
||||||
|
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
|
||||||
|
"Failed to connect to OpenAI API: {}",
|
||||||
|
response.error.message,
|
||||||
|
)),
|
||||||
|
|
||||||
|
_ => Err(anyhow!(
|
||||||
|
"Failed to connect to OpenAI API: {} {}",
|
||||||
|
response.status(),
|
||||||
|
body,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[ctor::ctor]
|
||||||
|
fn init_logger() {
|
||||||
|
if std::env::var("RUST_LOG").is_ok() {
|
||||||
|
env_logger::init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
293
crates/ai/src/streaming_diff.rs
Normal file
293
crates/ai/src/streaming_diff.rs
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
use collections::HashMap;
|
||||||
|
use ordered_float::OrderedFloat;
|
||||||
|
use std::{
|
||||||
|
cmp,
|
||||||
|
fmt::{self, Debug},
|
||||||
|
ops::Range,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Matrix {
|
||||||
|
cells: Vec<f64>,
|
||||||
|
rows: usize,
|
||||||
|
cols: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Matrix {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
cells: Vec::new(),
|
||||||
|
rows: 0,
|
||||||
|
cols: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize(&mut self, rows: usize, cols: usize) {
|
||||||
|
self.cells.resize(rows * cols, 0.);
|
||||||
|
self.rows = rows;
|
||||||
|
self.cols = cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, row: usize, col: usize) -> f64 {
|
||||||
|
if row >= self.rows {
|
||||||
|
panic!("row out of bounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
if col >= self.cols {
|
||||||
|
panic!("col out of bounds")
|
||||||
|
}
|
||||||
|
self.cells[col * self.rows + row]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&mut self, row: usize, col: usize, value: f64) {
|
||||||
|
if row >= self.rows {
|
||||||
|
panic!("row out of bounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
if col >= self.cols {
|
||||||
|
panic!("col out of bounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cells[col * self.rows + row] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Matrix {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
writeln!(f)?;
|
||||||
|
for i in 0..self.rows {
|
||||||
|
for j in 0..self.cols {
|
||||||
|
write!(f, "{:5}", self.get(i, j))?;
|
||||||
|
}
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Hunk {
|
||||||
|
Insert { text: String },
|
||||||
|
Remove { len: usize },
|
||||||
|
Keep { len: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StreamingDiff {
|
||||||
|
old: Vec<char>,
|
||||||
|
new: Vec<char>,
|
||||||
|
scores: Matrix,
|
||||||
|
old_text_ix: usize,
|
||||||
|
new_text_ix: usize,
|
||||||
|
equal_runs: HashMap<(usize, usize), u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamingDiff {
|
||||||
|
const INSERTION_SCORE: f64 = -1.;
|
||||||
|
const DELETION_SCORE: f64 = -20.;
|
||||||
|
const EQUALITY_BASE: f64 = 1.8;
|
||||||
|
const MAX_EQUALITY_EXPONENT: i32 = 16;
|
||||||
|
|
||||||
|
pub fn new(old: String) -> Self {
|
||||||
|
let old = old.chars().collect::<Vec<_>>();
|
||||||
|
let mut scores = Matrix::new();
|
||||||
|
scores.resize(old.len() + 1, 1);
|
||||||
|
for i in 0..=old.len() {
|
||||||
|
scores.set(i, 0, i as f64 * Self::DELETION_SCORE);
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
old,
|
||||||
|
new: Vec::new(),
|
||||||
|
scores,
|
||||||
|
old_text_ix: 0,
|
||||||
|
new_text_ix: 0,
|
||||||
|
equal_runs: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_new(&mut self, text: &str) -> Vec<Hunk> {
|
||||||
|
self.new.extend(text.chars());
|
||||||
|
self.scores.resize(self.old.len() + 1, self.new.len() + 1);
|
||||||
|
|
||||||
|
for j in self.new_text_ix + 1..=self.new.len() {
|
||||||
|
self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE);
|
||||||
|
for i in 1..=self.old.len() {
|
||||||
|
let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE;
|
||||||
|
let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE;
|
||||||
|
let equality_score = if self.old[i - 1] == self.new[j - 1] {
|
||||||
|
let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0);
|
||||||
|
equal_run += 1;
|
||||||
|
self.equal_runs.insert((i, j), equal_run);
|
||||||
|
|
||||||
|
let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT);
|
||||||
|
self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent)
|
||||||
|
} else {
|
||||||
|
f64::NEG_INFINITY
|
||||||
|
};
|
||||||
|
|
||||||
|
let score = insertion_score.max(deletion_score).max(equality_score);
|
||||||
|
self.scores.set(i, j, score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut max_score = f64::NEG_INFINITY;
|
||||||
|
let mut next_old_text_ix = self.old_text_ix;
|
||||||
|
let next_new_text_ix = self.new.len();
|
||||||
|
for i in self.old_text_ix..=self.old.len() {
|
||||||
|
let score = self.scores.get(i, next_new_text_ix);
|
||||||
|
if score > max_score {
|
||||||
|
max_score = score;
|
||||||
|
next_old_text_ix = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hunks = self.backtrack(next_old_text_ix, next_new_text_ix);
|
||||||
|
self.old_text_ix = next_old_text_ix;
|
||||||
|
self.new_text_ix = next_new_text_ix;
|
||||||
|
hunks
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<Hunk> {
|
||||||
|
let mut pending_insert: Option<Range<usize>> = None;
|
||||||
|
let mut hunks = Vec::new();
|
||||||
|
let mut i = old_text_ix;
|
||||||
|
let mut j = new_text_ix;
|
||||||
|
while (i, j) != (self.old_text_ix, self.new_text_ix) {
|
||||||
|
let insertion_score = if j > self.new_text_ix {
|
||||||
|
Some((i, j - 1))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let deletion_score = if i > self.old_text_ix {
|
||||||
|
Some((i - 1, j))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let equality_score = if i > self.old_text_ix && j > self.new_text_ix {
|
||||||
|
if self.old[i - 1] == self.new[j - 1] {
|
||||||
|
Some((i - 1, j - 1))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score]
|
||||||
|
.iter()
|
||||||
|
.max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j))))
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if prev_i == i && prev_j == j - 1 {
|
||||||
|
if let Some(pending_insert) = pending_insert.as_mut() {
|
||||||
|
pending_insert.start = prev_j;
|
||||||
|
} else {
|
||||||
|
pending_insert = Some(prev_j..j);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(range) = pending_insert.take() {
|
||||||
|
hunks.push(Hunk::Insert {
|
||||||
|
text: self.new[range].iter().collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let char_len = self.old[i - 1].len_utf8();
|
||||||
|
if prev_i == i - 1 && prev_j == j {
|
||||||
|
if let Some(Hunk::Remove { len }) = hunks.last_mut() {
|
||||||
|
*len += char_len;
|
||||||
|
} else {
|
||||||
|
hunks.push(Hunk::Remove { len: char_len })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(Hunk::Keep { len }) = hunks.last_mut() {
|
||||||
|
*len += char_len;
|
||||||
|
} else {
|
||||||
|
hunks.push(Hunk::Keep { len: char_len })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i = prev_i;
|
||||||
|
j = prev_j;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(range) = pending_insert.take() {
|
||||||
|
hunks.push(Hunk::Insert {
|
||||||
|
text: self.new[range].iter().collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hunks.reverse();
|
||||||
|
hunks
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(self) -> Vec<Hunk> {
|
||||||
|
self.backtrack(self.old.len(), self.new.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use rand::prelude::*;
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 100)]
|
||||||
|
fn test_random_diffs(mut rng: StdRng) {
|
||||||
|
let old_text_len = env::var("OLD_TEXT_LEN")
|
||||||
|
.map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
|
||||||
|
.unwrap_or(10);
|
||||||
|
let new_text_len = env::var("NEW_TEXT_LEN")
|
||||||
|
.map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable"))
|
||||||
|
.unwrap_or(10);
|
||||||
|
|
||||||
|
let old = util::RandomCharIter::new(&mut rng)
|
||||||
|
.take(old_text_len)
|
||||||
|
.collect::<String>();
|
||||||
|
log::info!("old text: {:?}", old);
|
||||||
|
|
||||||
|
let mut diff = StreamingDiff::new(old.clone());
|
||||||
|
let mut hunks = Vec::new();
|
||||||
|
let mut new_len = 0;
|
||||||
|
let mut new = String::new();
|
||||||
|
while new_len < new_text_len {
|
||||||
|
let new_chunk_len = rng.gen_range(1..=new_text_len - new_len);
|
||||||
|
let new_chunk = util::RandomCharIter::new(&mut rng)
|
||||||
|
.take(new_len)
|
||||||
|
.collect::<String>();
|
||||||
|
log::info!("new chunk: {:?}", new_chunk);
|
||||||
|
new_len += new_chunk_len;
|
||||||
|
new.push_str(&new_chunk);
|
||||||
|
let new_hunks = diff.push_new(&new_chunk);
|
||||||
|
log::info!("hunks: {:?}", new_hunks);
|
||||||
|
hunks.extend(new_hunks);
|
||||||
|
}
|
||||||
|
let final_hunks = diff.finish();
|
||||||
|
log::info!("final hunks: {:?}", final_hunks);
|
||||||
|
hunks.extend(final_hunks);
|
||||||
|
|
||||||
|
log::info!("new text: {:?}", new);
|
||||||
|
let mut old_ix = 0;
|
||||||
|
let mut new_ix = 0;
|
||||||
|
let mut patched = String::new();
|
||||||
|
for hunk in hunks {
|
||||||
|
match hunk {
|
||||||
|
Hunk::Keep { len } => {
|
||||||
|
assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]);
|
||||||
|
patched.push_str(&old[old_ix..old_ix + len]);
|
||||||
|
old_ix += len;
|
||||||
|
new_ix += len;
|
||||||
|
}
|
||||||
|
Hunk::Remove { len } => {
|
||||||
|
old_ix += len;
|
||||||
|
}
|
||||||
|
Hunk::Insert { text } => {
|
||||||
|
assert_eq!(text, &new[new_ix..new_ix + text.len()]);
|
||||||
|
patched.push_str(&text);
|
||||||
|
new_ix += text.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(patched, new);
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,7 +50,7 @@ impl View for Breadcrumbs {
|
||||||
let not_editor = active_item.downcast::<editor::Editor>().is_none();
|
let not_editor = active_item.downcast::<editor::Editor>().is_none();
|
||||||
|
|
||||||
let theme = theme::current(cx).clone();
|
let theme = theme::current(cx).clone();
|
||||||
let style = &theme.workspace.breadcrumbs;
|
let style = &theme.workspace.toolbar.breadcrumbs;
|
||||||
|
|
||||||
let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
|
let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
|
||||||
Some(breadcrumbs) => breadcrumbs,
|
Some(breadcrumbs) => breadcrumbs,
|
||||||
|
@ -60,7 +60,7 @@ impl View for Breadcrumbs {
|
||||||
.map(|breadcrumb| {
|
.map(|breadcrumb| {
|
||||||
Text::new(
|
Text::new(
|
||||||
breadcrumb.text,
|
breadcrumb.text,
|
||||||
theme.workspace.breadcrumbs.default.text.clone(),
|
theme.workspace.toolbar.breadcrumbs.default.text.clone(),
|
||||||
)
|
)
|
||||||
.with_highlights(breadcrumb.highlights.unwrap_or_default())
|
.with_highlights(breadcrumb.highlights.unwrap_or_default())
|
||||||
.into_any()
|
.into_any()
|
||||||
|
@ -68,10 +68,10 @@ impl View for Breadcrumbs {
|
||||||
|
|
||||||
let crumbs = Flex::row()
|
let crumbs = Flex::row()
|
||||||
.with_children(Itertools::intersperse_with(breadcrumbs, || {
|
.with_children(Itertools::intersperse_with(breadcrumbs, || {
|
||||||
Label::new(" 〉 ", style.default.text.clone()).into_any()
|
Label::new(" › ", style.default.text.clone()).into_any()
|
||||||
}))
|
}))
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(theme.workspace.breadcrumb_height)
|
.with_height(theme.workspace.toolbar.breadcrumb_height)
|
||||||
.contained();
|
.contained();
|
||||||
|
|
||||||
if not_editor || !self.pane_focused {
|
if not_editor || !self.pane_focused {
|
||||||
|
|
|
@ -2,70 +2,17 @@ use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
fmt, iter,
|
fmt, iter,
|
||||||
ops::{Add, AddAssign},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type ReplicaId = u16;
|
pub type ReplicaId = u16;
|
||||||
pub type Seq = u32;
|
pub type Seq = u32;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Ord, PartialOrd)]
|
|
||||||
pub struct Local {
|
|
||||||
pub replica_id: ReplicaId,
|
|
||||||
pub value: Seq,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
|
||||||
pub struct Lamport {
|
pub struct Lamport {
|
||||||
pub replica_id: ReplicaId,
|
pub replica_id: ReplicaId,
|
||||||
pub value: Seq,
|
pub value: Seq,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Local {
|
|
||||||
pub const MIN: Self = Self {
|
|
||||||
replica_id: ReplicaId::MIN,
|
|
||||||
value: Seq::MIN,
|
|
||||||
};
|
|
||||||
pub const MAX: Self = Self {
|
|
||||||
replica_id: ReplicaId::MAX,
|
|
||||||
value: Seq::MAX,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn new(replica_id: ReplicaId) -> Self {
|
|
||||||
Self {
|
|
||||||
replica_id,
|
|
||||||
value: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(&mut self) -> Self {
|
|
||||||
let timestamp = *self;
|
|
||||||
self.value += 1;
|
|
||||||
timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn observe(&mut self, timestamp: Self) {
|
|
||||||
if timestamp.replica_id == self.replica_id {
|
|
||||||
self.value = cmp::max(self.value, timestamp.value + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Add<&'a Self> for Local {
|
|
||||||
type Output = Local;
|
|
||||||
|
|
||||||
fn add(self, other: &'a Self) -> Self::Output {
|
|
||||||
*cmp::max(&self, other)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> AddAssign<&'a Local> for Local {
|
|
||||||
fn add_assign(&mut self, other: &Self) {
|
|
||||||
if *self < *other {
|
|
||||||
*self = *other;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A vector clock
|
/// A vector clock
|
||||||
#[derive(Clone, Default, Hash, Eq, PartialEq)]
|
#[derive(Clone, Default, Hash, Eq, PartialEq)]
|
||||||
pub struct Global(SmallVec<[u32; 8]>);
|
pub struct Global(SmallVec<[u32; 8]>);
|
||||||
|
@ -79,7 +26,7 @@ impl Global {
|
||||||
self.0.get(replica_id as usize).copied().unwrap_or(0) as Seq
|
self.0.get(replica_id as usize).copied().unwrap_or(0) as Seq
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe(&mut self, timestamp: Local) {
|
pub fn observe(&mut self, timestamp: Lamport) {
|
||||||
if timestamp.value > 0 {
|
if timestamp.value > 0 {
|
||||||
let new_len = timestamp.replica_id as usize + 1;
|
let new_len = timestamp.replica_id as usize + 1;
|
||||||
if new_len > self.0.len() {
|
if new_len > self.0.len() {
|
||||||
|
@ -126,7 +73,7 @@ impl Global {
|
||||||
self.0.resize(new_len, 0);
|
self.0.resize(new_len, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observed(&self, timestamp: Local) -> bool {
|
pub fn observed(&self, timestamp: Lamport) -> bool {
|
||||||
self.get(timestamp.replica_id) >= timestamp.value
|
self.get(timestamp.replica_id) >= timestamp.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,16 +125,16 @@ impl Global {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = Local> + '_ {
|
pub fn iter(&self) -> impl Iterator<Item = Lamport> + '_ {
|
||||||
self.0.iter().enumerate().map(|(replica_id, seq)| Local {
|
self.0.iter().enumerate().map(|(replica_id, seq)| Lamport {
|
||||||
replica_id: replica_id as ReplicaId,
|
replica_id: replica_id as ReplicaId,
|
||||||
value: *seq,
|
value: *seq,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromIterator<Local> for Global {
|
impl FromIterator<Lamport> for Global {
|
||||||
fn from_iter<T: IntoIterator<Item = Local>>(locals: T) -> Self {
|
fn from_iter<T: IntoIterator<Item = Lamport>>(locals: T) -> Self {
|
||||||
let mut result = Self::new();
|
let mut result = Self::new();
|
||||||
for local in locals {
|
for local in locals {
|
||||||
result.observe(local);
|
result.observe(local);
|
||||||
|
@ -212,6 +159,16 @@ impl PartialOrd for Lamport {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Lamport {
|
impl Lamport {
|
||||||
|
pub const MIN: Self = Self {
|
||||||
|
replica_id: ReplicaId::MIN,
|
||||||
|
value: Seq::MIN,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const MAX: Self = Self {
|
||||||
|
replica_id: ReplicaId::MAX,
|
||||||
|
value: Seq::MAX,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn new(replica_id: ReplicaId) -> Self {
|
pub fn new(replica_id: ReplicaId) -> Self {
|
||||||
Self {
|
Self {
|
||||||
value: 1,
|
value: 1,
|
||||||
|
@ -230,12 +187,6 @@ impl Lamport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Local {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "Local {{{}: {}}}", self.replica_id, self.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Lamport {
|
impl fmt::Debug for Lamport {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "Lamport {{{}: {}}}", self.replica_id, self.value)
|
write!(f, "Lamport {{{}: {}}}", self.replica_id, self.value)
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||||
default-run = "collab"
|
default-run = "collab"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.18.0"
|
version = "0.19.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use text::{EditOperation, InsertionTimestamp, UndoOperation};
|
use text::{EditOperation, UndoOperation};
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub async fn join_channel_buffer(
|
pub async fn join_channel_buffer(
|
||||||
|
@ -182,7 +182,6 @@ impl Database {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
pub async fn get_channel_buffer_collaborators(
|
pub async fn get_channel_buffer_collaborators(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
|
@ -370,7 +369,6 @@ fn operation_to_storage(
|
||||||
operation.replica_id,
|
operation.replica_id,
|
||||||
operation.lamport_timestamp,
|
operation.lamport_timestamp,
|
||||||
storage::Operation {
|
storage::Operation {
|
||||||
local_timestamp: operation.local_timestamp,
|
|
||||||
version: version_to_storage(&operation.version),
|
version: version_to_storage(&operation.version),
|
||||||
is_undo: false,
|
is_undo: false,
|
||||||
edit_ranges: operation
|
edit_ranges: operation
|
||||||
|
@ -389,7 +387,6 @@ fn operation_to_storage(
|
||||||
operation.replica_id,
|
operation.replica_id,
|
||||||
operation.lamport_timestamp,
|
operation.lamport_timestamp,
|
||||||
storage::Operation {
|
storage::Operation {
|
||||||
local_timestamp: operation.local_timestamp,
|
|
||||||
version: version_to_storage(&operation.version),
|
version: version_to_storage(&operation.version),
|
||||||
is_undo: true,
|
is_undo: true,
|
||||||
edit_ranges: Vec::new(),
|
edit_ranges: Vec::new(),
|
||||||
|
@ -399,7 +396,7 @@ fn operation_to_storage(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| storage::UndoCount {
|
.map(|entry| storage::UndoCount {
|
||||||
replica_id: entry.replica_id,
|
replica_id: entry.replica_id,
|
||||||
local_timestamp: entry.local_timestamp,
|
lamport_timestamp: entry.lamport_timestamp,
|
||||||
count: entry.count,
|
count: entry.count,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -427,7 +424,6 @@ fn operation_from_storage(
|
||||||
Ok(if operation.is_undo {
|
Ok(if operation.is_undo {
|
||||||
proto::operation::Variant::Undo(proto::operation::Undo {
|
proto::operation::Variant::Undo(proto::operation::Undo {
|
||||||
replica_id: row.replica_id as u32,
|
replica_id: row.replica_id as u32,
|
||||||
local_timestamp: operation.local_timestamp as u32,
|
|
||||||
lamport_timestamp: row.lamport_timestamp as u32,
|
lamport_timestamp: row.lamport_timestamp as u32,
|
||||||
version,
|
version,
|
||||||
counts: operation
|
counts: operation
|
||||||
|
@ -435,7 +431,7 @@ fn operation_from_storage(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| proto::UndoCount {
|
.map(|entry| proto::UndoCount {
|
||||||
replica_id: entry.replica_id,
|
replica_id: entry.replica_id,
|
||||||
local_timestamp: entry.local_timestamp,
|
lamport_timestamp: entry.lamport_timestamp,
|
||||||
count: entry.count,
|
count: entry.count,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -443,7 +439,6 @@ fn operation_from_storage(
|
||||||
} else {
|
} else {
|
||||||
proto::operation::Variant::Edit(proto::operation::Edit {
|
proto::operation::Variant::Edit(proto::operation::Edit {
|
||||||
replica_id: row.replica_id as u32,
|
replica_id: row.replica_id as u32,
|
||||||
local_timestamp: operation.local_timestamp as u32,
|
|
||||||
lamport_timestamp: row.lamport_timestamp as u32,
|
lamport_timestamp: row.lamport_timestamp as u32,
|
||||||
version,
|
version,
|
||||||
ranges: operation
|
ranges: operation
|
||||||
|
@ -483,10 +478,9 @@ fn version_from_storage(version: &Vec<storage::VectorClockEntry>) -> Vec<proto::
|
||||||
pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operation> {
|
pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operation> {
|
||||||
match operation.variant? {
|
match operation.variant? {
|
||||||
proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation {
|
proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation {
|
||||||
timestamp: InsertionTimestamp {
|
timestamp: clock::Lamport {
|
||||||
replica_id: edit.replica_id as text::ReplicaId,
|
replica_id: edit.replica_id as text::ReplicaId,
|
||||||
local: edit.local_timestamp,
|
value: edit.lamport_timestamp,
|
||||||
lamport: edit.lamport_timestamp,
|
|
||||||
},
|
},
|
||||||
version: version_from_wire(&edit.version),
|
version: version_from_wire(&edit.version),
|
||||||
ranges: edit
|
ranges: edit
|
||||||
|
@ -498,32 +492,26 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operatio
|
||||||
.collect(),
|
.collect(),
|
||||||
new_text: edit.new_text.into_iter().map(Arc::from).collect(),
|
new_text: edit.new_text.into_iter().map(Arc::from).collect(),
|
||||||
})),
|
})),
|
||||||
proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo {
|
proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo(UndoOperation {
|
||||||
lamport_timestamp: clock::Lamport {
|
timestamp: clock::Lamport {
|
||||||
replica_id: undo.replica_id as text::ReplicaId,
|
replica_id: undo.replica_id as text::ReplicaId,
|
||||||
value: undo.lamport_timestamp,
|
value: undo.lamport_timestamp,
|
||||||
},
|
},
|
||||||
undo: UndoOperation {
|
|
||||||
id: clock::Local {
|
|
||||||
replica_id: undo.replica_id as text::ReplicaId,
|
|
||||||
value: undo.local_timestamp,
|
|
||||||
},
|
|
||||||
version: version_from_wire(&undo.version),
|
version: version_from_wire(&undo.version),
|
||||||
counts: undo
|
counts: undo
|
||||||
.counts
|
.counts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
(
|
(
|
||||||
clock::Local {
|
clock::Lamport {
|
||||||
replica_id: c.replica_id as text::ReplicaId,
|
replica_id: c.replica_id as text::ReplicaId,
|
||||||
value: c.local_timestamp,
|
value: c.lamport_timestamp,
|
||||||
},
|
},
|
||||||
c.count,
|
c.count,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
},
|
})),
|
||||||
}),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -531,7 +519,7 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operatio
|
||||||
fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global {
|
fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global {
|
||||||
let mut version = clock::Global::new();
|
let mut version = clock::Global::new();
|
||||||
for entry in message {
|
for entry in message {
|
||||||
version.observe(clock::Local {
|
version.observe(clock::Lamport {
|
||||||
replica_id: entry.replica_id as text::ReplicaId,
|
replica_id: entry.replica_id as text::ReplicaId,
|
||||||
value: entry.timestamp,
|
value: entry.timestamp,
|
||||||
});
|
});
|
||||||
|
@ -546,8 +534,6 @@ mod storage {
|
||||||
|
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct Operation {
|
pub struct Operation {
|
||||||
#[prost(uint32, tag = "1")]
|
|
||||||
pub local_timestamp: u32,
|
|
||||||
#[prost(message, repeated, tag = "2")]
|
#[prost(message, repeated, tag = "2")]
|
||||||
pub version: Vec<VectorClockEntry>,
|
pub version: Vec<VectorClockEntry>,
|
||||||
#[prost(bool, tag = "3")]
|
#[prost(bool, tag = "3")]
|
||||||
|
@ -581,7 +567,7 @@ mod storage {
|
||||||
#[prost(uint32, tag = "1")]
|
#[prost(uint32, tag = "1")]
|
||||||
pub replica_id: u32,
|
pub replica_id: u32,
|
||||||
#[prost(uint32, tag = "2")]
|
#[prost(uint32, tag = "2")]
|
||||||
pub local_timestamp: u32,
|
pub lamport_timestamp: u32,
|
||||||
#[prost(uint32, tag = "3")]
|
#[prost(uint32, tag = "3")]
|
||||||
pub count: u32,
|
pub count: u32,
|
||||||
}
|
}
|
||||||
|
|
|
@ -241,7 +241,6 @@ impl Database {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
|
pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
|
let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
|
||||||
|
@ -257,7 +256,6 @@ impl Database {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
|
pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
user_feature::Entity::insert(user_feature::ActiveModel {
|
user_feature::Entity::insert(user_feature::ActiveModel {
|
||||||
|
|
|
@ -9,7 +9,7 @@ use editor::{
|
||||||
test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
|
test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
|
||||||
ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo,
|
ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo,
|
||||||
};
|
};
|
||||||
use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions};
|
use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions};
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle,
|
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle,
|
||||||
|
@ -19,7 +19,7 @@ use indoc::indoc;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
|
language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
|
||||||
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
|
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
|
||||||
LanguageConfig, OffsetRangeExt, Point, Rope,
|
LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
|
||||||
};
|
};
|
||||||
use live_kit_client::MacOSDisplay;
|
use live_kit_client::MacOSDisplay;
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
|
@ -33,7 +33,7 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, AtomicU32, Ordering::SeqCst},
|
atomic::{self, AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -7799,7 +7799,7 @@ async fn test_on_input_format_from_guest_to_host(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_mutual_editor_inlay_hint_cache_update(
|
async fn test_mutual_editor_inlay_hint_cache_update(
|
||||||
deterministic: Arc<Deterministic>,
|
deterministic: Arc<Deterministic>,
|
||||||
cx_a: &mut TestAppContext,
|
cx_a: &mut TestAppContext,
|
||||||
|
@ -7913,30 +7913,27 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Set up the language server to return an additional inlay hint on each request.
|
// Set up the language server to return an additional inlay hint on each request.
|
||||||
let next_call_id = Arc::new(AtomicU32::new(0));
|
let edits_made = Arc::new(AtomicUsize::new(0));
|
||||||
|
let closure_edits_made = Arc::clone(&edits_made);
|
||||||
fake_language_server
|
fake_language_server
|
||||||
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
||||||
let task_next_call_id = Arc::clone(&next_call_id);
|
let task_edits_made = Arc::clone(&closure_edits_made);
|
||||||
async move {
|
async move {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
params.text_document.uri,
|
params.text_document.uri,
|
||||||
lsp::Url::from_file_path("/a/main.rs").unwrap(),
|
lsp::Url::from_file_path("/a/main.rs").unwrap(),
|
||||||
);
|
);
|
||||||
let call_count = task_next_call_id.fetch_add(1, SeqCst);
|
let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
|
||||||
Ok(Some(
|
Ok(Some(vec![lsp::InlayHint {
|
||||||
(0..=call_count)
|
position: lsp::Position::new(0, edits_made as u32),
|
||||||
.map(|ix| lsp::InlayHint {
|
label: lsp::InlayHintLabel::String(edits_made.to_string()),
|
||||||
position: lsp::Position::new(0, ix),
|
|
||||||
label: lsp::InlayHintLabel::String(ix.to_string()),
|
|
||||||
kind: None,
|
kind: None,
|
||||||
text_edits: None,
|
text_edits: None,
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
padding_left: None,
|
padding_left: None,
|
||||||
padding_right: None,
|
padding_right: None,
|
||||||
data: None,
|
data: None,
|
||||||
})
|
}]))
|
||||||
.collect(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.next()
|
.next()
|
||||||
|
@ -7945,17 +7942,17 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||||
|
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
let mut edits_made = 1;
|
let initial_edit = edits_made.load(atomic::Ordering::Acquire);
|
||||||
editor_a.update(cx_a, |editor, _| {
|
editor_a.update(cx_a, |editor, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec!["0".to_string()],
|
vec![initial_edit.to_string()],
|
||||||
extract_hint_labels(editor),
|
extract_hint_labels(editor),
|
||||||
"Host should get its first hints when opens an editor"
|
"Host should get its first hints when opens an editor"
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version(),
|
inlay_cache.version(),
|
||||||
edits_made,
|
1,
|
||||||
"Host editor update the cache version after every cache/view change",
|
"Host editor update the cache version after every cache/view change",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -7972,144 +7969,104 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
editor_b.update(cx_b, |editor, _| {
|
editor_b.update(cx_b, |editor, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec!["0".to_string(), "1".to_string()],
|
vec![initial_edit.to_string()],
|
||||||
extract_hint_labels(editor),
|
extract_hint_labels(editor),
|
||||||
"Client should get its first hints when opens an editor"
|
"Client should get its first hints when opens an editor"
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version(),
|
inlay_cache.version(),
|
||||||
edits_made,
|
1,
|
||||||
"Guest editor update the cache version after every cache/view change"
|
"Guest editor update the cache version after every cache/view change"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
||||||
editor_b.update(cx_b, |editor, cx| {
|
editor_b.update(cx_b, |editor, cx| {
|
||||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
|
editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
|
||||||
editor.handle_input(":", cx);
|
editor.handle_input(":", cx);
|
||||||
cx.focus(&editor_b);
|
cx.focus(&editor_b);
|
||||||
edits_made += 1;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
editor_a.update(cx_a, |editor, _| {
|
editor_a.update(cx_a, |editor, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec![
|
vec![after_client_edit.to_string()],
|
||||||
"0".to_string(),
|
|
||||||
"1".to_string(),
|
|
||||||
"2".to_string(),
|
|
||||||
"3".to_string()
|
|
||||||
],
|
|
||||||
extract_hint_labels(editor),
|
extract_hint_labels(editor),
|
||||||
"Guest should get hints the 1st edit and 2nd LSP query"
|
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.version(), edits_made);
|
assert_eq!(inlay_cache.version(), 2);
|
||||||
});
|
});
|
||||||
editor_b.update(cx_b, |editor, _| {
|
editor_b.update(cx_b, |editor, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec!["0".to_string(), "1".to_string(), "2".to_string(),],
|
vec![after_client_edit.to_string()],
|
||||||
extract_hint_labels(editor),
|
extract_hint_labels(editor),
|
||||||
"Guest should get hints the 1st edit and 2nd LSP query"
|
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.version(), edits_made);
|
assert_eq!(inlay_cache.version(), 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
||||||
editor_a.update(cx_a, |editor, cx| {
|
editor_a.update(cx_a, |editor, cx| {
|
||||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||||
editor.handle_input("a change to increment both buffers' versions", cx);
|
editor.handle_input("a change to increment both buffers' versions", cx);
|
||||||
cx.focus(&editor_a);
|
cx.focus(&editor_a);
|
||||||
edits_made += 1;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
editor_a.update(cx_a, |editor, _| {
|
editor_a.update(cx_a, |editor, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec![
|
vec![after_host_edit.to_string()],
|
||||||
"0".to_string(),
|
|
||||||
"1".to_string(),
|
|
||||||
"2".to_string(),
|
|
||||||
"3".to_string(),
|
|
||||||
"4".to_string()
|
|
||||||
],
|
|
||||||
extract_hint_labels(editor),
|
extract_hint_labels(editor),
|
||||||
"Host should get hints from 3rd edit, 5th LSP query: \
|
|
||||||
4th query was made by guest (but not applied) due to cache invalidation logic"
|
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.version(), edits_made);
|
assert_eq!(inlay_cache.version(), 3);
|
||||||
});
|
});
|
||||||
editor_b.update(cx_b, |editor, _| {
|
editor_b.update(cx_b, |editor, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec![
|
vec![after_host_edit.to_string()],
|
||||||
"0".to_string(),
|
|
||||||
"1".to_string(),
|
|
||||||
"2".to_string(),
|
|
||||||
"3".to_string(),
|
|
||||||
"4".to_string(),
|
|
||||||
"5".to_string(),
|
|
||||||
],
|
|
||||||
extract_hint_labels(editor),
|
extract_hint_labels(editor),
|
||||||
"Guest should get hints from 3rd edit, 6th LSP query"
|
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.version(), edits_made);
|
assert_eq!(inlay_cache.version(), 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
|
||||||
fake_language_server
|
fake_language_server
|
||||||
.request::<lsp::request::InlayHintRefreshRequest>(())
|
.request::<lsp::request::InlayHintRefreshRequest>(())
|
||||||
.await
|
.await
|
||||||
.expect("inlay refresh request failed");
|
.expect("inlay refresh request failed");
|
||||||
edits_made += 1;
|
|
||||||
|
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
editor_a.update(cx_a, |editor, _| {
|
editor_a.update(cx_a, |editor, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec![
|
vec![after_special_edit_for_refresh.to_string()],
|
||||||
"0".to_string(),
|
|
||||||
"1".to_string(),
|
|
||||||
"2".to_string(),
|
|
||||||
"3".to_string(),
|
|
||||||
"4".to_string(),
|
|
||||||
"5".to_string(),
|
|
||||||
"6".to_string(),
|
|
||||||
],
|
|
||||||
extract_hint_labels(editor),
|
extract_hint_labels(editor),
|
||||||
"Host should react to /refresh LSP request and get new hints from 7th LSP query"
|
"Host should react to /refresh LSP request"
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version(),
|
inlay_cache.version(),
|
||||||
edits_made,
|
4,
|
||||||
"Host should accepted all edits and bump its cache version every time"
|
"Host should accepted all edits and bump its cache version every time"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
editor_b.update(cx_b, |editor, _| {
|
editor_b.update(cx_b, |editor, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec![
|
vec![after_special_edit_for_refresh.to_string()],
|
||||||
"0".to_string(),
|
|
||||||
"1".to_string(),
|
|
||||||
"2".to_string(),
|
|
||||||
"3".to_string(),
|
|
||||||
"4".to_string(),
|
|
||||||
"5".to_string(),
|
|
||||||
"6".to_string(),
|
|
||||||
"7".to_string(),
|
|
||||||
],
|
|
||||||
extract_hint_labels(editor),
|
extract_hint_labels(editor),
|
||||||
"Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query"
|
"Guest should get a /refresh LSP request propagated by host"
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version(),
|
inlay_cache.version(),
|
||||||
edits_made,
|
4,
|
||||||
"Guest should accepted all edits and bump its cache version every time"
|
"Guest should accepted all edits and bump its cache version every time"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_inlay_hint_refresh_is_forwarded(
|
async fn test_inlay_hint_refresh_is_forwarded(
|
||||||
deterministic: Arc<Deterministic>,
|
deterministic: Arc<Deterministic>,
|
||||||
cx_a: &mut TestAppContext,
|
cx_a: &mut TestAppContext,
|
||||||
|
@ -8223,35 +8180,34 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||||
.downcast::<Editor>()
|
.downcast::<Editor>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let other_hints = Arc::new(AtomicBool::new(false));
|
||||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||||
let next_call_id = Arc::new(AtomicU32::new(0));
|
let closure_other_hints = Arc::clone(&other_hints);
|
||||||
fake_language_server
|
fake_language_server
|
||||||
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
||||||
let task_next_call_id = Arc::clone(&next_call_id);
|
let task_other_hints = Arc::clone(&closure_other_hints);
|
||||||
async move {
|
async move {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
params.text_document.uri,
|
params.text_document.uri,
|
||||||
lsp::Url::from_file_path("/a/main.rs").unwrap(),
|
lsp::Url::from_file_path("/a/main.rs").unwrap(),
|
||||||
);
|
);
|
||||||
let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
|
let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
|
||||||
let mut new_hints = Vec::with_capacity(current_call_id as usize);
|
let character = if other_hints { 0 } else { 2 };
|
||||||
loop {
|
let label = if other_hints {
|
||||||
new_hints.push(lsp::InlayHint {
|
"other hint"
|
||||||
position: lsp::Position::new(0, current_call_id),
|
} else {
|
||||||
label: lsp::InlayHintLabel::String(current_call_id.to_string()),
|
"initial hint"
|
||||||
|
};
|
||||||
|
Ok(Some(vec![lsp::InlayHint {
|
||||||
|
position: lsp::Position::new(0, character),
|
||||||
|
label: lsp::InlayHintLabel::String(label.to_string()),
|
||||||
kind: None,
|
kind: None,
|
||||||
text_edits: None,
|
text_edits: None,
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
padding_left: None,
|
padding_left: None,
|
||||||
padding_right: None,
|
padding_right: None,
|
||||||
data: None,
|
data: None,
|
||||||
});
|
}]))
|
||||||
if current_call_id == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
current_call_id -= 1;
|
|
||||||
}
|
|
||||||
Ok(Some(new_hints))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.next()
|
.next()
|
||||||
|
@ -8270,26 +8226,26 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version(),
|
inlay_cache.version(),
|
||||||
0,
|
0,
|
||||||
"Host should not increment its cache version due to no changes",
|
"Turned off hints should not generate version updates"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut edits_made = 1;
|
|
||||||
cx_b.foreground().run_until_parked();
|
cx_b.foreground().run_until_parked();
|
||||||
editor_b.update(cx_b, |editor, _| {
|
editor_b.update(cx_b, |editor, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec!["0".to_string()],
|
vec!["initial hint".to_string()],
|
||||||
extract_hint_labels(editor),
|
extract_hint_labels(editor),
|
||||||
"Client should get its first hints when opens an editor"
|
"Client should get its first hints when opens an editor"
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version(),
|
inlay_cache.version(),
|
||||||
edits_made,
|
1,
|
||||||
"Guest editor update the cache version after every cache/view change"
|
"Should update cache verison after first hints"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
other_hints.fetch_or(true, atomic::Ordering::Release);
|
||||||
fake_language_server
|
fake_language_server
|
||||||
.request::<lsp::request::InlayHintRefreshRequest>(())
|
.request::<lsp::request::InlayHintRefreshRequest>(())
|
||||||
.await
|
.await
|
||||||
|
@ -8304,22 +8260,21 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version(),
|
inlay_cache.version(),
|
||||||
0,
|
0,
|
||||||
"Host should not increment its cache version due to no changes",
|
"Turned off hints should not generate version updates, again"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
edits_made += 1;
|
|
||||||
cx_b.foreground().run_until_parked();
|
cx_b.foreground().run_until_parked();
|
||||||
editor_b.update(cx_b, |editor, _| {
|
editor_b.update(cx_b, |editor, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec!["0".to_string(), "1".to_string(),],
|
vec!["other hint".to_string()],
|
||||||
extract_hint_labels(editor),
|
extract_hint_labels(editor),
|
||||||
"Guest should get a /refresh LSP request propagated by host despite host hints are off"
|
"Guest should get a /refresh LSP request propagated by host despite host hints are off"
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version(),
|
inlay_cache.version(),
|
||||||
edits_made,
|
2,
|
||||||
"Guest should accepted all edits and bump its cache version every time"
|
"Guest should accepted all edits and bump its cache version every time"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -762,7 +762,7 @@ async fn apply_client_operation(
|
||||||
|
|
||||||
client
|
client
|
||||||
.fs()
|
.fs()
|
||||||
.save(&path, &content.as_str().into(), fs::LineEnding::Unix)
|
.save(&path, &content.as_str().into(), text::LineEnding::Unix)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,7 +213,7 @@ impl Item for ChannelView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||||
true
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||||
|
|
|
@ -1106,23 +1106,17 @@ impl CollabPanel {
|
||||||
) -> AnyElement<Self> {
|
) -> AnyElement<Self> {
|
||||||
enum OpenSharedScreen {}
|
enum OpenSharedScreen {}
|
||||||
|
|
||||||
let font_cache = cx.font_cache();
|
let host_avatar_width = theme
|
||||||
let host_avatar_height = theme
|
|
||||||
.contact_avatar
|
.contact_avatar
|
||||||
.width
|
.width
|
||||||
.or(theme.contact_avatar.height)
|
.or(theme.contact_avatar.height)
|
||||||
.unwrap_or(0.);
|
.unwrap_or(0.);
|
||||||
let row = &theme.project_row.inactive_state().default;
|
|
||||||
let tree_branch = theme.tree_branch;
|
let tree_branch = theme.tree_branch;
|
||||||
let line_height = row.name.text.line_height(font_cache);
|
|
||||||
let cap_height = row.name.text.cap_height(font_cache);
|
|
||||||
let baseline_offset =
|
|
||||||
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
|
|
||||||
|
|
||||||
MouseEventHandler::new::<OpenSharedScreen, _>(
|
MouseEventHandler::new::<OpenSharedScreen, _>(
|
||||||
peer_id.as_u64() as usize,
|
peer_id.as_u64() as usize,
|
||||||
cx,
|
cx,
|
||||||
|mouse_state, _| {
|
|mouse_state, cx| {
|
||||||
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
||||||
let row = theme
|
let row = theme
|
||||||
.project_row
|
.project_row
|
||||||
|
@ -1130,49 +1124,20 @@ impl CollabPanel {
|
||||||
.style_for(mouse_state);
|
.style_for(mouse_state);
|
||||||
|
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(render_tree_branch(
|
||||||
Stack::new()
|
tree_branch,
|
||||||
.with_child(Canvas::new(move |scene, bounds, _, _, _| {
|
&row.name.text,
|
||||||
let start_x = bounds.min_x() + (bounds.width() / 2.)
|
is_last,
|
||||||
- (tree_branch.width / 2.);
|
vec2f(host_avatar_width, theme.row_height),
|
||||||
let end_x = bounds.max_x();
|
cx.font_cache(),
|
||||||
let start_y = bounds.min_y();
|
))
|
||||||
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
|
|
||||||
|
|
||||||
scene.push_quad(gpui::Quad {
|
|
||||||
bounds: RectF::from_points(
|
|
||||||
vec2f(start_x, start_y),
|
|
||||||
vec2f(
|
|
||||||
start_x + tree_branch.width,
|
|
||||||
if is_last { end_y } else { bounds.max_y() },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
background: Some(tree_branch.color),
|
|
||||||
border: gpui::Border::default(),
|
|
||||||
corner_radii: (0.).into(),
|
|
||||||
});
|
|
||||||
scene.push_quad(gpui::Quad {
|
|
||||||
bounds: RectF::from_points(
|
|
||||||
vec2f(start_x, end_y),
|
|
||||||
vec2f(end_x, end_y + tree_branch.width),
|
|
||||||
),
|
|
||||||
background: Some(tree_branch.color),
|
|
||||||
border: gpui::Border::default(),
|
|
||||||
corner_radii: (0.).into(),
|
|
||||||
});
|
|
||||||
}))
|
|
||||||
.constrained()
|
|
||||||
.with_width(host_avatar_height),
|
|
||||||
)
|
|
||||||
.with_child(
|
.with_child(
|
||||||
Svg::new("icons/disable_screen_sharing_12.svg")
|
Svg::new("icons/disable_screen_sharing_12.svg")
|
||||||
.with_color(row.icon.color)
|
.with_color(theme.channel_hash.color)
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_width(row.icon.width)
|
.with_width(theme.channel_hash.width)
|
||||||
.aligned()
|
.aligned()
|
||||||
.left()
|
.left(),
|
||||||
.contained()
|
|
||||||
.with_style(row.icon.container),
|
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Label::new("Screen", row.name.text.clone())
|
Label::new("Screen", row.name.text.clone())
|
||||||
|
@ -2553,27 +2518,16 @@ impl View for CollabPanel {
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row().with_child(
|
||||||
.with_child(
|
|
||||||
ChildView::new(&self.filter_editor, cx)
|
ChildView::new(&self.filter_editor, cx)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.user_query_editor.container)
|
.with_style(theme.user_query_editor.container)
|
||||||
.flex(1.0, true),
|
.flex(1.0, true),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.constrained()
|
.with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
|
||||||
.with_width(self.size(cx)),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
List::new(self.list_state.clone())
|
|
||||||
.constrained()
|
|
||||||
.with_width(self.size(cx))
|
|
||||||
.flex(1., true)
|
|
||||||
.into_any(),
|
|
||||||
)
|
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.container)
|
.with_style(theme.container)
|
||||||
.constrained()
|
|
||||||
.with_width(self.size(cx))
|
|
||||||
.into_any(),
|
.into_any(),
|
||||||
)
|
)
|
||||||
.with_children(
|
.with_children(
|
||||||
|
|
|
@ -213,7 +213,6 @@ impl CollabTitlebarItem {
|
||||||
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
|
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
|
||||||
let project_style = theme.titlebar.project_menu_button.clone();
|
let project_style = theme.titlebar.project_menu_button.clone();
|
||||||
let git_style = theme.titlebar.git_menu_button.clone();
|
let git_style = theme.titlebar.git_menu_button.clone();
|
||||||
let divider_style = theme.titlebar.project_name_divider.clone();
|
|
||||||
let item_spacing = theme.titlebar.item_spacing;
|
let item_spacing = theme.titlebar.item_spacing;
|
||||||
|
|
||||||
let mut ret = Flex::row().with_child(
|
let mut ret = Flex::row().with_child(
|
||||||
|
@ -248,21 +247,10 @@ impl CollabTitlebarItem {
|
||||||
);
|
);
|
||||||
if let Some(git_branch) = branch_prepended {
|
if let Some(git_branch) = branch_prepended {
|
||||||
ret = ret.with_child(
|
ret = ret.with_child(
|
||||||
Flex::row()
|
Flex::row().with_child(
|
||||||
.with_child(
|
|
||||||
Label::new("/", divider_style.text)
|
|
||||||
.contained()
|
|
||||||
.with_style(divider_style.container)
|
|
||||||
.aligned()
|
|
||||||
.left(),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::new::<ToggleVcsMenu, _>(
|
MouseEventHandler::new::<ToggleVcsMenu, _>(0, cx, |mouse_state, cx| {
|
||||||
0,
|
|
||||||
cx,
|
|
||||||
|mouse_state, cx| {
|
|
||||||
enum BranchPopoverTooltip {}
|
enum BranchPopoverTooltip {}
|
||||||
let style = git_style
|
let style = git_style
|
||||||
.in_state(self.branch_popover.is_some())
|
.in_state(self.branch_popover.is_some())
|
||||||
|
@ -281,8 +269,7 @@ impl CollabTitlebarItem {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.into_any_named("title-project-branch")
|
.into_any_named("title-project-branch")
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_down(MouseButton::Left, move |_, this, cx| {
|
.on_down(MouseButton::Left, move |_, this, cx| {
|
||||||
this.toggle_vcs_menu(&Default::default(), cx)
|
this.toggle_vcs_menu(&Default::default(), cx)
|
||||||
|
|
|
@ -1188,7 +1188,7 @@ mod tests {
|
||||||
_: u64,
|
_: u64,
|
||||||
_: &clock::Global,
|
_: &clock::Global,
|
||||||
_: language::RopeFingerprint,
|
_: language::RopeFingerprint,
|
||||||
_: ::fs::LineEnding,
|
_: language::LineEnding,
|
||||||
_: std::time::SystemTime,
|
_: std::time::SystemTime,
|
||||||
_: &mut AppContext,
|
_: &mut AppContext,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -37,10 +37,7 @@ impl BlinkManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
|
pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
if !self.visible {
|
self.show_cursor(cx);
|
||||||
self.visible = true;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
let epoch = self.next_blink_epoch();
|
let epoch = self.next_blink_epoch();
|
||||||
let interval = self.blink_interval;
|
let interval = self.blink_interval;
|
||||||
|
@ -82,7 +79,13 @@ impl BlinkManager {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
} else if !self.visible {
|
} else {
|
||||||
|
self.show_cursor(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) {
|
||||||
|
if !self.visible {
|
||||||
self.visible = true;
|
self.visible = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ use gpui::{
|
||||||
elements::*,
|
elements::*,
|
||||||
executor,
|
executor,
|
||||||
fonts::{self, HighlightStyle, TextStyle},
|
fonts::{self, HighlightStyle, TextStyle},
|
||||||
geometry::vector::Vector2F,
|
geometry::vector::{vec2f, Vector2F},
|
||||||
impl_actions,
|
impl_actions,
|
||||||
keymap_matcher::KeymapContext,
|
keymap_matcher::KeymapContext,
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, MouseButton},
|
||||||
|
@ -820,6 +820,7 @@ struct CompletionsMenu {
|
||||||
id: CompletionId,
|
id: CompletionId,
|
||||||
initial_position: Anchor,
|
initial_position: Anchor,
|
||||||
buffer: ModelHandle<Buffer>,
|
buffer: ModelHandle<Buffer>,
|
||||||
|
project: Option<ModelHandle<Project>>,
|
||||||
completions: Arc<[Completion]>,
|
completions: Arc<[Completion]>,
|
||||||
match_candidates: Vec<StringMatchCandidate>,
|
match_candidates: Vec<StringMatchCandidate>,
|
||||||
matches: Arc<[StringMatch]>,
|
matches: Arc<[StringMatch]>,
|
||||||
|
@ -863,6 +864,48 @@ impl CompletionsMenu {
|
||||||
fn render(&self, style: EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
|
fn render(&self, style: EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
|
||||||
enum CompletionTag {}
|
enum CompletionTag {}
|
||||||
|
|
||||||
|
let language_servers = self.project.as_ref().map(|project| {
|
||||||
|
project
|
||||||
|
.read(cx)
|
||||||
|
.language_servers_for_buffer(self.buffer.read(cx), cx)
|
||||||
|
.filter(|(_, server)| server.capabilities().completion_provider.is_some())
|
||||||
|
.map(|(adapter, server)| (server.server_id(), adapter.short_name))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
let needs_server_name = language_servers
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |servers| servers.len() > 1);
|
||||||
|
|
||||||
|
let get_server_name =
|
||||||
|
move |lookup_server_id: lsp::LanguageServerId| -> Option<&'static str> {
|
||||||
|
language_servers
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.find_map(|(server_id, server_name)| {
|
||||||
|
if *server_id == lookup_server_id {
|
||||||
|
Some(*server_name)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let widest_completion_ix = self
|
||||||
|
.matches
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.max_by_key(|(_, mat)| {
|
||||||
|
let completion = &self.completions[mat.candidate_id];
|
||||||
|
let mut len = completion.label.text.chars().count();
|
||||||
|
|
||||||
|
if let Some(server_name) = get_server_name(completion.server_id) {
|
||||||
|
len += server_name.chars().count();
|
||||||
|
}
|
||||||
|
|
||||||
|
len
|
||||||
|
})
|
||||||
|
.map(|(ix, _)| ix);
|
||||||
|
|
||||||
let completions = self.completions.clone();
|
let completions = self.completions.clone();
|
||||||
let matches = self.matches.clone();
|
let matches = self.matches.clone();
|
||||||
let selected_item = self.selected_item;
|
let selected_item = self.selected_item;
|
||||||
|
@ -889,9 +932,11 @@ impl CompletionsMenu {
|
||||||
style.autocomplete.item
|
style.autocomplete.item
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let completion_label =
|
||||||
Text::new(completion.label.text.clone(), style.text.clone())
|
Text::new(completion.label.text.clone(), style.text.clone())
|
||||||
.with_soft_wrap(false)
|
.with_soft_wrap(false)
|
||||||
.with_highlights(combine_syntax_and_fuzzy_match_highlights(
|
.with_highlights(
|
||||||
|
combine_syntax_and_fuzzy_match_highlights(
|
||||||
&completion.label.text,
|
&completion.label.text,
|
||||||
style.text.color.into(),
|
style.text.color.into(),
|
||||||
styled_runs_for_code_label(
|
styled_runs_for_code_label(
|
||||||
|
@ -899,9 +944,71 @@ impl CompletionsMenu {
|
||||||
&style.syntax,
|
&style.syntax,
|
||||||
),
|
),
|
||||||
&mat.positions,
|
&mat.positions,
|
||||||
))
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(server_name) = get_server_name(completion.server_id) {
|
||||||
|
Flex::row()
|
||||||
|
.with_child(completion_label)
|
||||||
|
.with_children((|| {
|
||||||
|
if !needs_server_name {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text_style = TextStyle {
|
||||||
|
color: style.autocomplete.server_name_color,
|
||||||
|
font_size: style.text.font_size
|
||||||
|
* style.autocomplete.server_name_size_percent,
|
||||||
|
..style.text.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let label = Text::new(server_name, text_style)
|
||||||
|
.aligned()
|
||||||
|
.constrained()
|
||||||
|
.dynamically(move |constraint, _, _| {
|
||||||
|
gpui::SizeConstraint {
|
||||||
|
min: constraint.min,
|
||||||
|
max: vec2f(
|
||||||
|
constraint.max.x(),
|
||||||
|
constraint.min.y(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if Some(item_ix) == widest_completion_ix {
|
||||||
|
Some(
|
||||||
|
label
|
||||||
|
.contained()
|
||||||
|
.with_style(
|
||||||
|
style
|
||||||
|
.autocomplete
|
||||||
|
.server_name_container,
|
||||||
|
)
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Some(label.flex_float().into_any())
|
||||||
|
}
|
||||||
|
})())
|
||||||
|
.into_any()
|
||||||
|
} else {
|
||||||
|
completion_label.into_any()
|
||||||
|
}
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(item_style)
|
.with_style(item_style)
|
||||||
|
.constrained()
|
||||||
|
.dynamically(
|
||||||
|
move |constraint, _, _| {
|
||||||
|
if Some(item_ix) == widest_completion_ix {
|
||||||
|
constraint
|
||||||
|
} else {
|
||||||
|
gpui::SizeConstraint {
|
||||||
|
min: constraint.min,
|
||||||
|
max: constraint.min,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
@ -918,19 +1025,7 @@ impl CompletionsMenu {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_width_from_item(
|
.with_width_from_item(widest_completion_ix)
|
||||||
self.matches
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.max_by_key(|(_, mat)| {
|
|
||||||
self.completions[mat.candidate_id]
|
|
||||||
.label
|
|
||||||
.text
|
|
||||||
.chars()
|
|
||||||
.count()
|
|
||||||
})
|
|
||||||
.map(|(ix, _)| ix),
|
|
||||||
)
|
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(container_style)
|
.with_style(container_style)
|
||||||
.into_any()
|
.into_any()
|
||||||
|
@ -1454,6 +1549,16 @@ impl Editor {
|
||||||
cx.observe(&display_map, Self::on_display_map_changed),
|
cx.observe(&display_map, Self::on_display_map_changed),
|
||||||
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
||||||
cx.observe_global::<SettingsStore, _>(Self::settings_changed),
|
cx.observe_global::<SettingsStore, _>(Self::settings_changed),
|
||||||
|
cx.observe_window_activation(|editor, active, cx| {
|
||||||
|
editor.blink_manager.update(cx, |blink_manager, cx| {
|
||||||
|
if active {
|
||||||
|
blink_manager.enable(cx);
|
||||||
|
} else {
|
||||||
|
blink_manager.show_cursor(cx);
|
||||||
|
blink_manager.disable(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1549,7 +1654,7 @@ impl Editor {
|
||||||
.excerpt_containing(self.selections.newest_anchor().head(), cx)
|
.excerpt_containing(self.selections.newest_anchor().head(), cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn style(&self, cx: &AppContext) -> EditorStyle {
|
pub fn style(&self, cx: &AppContext) -> EditorStyle {
|
||||||
build_style(
|
build_style(
|
||||||
settings::get::<ThemeSettings>(cx),
|
settings::get::<ThemeSettings>(cx),
|
||||||
self.get_field_editor_theme.as_deref(),
|
self.get_field_editor_theme.as_deref(),
|
||||||
|
@ -1625,6 +1730,15 @@ impl Editor {
|
||||||
self.read_only = read_only;
|
self.read_only = read_only;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_field_editor_style(
|
||||||
|
&mut self,
|
||||||
|
style: Option<Arc<GetFieldEditorTheme>>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.get_field_editor_theme = style;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
|
pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
|
||||||
self.replica_id_mapping.as_ref()
|
self.replica_id_mapping.as_ref()
|
||||||
}
|
}
|
||||||
|
@ -2964,6 +3078,7 @@ impl Editor {
|
||||||
});
|
});
|
||||||
|
|
||||||
let id = post_inc(&mut self.next_completion_id);
|
let id = post_inc(&mut self.next_completion_id);
|
||||||
|
let project = self.project.clone();
|
||||||
let task = cx.spawn(|this, mut cx| {
|
let task = cx.spawn(|this, mut cx| {
|
||||||
async move {
|
async move {
|
||||||
let menu = if let Some(completions) = completions.await.log_err() {
|
let menu = if let Some(completions) = completions.await.log_err() {
|
||||||
|
@ -2982,6 +3097,7 @@ impl Editor {
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
buffer,
|
buffer,
|
||||||
|
project,
|
||||||
completions: completions.into(),
|
completions: completions.into(),
|
||||||
matches: Vec::new().into(),
|
matches: Vec::new().into(),
|
||||||
selected_item: 0,
|
selected_item: 0,
|
||||||
|
@ -4979,6 +5095,9 @@ impl Editor {
|
||||||
self.unmark_text(cx);
|
self.unmark_text(cx);
|
||||||
self.refresh_copilot_suggestions(true, cx);
|
self.refresh_copilot_suggestions(true, cx);
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited);
|
||||||
|
cx.emit(Event::TransactionUndone {
|
||||||
|
transaction_id: tx_id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8418,6 +8537,9 @@ pub enum Event {
|
||||||
local: bool,
|
local: bool,
|
||||||
autoscroll: bool,
|
autoscroll: bool,
|
||||||
},
|
},
|
||||||
|
TransactionUndone {
|
||||||
|
transaction_id: TransactionId,
|
||||||
|
},
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8458,7 +8580,7 @@ impl View for Editor {
|
||||||
"Editor"
|
"Editor"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
if cx.is_self_focused() {
|
if cx.is_self_focused() {
|
||||||
let focused_event = EditorFocused(cx.handle());
|
let focused_event = EditorFocused(cx.handle());
|
||||||
cx.emit(Event::Focused);
|
cx.emit(Event::Focused);
|
||||||
|
@ -8466,7 +8588,7 @@ impl View for Editor {
|
||||||
}
|
}
|
||||||
if let Some(rename) = self.pending_rename.as_ref() {
|
if let Some(rename) = self.pending_rename.as_ref() {
|
||||||
cx.focus(&rename.editor);
|
cx.focus(&rename.editor);
|
||||||
} else {
|
} else if cx.is_self_focused() || !focused.is::<Editor>() {
|
||||||
if !self.focused {
|
if !self.focused {
|
||||||
self.blink_manager.update(cx, BlinkManager::enable);
|
self.blink_manager.update(cx, BlinkManager::enable);
|
||||||
}
|
}
|
||||||
|
@ -9161,6 +9283,7 @@ pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
.flat_map(|word| word.split_inclusive('_'))
|
.flat_map(|word| word.split_inclusive('_'))
|
||||||
|
.flat_map(|word| word.split_inclusive('-'))
|
||||||
}
|
}
|
||||||
|
|
||||||
trait RangeToAnchorExt {
|
trait RangeToAnchorExt {
|
||||||
|
|
|
@ -19,7 +19,8 @@ use gpui::{
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
||||||
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
|
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
|
||||||
|
Override, Point,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::project_settings::{LspSettings, ProjectSettings};
|
use project::project_settings::{LspSettings, ProjectSettings};
|
||||||
|
@ -7688,6 +7689,105 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
|
||||||
cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
|
cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
|
||||||
|
let mut cx = EditorLspTestContext::new(
|
||||||
|
Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
path_suffixes: vec!["jsx".into()],
|
||||||
|
overrides: [(
|
||||||
|
"element".into(),
|
||||||
|
LanguageConfigOverride {
|
||||||
|
word_characters: Override::Set(['-'].into_iter().collect()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_typescript::language_tsx()),
|
||||||
|
)
|
||||||
|
.with_override_query("(jsx_self_closing_element) @element")
|
||||||
|
.unwrap(),
|
||||||
|
lsp::ServerCapabilities {
|
||||||
|
completion_provider: Some(lsp::CompletionOptions {
|
||||||
|
trigger_characters: Some(vec![":".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.lsp
|
||||||
|
.handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||||
|
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||||
|
lsp::CompletionItem {
|
||||||
|
label: "bg-blue".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
lsp::CompletionItem {
|
||||||
|
label: "bg-red".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
lsp::CompletionItem {
|
||||||
|
label: "bg-yellow".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
])))
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.set_state(r#"<p class="bgˇ" />"#);
|
||||||
|
|
||||||
|
// Trigger completion when typing a dash, because the dash is an extra
|
||||||
|
// word character in the 'element' scope, which contains the cursor.
|
||||||
|
cx.simulate_keystroke("-");
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
cx.update_editor(|editor, _| {
|
||||||
|
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||||
|
assert_eq!(
|
||||||
|
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||||
|
&["bg-red", "bg-blue", "bg-yellow"]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("expected completion menu to be open");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.simulate_keystroke("l");
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
cx.update_editor(|editor, _| {
|
||||||
|
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||||
|
assert_eq!(
|
||||||
|
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||||
|
&["bg-blue", "bg-yellow"]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("expected completion menu to be open");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When filtering completions, consider the character after the '-' to
|
||||||
|
// be the start of a subword.
|
||||||
|
cx.set_state(r#"<p class="yelˇ" />"#);
|
||||||
|
cx.simulate_keystroke("l");
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
cx.update_editor(|editor, _| {
|
||||||
|
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||||
|
assert_eq!(
|
||||||
|
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||||
|
&["bg-yellow"]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("expected completion menu to be open");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||||
let point = DisplayPoint::new(row as u32, column as u32);
|
let point = DisplayPoint::new(row as u32, column as u32);
|
||||||
point..point
|
point..point
|
||||||
|
|
|
@ -2251,7 +2251,7 @@ impl Element<Editor> for EditorElement {
|
||||||
let replica_id = if let Some(mapping) = &editor.replica_id_mapping {
|
let replica_id = if let Some(mapping) = &editor.replica_id_mapping {
|
||||||
mapping.get(&replica_id).copied()
|
mapping.get(&replica_id).copied()
|
||||||
} else {
|
} else {
|
||||||
None
|
Some(replica_id)
|
||||||
};
|
};
|
||||||
|
|
||||||
// The local selections match the leader's selections.
|
// The local selections match the leader's selections.
|
||||||
|
|
|
@ -183,20 +183,21 @@ pub fn line_end(
|
||||||
|
|
||||||
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
let raw_point = point.to_point(map);
|
let raw_point = point.to_point(map);
|
||||||
let language = map.buffer_snapshot.language_at(raw_point);
|
let scope = map.buffer_snapshot.language_scope_at(raw_point);
|
||||||
|
|
||||||
find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
|
find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||||
(char_kind(language, left) != char_kind(language, right) && !right.is_whitespace())
|
(char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
|
||||||
|| left == '\n'
|
|| left == '\n'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
let raw_point = point.to_point(map);
|
let raw_point = point.to_point(map);
|
||||||
let language = map.buffer_snapshot.language_at(raw_point);
|
let scope = map.buffer_snapshot.language_scope_at(raw_point);
|
||||||
|
|
||||||
find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
|
find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||||
let is_word_start =
|
let is_word_start =
|
||||||
char_kind(language, left) != char_kind(language, right) && !right.is_whitespace();
|
char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
|
||||||
let is_subword_start =
|
let is_subword_start =
|
||||||
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
|
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
|
||||||
is_word_start || is_subword_start || left == '\n'
|
is_word_start || is_subword_start || left == '\n'
|
||||||
|
@ -205,19 +206,21 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
|
||||||
|
|
||||||
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
let raw_point = point.to_point(map);
|
let raw_point = point.to_point(map);
|
||||||
let language = map.buffer_snapshot.language_at(raw_point);
|
let scope = map.buffer_snapshot.language_scope_at(raw_point);
|
||||||
|
|
||||||
find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||||
(char_kind(language, left) != char_kind(language, right) && !left.is_whitespace())
|
(char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
|
||||||
|| right == '\n'
|
|| right == '\n'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
let raw_point = point.to_point(map);
|
let raw_point = point.to_point(map);
|
||||||
let language = map.buffer_snapshot.language_at(raw_point);
|
let scope = map.buffer_snapshot.language_scope_at(raw_point);
|
||||||
|
|
||||||
find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||||
let is_word_end =
|
let is_word_end =
|
||||||
(char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace();
|
(char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
|
||||||
let is_subword_end =
|
let is_subword_end =
|
||||||
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
|
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
|
||||||
is_word_end || is_subword_end || right == '\n'
|
is_word_end || is_subword_end || right == '\n'
|
||||||
|
@ -339,14 +342,14 @@ pub fn find_boundary(
|
||||||
|
|
||||||
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||||
let raw_point = point.to_point(map);
|
let raw_point = point.to_point(map);
|
||||||
let language = map.buffer_snapshot.language_at(raw_point);
|
let scope = map.buffer_snapshot.language_scope_at(raw_point);
|
||||||
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
|
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
|
||||||
let text = &map.buffer_snapshot;
|
let text = &map.buffer_snapshot;
|
||||||
let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c));
|
let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c));
|
||||||
let prev_char_kind = text
|
let prev_char_kind = text
|
||||||
.reversed_chars_at(ix)
|
.reversed_chars_at(ix)
|
||||||
.next()
|
.next()
|
||||||
.map(|c| char_kind(language, c));
|
.map(|c| char_kind(&scope, c));
|
||||||
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
|
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -617,6 +617,42 @@ impl MultiBuffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn merge_transactions(
|
||||||
|
&mut self,
|
||||||
|
transaction: TransactionId,
|
||||||
|
destination: TransactionId,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
if let Some(buffer) = self.as_singleton() {
|
||||||
|
buffer.update(cx, |buffer, _| {
|
||||||
|
buffer.merge_transactions(transaction, destination)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if let Some(transaction) = self.history.forget(transaction) {
|
||||||
|
if let Some(destination) = self.history.transaction_mut(destination) {
|
||||||
|
for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions {
|
||||||
|
if let Some(destination_buffer_transaction_id) =
|
||||||
|
destination.buffer_transactions.get(&buffer_id)
|
||||||
|
{
|
||||||
|
if let Some(state) = self.buffers.borrow().get(&buffer_id) {
|
||||||
|
state.buffer.update(cx, |buffer, _| {
|
||||||
|
buffer.merge_transactions(
|
||||||
|
buffer_transaction_id,
|
||||||
|
*destination_buffer_transaction_id,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
destination
|
||||||
|
.buffer_transactions
|
||||||
|
.insert(buffer_id, buffer_transaction_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn finalize_last_transaction(&mut self, cx: &mut ModelContext<Self>) {
|
pub fn finalize_last_transaction(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
self.history.finalize_last_transaction();
|
self.history.finalize_last_transaction();
|
||||||
for BufferState { buffer, .. } in self.buffers.borrow().values() {
|
for BufferState { buffer, .. } in self.buffers.borrow().values() {
|
||||||
|
@ -788,6 +824,20 @@ impl MultiBuffer {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut ModelContext<Self>) {
|
||||||
|
if let Some(buffer) = self.as_singleton() {
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||||
|
} else if let Some(transaction) = self.history.remove_from_undo(transaction_id) {
|
||||||
|
for (buffer_id, transaction_id) in &transaction.buffer_transactions {
|
||||||
|
if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) {
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.undo_transaction(*transaction_id, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn stream_excerpts_with_context_lines(
|
pub fn stream_excerpts_with_context_lines(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: ModelHandle<Buffer>,
|
buffer: ModelHandle<Buffer>,
|
||||||
|
@ -1367,13 +1417,13 @@ impl MultiBuffer {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let language = self.language_at(position.clone(), cx);
|
let snapshot = self.snapshot(cx);
|
||||||
|
let position = position.to_offset(&snapshot);
|
||||||
if char_kind(language.as_ref(), char) == CharKind::Word {
|
let scope = snapshot.language_scope_at(position);
|
||||||
|
if char_kind(&scope, char) == CharKind::Word {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let snapshot = self.snapshot(cx);
|
|
||||||
let anchor = snapshot.anchor_before(position);
|
let anchor = snapshot.anchor_before(position);
|
||||||
anchor
|
anchor
|
||||||
.buffer_id
|
.buffer_id
|
||||||
|
@ -1875,8 +1925,8 @@ impl MultiBufferSnapshot {
|
||||||
let mut next_chars = self.chars_at(start).peekable();
|
let mut next_chars = self.chars_at(start).peekable();
|
||||||
let mut prev_chars = self.reversed_chars_at(start).peekable();
|
let mut prev_chars = self.reversed_chars_at(start).peekable();
|
||||||
|
|
||||||
let language = self.language_at(start);
|
let scope = self.language_scope_at(start);
|
||||||
let kind = |c| char_kind(language, c);
|
let kind = |c| char_kind(&scope, c);
|
||||||
let word_kind = cmp::max(
|
let word_kind = cmp::max(
|
||||||
prev_chars.peek().copied().map(kind),
|
prev_chars.peek().copied().map(kind),
|
||||||
next_chars.peek().copied().map(kind),
|
next_chars.peek().copied().map(kind),
|
||||||
|
@ -2316,6 +2366,16 @@ impl MultiBufferSnapshot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prev_non_blank_row(&self, mut row: u32) -> Option<u32> {
|
||||||
|
while row > 0 {
|
||||||
|
row -= 1;
|
||||||
|
if !self.is_line_blank(row) {
|
||||||
|
return Some(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn line_len(&self, row: u32) -> u32 {
|
pub fn line_len(&self, row: u32) -> u32 {
|
||||||
if let Some((_, range)) = self.buffer_line_for_row(row) {
|
if let Some((_, range)) = self.buffer_line_for_row(row) {
|
||||||
range.end.column - range.start.column
|
range.end.column - range.start.column
|
||||||
|
@ -3347,6 +3407,35 @@ impl History {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn forget(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
|
||||||
|
if let Some(ix) = self
|
||||||
|
.undo_stack
|
||||||
|
.iter()
|
||||||
|
.rposition(|transaction| transaction.id == transaction_id)
|
||||||
|
{
|
||||||
|
Some(self.undo_stack.remove(ix))
|
||||||
|
} else if let Some(ix) = self
|
||||||
|
.redo_stack
|
||||||
|
.iter()
|
||||||
|
.rposition(|transaction| transaction.id == transaction_id)
|
||||||
|
{
|
||||||
|
Some(self.redo_stack.remove(ix))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> {
|
||||||
|
self.undo_stack
|
||||||
|
.iter_mut()
|
||||||
|
.find(|transaction| transaction.id == transaction_id)
|
||||||
|
.or_else(|| {
|
||||||
|
self.redo_stack
|
||||||
|
.iter_mut()
|
||||||
|
.find(|transaction| transaction.id == transaction_id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn pop_undo(&mut self) -> Option<&mut Transaction> {
|
fn pop_undo(&mut self) -> Option<&mut Transaction> {
|
||||||
assert_eq!(self.transaction_depth, 0);
|
assert_eq!(self.transaction_depth, 0);
|
||||||
if let Some(transaction) = self.undo_stack.pop() {
|
if let Some(transaction) = self.undo_stack.pop() {
|
||||||
|
@ -3367,6 +3456,16 @@ impl History {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
|
||||||
|
let ix = self
|
||||||
|
.undo_stack
|
||||||
|
.iter()
|
||||||
|
.rposition(|transaction| transaction.id == transaction_id)?;
|
||||||
|
let transaction = self.undo_stack.remove(ix);
|
||||||
|
self.redo_stack.push(transaction);
|
||||||
|
self.redo_stack.last()
|
||||||
|
}
|
||||||
|
|
||||||
fn group(&mut self) -> Option<TransactionId> {
|
fn group(&mut self) -> Option<TransactionId> {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
let mut transactions = self.undo_stack.iter();
|
let mut transactions = self.undo_stack.iter();
|
||||||
|
|
|
@ -39,7 +39,7 @@ impl ScrollAmount {
|
||||||
.visible_line_count()
|
.visible_line_count()
|
||||||
// subtract one to leave an anchor line
|
// subtract one to leave an anchor line
|
||||||
// round towards zero (so page-up and page-down are symmetric)
|
// round towards zero (so page-up and page-down are symmetric)
|
||||||
.map(|l| ((l - 1.) * count).trunc())
|
.map(|l| (l * count).trunc() - count.signum())
|
||||||
.unwrap_or(0.),
|
.unwrap_or(0.),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||||
language
|
language
|
||||||
.path_suffixes()
|
.path_suffixes()
|
||||||
.first()
|
.first()
|
||||||
.unwrap_or(&"txt".to_string())
|
.expect("language must have a path suffix for EditorLspTestContext")
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut fake_servers = language
|
let mut fake_servers = language
|
||||||
|
|
|
@ -12,6 +12,7 @@ collections = { path = "../collections" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
rope = { path = "../rope" }
|
rope = { path = "../rope" }
|
||||||
|
text = { path = "../text" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
|
|
|
@ -4,14 +4,10 @@ use anyhow::{anyhow, Result};
|
||||||
use fsevent::EventStream;
|
use fsevent::EventStream;
|
||||||
use futures::{future::BoxFuture, Stream, StreamExt};
|
use futures::{future::BoxFuture, Stream, StreamExt};
|
||||||
use git2::Repository as LibGitRepository;
|
use git2::Repository as LibGitRepository;
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use regex::Regex;
|
|
||||||
use repository::GitRepository;
|
use repository::GitRepository;
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::cmp;
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -22,6 +18,7 @@ use std::{
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
use text::LineEnding;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
@ -33,66 +30,6 @@ use std::ffi::OsStr;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use std::sync::Weak;
|
use std::sync::Weak;
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum LineEnding {
|
|
||||||
Unix,
|
|
||||||
Windows,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LineEnding {
|
|
||||||
fn default() -> Self {
|
|
||||||
#[cfg(unix)]
|
|
||||||
return Self::Unix;
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
return Self::CRLF;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LineEnding {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
LineEnding::Unix => "\n",
|
|
||||||
LineEnding::Windows => "\r\n",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn detect(text: &str) -> Self {
|
|
||||||
let mut max_ix = cmp::min(text.len(), 1000);
|
|
||||||
while !text.is_char_boundary(max_ix) {
|
|
||||||
max_ix -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ix) = text[..max_ix].find(&['\n']) {
|
|
||||||
if ix > 0 && text.as_bytes()[ix - 1] == b'\r' {
|
|
||||||
Self::Windows
|
|
||||||
} else {
|
|
||||||
Self::Unix
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn normalize(text: &mut String) {
|
|
||||||
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") {
|
|
||||||
*text = replaced;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
|
|
||||||
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
|
|
||||||
replaced.into()
|
|
||||||
} else {
|
|
||||||
text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait Fs: Send + Sync {
|
pub trait Fs: Send + Sync {
|
||||||
async fn create_dir(&self, path: &Path) -> Result<()>;
|
async fn create_dir(&self, path: &Path) -> Result<()>;
|
||||||
|
@ -520,7 +457,7 @@ impl FakeFsState {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
|
pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ use crate::{
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
pub use clock::ReplicaId;
|
pub use clock::ReplicaId;
|
||||||
use fs::LineEnding;
|
|
||||||
use futures::FutureExt as _;
|
use futures::FutureExt as _;
|
||||||
use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
|
use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
|
@ -149,6 +148,7 @@ pub struct Completion {
|
||||||
pub old_range: Range<Anchor>,
|
pub old_range: Range<Anchor>,
|
||||||
pub new_text: String,
|
pub new_text: String,
|
||||||
pub label: CodeLabel,
|
pub label: CodeLabel,
|
||||||
|
pub server_id: LanguageServerId,
|
||||||
pub lsp_completion: lsp::CompletionItem,
|
pub lsp_completion: lsp::CompletionItem,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,7 +439,7 @@ impl Buffer {
|
||||||
operations.extend(
|
operations.extend(
|
||||||
text_operations
|
text_operations
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, op)| !since.observed(op.local_timestamp()))
|
.filter(|(_, op)| !since.observed(op.timestamp()))
|
||||||
.map(|(_, op)| proto::serialize_operation(&Operation::Buffer(op.clone()))),
|
.map(|(_, op)| proto::serialize_operation(&Operation::Buffer(op.clone()))),
|
||||||
);
|
);
|
||||||
operations.sort_unstable_by_key(proto::lamport_timestamp_for_operation);
|
operations.sort_unstable_by_key(proto::lamport_timestamp_for_operation);
|
||||||
|
@ -1298,9 +1298,13 @@ impl Buffer {
|
||||||
self.text.forget_transaction(transaction_id);
|
self.text.forget_transaction(transaction_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
|
||||||
|
self.text.merge_transactions(transaction, destination);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn wait_for_edits(
|
pub fn wait_for_edits(
|
||||||
&mut self,
|
&mut self,
|
||||||
edit_ids: impl IntoIterator<Item = clock::Local>,
|
edit_ids: impl IntoIterator<Item = clock::Lamport>,
|
||||||
) -> impl Future<Output = Result<()>> {
|
) -> impl Future<Output = Result<()>> {
|
||||||
self.text.wait_for_edits(edit_ids)
|
self.text.wait_for_edits(edit_ids)
|
||||||
}
|
}
|
||||||
|
@ -1358,7 +1362,7 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Local>
|
pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Lamport>
|
||||||
where
|
where
|
||||||
T: Into<Arc<str>>,
|
T: Into<Arc<str>>,
|
||||||
{
|
{
|
||||||
|
@ -1371,7 +1375,7 @@ impl Buffer {
|
||||||
edits_iter: I,
|
edits_iter: I,
|
||||||
autoindent_mode: Option<AutoindentMode>,
|
autoindent_mode: Option<AutoindentMode>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Option<clock::Local>
|
) -> Option<clock::Lamport>
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = (Range<S>, T)>,
|
I: IntoIterator<Item = (Range<S>, T)>,
|
||||||
S: ToOffset,
|
S: ToOffset,
|
||||||
|
@ -1408,7 +1412,7 @@ impl Buffer {
|
||||||
.and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode)));
|
.and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode)));
|
||||||
|
|
||||||
let edit_operation = self.text.edit(edits.iter().cloned());
|
let edit_operation = self.text.edit(edits.iter().cloned());
|
||||||
let edit_id = edit_operation.local_timestamp();
|
let edit_id = edit_operation.timestamp();
|
||||||
|
|
||||||
if let Some((before_edit, mode)) = autoindent_request {
|
if let Some((before_edit, mode)) = autoindent_request {
|
||||||
let mut delta = 0isize;
|
let mut delta = 0isize;
|
||||||
|
@ -1664,6 +1668,22 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn undo_transaction(
|
||||||
|
&mut self,
|
||||||
|
transaction_id: TransactionId,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> bool {
|
||||||
|
let was_dirty = self.is_dirty();
|
||||||
|
let old_version = self.version.clone();
|
||||||
|
if let Some(operation) = self.text.undo_transaction(transaction_id) {
|
||||||
|
self.send_operation(Operation::Buffer(operation), cx);
|
||||||
|
self.did_edit(&old_version, was_dirty, cx);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn undo_to_transaction(
|
pub fn undo_to_transaction(
|
||||||
&mut self,
|
&mut self,
|
||||||
transaction_id: TransactionId,
|
transaction_id: TransactionId,
|
||||||
|
@ -2197,8 +2217,8 @@ impl BufferSnapshot {
|
||||||
let mut next_chars = self.chars_at(start).peekable();
|
let mut next_chars = self.chars_at(start).peekable();
|
||||||
let mut prev_chars = self.reversed_chars_at(start).peekable();
|
let mut prev_chars = self.reversed_chars_at(start).peekable();
|
||||||
|
|
||||||
let language = self.language_at(start);
|
let scope = self.language_scope_at(start);
|
||||||
let kind = |c| char_kind(language, c);
|
let kind = |c| char_kind(&scope, c);
|
||||||
let word_kind = cmp::max(
|
let word_kind = cmp::max(
|
||||||
prev_chars.peek().copied().map(kind),
|
prev_chars.peek().copied().map(kind),
|
||||||
next_chars.peek().copied().map(kind),
|
next_chars.peek().copied().map(kind),
|
||||||
|
@ -3012,17 +3032,21 @@ pub fn contiguous_ranges(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn char_kind(language: Option<&Arc<Language>>, c: char) -> CharKind {
|
pub fn char_kind(scope: &Option<LanguageScope>, c: char) -> CharKind {
|
||||||
if c.is_whitespace() {
|
if c.is_whitespace() {
|
||||||
return CharKind::Whitespace;
|
return CharKind::Whitespace;
|
||||||
} else if c.is_alphanumeric() || c == '_' {
|
} else if c.is_alphanumeric() || c == '_' {
|
||||||
return CharKind::Word;
|
return CharKind::Word;
|
||||||
}
|
}
|
||||||
if let Some(language) = language {
|
|
||||||
if language.config.word_characters.contains(&c) {
|
if let Some(scope) = scope {
|
||||||
|
if let Some(characters) = scope.word_characters() {
|
||||||
|
if characters.contains(&c) {
|
||||||
return CharKind::Word;
|
return CharKind::Word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CharKind::Punctuation
|
CharKind::Punctuation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ use crate::language_settings::{
|
||||||
use super::*;
|
use super::*;
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use fs::LineEnding;
|
|
||||||
use gpui::{AppContext, ModelHandle};
|
use gpui::{AppContext, ModelHandle};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use proto::deserialize_operation;
|
use proto::deserialize_operation;
|
||||||
|
@ -20,6 +19,7 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use text::network::Network;
|
use text::network::Network;
|
||||||
|
use text::LineEnding;
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
|
use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ use theme::{SyntaxTheme, Theme};
|
||||||
use tree_sitter::{self, Query};
|
use tree_sitter::{self, Query};
|
||||||
use unicase::UniCase;
|
use unicase::UniCase;
|
||||||
use util::{http::HttpClient, paths::PathExt};
|
use util::{http::HttpClient, paths::PathExt};
|
||||||
use util::{merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
|
use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use futures::channel::mpsc;
|
use futures::channel::mpsc;
|
||||||
|
@ -57,6 +57,7 @@ pub use diagnostic_set::DiagnosticEntry;
|
||||||
pub use lsp::LanguageServerId;
|
pub use lsp::LanguageServerId;
|
||||||
pub use outline::{Outline, OutlineItem};
|
pub use outline::{Outline, OutlineItem};
|
||||||
pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo};
|
pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo};
|
||||||
|
pub use text::LineEnding;
|
||||||
pub use tree_sitter::{Parser, Tree};
|
pub use tree_sitter::{Parser, Tree};
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
@ -90,6 +91,7 @@ pub struct LanguageServerName(pub Arc<str>);
|
||||||
/// once at startup, and caches the results.
|
/// once at startup, and caches the results.
|
||||||
pub struct CachedLspAdapter {
|
pub struct CachedLspAdapter {
|
||||||
pub name: LanguageServerName,
|
pub name: LanguageServerName,
|
||||||
|
pub short_name: &'static str,
|
||||||
pub initialization_options: Option<Value>,
|
pub initialization_options: Option<Value>,
|
||||||
pub disk_based_diagnostic_sources: Vec<String>,
|
pub disk_based_diagnostic_sources: Vec<String>,
|
||||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||||
|
@ -100,6 +102,7 @@ pub struct CachedLspAdapter {
|
||||||
impl CachedLspAdapter {
|
impl CachedLspAdapter {
|
||||||
pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
|
pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
|
||||||
let name = adapter.name().await;
|
let name = adapter.name().await;
|
||||||
|
let short_name = adapter.short_name();
|
||||||
let initialization_options = adapter.initialization_options().await;
|
let initialization_options = adapter.initialization_options().await;
|
||||||
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await;
|
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await;
|
||||||
let disk_based_diagnostics_progress_token =
|
let disk_based_diagnostics_progress_token =
|
||||||
|
@ -108,6 +111,7 @@ impl CachedLspAdapter {
|
||||||
|
|
||||||
Arc::new(CachedLspAdapter {
|
Arc::new(CachedLspAdapter {
|
||||||
name,
|
name,
|
||||||
|
short_name,
|
||||||
initialization_options,
|
initialization_options,
|
||||||
disk_based_diagnostic_sources,
|
disk_based_diagnostic_sources,
|
||||||
disk_based_diagnostics_progress_token,
|
disk_based_diagnostics_progress_token,
|
||||||
|
@ -175,10 +179,7 @@ impl CachedLspAdapter {
|
||||||
self.adapter.code_action_kinds()
|
self.adapter.code_action_kinds()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn workspace_configuration(
|
pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
|
||||||
&self,
|
|
||||||
cx: &mut AppContext,
|
|
||||||
) -> Option<BoxFuture<'static, Value>> {
|
|
||||||
self.adapter.workspace_configuration(cx)
|
self.adapter.workspace_configuration(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,6 +220,8 @@ pub trait LspAdapterDelegate: Send + Sync {
|
||||||
pub trait LspAdapter: 'static + Send + Sync {
|
pub trait LspAdapter: 'static + Send + Sync {
|
||||||
async fn name(&self) -> LanguageServerName;
|
async fn name(&self) -> LanguageServerName;
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str;
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
delegate: &dyn LspAdapterDelegate,
|
delegate: &dyn LspAdapterDelegate,
|
||||||
|
@ -287,8 +290,8 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_configuration(&self, _: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
|
fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
|
||||||
None
|
futures::future::ready(serde_json::json!({})).boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
||||||
|
@ -343,6 +346,8 @@ pub struct LanguageConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub block_comment: Option<(Arc<str>, Arc<str>)>,
|
pub block_comment: Option<(Arc<str>, Arc<str>)>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub scope_opt_in_language_servers: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
pub overrides: HashMap<String, LanguageConfigOverride>,
|
pub overrides: HashMap<String, LanguageConfigOverride>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub word_characters: HashSet<char>,
|
pub word_characters: HashSet<char>,
|
||||||
|
@ -373,6 +378,10 @@ pub struct LanguageConfigOverride {
|
||||||
pub block_comment: Override<(Arc<str>, Arc<str>)>,
|
pub block_comment: Override<(Arc<str>, Arc<str>)>,
|
||||||
#[serde(skip_deserializing)]
|
#[serde(skip_deserializing)]
|
||||||
pub disabled_bracket_ixs: Vec<u16>,
|
pub disabled_bracket_ixs: Vec<u16>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub word_characters: Override<HashSet<char>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub opt_into_language_servers: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Debug)]
|
#[derive(Clone, Deserialize, Debug)]
|
||||||
|
@ -411,6 +420,7 @@ impl Default for LanguageConfig {
|
||||||
autoclose_before: Default::default(),
|
autoclose_before: Default::default(),
|
||||||
line_comment: Default::default(),
|
line_comment: Default::default(),
|
||||||
block_comment: Default::default(),
|
block_comment: Default::default(),
|
||||||
|
scope_opt_in_language_servers: Default::default(),
|
||||||
overrides: Default::default(),
|
overrides: Default::default(),
|
||||||
collapsed_placeholder: Default::default(),
|
collapsed_placeholder: Default::default(),
|
||||||
word_characters: Default::default(),
|
word_characters: Default::default(),
|
||||||
|
@ -685,41 +695,6 @@ impl LanguageRegistry {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn workspace_configuration(&self, cx: &mut AppContext) -> Task<serde_json::Value> {
|
|
||||||
let lsp_adapters = {
|
|
||||||
let state = self.state.read();
|
|
||||||
state
|
|
||||||
.available_languages
|
|
||||||
.iter()
|
|
||||||
.filter(|l| !l.loaded)
|
|
||||||
.flat_map(|l| l.lsp_adapters.clone())
|
|
||||||
.chain(
|
|
||||||
state
|
|
||||||
.languages
|
|
||||||
.iter()
|
|
||||||
.flat_map(|language| &language.adapters)
|
|
||||||
.map(|adapter| adapter.adapter.clone()),
|
|
||||||
)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut language_configs = Vec::new();
|
|
||||||
for adapter in &lsp_adapters {
|
|
||||||
if let Some(language_config) = adapter.workspace_configuration(cx) {
|
|
||||||
language_configs.push(language_config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.background().spawn(async move {
|
|
||||||
let mut config = serde_json::json!({});
|
|
||||||
let language_configs = futures::future::join_all(language_configs).await;
|
|
||||||
for language_config in language_configs {
|
|
||||||
merge_json_value_into(language_config, &mut config);
|
|
||||||
}
|
|
||||||
config
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&self, language: Arc<Language>) {
|
pub fn add(&self, language: Arc<Language>) {
|
||||||
self.state.write().add(language);
|
self.state.write().add(language);
|
||||||
}
|
}
|
||||||
|
@ -1383,13 +1358,23 @@ impl Language {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_override_query(mut self, source: &str) -> Result<Self> {
|
pub fn with_override_query(mut self, source: &str) -> anyhow::Result<Self> {
|
||||||
let query = Query::new(self.grammar_mut().ts_language, source)?;
|
let query = Query::new(self.grammar_mut().ts_language, source)?;
|
||||||
|
|
||||||
let mut override_configs_by_id = HashMap::default();
|
let mut override_configs_by_id = HashMap::default();
|
||||||
for (ix, name) in query.capture_names().iter().enumerate() {
|
for (ix, name) in query.capture_names().iter().enumerate() {
|
||||||
if !name.starts_with('_') {
|
if !name.starts_with('_') {
|
||||||
let value = self.config.overrides.remove(name).unwrap_or_default();
|
let value = self.config.overrides.remove(name).unwrap_or_default();
|
||||||
|
for server_name in &value.opt_into_language_servers {
|
||||||
|
if !self
|
||||||
|
.config
|
||||||
|
.scope_opt_in_language_servers
|
||||||
|
.contains(server_name)
|
||||||
|
{
|
||||||
|
util::debug_panic!("Server {server_name:?} has been opted-in by scope {name:?} but has not been marked as an opt-in server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override_configs_by_id.insert(ix as u32, (name.clone(), value));
|
override_configs_by_id.insert(ix as u32, (name.clone(), value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1595,6 +1580,13 @@ impl LanguageScope {
|
||||||
.map(|e| (&e.0, &e.1))
|
.map(|e| (&e.0, &e.1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn word_characters(&self) -> Option<&HashSet<char>> {
|
||||||
|
Override::as_option(
|
||||||
|
self.config_override().map(|o| &o.word_characters),
|
||||||
|
Some(&self.language.config.word_characters),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn brackets(&self) -> impl Iterator<Item = (&BracketPair, bool)> {
|
pub fn brackets(&self) -> impl Iterator<Item = (&BracketPair, bool)> {
|
||||||
let mut disabled_ids = self
|
let mut disabled_ids = self
|
||||||
.config_override()
|
.config_override()
|
||||||
|
@ -1621,6 +1613,20 @@ impl LanguageScope {
|
||||||
c.is_whitespace() || self.language.config.autoclose_before.contains(c)
|
c.is_whitespace() || self.language.config.autoclose_before.contains(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn language_allowed(&self, name: &LanguageServerName) -> bool {
|
||||||
|
let config = &self.language.config;
|
||||||
|
let opt_in_servers = &config.scope_opt_in_language_servers;
|
||||||
|
if opt_in_servers.iter().any(|o| *o == *name.0) {
|
||||||
|
if let Some(over) = self.config_override() {
|
||||||
|
over.opt_into_language_servers.iter().any(|o| *o == *name.0)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn config_override(&self) -> Option<&LanguageConfigOverride> {
|
fn config_override(&self) -> Option<&LanguageConfigOverride> {
|
||||||
let id = self.override_id?;
|
let id = self.override_id?;
|
||||||
let grammar = self.language.grammar.as_ref()?;
|
let grammar = self.language.grammar.as_ref()?;
|
||||||
|
@ -1725,6 +1731,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
|
||||||
LanguageServerName(self.name.into())
|
LanguageServerName(self.name.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"FakeLspAdapter"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
_: &dyn LspAdapterDelegate,
|
_: &dyn LspAdapterDelegate,
|
||||||
|
|
|
@ -20,17 +20,17 @@ pub fn deserialize_fingerprint(fingerprint: &str) -> Result<RopeFingerprint> {
|
||||||
.map_err(|error| anyhow!("invalid fingerprint: {}", error))
|
.map_err(|error| anyhow!("invalid fingerprint: {}", error))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding {
|
pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
|
||||||
match message {
|
match message {
|
||||||
proto::LineEnding::Unix => fs::LineEnding::Unix,
|
proto::LineEnding::Unix => text::LineEnding::Unix,
|
||||||
proto::LineEnding::Windows => fs::LineEnding::Windows,
|
proto::LineEnding::Windows => text::LineEnding::Windows,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize_line_ending(message: fs::LineEnding) -> proto::LineEnding {
|
pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding {
|
||||||
match message {
|
match message {
|
||||||
fs::LineEnding::Unix => proto::LineEnding::Unix,
|
text::LineEnding::Unix => proto::LineEnding::Unix,
|
||||||
fs::LineEnding::Windows => proto::LineEnding::Windows,
|
text::LineEnding::Windows => proto::LineEnding::Windows,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,24 +41,22 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
|
||||||
proto::operation::Variant::Edit(serialize_edit_operation(edit))
|
proto::operation::Variant::Edit(serialize_edit_operation(edit))
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::Operation::Buffer(text::Operation::Undo {
|
crate::Operation::Buffer(text::Operation::Undo(undo)) => {
|
||||||
undo,
|
proto::operation::Variant::Undo(proto::operation::Undo {
|
||||||
lamport_timestamp,
|
replica_id: undo.timestamp.replica_id as u32,
|
||||||
}) => proto::operation::Variant::Undo(proto::operation::Undo {
|
lamport_timestamp: undo.timestamp.value,
|
||||||
replica_id: undo.id.replica_id as u32,
|
|
||||||
local_timestamp: undo.id.value,
|
|
||||||
lamport_timestamp: lamport_timestamp.value,
|
|
||||||
version: serialize_version(&undo.version),
|
version: serialize_version(&undo.version),
|
||||||
counts: undo
|
counts: undo
|
||||||
.counts
|
.counts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(edit_id, count)| proto::UndoCount {
|
.map(|(edit_id, count)| proto::UndoCount {
|
||||||
replica_id: edit_id.replica_id as u32,
|
replica_id: edit_id.replica_id as u32,
|
||||||
local_timestamp: edit_id.value,
|
lamport_timestamp: edit_id.value,
|
||||||
count: *count,
|
count: *count,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}),
|
})
|
||||||
|
}
|
||||||
|
|
||||||
crate::Operation::UpdateSelections {
|
crate::Operation::UpdateSelections {
|
||||||
selections,
|
selections,
|
||||||
|
@ -101,8 +99,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
|
||||||
pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit {
|
pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit {
|
||||||
proto::operation::Edit {
|
proto::operation::Edit {
|
||||||
replica_id: operation.timestamp.replica_id as u32,
|
replica_id: operation.timestamp.replica_id as u32,
|
||||||
local_timestamp: operation.timestamp.local,
|
lamport_timestamp: operation.timestamp.value,
|
||||||
lamport_timestamp: operation.timestamp.lamport,
|
|
||||||
version: serialize_version(&operation.version),
|
version: serialize_version(&operation.version),
|
||||||
ranges: operation.ranges.iter().map(serialize_range).collect(),
|
ranges: operation.ranges.iter().map(serialize_range).collect(),
|
||||||
new_text: operation
|
new_text: operation
|
||||||
|
@ -114,7 +111,7 @@ pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize_undo_map_entry(
|
pub fn serialize_undo_map_entry(
|
||||||
(edit_id, counts): (&clock::Local, &[(clock::Local, u32)]),
|
(edit_id, counts): (&clock::Lamport, &[(clock::Lamport, u32)]),
|
||||||
) -> proto::UndoMapEntry {
|
) -> proto::UndoMapEntry {
|
||||||
proto::UndoMapEntry {
|
proto::UndoMapEntry {
|
||||||
replica_id: edit_id.replica_id as u32,
|
replica_id: edit_id.replica_id as u32,
|
||||||
|
@ -123,7 +120,7 @@ pub fn serialize_undo_map_entry(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(undo_id, count)| proto::UndoCount {
|
.map(|(undo_id, count)| proto::UndoCount {
|
||||||
replica_id: undo_id.replica_id as u32,
|
replica_id: undo_id.replica_id as u32,
|
||||||
local_timestamp: undo_id.value,
|
lamport_timestamp: undo_id.value,
|
||||||
count: *count,
|
count: *count,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -197,7 +194,7 @@ pub fn serialize_diagnostics<'a>(
|
||||||
pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
|
pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
|
||||||
proto::Anchor {
|
proto::Anchor {
|
||||||
replica_id: anchor.timestamp.replica_id as u32,
|
replica_id: anchor.timestamp.replica_id as u32,
|
||||||
local_timestamp: anchor.timestamp.value,
|
timestamp: anchor.timestamp.value,
|
||||||
offset: anchor.offset as u64,
|
offset: anchor.offset as u64,
|
||||||
bias: match anchor.bias {
|
bias: match anchor.bias {
|
||||||
Bias::Left => proto::Bias::Left as i32,
|
Bias::Left => proto::Bias::Left as i32,
|
||||||
|
@ -218,32 +215,26 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
|
||||||
crate::Operation::Buffer(text::Operation::Edit(deserialize_edit_operation(edit)))
|
crate::Operation::Buffer(text::Operation::Edit(deserialize_edit_operation(edit)))
|
||||||
}
|
}
|
||||||
proto::operation::Variant::Undo(undo) => {
|
proto::operation::Variant::Undo(undo) => {
|
||||||
crate::Operation::Buffer(text::Operation::Undo {
|
crate::Operation::Buffer(text::Operation::Undo(UndoOperation {
|
||||||
lamport_timestamp: clock::Lamport {
|
timestamp: clock::Lamport {
|
||||||
replica_id: undo.replica_id as ReplicaId,
|
replica_id: undo.replica_id as ReplicaId,
|
||||||
value: undo.lamport_timestamp,
|
value: undo.lamport_timestamp,
|
||||||
},
|
},
|
||||||
undo: UndoOperation {
|
|
||||||
id: clock::Local {
|
|
||||||
replica_id: undo.replica_id as ReplicaId,
|
|
||||||
value: undo.local_timestamp,
|
|
||||||
},
|
|
||||||
version: deserialize_version(&undo.version),
|
version: deserialize_version(&undo.version),
|
||||||
counts: undo
|
counts: undo
|
||||||
.counts
|
.counts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
(
|
(
|
||||||
clock::Local {
|
clock::Lamport {
|
||||||
replica_id: c.replica_id as ReplicaId,
|
replica_id: c.replica_id as ReplicaId,
|
||||||
value: c.local_timestamp,
|
value: c.lamport_timestamp,
|
||||||
},
|
},
|
||||||
c.count,
|
c.count,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
},
|
}))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
proto::operation::Variant::UpdateSelections(message) => {
|
proto::operation::Variant::UpdateSelections(message) => {
|
||||||
let selections = message
|
let selections = message
|
||||||
|
@ -298,10 +289,9 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
|
||||||
|
|
||||||
pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation {
|
pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation {
|
||||||
EditOperation {
|
EditOperation {
|
||||||
timestamp: InsertionTimestamp {
|
timestamp: clock::Lamport {
|
||||||
replica_id: edit.replica_id as ReplicaId,
|
replica_id: edit.replica_id as ReplicaId,
|
||||||
local: edit.local_timestamp,
|
value: edit.lamport_timestamp,
|
||||||
lamport: edit.lamport_timestamp,
|
|
||||||
},
|
},
|
||||||
version: deserialize_version(&edit.version),
|
version: deserialize_version(&edit.version),
|
||||||
ranges: edit.ranges.into_iter().map(deserialize_range).collect(),
|
ranges: edit.ranges.into_iter().map(deserialize_range).collect(),
|
||||||
|
@ -311,9 +301,9 @@ pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation
|
||||||
|
|
||||||
pub fn deserialize_undo_map_entry(
|
pub fn deserialize_undo_map_entry(
|
||||||
entry: proto::UndoMapEntry,
|
entry: proto::UndoMapEntry,
|
||||||
) -> (clock::Local, Vec<(clock::Local, u32)>) {
|
) -> (clock::Lamport, Vec<(clock::Lamport, u32)>) {
|
||||||
(
|
(
|
||||||
clock::Local {
|
clock::Lamport {
|
||||||
replica_id: entry.replica_id as u16,
|
replica_id: entry.replica_id as u16,
|
||||||
value: entry.local_timestamp,
|
value: entry.local_timestamp,
|
||||||
},
|
},
|
||||||
|
@ -322,9 +312,9 @@ pub fn deserialize_undo_map_entry(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|undo_count| {
|
.map(|undo_count| {
|
||||||
(
|
(
|
||||||
clock::Local {
|
clock::Lamport {
|
||||||
replica_id: undo_count.replica_id as u16,
|
replica_id: undo_count.replica_id as u16,
|
||||||
value: undo_count.local_timestamp,
|
value: undo_count.lamport_timestamp,
|
||||||
},
|
},
|
||||||
undo_count.count,
|
undo_count.count,
|
||||||
)
|
)
|
||||||
|
@ -384,9 +374,9 @@ pub fn deserialize_diagnostics(
|
||||||
|
|
||||||
pub fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {
|
pub fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {
|
||||||
Some(Anchor {
|
Some(Anchor {
|
||||||
timestamp: clock::Local {
|
timestamp: clock::Lamport {
|
||||||
replica_id: anchor.replica_id as ReplicaId,
|
replica_id: anchor.replica_id as ReplicaId,
|
||||||
value: anchor.local_timestamp,
|
value: anchor.timestamp,
|
||||||
},
|
},
|
||||||
offset: anchor.offset as usize,
|
offset: anchor.offset as usize,
|
||||||
bias: match proto::Bias::from_i32(anchor.bias)? {
|
bias: match proto::Bias::from_i32(anchor.bias)? {
|
||||||
|
@ -434,6 +424,7 @@ pub fn serialize_completion(completion: &Completion) -> proto::Completion {
|
||||||
old_start: Some(serialize_anchor(&completion.old_range.start)),
|
old_start: Some(serialize_anchor(&completion.old_range.start)),
|
||||||
old_end: Some(serialize_anchor(&completion.old_range.end)),
|
old_end: Some(serialize_anchor(&completion.old_range.end)),
|
||||||
new_text: completion.new_text.clone(),
|
new_text: completion.new_text.clone(),
|
||||||
|
server_id: completion.server_id.0 as u64,
|
||||||
lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(),
|
lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,6 +457,7 @@ pub async fn deserialize_completion(
|
||||||
lsp_completion.filter_text.as_deref(),
|
lsp_completion.filter_text.as_deref(),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
server_id: LanguageServerId(completion.server_id as usize),
|
||||||
lsp_completion,
|
lsp_completion,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -498,12 +490,12 @@ pub fn deserialize_code_action(action: proto::CodeAction) -> Result<CodeAction>
|
||||||
|
|
||||||
pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction {
|
pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction {
|
||||||
proto::Transaction {
|
proto::Transaction {
|
||||||
id: Some(serialize_local_timestamp(transaction.id)),
|
id: Some(serialize_timestamp(transaction.id)),
|
||||||
edit_ids: transaction
|
edit_ids: transaction
|
||||||
.edit_ids
|
.edit_ids
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.map(serialize_local_timestamp)
|
.map(serialize_timestamp)
|
||||||
.collect(),
|
.collect(),
|
||||||
start: serialize_version(&transaction.start),
|
start: serialize_version(&transaction.start),
|
||||||
}
|
}
|
||||||
|
@ -511,7 +503,7 @@ pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction {
|
||||||
|
|
||||||
pub fn deserialize_transaction(transaction: proto::Transaction) -> Result<Transaction> {
|
pub fn deserialize_transaction(transaction: proto::Transaction) -> Result<Transaction> {
|
||||||
Ok(Transaction {
|
Ok(Transaction {
|
||||||
id: deserialize_local_timestamp(
|
id: deserialize_timestamp(
|
||||||
transaction
|
transaction
|
||||||
.id
|
.id
|
||||||
.ok_or_else(|| anyhow!("missing transaction id"))?,
|
.ok_or_else(|| anyhow!("missing transaction id"))?,
|
||||||
|
@ -519,21 +511,21 @@ pub fn deserialize_transaction(transaction: proto::Transaction) -> Result<Transa
|
||||||
edit_ids: transaction
|
edit_ids: transaction
|
||||||
.edit_ids
|
.edit_ids
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(deserialize_local_timestamp)
|
.map(deserialize_timestamp)
|
||||||
.collect(),
|
.collect(),
|
||||||
start: deserialize_version(&transaction.start),
|
start: deserialize_version(&transaction.start),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize_local_timestamp(timestamp: clock::Local) -> proto::LocalTimestamp {
|
pub fn serialize_timestamp(timestamp: clock::Lamport) -> proto::LamportTimestamp {
|
||||||
proto::LocalTimestamp {
|
proto::LamportTimestamp {
|
||||||
replica_id: timestamp.replica_id as u32,
|
replica_id: timestamp.replica_id as u32,
|
||||||
value: timestamp.value,
|
value: timestamp.value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deserialize_local_timestamp(timestamp: proto::LocalTimestamp) -> clock::Local {
|
pub fn deserialize_timestamp(timestamp: proto::LamportTimestamp) -> clock::Lamport {
|
||||||
clock::Local {
|
clock::Lamport {
|
||||||
replica_id: timestamp.replica_id as ReplicaId,
|
replica_id: timestamp.replica_id as ReplicaId,
|
||||||
value: timestamp.value,
|
value: timestamp.value,
|
||||||
}
|
}
|
||||||
|
@ -553,7 +545,7 @@ pub fn deserialize_range(range: proto::Range) -> Range<FullOffset> {
|
||||||
pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global {
|
pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global {
|
||||||
let mut version = clock::Global::new();
|
let mut version = clock::Global::new();
|
||||||
for entry in message {
|
for entry in message {
|
||||||
version.observe(clock::Local {
|
version.observe(clock::Lamport {
|
||||||
replica_id: entry.replica_id as ReplicaId,
|
replica_id: entry.replica_id as ReplicaId,
|
||||||
value: entry.timestamp,
|
value: entry.timestamp,
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,6 +12,7 @@ use gpui::{
|
||||||
ViewHandle, WeakModelHandle,
|
ViewHandle, WeakModelHandle,
|
||||||
};
|
};
|
||||||
use language::{Buffer, LanguageServerId, LanguageServerName};
|
use language::{Buffer, LanguageServerId, LanguageServerName};
|
||||||
|
use lsp::IoKind;
|
||||||
use project::{Project, Worktree};
|
use project::{Project, Worktree};
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
use theme::{ui, Theme};
|
use theme::{ui, Theme};
|
||||||
|
@ -26,7 +27,7 @@ const RECEIVE_LINE: &str = "// Receive:\n";
|
||||||
|
|
||||||
pub struct LogStore {
|
pub struct LogStore {
|
||||||
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
|
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
|
||||||
io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
|
io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, IoKind, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProjectState {
|
struct ProjectState {
|
||||||
|
@ -37,12 +38,12 @@ struct ProjectState {
|
||||||
struct LanguageServerState {
|
struct LanguageServerState {
|
||||||
log_buffer: ModelHandle<Buffer>,
|
log_buffer: ModelHandle<Buffer>,
|
||||||
rpc_state: Option<LanguageServerRpcState>,
|
rpc_state: Option<LanguageServerRpcState>,
|
||||||
|
_subscription: Option<lsp::Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LanguageServerRpcState {
|
struct LanguageServerRpcState {
|
||||||
buffer: ModelHandle<Buffer>,
|
buffer: ModelHandle<Buffer>,
|
||||||
last_message_kind: Option<MessageKind>,
|
last_message_kind: Option<MessageKind>,
|
||||||
_subscription: lsp::Subscription,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LspLogView {
|
pub struct LspLogView {
|
||||||
|
@ -118,11 +119,11 @@ impl LogStore {
|
||||||
io_tx,
|
io_tx,
|
||||||
};
|
};
|
||||||
cx.spawn_weak(|this, mut cx| async move {
|
cx.spawn_weak(|this, mut cx| async move {
|
||||||
while let Some((project, server_id, is_output, mut message)) = io_rx.next().await {
|
while let Some((project, server_id, io_kind, mut message)) = io_rx.next().await {
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
message.push('\n');
|
message.push('\n');
|
||||||
this.on_io(project, server_id, is_output, &message, cx);
|
this.on_io(project, server_id, io_kind, &message, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,22 +169,29 @@ impl LogStore {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Option<ModelHandle<Buffer>> {
|
) -> Option<ModelHandle<Buffer>> {
|
||||||
let project_state = self.projects.get_mut(&project.downgrade())?;
|
let project_state = self.projects.get_mut(&project.downgrade())?;
|
||||||
Some(
|
let server_state = project_state.servers.entry(id).or_insert_with(|| {
|
||||||
project_state
|
|
||||||
.servers
|
|
||||||
.entry(id)
|
|
||||||
.or_insert_with(|| {
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
LanguageServerState {
|
LanguageServerState {
|
||||||
rpc_state: None,
|
rpc_state: None,
|
||||||
log_buffer: cx
|
log_buffer: cx
|
||||||
.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""))
|
.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""))
|
||||||
.clone(),
|
.clone(),
|
||||||
|
_subscription: None,
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let server = project.read(cx).language_server_for_id(id);
|
||||||
|
let weak_project = project.downgrade();
|
||||||
|
let io_tx = self.io_tx.clone();
|
||||||
|
server_state._subscription = server.map(|server| {
|
||||||
|
server.on_io(move |io_kind, message| {
|
||||||
|
io_tx
|
||||||
|
.unbounded_send((weak_project, id, io_kind, message.to_string()))
|
||||||
|
.ok();
|
||||||
})
|
})
|
||||||
.log_buffer
|
});
|
||||||
.clone(),
|
|
||||||
)
|
Some(server_state.log_buffer.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_language_server_log(
|
fn add_language_server_log(
|
||||||
|
@ -230,7 +238,7 @@ impl LogStore {
|
||||||
Some(server_state.log_buffer.clone())
|
Some(server_state.log_buffer.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enable_rpc_trace_for_language_server(
|
fn enable_rpc_trace_for_language_server(
|
||||||
&mut self,
|
&mut self,
|
||||||
project: &ModelHandle<Project>,
|
project: &ModelHandle<Project>,
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
|
@ -239,9 +247,7 @@ impl LogStore {
|
||||||
let weak_project = project.downgrade();
|
let weak_project = project.downgrade();
|
||||||
let project_state = self.projects.get_mut(&weak_project)?;
|
let project_state = self.projects.get_mut(&weak_project)?;
|
||||||
let server_state = project_state.servers.get_mut(&server_id)?;
|
let server_state = project_state.servers.get_mut(&server_id)?;
|
||||||
let server = project.read(cx).language_server_for_id(server_id)?;
|
|
||||||
let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
|
let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
|
||||||
let io_tx = self.io_tx.clone();
|
|
||||||
let language = project.read(cx).languages().language_for_name("JSON");
|
let language = project.read(cx).languages().language_for_name("JSON");
|
||||||
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
|
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
|
||||||
cx.spawn_weak({
|
cx.spawn_weak({
|
||||||
|
@ -258,11 +264,6 @@ impl LogStore {
|
||||||
LanguageServerRpcState {
|
LanguageServerRpcState {
|
||||||
buffer,
|
buffer,
|
||||||
last_message_kind: None,
|
last_message_kind: None,
|
||||||
_subscription: server.on_io(move |is_received, json| {
|
|
||||||
io_tx
|
|
||||||
.unbounded_send((weak_project, server_id, is_received, json.to_string()))
|
|
||||||
.ok();
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Some(rpc_state.buffer.clone())
|
Some(rpc_state.buffer.clone())
|
||||||
|
@ -285,10 +286,25 @@ impl LogStore {
|
||||||
&mut self,
|
&mut self,
|
||||||
project: WeakModelHandle<Project>,
|
project: WeakModelHandle<Project>,
|
||||||
language_server_id: LanguageServerId,
|
language_server_id: LanguageServerId,
|
||||||
is_received: bool,
|
io_kind: IoKind,
|
||||||
message: &str,
|
message: &str,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
|
let is_received = match io_kind {
|
||||||
|
IoKind::StdOut => true,
|
||||||
|
IoKind::StdIn => false,
|
||||||
|
IoKind::StdErr => {
|
||||||
|
let project = project.upgrade(cx)?;
|
||||||
|
project.update(cx, |_, cx| {
|
||||||
|
cx.emit(project::Event::LanguageServerLog(
|
||||||
|
language_server_id,
|
||||||
|
format!("stderr: {}\n", message.trim()),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
return Some(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let state = self
|
let state = self
|
||||||
.projects
|
.projects
|
||||||
.get_mut(&project)?
|
.get_mut(&project)?
|
||||||
|
|
|
@ -20,7 +20,7 @@ anyhow.workspace = true
|
||||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
|
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
lsp-types = "0.94"
|
lsp-types = { git = "https://github.com/zed-industries/lsp-types", branch = "updated-completion-list-item-defaults" }
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
@ -4,7 +4,7 @@ pub use lsp_types::*;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite};
|
use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt};
|
||||||
use gpui::{executor, AsyncAppContext, Task};
|
use gpui::{executor, AsyncAppContext, Task};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::{barrier, prelude::Stream};
|
use postage::{barrier, prelude::Stream};
|
||||||
|
@ -26,16 +26,25 @@ use std::{
|
||||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||||
Arc, Weak,
|
Arc, Weak,
|
||||||
},
|
},
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use std::{path::Path, process::Stdio};
|
use std::{path::Path, process::Stdio};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
|
||||||
const JSON_RPC_VERSION: &str = "2.0";
|
const JSON_RPC_VERSION: &str = "2.0";
|
||||||
const CONTENT_LEN_HEADER: &str = "Content-Length: ";
|
const CONTENT_LEN_HEADER: &str = "Content-Length: ";
|
||||||
|
const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
|
||||||
|
|
||||||
type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
|
type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
|
||||||
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
|
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
|
||||||
type IoHandler = Box<dyn Send + FnMut(bool, &str)>;
|
type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum IoKind {
|
||||||
|
StdOut,
|
||||||
|
StdIn,
|
||||||
|
StdErr,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct LanguageServerBinary {
|
pub struct LanguageServerBinary {
|
||||||
|
@ -144,16 +153,18 @@ impl LanguageServer {
|
||||||
.args(binary.arguments)
|
.args(binary.arguments)
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(Stdio::piped())
|
||||||
.kill_on_drop(true)
|
.kill_on_drop(true)
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
|
|
||||||
let stdin = server.stdin.take().unwrap();
|
let stdin = server.stdin.take().unwrap();
|
||||||
let stout = server.stdout.take().unwrap();
|
let stdout = server.stdout.take().unwrap();
|
||||||
|
let stderr = server.stderr.take().unwrap();
|
||||||
let mut server = Self::new_internal(
|
let mut server = Self::new_internal(
|
||||||
server_id.clone(),
|
server_id.clone(),
|
||||||
stdin,
|
stdin,
|
||||||
stout,
|
stdout,
|
||||||
|
Some(stderr),
|
||||||
Some(server),
|
Some(server),
|
||||||
root_path,
|
root_path,
|
||||||
code_action_kinds,
|
code_action_kinds,
|
||||||
|
@ -181,10 +192,11 @@ impl LanguageServer {
|
||||||
Ok(server)
|
Ok(server)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_internal<Stdin, Stdout, F>(
|
fn new_internal<Stdin, Stdout, Stderr, F>(
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
stdin: Stdin,
|
stdin: Stdin,
|
||||||
stdout: Stdout,
|
stdout: Stdout,
|
||||||
|
stderr: Option<Stderr>,
|
||||||
server: Option<Child>,
|
server: Option<Child>,
|
||||||
root_path: &Path,
|
root_path: &Path,
|
||||||
code_action_kinds: Option<Vec<CodeActionKind>>,
|
code_action_kinds: Option<Vec<CodeActionKind>>,
|
||||||
|
@ -194,7 +206,8 @@ impl LanguageServer {
|
||||||
where
|
where
|
||||||
Stdin: AsyncWrite + Unpin + Send + 'static,
|
Stdin: AsyncWrite + Unpin + Send + 'static,
|
||||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||||
F: FnMut(AnyNotification) + 'static + Send,
|
Stderr: AsyncRead + Unpin + Send + 'static,
|
||||||
|
F: FnMut(AnyNotification) + 'static + Send + Clone,
|
||||||
{
|
{
|
||||||
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
|
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
|
||||||
let (output_done_tx, output_done_rx) = barrier::channel();
|
let (output_done_tx, output_done_rx) = barrier::channel();
|
||||||
|
@ -203,17 +216,27 @@ impl LanguageServer {
|
||||||
let response_handlers =
|
let response_handlers =
|
||||||
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
|
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
|
||||||
let io_handlers = Arc::new(Mutex::new(HashMap::default()));
|
let io_handlers = Arc::new(Mutex::new(HashMap::default()));
|
||||||
let input_task = cx.spawn(|cx| {
|
|
||||||
|
let stdout_input_task = cx.spawn(|cx| {
|
||||||
|
{
|
||||||
Self::handle_input(
|
Self::handle_input(
|
||||||
stdout,
|
stdout,
|
||||||
on_unhandled_notification,
|
on_unhandled_notification.clone(),
|
||||||
notification_handlers.clone(),
|
notification_handlers.clone(),
|
||||||
response_handlers.clone(),
|
response_handlers.clone(),
|
||||||
io_handlers.clone(),
|
io_handlers.clone(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
.log_err()
|
.log_err()
|
||||||
});
|
});
|
||||||
|
let stderr_input_task = stderr
|
||||||
|
.map(|stderr| cx.spawn(|_| Self::handle_stderr(stderr, io_handlers.clone()).log_err()))
|
||||||
|
.unwrap_or_else(|| Task::Ready(Some(None)));
|
||||||
|
let input_task = cx.spawn(|_| async move {
|
||||||
|
let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task);
|
||||||
|
stdout.or(stderr)
|
||||||
|
});
|
||||||
let output_task = cx.background().spawn({
|
let output_task = cx.background().spawn({
|
||||||
Self::handle_output(
|
Self::handle_output(
|
||||||
stdin,
|
stdin,
|
||||||
|
@ -282,9 +305,9 @@ impl LanguageServer {
|
||||||
stdout.read_exact(&mut buffer).await?;
|
stdout.read_exact(&mut buffer).await?;
|
||||||
|
|
||||||
if let Ok(message) = str::from_utf8(&buffer) {
|
if let Ok(message) = str::from_utf8(&buffer) {
|
||||||
log::trace!("incoming message:{}", message);
|
log::trace!("incoming message: {}", message);
|
||||||
for handler in io_handlers.lock().values_mut() {
|
for handler in io_handlers.lock().values_mut() {
|
||||||
handler(true, message);
|
handler(IoKind::StdOut, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,6 +350,30 @@ impl LanguageServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_stderr<Stderr>(
|
||||||
|
stderr: Stderr,
|
||||||
|
io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
|
||||||
|
) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
Stderr: AsyncRead + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
|
let mut stderr = BufReader::new(stderr);
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
loop {
|
||||||
|
buffer.clear();
|
||||||
|
stderr.read_until(b'\n', &mut buffer).await?;
|
||||||
|
if let Ok(message) = str::from_utf8(&buffer) {
|
||||||
|
log::trace!("incoming stderr message:{message}");
|
||||||
|
for handler in io_handlers.lock().values_mut() {
|
||||||
|
handler(IoKind::StdErr, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't starve the main thread when receiving lots of messages at once.
|
||||||
|
smol::future::yield_now().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_output<Stdin>(
|
async fn handle_output<Stdin>(
|
||||||
stdin: Stdin,
|
stdin: Stdin,
|
||||||
outbound_rx: channel::Receiver<String>,
|
outbound_rx: channel::Receiver<String>,
|
||||||
|
@ -348,7 +395,7 @@ impl LanguageServer {
|
||||||
while let Ok(message) = outbound_rx.recv().await {
|
while let Ok(message) = outbound_rx.recv().await {
|
||||||
log::trace!("outgoing message:{}", message);
|
log::trace!("outgoing message:{}", message);
|
||||||
for handler in io_handlers.lock().values_mut() {
|
for handler in io_handlers.lock().values_mut() {
|
||||||
handler(false, &message);
|
handler(IoKind::StdIn, &message);
|
||||||
}
|
}
|
||||||
|
|
||||||
content_len_buffer.clear();
|
content_len_buffer.clear();
|
||||||
|
@ -423,6 +470,14 @@ impl LanguageServer {
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
|
completion_list: Some(CompletionListCapability {
|
||||||
|
item_defaults: Some(vec![
|
||||||
|
"commitCharacters".to_owned(),
|
||||||
|
"editRange".to_owned(),
|
||||||
|
"insertTextMode".to_owned(),
|
||||||
|
"data".to_owned(),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
rename: Some(RenameClientCapabilities {
|
rename: Some(RenameClientCapabilities {
|
||||||
|
@ -532,7 +587,7 @@ impl LanguageServer {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn on_io<F>(&self, f: F) -> Subscription
|
pub fn on_io<F>(&self, f: F) -> Subscription
|
||||||
where
|
where
|
||||||
F: 'static + Send + FnMut(bool, &str),
|
F: 'static + Send + FnMut(IoKind, &str),
|
||||||
{
|
{
|
||||||
let id = self.next_id.fetch_add(1, SeqCst);
|
let id = self.next_id.fetch_add(1, SeqCst);
|
||||||
self.io_handlers.lock().insert(id, Box::new(f));
|
self.io_handlers.lock().insert(id, Box::new(f));
|
||||||
|
@ -695,7 +750,7 @@ impl LanguageServer {
|
||||||
outbound_tx: &channel::Sender<String>,
|
outbound_tx: &channel::Sender<String>,
|
||||||
executor: &Arc<executor::Background>,
|
executor: &Arc<executor::Background>,
|
||||||
params: T::Params,
|
params: T::Params,
|
||||||
) -> impl 'static + Future<Output = Result<T::Result>>
|
) -> impl 'static + Future<Output = anyhow::Result<T::Result>>
|
||||||
where
|
where
|
||||||
T::Result: 'static + Send,
|
T::Result: 'static + Send,
|
||||||
{
|
{
|
||||||
|
@ -736,10 +791,25 @@ impl LanguageServer {
|
||||||
.try_send(message)
|
.try_send(message)
|
||||||
.context("failed to write to language server's stdin");
|
.context("failed to write to language server's stdin");
|
||||||
|
|
||||||
|
let mut timeout = executor.timer(LSP_REQUEST_TIMEOUT).fuse();
|
||||||
|
let started = Instant::now();
|
||||||
async move {
|
async move {
|
||||||
handle_response?;
|
handle_response?;
|
||||||
send?;
|
send?;
|
||||||
rx.await?
|
|
||||||
|
let method = T::METHOD;
|
||||||
|
futures::select! {
|
||||||
|
response = rx.fuse() => {
|
||||||
|
let elapsed = started.elapsed();
|
||||||
|
log::trace!("Took {elapsed:?} to recieve response to {method:?} id {id}");
|
||||||
|
response?
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = timeout => {
|
||||||
|
log::error!("Cancelled LSP request task for {method:?} id {id} which took over {LSP_REQUEST_TIMEOUT:?}");
|
||||||
|
anyhow::bail!("LSP request timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -851,6 +921,7 @@ impl LanguageServer {
|
||||||
LanguageServerId(0),
|
LanguageServerId(0),
|
||||||
stdin_writer,
|
stdin_writer,
|
||||||
stdout_reader,
|
stdout_reader,
|
||||||
|
None::<async_pipe::PipeReader>,
|
||||||
None,
|
None,
|
||||||
Path::new("/"),
|
Path::new("/"),
|
||||||
None,
|
None,
|
||||||
|
@ -862,6 +933,7 @@ impl LanguageServer {
|
||||||
LanguageServerId(0),
|
LanguageServerId(0),
|
||||||
stdout_writer,
|
stdout_writer,
|
||||||
stdin_reader,
|
stdin_reader,
|
||||||
|
None::<async_pipe::PipeReader>,
|
||||||
None,
|
None,
|
||||||
Path::new("/"),
|
Path::new("/"),
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -6,7 +6,6 @@ use crate::{
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use client::proto::{self, PeerId};
|
use client::proto::{self, PeerId};
|
||||||
use fs::LineEnding;
|
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use gpui::{AppContext, AsyncAppContext, ModelHandle};
|
use gpui::{AppContext, AsyncAppContext, ModelHandle};
|
||||||
use language::{
|
use language::{
|
||||||
|
@ -17,8 +16,12 @@ use language::{
|
||||||
CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
|
CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
|
||||||
Unclipped,
|
Unclipped,
|
||||||
};
|
};
|
||||||
use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities};
|
use lsp::{
|
||||||
|
CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId,
|
||||||
|
OneOf, ServerCapabilities,
|
||||||
|
};
|
||||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||||
|
use text::LineEnding;
|
||||||
|
|
||||||
pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
|
pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
|
||||||
lsp::FormattingOptions {
|
lsp::FormattingOptions {
|
||||||
|
@ -1340,13 +1343,19 @@ impl LspCommand for GetCompletions {
|
||||||
completions: Option<lsp::CompletionResponse>,
|
completions: Option<lsp::CompletionResponse>,
|
||||||
_: ModelHandle<Project>,
|
_: ModelHandle<Project>,
|
||||||
buffer: ModelHandle<Buffer>,
|
buffer: ModelHandle<Buffer>,
|
||||||
_: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
cx: AsyncAppContext,
|
cx: AsyncAppContext,
|
||||||
) -> Result<Vec<Completion>> {
|
) -> Result<Vec<Completion>> {
|
||||||
|
let mut response_list = None;
|
||||||
let completions = if let Some(completions) = completions {
|
let completions = if let Some(completions) = completions {
|
||||||
match completions {
|
match completions {
|
||||||
lsp::CompletionResponse::Array(completions) => completions,
|
lsp::CompletionResponse::Array(completions) => completions,
|
||||||
lsp::CompletionResponse::List(list) => list.items,
|
|
||||||
|
lsp::CompletionResponse::List(mut list) => {
|
||||||
|
let items = std::mem::take(&mut list.items);
|
||||||
|
response_list = Some(list);
|
||||||
|
items
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Default::default()
|
Default::default()
|
||||||
|
@ -1356,6 +1365,7 @@ impl LspCommand for GetCompletions {
|
||||||
let language = buffer.language().cloned();
|
let language = buffer.language().cloned();
|
||||||
let snapshot = buffer.snapshot();
|
let snapshot = buffer.snapshot();
|
||||||
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
||||||
|
|
||||||
let mut range_for_token = None;
|
let mut range_for_token = None;
|
||||||
completions
|
completions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -1376,6 +1386,7 @@ impl LspCommand for GetCompletions {
|
||||||
edit.new_text.clone(),
|
edit.new_text.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the language server does not provide a range, then infer
|
// If the language server does not provide a range, then infer
|
||||||
// the range based on the syntax tree.
|
// the range based on the syntax tree.
|
||||||
None => {
|
None => {
|
||||||
|
@ -1383,27 +1394,51 @@ impl LspCommand for GetCompletions {
|
||||||
log::info!("completion out of expected range");
|
log::info!("completion out of expected range");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let Range { start, end } = range_for_token
|
|
||||||
|
let default_edit_range = response_list
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|list| list.item_defaults.as_ref())
|
||||||
|
.and_then(|defaults| defaults.edit_range.as_ref())
|
||||||
|
.and_then(|range| match range {
|
||||||
|
CompletionListItemDefaultsEditRange::Range(r) => Some(r),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let range = if let Some(range) = default_edit_range {
|
||||||
|
let range = range_from_lsp(range.clone());
|
||||||
|
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||||
|
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||||
|
if start != range.start.0 || end != range.end.0 {
|
||||||
|
log::info!("completion out of expected range");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.anchor_before(start)..snapshot.anchor_after(end)
|
||||||
|
} else {
|
||||||
|
range_for_token
|
||||||
.get_or_insert_with(|| {
|
.get_or_insert_with(|| {
|
||||||
let offset = self.position.to_offset(&snapshot);
|
let offset = self.position.to_offset(&snapshot);
|
||||||
let (range, kind) = snapshot.surrounding_word(offset);
|
let (range, kind) = snapshot.surrounding_word(offset);
|
||||||
if kind == Some(CharKind::Word) {
|
let range = if kind == Some(CharKind::Word) {
|
||||||
range
|
range
|
||||||
} else {
|
} else {
|
||||||
offset..offset
|
offset..offset
|
||||||
}
|
};
|
||||||
|
|
||||||
|
snapshot.anchor_before(range.start)
|
||||||
|
..snapshot.anchor_after(range.end)
|
||||||
})
|
})
|
||||||
.clone();
|
.clone()
|
||||||
|
};
|
||||||
|
|
||||||
let text = lsp_completion
|
let text = lsp_completion
|
||||||
.insert_text
|
.insert_text
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap_or(&lsp_completion.label)
|
.unwrap_or(&lsp_completion.label)
|
||||||
.clone();
|
.clone();
|
||||||
(
|
(range, text)
|
||||||
snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
|
Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
|
||||||
log::info!("unsupported insert/replace completion");
|
log::info!("unsupported insert/replace completion");
|
||||||
return None;
|
return None;
|
||||||
|
@ -1427,6 +1462,7 @@ impl LspCommand for GetCompletions {
|
||||||
lsp_completion.filter_text.as_deref(),
|
lsp_completion.filter_text.as_deref(),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
server_id,
|
||||||
lsp_completion,
|
lsp_completion,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -156,6 +156,11 @@ struct DelayedDebounced {
|
||||||
cancel_channel: Option<oneshot::Sender<()>>,
|
cancel_channel: Option<oneshot::Sender<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LanguageServerToQuery {
|
||||||
|
Primary,
|
||||||
|
Other(LanguageServerId),
|
||||||
|
}
|
||||||
|
|
||||||
impl DelayedDebounced {
|
impl DelayedDebounced {
|
||||||
fn new() -> DelayedDebounced {
|
fn new() -> DelayedDebounced {
|
||||||
DelayedDebounced {
|
DelayedDebounced {
|
||||||
|
@ -634,7 +639,7 @@ impl Project {
|
||||||
cx.observe_global::<SettingsStore, _>(Self::on_settings_changed)
|
cx.observe_global::<SettingsStore, _>(Self::on_settings_changed)
|
||||||
],
|
],
|
||||||
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
|
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
|
||||||
_maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
|
_maintain_workspace_config: Self::maintain_workspace_config(cx),
|
||||||
active_entry: None,
|
active_entry: None,
|
||||||
languages,
|
languages,
|
||||||
client,
|
client,
|
||||||
|
@ -704,7 +709,7 @@ impl Project {
|
||||||
collaborators: Default::default(),
|
collaborators: Default::default(),
|
||||||
join_project_response_message_id: response.message_id,
|
join_project_response_message_id: response.message_id,
|
||||||
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
|
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
|
||||||
_maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
|
_maintain_workspace_config: Self::maintain_workspace_config(cx),
|
||||||
languages,
|
languages,
|
||||||
user_store: user_store.clone(),
|
user_store: user_store.clone(),
|
||||||
fs,
|
fs,
|
||||||
|
@ -2472,23 +2477,35 @@ impl Project {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maintain_workspace_config(
|
fn maintain_workspace_config(cx: &mut ModelContext<Project>) -> Task<()> {
|
||||||
languages: Arc<LanguageRegistry>,
|
|
||||||
cx: &mut ModelContext<Project>,
|
|
||||||
) -> Task<()> {
|
|
||||||
let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel();
|
let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel();
|
||||||
let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx);
|
let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx);
|
||||||
|
|
||||||
let settings_observation = cx.observe_global::<SettingsStore, _>(move |_, _| {
|
let settings_observation = cx.observe_global::<SettingsStore, _>(move |_, _| {
|
||||||
*settings_changed_tx.borrow_mut() = ();
|
*settings_changed_tx.borrow_mut() = ();
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_weak(|this, mut cx| async move {
|
cx.spawn_weak(|this, mut cx| async move {
|
||||||
while let Some(_) = settings_changed_rx.next().await {
|
while let Some(_) = settings_changed_rx.next().await {
|
||||||
let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
|
let Some(this) = this.upgrade(&cx) else {
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
break;
|
||||||
this.read_with(&cx, |this, _| {
|
};
|
||||||
for server_state in this.language_servers.values() {
|
|
||||||
if let LanguageServerState::Running { server, .. } = server_state {
|
let servers: Vec<_> = this.read_with(&cx, |this, _| {
|
||||||
|
this.language_servers
|
||||||
|
.values()
|
||||||
|
.filter_map(|state| match state {
|
||||||
|
LanguageServerState::Starting(_) => None,
|
||||||
|
LanguageServerState::Running {
|
||||||
|
adapter, server, ..
|
||||||
|
} => Some((adapter.clone(), server.clone())),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
for (adapter, server) in servers {
|
||||||
|
let workspace_config =
|
||||||
|
cx.update(|cx| adapter.workspace_configuration(cx)).await;
|
||||||
server
|
server
|
||||||
.notify::<lsp::notification::DidChangeConfiguration>(
|
.notify::<lsp::notification::DidChangeConfiguration>(
|
||||||
lsp::DidChangeConfigurationParams {
|
lsp::DidChangeConfigurationParams {
|
||||||
|
@ -2498,11 +2515,6 @@ impl Project {
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(settings_observation);
|
drop(settings_observation);
|
||||||
})
|
})
|
||||||
|
@ -2615,7 +2627,6 @@ impl Project {
|
||||||
let state = LanguageServerState::Starting({
|
let state = LanguageServerState::Starting({
|
||||||
let adapter = adapter.clone();
|
let adapter = adapter.clone();
|
||||||
let server_name = adapter.name.0.clone();
|
let server_name = adapter.name.0.clone();
|
||||||
let languages = self.languages.clone();
|
|
||||||
let language = language.clone();
|
let language = language.clone();
|
||||||
let key = key.clone();
|
let key = key.clone();
|
||||||
|
|
||||||
|
@ -2625,7 +2636,6 @@ impl Project {
|
||||||
initialization_options,
|
initialization_options,
|
||||||
pending_server,
|
pending_server,
|
||||||
adapter.clone(),
|
adapter.clone(),
|
||||||
languages,
|
|
||||||
language.clone(),
|
language.clone(),
|
||||||
server_id,
|
server_id,
|
||||||
key,
|
key,
|
||||||
|
@ -2729,7 +2739,6 @@ impl Project {
|
||||||
initialization_options: Option<serde_json::Value>,
|
initialization_options: Option<serde_json::Value>,
|
||||||
pending_server: PendingLanguageServer,
|
pending_server: PendingLanguageServer,
|
||||||
adapter: Arc<CachedLspAdapter>,
|
adapter: Arc<CachedLspAdapter>,
|
||||||
languages: Arc<LanguageRegistry>,
|
|
||||||
language: Arc<Language>,
|
language: Arc<Language>,
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
key: (WorktreeId, LanguageServerName),
|
key: (WorktreeId, LanguageServerName),
|
||||||
|
@ -2740,7 +2749,6 @@ impl Project {
|
||||||
initialization_options,
|
initialization_options,
|
||||||
pending_server,
|
pending_server,
|
||||||
adapter.clone(),
|
adapter.clone(),
|
||||||
languages,
|
|
||||||
server_id,
|
server_id,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -2773,16 +2781,13 @@ impl Project {
|
||||||
initialization_options: Option<serde_json::Value>,
|
initialization_options: Option<serde_json::Value>,
|
||||||
pending_server: PendingLanguageServer,
|
pending_server: PendingLanguageServer,
|
||||||
adapter: Arc<CachedLspAdapter>,
|
adapter: Arc<CachedLspAdapter>,
|
||||||
languages: Arc<LanguageRegistry>,
|
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
cx: &mut AsyncAppContext,
|
cx: &mut AsyncAppContext,
|
||||||
) -> Result<Option<Arc<LanguageServer>>> {
|
) -> Result<Option<Arc<LanguageServer>>> {
|
||||||
let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
|
let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await;
|
||||||
let language_server = match pending_server.task.await? {
|
let language_server = match pending_server.task.await? {
|
||||||
Some(server) => server.initialize(initialization_options).await?,
|
Some(server) => server,
|
||||||
None => {
|
None => return Ok(None),
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
language_server
|
language_server
|
||||||
|
@ -2821,12 +2826,12 @@ impl Project {
|
||||||
|
|
||||||
language_server
|
language_server
|
||||||
.on_request::<lsp::request::WorkspaceConfiguration, _, _>({
|
.on_request::<lsp::request::WorkspaceConfiguration, _, _>({
|
||||||
let languages = languages.clone();
|
let adapter = adapter.clone();
|
||||||
move |params, mut cx| {
|
move |params, mut cx| {
|
||||||
let languages = languages.clone();
|
let adapter = adapter.clone();
|
||||||
async move {
|
async move {
|
||||||
let workspace_config =
|
let workspace_config =
|
||||||
cx.update(|cx| languages.workspace_configuration(cx)).await;
|
cx.update(|cx| adapter.workspace_configuration(cx)).await;
|
||||||
Ok(params
|
Ok(params
|
||||||
.items
|
.items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -2932,6 +2937,8 @@ impl Project {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let language_server = language_server.initialize(initialization_options).await?;
|
||||||
|
|
||||||
language_server
|
language_server
|
||||||
.notify::<lsp::notification::DidChangeConfiguration>(
|
.notify::<lsp::notification::DidChangeConfiguration>(
|
||||||
lsp::DidChangeConfigurationParams {
|
lsp::DidChangeConfigurationParams {
|
||||||
|
@ -3892,7 +3899,7 @@ impl Project {
|
||||||
let file = File::from_dyn(buffer.file())?;
|
let file = File::from_dyn(buffer.file())?;
|
||||||
let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
|
let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
|
||||||
let server = self
|
let server = self
|
||||||
.primary_language_servers_for_buffer(buffer, cx)
|
.primary_language_server_for_buffer(buffer, cx)
|
||||||
.map(|s| s.1.clone());
|
.map(|s| s.1.clone());
|
||||||
Some((buffer_handle, buffer_abs_path, server))
|
Some((buffer_handle, buffer_abs_path, server))
|
||||||
})
|
})
|
||||||
|
@ -4197,7 +4204,12 @@ impl Project {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Vec<LocationLink>>> {
|
) -> Task<Result<Vec<LocationLink>>> {
|
||||||
let position = position.to_point_utf16(buffer.read(cx));
|
let position = position.to_point_utf16(buffer.read(cx));
|
||||||
self.request_lsp(buffer.clone(), GetDefinition { position }, cx)
|
self.request_lsp(
|
||||||
|
buffer.clone(),
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
|
GetDefinition { position },
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn type_definition<T: ToPointUtf16>(
|
pub fn type_definition<T: ToPointUtf16>(
|
||||||
|
@ -4207,7 +4219,12 @@ impl Project {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Vec<LocationLink>>> {
|
) -> Task<Result<Vec<LocationLink>>> {
|
||||||
let position = position.to_point_utf16(buffer.read(cx));
|
let position = position.to_point_utf16(buffer.read(cx));
|
||||||
self.request_lsp(buffer.clone(), GetTypeDefinition { position }, cx)
|
self.request_lsp(
|
||||||
|
buffer.clone(),
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
|
GetTypeDefinition { position },
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn references<T: ToPointUtf16>(
|
pub fn references<T: ToPointUtf16>(
|
||||||
|
@ -4217,7 +4234,12 @@ impl Project {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Vec<Location>>> {
|
) -> Task<Result<Vec<Location>>> {
|
||||||
let position = position.to_point_utf16(buffer.read(cx));
|
let position = position.to_point_utf16(buffer.read(cx));
|
||||||
self.request_lsp(buffer.clone(), GetReferences { position }, cx)
|
self.request_lsp(
|
||||||
|
buffer.clone(),
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
|
GetReferences { position },
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn document_highlights<T: ToPointUtf16>(
|
pub fn document_highlights<T: ToPointUtf16>(
|
||||||
|
@ -4227,7 +4249,12 @@ impl Project {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Vec<DocumentHighlight>>> {
|
) -> Task<Result<Vec<DocumentHighlight>>> {
|
||||||
let position = position.to_point_utf16(buffer.read(cx));
|
let position = position.to_point_utf16(buffer.read(cx));
|
||||||
self.request_lsp(buffer.clone(), GetDocumentHighlights { position }, cx)
|
self.request_lsp(
|
||||||
|
buffer.clone(),
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
|
GetDocumentHighlights { position },
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
|
pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
|
||||||
|
@ -4455,17 +4482,66 @@ impl Project {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Option<Hover>>> {
|
) -> Task<Result<Option<Hover>>> {
|
||||||
let position = position.to_point_utf16(buffer.read(cx));
|
let position = position.to_point_utf16(buffer.read(cx));
|
||||||
self.request_lsp(buffer.clone(), GetHover { position }, cx)
|
self.request_lsp(
|
||||||
|
buffer.clone(),
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
|
GetHover { position },
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn completions<T: ToPointUtf16>(
|
pub fn completions<T: ToOffset + ToPointUtf16>(
|
||||||
&self,
|
&self,
|
||||||
buffer: &ModelHandle<Buffer>,
|
buffer: &ModelHandle<Buffer>,
|
||||||
position: T,
|
position: T,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Vec<Completion>>> {
|
) -> Task<Result<Vec<Completion>>> {
|
||||||
let position = position.to_point_utf16(buffer.read(cx));
|
let position = position.to_point_utf16(buffer.read(cx));
|
||||||
self.request_lsp(buffer.clone(), GetCompletions { position }, cx)
|
if self.is_local() {
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
let offset = position.to_offset(&snapshot);
|
||||||
|
let scope = snapshot.language_scope_at(offset);
|
||||||
|
|
||||||
|
let server_ids: Vec<_> = self
|
||||||
|
.language_servers_for_buffer(buffer.read(cx), cx)
|
||||||
|
.filter(|(_, server)| server.capabilities().completion_provider.is_some())
|
||||||
|
.filter(|(adapter, _)| {
|
||||||
|
scope
|
||||||
|
.as_ref()
|
||||||
|
.map(|scope| scope.language_allowed(&adapter.name))
|
||||||
|
.unwrap_or(true)
|
||||||
|
})
|
||||||
|
.map(|(_, server)| server.server_id())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let buffer = buffer.clone();
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let mut tasks = Vec::with_capacity(server_ids.len());
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
for server_id in server_ids {
|
||||||
|
tasks.push(this.request_lsp(
|
||||||
|
buffer.clone(),
|
||||||
|
LanguageServerToQuery::Other(server_id),
|
||||||
|
GetCompletions { position },
|
||||||
|
cx,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut completions = Vec::new();
|
||||||
|
for task in tasks {
|
||||||
|
if let Ok(new_completions) = task.await {
|
||||||
|
completions.extend_from_slice(&new_completions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(completions)
|
||||||
|
})
|
||||||
|
} else if let Some(project_id) = self.remote_id() {
|
||||||
|
self.send_lsp_proto_request(buffer.clone(), project_id, GetCompletions { position }, cx)
|
||||||
|
} else {
|
||||||
|
Task::ready(Ok(Default::default()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_additional_edits_for_completion(
|
pub fn apply_additional_edits_for_completion(
|
||||||
|
@ -4479,7 +4555,8 @@ impl Project {
|
||||||
let buffer_id = buffer.remote_id();
|
let buffer_id = buffer.remote_id();
|
||||||
|
|
||||||
if self.is_local() {
|
if self.is_local() {
|
||||||
let lang_server = match self.primary_language_servers_for_buffer(buffer, cx) {
|
let server_id = completion.server_id;
|
||||||
|
let lang_server = match self.language_server_for_buffer(buffer, server_id, cx) {
|
||||||
Some((_, server)) => server.clone(),
|
Some((_, server)) => server.clone(),
|
||||||
_ => return Task::ready(Ok(Default::default())),
|
_ => return Task::ready(Ok(Default::default())),
|
||||||
};
|
};
|
||||||
|
@ -4586,7 +4663,12 @@ impl Project {
|
||||||
) -> Task<Result<Vec<CodeAction>>> {
|
) -> Task<Result<Vec<CodeAction>>> {
|
||||||
let buffer = buffer_handle.read(cx);
|
let buffer = buffer_handle.read(cx);
|
||||||
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
|
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
|
||||||
self.request_lsp(buffer_handle.clone(), GetCodeActions { range }, cx)
|
self.request_lsp(
|
||||||
|
buffer_handle.clone(),
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
|
GetCodeActions { range },
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_code_action(
|
pub fn apply_code_action(
|
||||||
|
@ -4942,7 +5024,12 @@ impl Project {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Option<Range<Anchor>>>> {
|
) -> Task<Result<Option<Range<Anchor>>>> {
|
||||||
let position = position.to_point_utf16(buffer.read(cx));
|
let position = position.to_point_utf16(buffer.read(cx));
|
||||||
self.request_lsp(buffer, PrepareRename { position }, cx)
|
self.request_lsp(
|
||||||
|
buffer,
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
|
PrepareRename { position },
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn perform_rename<T: ToPointUtf16>(
|
pub fn perform_rename<T: ToPointUtf16>(
|
||||||
|
@ -4956,6 +5043,7 @@ impl Project {
|
||||||
let position = position.to_point_utf16(buffer.read(cx));
|
let position = position.to_point_utf16(buffer.read(cx));
|
||||||
self.request_lsp(
|
self.request_lsp(
|
||||||
buffer,
|
buffer,
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
PerformRename {
|
PerformRename {
|
||||||
position,
|
position,
|
||||||
new_name,
|
new_name,
|
||||||
|
@ -4983,6 +5071,7 @@ impl Project {
|
||||||
});
|
});
|
||||||
self.request_lsp(
|
self.request_lsp(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
OnTypeFormatting {
|
OnTypeFormatting {
|
||||||
position,
|
position,
|
||||||
trigger,
|
trigger,
|
||||||
|
@ -5008,7 +5097,12 @@ impl Project {
|
||||||
let lsp_request = InlayHints { range };
|
let lsp_request = InlayHints { range };
|
||||||
|
|
||||||
if self.is_local() {
|
if self.is_local() {
|
||||||
let lsp_request_task = self.request_lsp(buffer_handle.clone(), lsp_request, cx);
|
let lsp_request_task = self.request_lsp(
|
||||||
|
buffer_handle.clone(),
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
|
lsp_request,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
buffer_handle
|
buffer_handle
|
||||||
.update(&mut cx, |buffer, _| {
|
.update(&mut cx, |buffer, _| {
|
||||||
|
@ -5441,10 +5535,10 @@ impl Project {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Wire this up to allow selecting a server?
|
|
||||||
fn request_lsp<R: LspCommand>(
|
fn request_lsp<R: LspCommand>(
|
||||||
&self,
|
&self,
|
||||||
buffer_handle: ModelHandle<Buffer>,
|
buffer_handle: ModelHandle<Buffer>,
|
||||||
|
server: LanguageServerToQuery,
|
||||||
request: R,
|
request: R,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<R::Response>>
|
) -> Task<Result<R::Response>>
|
||||||
|
@ -5453,11 +5547,19 @@ impl Project {
|
||||||
{
|
{
|
||||||
let buffer = buffer_handle.read(cx);
|
let buffer = buffer_handle.read(cx);
|
||||||
if self.is_local() {
|
if self.is_local() {
|
||||||
|
let language_server = match server {
|
||||||
|
LanguageServerToQuery::Primary => {
|
||||||
|
match self.primary_language_server_for_buffer(buffer, cx) {
|
||||||
|
Some((_, server)) => Some(Arc::clone(server)),
|
||||||
|
None => return Task::ready(Ok(Default::default())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LanguageServerToQuery::Other(id) => self
|
||||||
|
.language_server_for_buffer(buffer, id, cx)
|
||||||
|
.map(|(_, server)| Arc::clone(server)),
|
||||||
|
};
|
||||||
let file = File::from_dyn(buffer.file()).and_then(File::as_local);
|
let file = File::from_dyn(buffer.file()).and_then(File::as_local);
|
||||||
if let Some((file, language_server)) = file.zip(
|
if let (Some(file), Some(language_server)) = (file, language_server) {
|
||||||
self.primary_language_servers_for_buffer(buffer, cx)
|
|
||||||
.map(|(_, server)| server.clone()),
|
|
||||||
) {
|
|
||||||
let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx);
|
let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx);
|
||||||
return cx.spawn(|this, cx| async move {
|
return cx.spawn(|this, cx| async move {
|
||||||
if !request.check_capabilities(language_server.capabilities()) {
|
if !request.check_capabilities(language_server.capabilities()) {
|
||||||
|
@ -5490,16 +5592,27 @@ impl Project {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if let Some(project_id) = self.remote_id() {
|
} else if let Some(project_id) = self.remote_id() {
|
||||||
|
return self.send_lsp_proto_request(buffer_handle, project_id, request, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::ready(Ok(Default::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_lsp_proto_request<R: LspCommand>(
|
||||||
|
&self,
|
||||||
|
buffer: ModelHandle<Buffer>,
|
||||||
|
project_id: u64,
|
||||||
|
request: R,
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) -> Task<anyhow::Result<<R as LspCommand>::Response>> {
|
||||||
let rpc = self.client.clone();
|
let rpc = self.client.clone();
|
||||||
let message = request.to_proto(project_id, buffer);
|
let message = request.to_proto(project_id, buffer.read(cx));
|
||||||
return cx.spawn_weak(|this, cx| async move {
|
cx.spawn_weak(|this, cx| async move {
|
||||||
// Ensure the project is still alive by the time the task
|
// Ensure the project is still alive by the time the task
|
||||||
// is scheduled.
|
// is scheduled.
|
||||||
this.upgrade(&cx)
|
this.upgrade(&cx)
|
||||||
.ok_or_else(|| anyhow!("project dropped"))?;
|
.ok_or_else(|| anyhow!("project dropped"))?;
|
||||||
|
|
||||||
let response = rpc.request(message).await?;
|
let response = rpc.request(message).await?;
|
||||||
|
|
||||||
let this = this
|
let this = this
|
||||||
.upgrade(&cx)
|
.upgrade(&cx)
|
||||||
.ok_or_else(|| anyhow!("project dropped"))?;
|
.ok_or_else(|| anyhow!("project dropped"))?;
|
||||||
|
@ -5507,12 +5620,10 @@ impl Project {
|
||||||
Err(anyhow!("disconnected before completing request"))
|
Err(anyhow!("disconnected before completing request"))
|
||||||
} else {
|
} else {
|
||||||
request
|
request
|
||||||
.response_from_proto(response, this, buffer_handle, cx)
|
.response_from_proto(response, this, buffer, cx)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
|
||||||
Task::ready(Ok(Default::default()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_candidates_and_open_buffers(
|
fn sort_candidates_and_open_buffers(
|
||||||
|
@ -7150,7 +7261,7 @@ impl Project {
|
||||||
let buffer_version = buffer_handle.read_with(&cx, |buffer, _| buffer.version());
|
let buffer_version = buffer_handle.read_with(&cx, |buffer, _| buffer.version());
|
||||||
let response = this
|
let response = this
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
this.request_lsp(buffer_handle, request, cx)
|
this.request_lsp(buffer_handle, LanguageServerToQuery::Primary, request, cx)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
|
@ -7867,7 +7978,7 @@ impl Project {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn primary_language_servers_for_buffer(
|
fn primary_language_server_for_buffer(
|
||||||
&self,
|
&self,
|
||||||
buffer: &Buffer,
|
buffer: &Buffer,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *};
|
use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *};
|
||||||
use fs::{FakeFs, LineEnding, RealFs};
|
use fs::{FakeFs, RealFs};
|
||||||
use futures::{future, StreamExt};
|
use futures::{future, StreamExt};
|
||||||
use gpui::{executor::Deterministic, test::subscribe, AppContext};
|
use gpui::{executor::Deterministic, test::subscribe, AppContext};
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, LanguageSettingsContent},
|
language_settings::{AllLanguageSettings, LanguageSettingsContent},
|
||||||
tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
|
tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
|
||||||
OffsetRangeExt, Point, ToPoint,
|
LineEnding, OffsetRangeExt, Point, ToPoint,
|
||||||
};
|
};
|
||||||
use lsp::Url;
|
use lsp::Url;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -2272,7 +2272,18 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
|
||||||
},
|
},
|
||||||
Some(tree_sitter_typescript::language_typescript()),
|
Some(tree_sitter_typescript::language_typescript()),
|
||||||
);
|
);
|
||||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
let mut fake_language_servers = language
|
||||||
|
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||||
|
capabilities: lsp::ServerCapabilities {
|
||||||
|
completion_provider: Some(lsp::CompletionOptions {
|
||||||
|
trigger_characters: Some(vec![":".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
|
@ -2358,7 +2369,18 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
|
||||||
},
|
},
|
||||||
Some(tree_sitter_typescript::language_typescript()),
|
Some(tree_sitter_typescript::language_typescript()),
|
||||||
);
|
);
|
||||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
let mut fake_language_servers = language
|
||||||
|
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||||
|
capabilities: lsp::ServerCapabilities {
|
||||||
|
completion_provider: Some(lsp::CompletionOptions {
|
||||||
|
trigger_characters: Some(vec![":".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
|
|
|
@ -225,15 +225,14 @@ impl SearchQuery {
|
||||||
if self.as_str().is_empty() {
|
if self.as_str().is_empty() {
|
||||||
return Default::default();
|
return Default::default();
|
||||||
}
|
}
|
||||||
let language = buffer.language_at(0);
|
|
||||||
|
let range_offset = subrange.as_ref().map(|r| r.start).unwrap_or(0);
|
||||||
let rope = if let Some(range) = subrange {
|
let rope = if let Some(range) = subrange {
|
||||||
buffer.as_rope().slice(range)
|
buffer.as_rope().slice(range)
|
||||||
} else {
|
} else {
|
||||||
buffer.as_rope().clone()
|
buffer.as_rope().clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let kind = |c| char_kind(language, c);
|
|
||||||
|
|
||||||
let mut matches = Vec::new();
|
let mut matches = Vec::new();
|
||||||
match self {
|
match self {
|
||||||
Self::Text {
|
Self::Text {
|
||||||
|
@ -249,6 +248,9 @@ impl SearchQuery {
|
||||||
|
|
||||||
let mat = mat.unwrap();
|
let mat = mat.unwrap();
|
||||||
if *whole_word {
|
if *whole_word {
|
||||||
|
let scope = buffer.language_scope_at(range_offset + mat.start());
|
||||||
|
let kind = |c| char_kind(&scope, c);
|
||||||
|
|
||||||
let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind);
|
let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind);
|
||||||
let start_kind = kind(rope.chars_at(mat.start()).next().unwrap());
|
let start_kind = kind(rope.chars_at(mat.start()).next().unwrap());
|
||||||
let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap());
|
let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap());
|
||||||
|
|
|
@ -8,7 +8,7 @@ use clock::ReplicaId;
|
||||||
use collections::{HashMap, HashSet, VecDeque};
|
use collections::{HashMap, HashSet, VecDeque};
|
||||||
use fs::{
|
use fs::{
|
||||||
repository::{GitFileStatus, GitRepository, RepoPath},
|
repository::{GitFileStatus, GitRepository, RepoPath},
|
||||||
Fs, LineEnding,
|
Fs,
|
||||||
};
|
};
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::{
|
channel::{
|
||||||
|
@ -27,7 +27,7 @@ use language::{
|
||||||
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
|
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
|
||||||
serialize_version,
|
serialize_version,
|
||||||
},
|
},
|
||||||
Buffer, DiagnosticEntry, File as _, PointUtf16, Rope, RopeFingerprint, Unclipped,
|
Buffer, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped,
|
||||||
};
|
};
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
|
|
@ -9,6 +9,7 @@ path = "src/quick_action_bar.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ai = { path = "../ai" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
search = { path = "../search" }
|
search = { path = "../search" }
|
||||||
|
|
|
@ -1,25 +1,29 @@
|
||||||
|
use ai::{assistant::InlineAssist, AssistantPanel};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
|
elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, MouseButton},
|
||||||
Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
|
Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
|
||||||
|
WeakViewHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use search::{buffer_search, BufferSearchBar};
|
use search::{buffer_search, BufferSearchBar};
|
||||||
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
|
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspace};
|
||||||
|
|
||||||
pub struct QuickActionBar {
|
pub struct QuickActionBar {
|
||||||
buffer_search_bar: ViewHandle<BufferSearchBar>,
|
buffer_search_bar: ViewHandle<BufferSearchBar>,
|
||||||
active_item: Option<Box<dyn ItemHandle>>,
|
active_item: Option<Box<dyn ItemHandle>>,
|
||||||
_inlay_hints_enabled_subscription: Option<Subscription>,
|
_inlay_hints_enabled_subscription: Option<Subscription>,
|
||||||
|
workspace: WeakViewHandle<Workspace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QuickActionBar {
|
impl QuickActionBar {
|
||||||
pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>) -> Self {
|
pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>, workspace: &Workspace) -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffer_search_bar,
|
buffer_search_bar,
|
||||||
active_item: None,
|
active_item: None,
|
||||||
_inlay_hints_enabled_subscription: None,
|
_inlay_hints_enabled_subscription: None,
|
||||||
|
workspace: workspace.weak_handle(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +92,21 @@ impl View for QuickActionBar {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bar.add_child(render_quick_action_bar_button(
|
||||||
|
2,
|
||||||
|
"icons/radix/magic-wand.svg",
|
||||||
|
false,
|
||||||
|
("Inline Assist".into(), Some(Box::new(InlineAssist))),
|
||||||
|
cx,
|
||||||
|
move |this, cx| {
|
||||||
|
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
AssistantPanel::inline_assist(workspace, &Default::default(), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
bar.into_any()
|
bar.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -384,6 +384,16 @@ impl<'a> From<&'a str> for Rope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> FromIterator<&'a str> for Rope {
|
||||||
|
fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
|
||||||
|
let mut rope = Rope::new();
|
||||||
|
for chunk in iter {
|
||||||
|
rope.push(chunk);
|
||||||
|
}
|
||||||
|
rope
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<String> for Rope {
|
impl From<String> for Rope {
|
||||||
fn from(text: String) -> Self {
|
fn from(text: String) -> Self {
|
||||||
Rope::from(text.as_str())
|
Rope::from(text.as_str())
|
||||||
|
|
|
@ -657,7 +657,8 @@ message Completion {
|
||||||
Anchor old_start = 1;
|
Anchor old_start = 1;
|
||||||
Anchor old_end = 2;
|
Anchor old_end = 2;
|
||||||
string new_text = 3;
|
string new_text = 3;
|
||||||
bytes lsp_completion = 4;
|
uint64 server_id = 4;
|
||||||
|
bytes lsp_completion = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetCodeActions {
|
message GetCodeActions {
|
||||||
|
@ -860,12 +861,12 @@ message ProjectTransaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
message Transaction {
|
message Transaction {
|
||||||
LocalTimestamp id = 1;
|
LamportTimestamp id = 1;
|
||||||
repeated LocalTimestamp edit_ids = 2;
|
repeated LamportTimestamp edit_ids = 2;
|
||||||
repeated VectorClockEntry start = 3;
|
repeated VectorClockEntry start = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LocalTimestamp {
|
message LamportTimestamp {
|
||||||
uint32 replica_id = 1;
|
uint32 replica_id = 1;
|
||||||
uint32 value = 2;
|
uint32 value = 2;
|
||||||
}
|
}
|
||||||
|
@ -1279,7 +1280,7 @@ message Excerpt {
|
||||||
|
|
||||||
message Anchor {
|
message Anchor {
|
||||||
uint32 replica_id = 1;
|
uint32 replica_id = 1;
|
||||||
uint32 local_timestamp = 2;
|
uint32 timestamp = 2;
|
||||||
uint64 offset = 3;
|
uint64 offset = 3;
|
||||||
Bias bias = 4;
|
Bias bias = 4;
|
||||||
optional uint64 buffer_id = 5;
|
optional uint64 buffer_id = 5;
|
||||||
|
@ -1323,19 +1324,17 @@ message Operation {
|
||||||
|
|
||||||
message Edit {
|
message Edit {
|
||||||
uint32 replica_id = 1;
|
uint32 replica_id = 1;
|
||||||
uint32 local_timestamp = 2;
|
uint32 lamport_timestamp = 2;
|
||||||
uint32 lamport_timestamp = 3;
|
repeated VectorClockEntry version = 3;
|
||||||
repeated VectorClockEntry version = 4;
|
repeated Range ranges = 4;
|
||||||
repeated Range ranges = 5;
|
repeated string new_text = 5;
|
||||||
repeated string new_text = 6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Undo {
|
message Undo {
|
||||||
uint32 replica_id = 1;
|
uint32 replica_id = 1;
|
||||||
uint32 local_timestamp = 2;
|
uint32 lamport_timestamp = 2;
|
||||||
uint32 lamport_timestamp = 3;
|
repeated VectorClockEntry version = 3;
|
||||||
repeated VectorClockEntry version = 4;
|
repeated UndoCount counts = 4;
|
||||||
repeated UndoCount counts = 5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message UpdateSelections {
|
message UpdateSelections {
|
||||||
|
@ -1361,7 +1360,7 @@ message UndoMapEntry {
|
||||||
|
|
||||||
message UndoCount {
|
message UndoCount {
|
||||||
uint32 replica_id = 1;
|
uint32 replica_id = 1;
|
||||||
uint32 local_timestamp = 2;
|
uint32 lamport_timestamp = 2;
|
||||||
uint32 count = 3;
|
uint32 count = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
||||||
pub use peer::*;
|
pub use peer::*;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
pub const PROTOCOL_VERSION: u32 = 61;
|
pub const PROTOCOL_VERSION: u32 = 62;
|
||||||
|
|
|
@ -14,7 +14,6 @@ test-support = ["rand"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
fs = { path = "../fs" }
|
|
||||||
rope = { path = "../rope" }
|
rope = { path = "../rope" }
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
@ -32,6 +31,7 @@ regex.workspace = true
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
util = { path = "../util", features = ["test-support"] }
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
|
|
@ -8,7 +8,7 @@ use sum_tree::Bias;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)]
|
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)]
|
||||||
pub struct Anchor {
|
pub struct Anchor {
|
||||||
pub timestamp: clock::Local,
|
pub timestamp: clock::Lamport,
|
||||||
pub offset: usize,
|
pub offset: usize,
|
||||||
pub bias: Bias,
|
pub bias: Bias,
|
||||||
pub buffer_id: Option<u64>,
|
pub buffer_id: Option<u64>,
|
||||||
|
@ -16,14 +16,14 @@ pub struct Anchor {
|
||||||
|
|
||||||
impl Anchor {
|
impl Anchor {
|
||||||
pub const MIN: Self = Self {
|
pub const MIN: Self = Self {
|
||||||
timestamp: clock::Local::MIN,
|
timestamp: clock::Lamport::MIN,
|
||||||
offset: usize::MIN,
|
offset: usize::MIN,
|
||||||
bias: Bias::Left,
|
bias: Bias::Left,
|
||||||
buffer_id: None,
|
buffer_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const MAX: Self = Self {
|
pub const MAX: Self = Self {
|
||||||
timestamp: clock::Local::MAX,
|
timestamp: clock::Lamport::MAX,
|
||||||
offset: usize::MAX,
|
offset: usize::MAX,
|
||||||
bias: Bias::Right,
|
bias: Bias::Right,
|
||||||
buffer_id: None,
|
buffer_id: None,
|
||||||
|
|
|
@ -14,16 +14,17 @@ pub use anchor::*;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
pub use clock::ReplicaId;
|
pub use clock::ReplicaId;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use fs::LineEnding;
|
|
||||||
use locator::Locator;
|
use locator::Locator;
|
||||||
use operation_queue::OperationQueue;
|
use operation_queue::OperationQueue;
|
||||||
pub use patch::Patch;
|
pub use patch::Patch;
|
||||||
use postage::{oneshot, prelude::*};
|
use postage::{oneshot, prelude::*};
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
pub use rope::*;
|
pub use rope::*;
|
||||||
pub use selection::*;
|
pub use selection::*;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
cmp::{self, Ordering, Reverse},
|
cmp::{self, Ordering, Reverse},
|
||||||
future::Future,
|
future::Future,
|
||||||
iter::Iterator,
|
iter::Iterator,
|
||||||
|
@ -36,22 +37,25 @@ pub use subscription::*;
|
||||||
pub use sum_tree::Bias;
|
pub use sum_tree::Bias;
|
||||||
use sum_tree::{FilterCursor, SumTree, TreeMap};
|
use sum_tree::{FilterCursor, SumTree, TreeMap};
|
||||||
use undo_map::UndoMap;
|
use undo_map::UndoMap;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use util::RandomCharIter;
|
use util::RandomCharIter;
|
||||||
|
|
||||||
pub type TransactionId = clock::Local;
|
lazy_static! {
|
||||||
|
static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type TransactionId = clock::Lamport;
|
||||||
|
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
snapshot: BufferSnapshot,
|
snapshot: BufferSnapshot,
|
||||||
history: History,
|
history: History,
|
||||||
deferred_ops: OperationQueue<Operation>,
|
deferred_ops: OperationQueue<Operation>,
|
||||||
deferred_replicas: HashSet<ReplicaId>,
|
deferred_replicas: HashSet<ReplicaId>,
|
||||||
replica_id: ReplicaId,
|
|
||||||
local_clock: clock::Local,
|
|
||||||
pub lamport_clock: clock::Lamport,
|
pub lamport_clock: clock::Lamport,
|
||||||
subscriptions: Topic,
|
subscriptions: Topic,
|
||||||
edit_id_resolvers: HashMap<clock::Local, Vec<oneshot::Sender<()>>>,
|
edit_id_resolvers: HashMap<clock::Lamport, Vec<oneshot::Sender<()>>>,
|
||||||
wait_for_version_txs: Vec<(clock::Global, oneshot::Sender<()>)>,
|
wait_for_version_txs: Vec<(clock::Global, oneshot::Sender<()>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +83,7 @@ pub struct HistoryEntry {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Transaction {
|
pub struct Transaction {
|
||||||
pub id: TransactionId,
|
pub id: TransactionId,
|
||||||
pub edit_ids: Vec<clock::Local>,
|
pub edit_ids: Vec<clock::Lamport>,
|
||||||
pub start: clock::Global,
|
pub start: clock::Global,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,8 +95,8 @@ impl HistoryEntry {
|
||||||
|
|
||||||
struct History {
|
struct History {
|
||||||
base_text: Rope,
|
base_text: Rope,
|
||||||
operations: TreeMap<clock::Local, Operation>,
|
operations: TreeMap<clock::Lamport, Operation>,
|
||||||
insertion_slices: HashMap<clock::Local, Vec<InsertionSlice>>,
|
insertion_slices: HashMap<clock::Lamport, Vec<InsertionSlice>>,
|
||||||
undo_stack: Vec<HistoryEntry>,
|
undo_stack: Vec<HistoryEntry>,
|
||||||
redo_stack: Vec<HistoryEntry>,
|
redo_stack: Vec<HistoryEntry>,
|
||||||
transaction_depth: usize,
|
transaction_depth: usize,
|
||||||
|
@ -101,7 +105,7 @@ struct History {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct InsertionSlice {
|
struct InsertionSlice {
|
||||||
insertion_id: clock::Local,
|
insertion_id: clock::Lamport,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,18 +127,18 @@ impl History {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(&mut self, op: Operation) {
|
fn push(&mut self, op: Operation) {
|
||||||
self.operations.insert(op.local_timestamp(), op);
|
self.operations.insert(op.timestamp(), op);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_transaction(
|
fn start_transaction(
|
||||||
&mut self,
|
&mut self,
|
||||||
start: clock::Global,
|
start: clock::Global,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
local_clock: &mut clock::Local,
|
clock: &mut clock::Lamport,
|
||||||
) -> Option<TransactionId> {
|
) -> Option<TransactionId> {
|
||||||
self.transaction_depth += 1;
|
self.transaction_depth += 1;
|
||||||
if self.transaction_depth == 1 {
|
if self.transaction_depth == 1 {
|
||||||
let id = local_clock.tick();
|
let id = clock.tick();
|
||||||
self.undo_stack.push(HistoryEntry {
|
self.undo_stack.push(HistoryEntry {
|
||||||
transaction: Transaction {
|
transaction: Transaction {
|
||||||
id,
|
id,
|
||||||
|
@ -245,7 +249,7 @@ impl History {
|
||||||
self.redo_stack.clear();
|
self.redo_stack.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_undo(&mut self, op_id: clock::Local) {
|
fn push_undo(&mut self, op_id: clock::Lamport) {
|
||||||
assert_ne!(self.transaction_depth, 0);
|
assert_ne!(self.transaction_depth, 0);
|
||||||
if let Some(Operation::Edit(_)) = self.operations.get(&op_id) {
|
if let Some(Operation::Edit(_)) = self.operations.get(&op_id) {
|
||||||
let last_transaction = self.undo_stack.last_mut().unwrap();
|
let last_transaction = self.undo_stack.last_mut().unwrap();
|
||||||
|
@ -263,7 +267,19 @@ impl History {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] {
|
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&HistoryEntry> {
|
||||||
|
assert_eq!(self.transaction_depth, 0);
|
||||||
|
|
||||||
|
let entry_ix = self
|
||||||
|
.undo_stack
|
||||||
|
.iter()
|
||||||
|
.rposition(|entry| entry.transaction.id == transaction_id)?;
|
||||||
|
let entry = self.undo_stack.remove(entry_ix);
|
||||||
|
self.redo_stack.push(entry);
|
||||||
|
self.redo_stack.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_from_undo_until(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] {
|
||||||
assert_eq!(self.transaction_depth, 0);
|
assert_eq!(self.transaction_depth, 0);
|
||||||
|
|
||||||
let redo_stack_start_len = self.redo_stack.len();
|
let redo_stack_start_len = self.redo_stack.len();
|
||||||
|
@ -278,20 +294,43 @@ impl History {
|
||||||
&self.redo_stack[redo_stack_start_len..]
|
&self.redo_stack[redo_stack_start_len..]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn forget(&mut self, transaction_id: TransactionId) {
|
fn forget(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
|
||||||
assert_eq!(self.transaction_depth, 0);
|
assert_eq!(self.transaction_depth, 0);
|
||||||
if let Some(entry_ix) = self
|
if let Some(entry_ix) = self
|
||||||
.undo_stack
|
.undo_stack
|
||||||
.iter()
|
.iter()
|
||||||
.rposition(|entry| entry.transaction.id == transaction_id)
|
.rposition(|entry| entry.transaction.id == transaction_id)
|
||||||
{
|
{
|
||||||
self.undo_stack.remove(entry_ix);
|
Some(self.undo_stack.remove(entry_ix).transaction)
|
||||||
} else if let Some(entry_ix) = self
|
} else if let Some(entry_ix) = self
|
||||||
.redo_stack
|
.redo_stack
|
||||||
.iter()
|
.iter()
|
||||||
.rposition(|entry| entry.transaction.id == transaction_id)
|
.rposition(|entry| entry.transaction.id == transaction_id)
|
||||||
{
|
{
|
||||||
self.undo_stack.remove(entry_ix);
|
Some(self.redo_stack.remove(entry_ix).transaction)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> {
|
||||||
|
let entry = self
|
||||||
|
.undo_stack
|
||||||
|
.iter_mut()
|
||||||
|
.rfind(|entry| entry.transaction.id == transaction_id)
|
||||||
|
.or_else(|| {
|
||||||
|
self.redo_stack
|
||||||
|
.iter_mut()
|
||||||
|
.rfind(|entry| entry.transaction.id == transaction_id)
|
||||||
|
})?;
|
||||||
|
Some(&mut entry.transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
|
||||||
|
if let Some(transaction) = self.forget(transaction) {
|
||||||
|
if let Some(destination) = self.transaction_mut(destination) {
|
||||||
|
destination.edit_ids.extend(transaction.edit_ids);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,37 +410,14 @@ impl<D1, D2> Edit<(D1, D2)> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)]
|
|
||||||
pub struct InsertionTimestamp {
|
|
||||||
pub replica_id: ReplicaId,
|
|
||||||
pub local: clock::Seq,
|
|
||||||
pub lamport: clock::Seq,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InsertionTimestamp {
|
|
||||||
pub fn local(&self) -> clock::Local {
|
|
||||||
clock::Local {
|
|
||||||
replica_id: self.replica_id,
|
|
||||||
value: self.local,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lamport(&self) -> clock::Lamport {
|
|
||||||
clock::Lamport {
|
|
||||||
replica_id: self.replica_id,
|
|
||||||
value: self.lamport,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Clone, Debug)]
|
#[derive(Eq, PartialEq, Clone, Debug)]
|
||||||
pub struct Fragment {
|
pub struct Fragment {
|
||||||
pub id: Locator,
|
pub id: Locator,
|
||||||
pub insertion_timestamp: InsertionTimestamp,
|
pub timestamp: clock::Lamport,
|
||||||
pub insertion_offset: usize,
|
pub insertion_offset: usize,
|
||||||
pub len: usize,
|
pub len: usize,
|
||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
pub deletions: HashSet<clock::Local>,
|
pub deletions: HashSet<clock::Lamport>,
|
||||||
pub max_undos: clock::Global,
|
pub max_undos: clock::Global,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,29 +445,26 @@ impl<'a> sum_tree::Dimension<'a, FragmentSummary> for FragmentTextSummary {
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Clone, Debug)]
|
#[derive(Eq, PartialEq, Clone, Debug)]
|
||||||
struct InsertionFragment {
|
struct InsertionFragment {
|
||||||
timestamp: clock::Local,
|
timestamp: clock::Lamport,
|
||||||
split_offset: usize,
|
split_offset: usize,
|
||||||
fragment_id: Locator,
|
fragment_id: Locator,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
struct InsertionFragmentKey {
|
struct InsertionFragmentKey {
|
||||||
timestamp: clock::Local,
|
timestamp: clock::Lamport,
|
||||||
split_offset: usize,
|
split_offset: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
Edit(EditOperation),
|
Edit(EditOperation),
|
||||||
Undo {
|
Undo(UndoOperation),
|
||||||
undo: UndoOperation,
|
|
||||||
lamport_timestamp: clock::Lamport,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct EditOperation {
|
pub struct EditOperation {
|
||||||
pub timestamp: InsertionTimestamp,
|
pub timestamp: clock::Lamport,
|
||||||
pub version: clock::Global,
|
pub version: clock::Global,
|
||||||
pub ranges: Vec<Range<FullOffset>>,
|
pub ranges: Vec<Range<FullOffset>>,
|
||||||
pub new_text: Vec<Arc<str>>,
|
pub new_text: Vec<Arc<str>>,
|
||||||
|
@ -459,9 +472,9 @@ pub struct EditOperation {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct UndoOperation {
|
pub struct UndoOperation {
|
||||||
pub id: clock::Local,
|
pub timestamp: clock::Lamport,
|
||||||
pub counts: HashMap<clock::Local, u32>,
|
|
||||||
pub version: clock::Global,
|
pub version: clock::Global,
|
||||||
|
pub counts: HashMap<clock::Lamport, u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Buffer {
|
impl Buffer {
|
||||||
|
@ -473,24 +486,21 @@ impl Buffer {
|
||||||
let mut fragments = SumTree::new();
|
let mut fragments = SumTree::new();
|
||||||
let mut insertions = SumTree::new();
|
let mut insertions = SumTree::new();
|
||||||
|
|
||||||
let mut local_clock = clock::Local::new(replica_id);
|
|
||||||
let mut lamport_clock = clock::Lamport::new(replica_id);
|
let mut lamport_clock = clock::Lamport::new(replica_id);
|
||||||
let mut version = clock::Global::new();
|
let mut version = clock::Global::new();
|
||||||
|
|
||||||
let visible_text = history.base_text.clone();
|
let visible_text = history.base_text.clone();
|
||||||
if !visible_text.is_empty() {
|
if !visible_text.is_empty() {
|
||||||
let insertion_timestamp = InsertionTimestamp {
|
let insertion_timestamp = clock::Lamport {
|
||||||
replica_id: 0,
|
replica_id: 0,
|
||||||
local: 1,
|
value: 1,
|
||||||
lamport: 1,
|
|
||||||
};
|
};
|
||||||
local_clock.observe(insertion_timestamp.local());
|
lamport_clock.observe(insertion_timestamp);
|
||||||
lamport_clock.observe(insertion_timestamp.lamport());
|
version.observe(insertion_timestamp);
|
||||||
version.observe(insertion_timestamp.local());
|
|
||||||
let fragment_id = Locator::between(&Locator::min(), &Locator::max());
|
let fragment_id = Locator::between(&Locator::min(), &Locator::max());
|
||||||
let fragment = Fragment {
|
let fragment = Fragment {
|
||||||
id: fragment_id,
|
id: fragment_id,
|
||||||
insertion_timestamp,
|
timestamp: insertion_timestamp,
|
||||||
insertion_offset: 0,
|
insertion_offset: 0,
|
||||||
len: visible_text.len(),
|
len: visible_text.len(),
|
||||||
visible: true,
|
visible: true,
|
||||||
|
@ -516,8 +526,6 @@ impl Buffer {
|
||||||
history,
|
history,
|
||||||
deferred_ops: OperationQueue::new(),
|
deferred_ops: OperationQueue::new(),
|
||||||
deferred_replicas: HashSet::default(),
|
deferred_replicas: HashSet::default(),
|
||||||
replica_id,
|
|
||||||
local_clock,
|
|
||||||
lamport_clock,
|
lamport_clock,
|
||||||
subscriptions: Default::default(),
|
subscriptions: Default::default(),
|
||||||
edit_id_resolvers: Default::default(),
|
edit_id_resolvers: Default::default(),
|
||||||
|
@ -534,7 +542,7 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replica_id(&self) -> ReplicaId {
|
pub fn replica_id(&self) -> ReplicaId {
|
||||||
self.local_clock.replica_id
|
self.lamport_clock.replica_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remote_id(&self) -> u64 {
|
pub fn remote_id(&self) -> u64 {
|
||||||
|
@ -561,16 +569,12 @@ impl Buffer {
|
||||||
.map(|(range, new_text)| (range, new_text.into()));
|
.map(|(range, new_text)| (range, new_text.into()));
|
||||||
|
|
||||||
self.start_transaction();
|
self.start_transaction();
|
||||||
let timestamp = InsertionTimestamp {
|
let timestamp = self.lamport_clock.tick();
|
||||||
replica_id: self.replica_id,
|
|
||||||
local: self.local_clock.tick().value,
|
|
||||||
lamport: self.lamport_clock.tick().value,
|
|
||||||
};
|
|
||||||
let operation = Operation::Edit(self.apply_local_edit(edits, timestamp));
|
let operation = Operation::Edit(self.apply_local_edit(edits, timestamp));
|
||||||
|
|
||||||
self.history.push(operation.clone());
|
self.history.push(operation.clone());
|
||||||
self.history.push_undo(operation.local_timestamp());
|
self.history.push_undo(operation.timestamp());
|
||||||
self.snapshot.version.observe(operation.local_timestamp());
|
self.snapshot.version.observe(operation.timestamp());
|
||||||
self.end_transaction();
|
self.end_transaction();
|
||||||
operation
|
operation
|
||||||
}
|
}
|
||||||
|
@ -578,7 +582,7 @@ impl Buffer {
|
||||||
fn apply_local_edit<S: ToOffset, T: Into<Arc<str>>>(
|
fn apply_local_edit<S: ToOffset, T: Into<Arc<str>>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
edits: impl ExactSizeIterator<Item = (Range<S>, T)>,
|
edits: impl ExactSizeIterator<Item = (Range<S>, T)>,
|
||||||
timestamp: InsertionTimestamp,
|
timestamp: clock::Lamport,
|
||||||
) -> EditOperation {
|
) -> EditOperation {
|
||||||
let mut edits_patch = Patch::default();
|
let mut edits_patch = Patch::default();
|
||||||
let mut edit_op = EditOperation {
|
let mut edit_op = EditOperation {
|
||||||
|
@ -655,7 +659,7 @@ impl Buffer {
|
||||||
.item()
|
.item()
|
||||||
.map_or(&Locator::max(), |old_fragment| &old_fragment.id),
|
.map_or(&Locator::max(), |old_fragment| &old_fragment.id),
|
||||||
),
|
),
|
||||||
insertion_timestamp: timestamp,
|
timestamp,
|
||||||
insertion_offset,
|
insertion_offset,
|
||||||
len: new_text.len(),
|
len: new_text.len(),
|
||||||
deletions: Default::default(),
|
deletions: Default::default(),
|
||||||
|
@ -685,7 +689,7 @@ impl Buffer {
|
||||||
intersection.insertion_offset += fragment_start - old_fragments.start().visible;
|
intersection.insertion_offset += fragment_start - old_fragments.start().visible;
|
||||||
intersection.id =
|
intersection.id =
|
||||||
Locator::between(&new_fragments.summary().max_id, &intersection.id);
|
Locator::between(&new_fragments.summary().max_id, &intersection.id);
|
||||||
intersection.deletions.insert(timestamp.local());
|
intersection.deletions.insert(timestamp);
|
||||||
intersection.visible = false;
|
intersection.visible = false;
|
||||||
}
|
}
|
||||||
if intersection.len > 0 {
|
if intersection.len > 0 {
|
||||||
|
@ -740,7 +744,7 @@ impl Buffer {
|
||||||
self.subscriptions.publish_mut(&edits_patch);
|
self.subscriptions.publish_mut(&edits_patch);
|
||||||
self.history
|
self.history
|
||||||
.insertion_slices
|
.insertion_slices
|
||||||
.insert(timestamp.local(), insertion_slices);
|
.insert(timestamp, insertion_slices);
|
||||||
edit_op
|
edit_op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -767,28 +771,23 @@ impl Buffer {
|
||||||
fn apply_op(&mut self, op: Operation) -> Result<()> {
|
fn apply_op(&mut self, op: Operation) -> Result<()> {
|
||||||
match op {
|
match op {
|
||||||
Operation::Edit(edit) => {
|
Operation::Edit(edit) => {
|
||||||
if !self.version.observed(edit.timestamp.local()) {
|
if !self.version.observed(edit.timestamp) {
|
||||||
self.apply_remote_edit(
|
self.apply_remote_edit(
|
||||||
&edit.version,
|
&edit.version,
|
||||||
&edit.ranges,
|
&edit.ranges,
|
||||||
&edit.new_text,
|
&edit.new_text,
|
||||||
edit.timestamp,
|
edit.timestamp,
|
||||||
);
|
);
|
||||||
self.snapshot.version.observe(edit.timestamp.local());
|
self.snapshot.version.observe(edit.timestamp);
|
||||||
self.local_clock.observe(edit.timestamp.local());
|
self.lamport_clock.observe(edit.timestamp);
|
||||||
self.lamport_clock.observe(edit.timestamp.lamport());
|
self.resolve_edit(edit.timestamp);
|
||||||
self.resolve_edit(edit.timestamp.local());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Operation::Undo {
|
Operation::Undo(undo) => {
|
||||||
undo,
|
if !self.version.observed(undo.timestamp) {
|
||||||
lamport_timestamp,
|
|
||||||
} => {
|
|
||||||
if !self.version.observed(undo.id) {
|
|
||||||
self.apply_undo(&undo)?;
|
self.apply_undo(&undo)?;
|
||||||
self.snapshot.version.observe(undo.id);
|
self.snapshot.version.observe(undo.timestamp);
|
||||||
self.local_clock.observe(undo.id);
|
self.lamport_clock.observe(undo.timestamp);
|
||||||
self.lamport_clock.observe(lamport_timestamp);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -808,7 +807,7 @@ impl Buffer {
|
||||||
version: &clock::Global,
|
version: &clock::Global,
|
||||||
ranges: &[Range<FullOffset>],
|
ranges: &[Range<FullOffset>],
|
||||||
new_text: &[Arc<str>],
|
new_text: &[Arc<str>],
|
||||||
timestamp: InsertionTimestamp,
|
timestamp: clock::Lamport,
|
||||||
) {
|
) {
|
||||||
if ranges.is_empty() {
|
if ranges.is_empty() {
|
||||||
return;
|
return;
|
||||||
|
@ -875,9 +874,7 @@ impl Buffer {
|
||||||
// Skip over insertions that are concurrent to this edit, but have a lower lamport
|
// Skip over insertions that are concurrent to this edit, but have a lower lamport
|
||||||
// timestamp.
|
// timestamp.
|
||||||
while let Some(fragment) = old_fragments.item() {
|
while let Some(fragment) = old_fragments.item() {
|
||||||
if fragment_start == range.start
|
if fragment_start == range.start && fragment.timestamp > timestamp {
|
||||||
&& fragment.insertion_timestamp.lamport() > timestamp.lamport()
|
|
||||||
{
|
|
||||||
new_ropes.push_fragment(fragment, fragment.visible);
|
new_ropes.push_fragment(fragment, fragment.visible);
|
||||||
new_fragments.push(fragment.clone(), &None);
|
new_fragments.push(fragment.clone(), &None);
|
||||||
old_fragments.next(&cx);
|
old_fragments.next(&cx);
|
||||||
|
@ -914,7 +911,7 @@ impl Buffer {
|
||||||
.item()
|
.item()
|
||||||
.map_or(&Locator::max(), |old_fragment| &old_fragment.id),
|
.map_or(&Locator::max(), |old_fragment| &old_fragment.id),
|
||||||
),
|
),
|
||||||
insertion_timestamp: timestamp,
|
timestamp,
|
||||||
insertion_offset,
|
insertion_offset,
|
||||||
len: new_text.len(),
|
len: new_text.len(),
|
||||||
deletions: Default::default(),
|
deletions: Default::default(),
|
||||||
|
@ -945,7 +942,7 @@ impl Buffer {
|
||||||
fragment_start - old_fragments.start().0.full_offset();
|
fragment_start - old_fragments.start().0.full_offset();
|
||||||
intersection.id =
|
intersection.id =
|
||||||
Locator::between(&new_fragments.summary().max_id, &intersection.id);
|
Locator::between(&new_fragments.summary().max_id, &intersection.id);
|
||||||
intersection.deletions.insert(timestamp.local());
|
intersection.deletions.insert(timestamp);
|
||||||
intersection.visible = false;
|
intersection.visible = false;
|
||||||
insertion_slices.push(intersection.insertion_slice());
|
insertion_slices.push(intersection.insertion_slice());
|
||||||
}
|
}
|
||||||
|
@ -997,13 +994,13 @@ impl Buffer {
|
||||||
self.snapshot.insertions.edit(new_insertions, &());
|
self.snapshot.insertions.edit(new_insertions, &());
|
||||||
self.history
|
self.history
|
||||||
.insertion_slices
|
.insertion_slices
|
||||||
.insert(timestamp.local(), insertion_slices);
|
.insert(timestamp, insertion_slices);
|
||||||
self.subscriptions.publish_mut(&edits_patch)
|
self.subscriptions.publish_mut(&edits_patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fragment_ids_for_edits<'a>(
|
fn fragment_ids_for_edits<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
edit_ids: impl Iterator<Item = &'a clock::Local>,
|
edit_ids: impl Iterator<Item = &'a clock::Lamport>,
|
||||||
) -> Vec<&'a Locator> {
|
) -> Vec<&'a Locator> {
|
||||||
// Get all of the insertion slices changed by the given edits.
|
// Get all of the insertion slices changed by the given edits.
|
||||||
let mut insertion_slices = Vec::new();
|
let mut insertion_slices = Vec::new();
|
||||||
|
@ -1064,7 +1061,7 @@ impl Buffer {
|
||||||
let fragment_was_visible = fragment.visible;
|
let fragment_was_visible = fragment.visible;
|
||||||
|
|
||||||
fragment.visible = fragment.is_visible(&self.undo_map);
|
fragment.visible = fragment.is_visible(&self.undo_map);
|
||||||
fragment.max_undos.observe(undo.id);
|
fragment.max_undos.observe(undo.timestamp);
|
||||||
|
|
||||||
let old_start = old_fragments.start().1;
|
let old_start = old_fragments.start().1;
|
||||||
let new_start = new_fragments.summary().text.visible;
|
let new_start = new_fragments.summary().text.visible;
|
||||||
|
@ -1118,10 +1115,10 @@ impl Buffer {
|
||||||
if self.deferred_replicas.contains(&op.replica_id()) {
|
if self.deferred_replicas.contains(&op.replica_id()) {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
match op {
|
self.version.observed_all(match op {
|
||||||
Operation::Edit(edit) => self.version.observed_all(&edit.version),
|
Operation::Edit(edit) => &edit.version,
|
||||||
Operation::Undo { undo, .. } => self.version.observed_all(&undo.version),
|
Operation::Undo(undo) => &undo.version,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1139,7 +1136,7 @@ impl Buffer {
|
||||||
|
|
||||||
pub fn start_transaction_at(&mut self, now: Instant) -> Option<TransactionId> {
|
pub fn start_transaction_at(&mut self, now: Instant) -> Option<TransactionId> {
|
||||||
self.history
|
self.history
|
||||||
.start_transaction(self.version.clone(), now, &mut self.local_clock)
|
.start_transaction(self.version.clone(), now, &mut self.lamport_clock)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn end_transaction(&mut self) -> Option<(TransactionId, clock::Global)> {
|
pub fn end_transaction(&mut self) -> Option<(TransactionId, clock::Global)> {
|
||||||
|
@ -1168,7 +1165,7 @@ impl Buffer {
|
||||||
&self.history.base_text
|
&self.history.base_text
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn operations(&self) -> &TreeMap<clock::Local, Operation> {
|
pub fn operations(&self) -> &TreeMap<clock::Lamport, Operation> {
|
||||||
&self.history.operations
|
&self.history.operations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1183,11 +1180,20 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> {
|
||||||
|
let transaction = self
|
||||||
|
.history
|
||||||
|
.remove_from_undo(transaction_id)?
|
||||||
|
.transaction
|
||||||
|
.clone();
|
||||||
|
self.undo_or_redo(transaction).log_err()
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_collect)]
|
#[allow(clippy::needless_collect)]
|
||||||
pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
|
pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
|
||||||
let transactions = self
|
let transactions = self
|
||||||
.history
|
.history
|
||||||
.remove_from_undo(transaction_id)
|
.remove_from_undo_until(transaction_id)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| entry.transaction.clone())
|
.map(|entry| entry.transaction.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -1202,6 +1208,10 @@ impl Buffer {
|
||||||
self.history.forget(transaction_id);
|
self.history.forget(transaction_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
|
||||||
|
self.history.merge_transactions(transaction, destination);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn redo(&mut self) -> Option<(TransactionId, Operation)> {
|
pub fn redo(&mut self) -> Option<(TransactionId, Operation)> {
|
||||||
if let Some(entry) = self.history.pop_redo() {
|
if let Some(entry) = self.history.pop_redo() {
|
||||||
let transaction = entry.transaction.clone();
|
let transaction = entry.transaction.clone();
|
||||||
|
@ -1235,16 +1245,13 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
let undo = UndoOperation {
|
let undo = UndoOperation {
|
||||||
id: self.local_clock.tick(),
|
timestamp: self.lamport_clock.tick(),
|
||||||
version: self.version(),
|
version: self.version(),
|
||||||
counts,
|
counts,
|
||||||
};
|
};
|
||||||
self.apply_undo(&undo)?;
|
self.apply_undo(&undo)?;
|
||||||
let operation = Operation::Undo {
|
self.snapshot.version.observe(undo.timestamp);
|
||||||
undo,
|
let operation = Operation::Undo(undo);
|
||||||
lamport_timestamp: self.lamport_clock.tick(),
|
|
||||||
};
|
|
||||||
self.snapshot.version.observe(operation.local_timestamp());
|
|
||||||
self.history.push(operation.clone());
|
self.history.push(operation.clone());
|
||||||
Ok(operation)
|
Ok(operation)
|
||||||
}
|
}
|
||||||
|
@ -1309,7 +1316,7 @@ impl Buffer {
|
||||||
|
|
||||||
pub fn wait_for_edits(
|
pub fn wait_for_edits(
|
||||||
&mut self,
|
&mut self,
|
||||||
edit_ids: impl IntoIterator<Item = clock::Local>,
|
edit_ids: impl IntoIterator<Item = clock::Lamport>,
|
||||||
) -> impl 'static + Future<Output = Result<()>> {
|
) -> impl 'static + Future<Output = Result<()>> {
|
||||||
let mut futures = Vec::new();
|
let mut futures = Vec::new();
|
||||||
for edit_id in edit_ids {
|
for edit_id in edit_ids {
|
||||||
|
@ -1381,7 +1388,7 @@ impl Buffer {
|
||||||
self.wait_for_version_txs.clear();
|
self.wait_for_version_txs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_edit(&mut self, edit_id: clock::Local) {
|
fn resolve_edit(&mut self, edit_id: clock::Lamport) {
|
||||||
for mut tx in self
|
for mut tx in self
|
||||||
.edit_id_resolvers
|
.edit_id_resolvers
|
||||||
.remove(&edit_id)
|
.remove(&edit_id)
|
||||||
|
@ -1459,7 +1466,7 @@ impl Buffer {
|
||||||
.insertions
|
.insertions
|
||||||
.get(
|
.get(
|
||||||
&InsertionFragmentKey {
|
&InsertionFragmentKey {
|
||||||
timestamp: fragment.insertion_timestamp.local(),
|
timestamp: fragment.timestamp,
|
||||||
split_offset: fragment.insertion_offset,
|
split_offset: fragment.insertion_offset,
|
||||||
},
|
},
|
||||||
&(),
|
&(),
|
||||||
|
@ -1942,7 +1949,7 @@ impl BufferSnapshot {
|
||||||
let fragment = fragment_cursor.item().unwrap();
|
let fragment = fragment_cursor.item().unwrap();
|
||||||
let overshoot = offset - *fragment_cursor.start();
|
let overshoot = offset - *fragment_cursor.start();
|
||||||
Anchor {
|
Anchor {
|
||||||
timestamp: fragment.insertion_timestamp.local(),
|
timestamp: fragment.timestamp,
|
||||||
offset: fragment.insertion_offset + overshoot,
|
offset: fragment.insertion_offset + overshoot,
|
||||||
bias,
|
bias,
|
||||||
buffer_id: Some(self.remote_id),
|
buffer_id: Some(self.remote_id),
|
||||||
|
@ -2134,15 +2141,14 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let timestamp = fragment.insertion_timestamp.local();
|
|
||||||
let start_anchor = Anchor {
|
let start_anchor = Anchor {
|
||||||
timestamp,
|
timestamp: fragment.timestamp,
|
||||||
offset: fragment.insertion_offset,
|
offset: fragment.insertion_offset,
|
||||||
bias: Bias::Right,
|
bias: Bias::Right,
|
||||||
buffer_id: Some(self.buffer_id),
|
buffer_id: Some(self.buffer_id),
|
||||||
};
|
};
|
||||||
let end_anchor = Anchor {
|
let end_anchor = Anchor {
|
||||||
timestamp,
|
timestamp: fragment.timestamp,
|
||||||
offset: fragment.insertion_offset + fragment.len,
|
offset: fragment.insertion_offset + fragment.len,
|
||||||
bias: Bias::Left,
|
bias: Bias::Left,
|
||||||
buffer_id: Some(self.buffer_id),
|
buffer_id: Some(self.buffer_id),
|
||||||
|
@ -2215,19 +2221,17 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo
|
||||||
impl Fragment {
|
impl Fragment {
|
||||||
fn insertion_slice(&self) -> InsertionSlice {
|
fn insertion_slice(&self) -> InsertionSlice {
|
||||||
InsertionSlice {
|
InsertionSlice {
|
||||||
insertion_id: self.insertion_timestamp.local(),
|
insertion_id: self.timestamp,
|
||||||
range: self.insertion_offset..self.insertion_offset + self.len,
|
range: self.insertion_offset..self.insertion_offset + self.len,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_visible(&self, undos: &UndoMap) -> bool {
|
fn is_visible(&self, undos: &UndoMap) -> bool {
|
||||||
!undos.is_undone(self.insertion_timestamp.local())
|
!undos.is_undone(self.timestamp) && self.deletions.iter().all(|d| undos.is_undone(*d))
|
||||||
&& self.deletions.iter().all(|d| undos.is_undone(*d))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn was_visible(&self, version: &clock::Global, undos: &UndoMap) -> bool {
|
fn was_visible(&self, version: &clock::Global, undos: &UndoMap) -> bool {
|
||||||
(version.observed(self.insertion_timestamp.local())
|
(version.observed(self.timestamp) && !undos.was_undone(self.timestamp, version))
|
||||||
&& !undos.was_undone(self.insertion_timestamp.local(), version))
|
|
||||||
&& self
|
&& self
|
||||||
.deletions
|
.deletions
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -2240,14 +2244,14 @@ impl sum_tree::Item for Fragment {
|
||||||
|
|
||||||
fn summary(&self) -> Self::Summary {
|
fn summary(&self) -> Self::Summary {
|
||||||
let mut max_version = clock::Global::new();
|
let mut max_version = clock::Global::new();
|
||||||
max_version.observe(self.insertion_timestamp.local());
|
max_version.observe(self.timestamp);
|
||||||
for deletion in &self.deletions {
|
for deletion in &self.deletions {
|
||||||
max_version.observe(*deletion);
|
max_version.observe(*deletion);
|
||||||
}
|
}
|
||||||
max_version.join(&self.max_undos);
|
max_version.join(&self.max_undos);
|
||||||
|
|
||||||
let mut min_insertion_version = clock::Global::new();
|
let mut min_insertion_version = clock::Global::new();
|
||||||
min_insertion_version.observe(self.insertion_timestamp.local());
|
min_insertion_version.observe(self.timestamp);
|
||||||
let max_insertion_version = min_insertion_version.clone();
|
let max_insertion_version = min_insertion_version.clone();
|
||||||
if self.visible {
|
if self.visible {
|
||||||
FragmentSummary {
|
FragmentSummary {
|
||||||
|
@ -2324,7 +2328,7 @@ impl sum_tree::KeyedItem for InsertionFragment {
|
||||||
impl InsertionFragment {
|
impl InsertionFragment {
|
||||||
fn new(fragment: &Fragment) -> Self {
|
fn new(fragment: &Fragment) -> Self {
|
||||||
Self {
|
Self {
|
||||||
timestamp: fragment.insertion_timestamp.local(),
|
timestamp: fragment.timestamp,
|
||||||
split_offset: fragment.insertion_offset,
|
split_offset: fragment.insertion_offset,
|
||||||
fragment_id: fragment.id.clone(),
|
fragment_id: fragment.id.clone(),
|
||||||
}
|
}
|
||||||
|
@ -2447,10 +2451,10 @@ impl Operation {
|
||||||
operation_queue::Operation::lamport_timestamp(self).replica_id
|
operation_queue::Operation::lamport_timestamp(self).replica_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local_timestamp(&self) -> clock::Local {
|
pub fn timestamp(&self) -> clock::Lamport {
|
||||||
match self {
|
match self {
|
||||||
Operation::Edit(edit) => edit.timestamp.local(),
|
Operation::Edit(edit) => edit.timestamp,
|
||||||
Operation::Undo { undo, .. } => undo.id,
|
Operation::Undo(undo) => undo.timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2469,10 +2473,8 @@ impl Operation {
|
||||||
impl operation_queue::Operation for Operation {
|
impl operation_queue::Operation for Operation {
|
||||||
fn lamport_timestamp(&self) -> clock::Lamport {
|
fn lamport_timestamp(&self) -> clock::Lamport {
|
||||||
match self {
|
match self {
|
||||||
Operation::Edit(edit) => edit.timestamp.lamport(),
|
Operation::Edit(edit) => edit.timestamp,
|
||||||
Operation::Undo {
|
Operation::Undo(undo) => undo.timestamp,
|
||||||
lamport_timestamp, ..
|
|
||||||
} => *lamport_timestamp,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2622,3 +2624,59 @@ impl FromAnchor for usize {
|
||||||
snapshot.summary_for_anchor(anchor)
|
snapshot.summary_for_anchor(anchor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum LineEnding {
|
||||||
|
Unix,
|
||||||
|
Windows,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LineEnding {
|
||||||
|
fn default() -> Self {
|
||||||
|
#[cfg(unix)]
|
||||||
|
return Self::Unix;
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
return Self::CRLF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineEnding {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
LineEnding::Unix => "\n",
|
||||||
|
LineEnding::Windows => "\r\n",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect(text: &str) -> Self {
|
||||||
|
let mut max_ix = cmp::min(text.len(), 1000);
|
||||||
|
while !text.is_char_boundary(max_ix) {
|
||||||
|
max_ix -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ix) = text[..max_ix].find(&['\n']) {
|
||||||
|
if ix > 0 && text.as_bytes()[ix - 1] == b'\r' {
|
||||||
|
Self::Windows
|
||||||
|
} else {
|
||||||
|
Self::Unix
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize(text: &mut String) {
|
||||||
|
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") {
|
||||||
|
*text = replaced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
|
||||||
|
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
|
||||||
|
replaced.into()
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -26,8 +26,8 @@ impl sum_tree::KeyedItem for UndoMapEntry {
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
struct UndoMapKey {
|
struct UndoMapKey {
|
||||||
edit_id: clock::Local,
|
edit_id: clock::Lamport,
|
||||||
undo_id: clock::Local,
|
undo_id: clock::Lamport,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl sum_tree::Summary for UndoMapKey {
|
impl sum_tree::Summary for UndoMapKey {
|
||||||
|
@ -50,7 +50,7 @@ impl UndoMap {
|
||||||
sum_tree::Edit::Insert(UndoMapEntry {
|
sum_tree::Edit::Insert(UndoMapEntry {
|
||||||
key: UndoMapKey {
|
key: UndoMapKey {
|
||||||
edit_id: *edit_id,
|
edit_id: *edit_id,
|
||||||
undo_id: undo.id,
|
undo_id: undo.timestamp,
|
||||||
},
|
},
|
||||||
undo_count: *count,
|
undo_count: *count,
|
||||||
})
|
})
|
||||||
|
@ -59,11 +59,11 @@ impl UndoMap {
|
||||||
self.0.edit(edits, &());
|
self.0.edit(edits, &());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_undone(&self, edit_id: clock::Local) -> bool {
|
pub fn is_undone(&self, edit_id: clock::Lamport) -> bool {
|
||||||
self.undo_count(edit_id) % 2 == 1
|
self.undo_count(edit_id) % 2 == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn was_undone(&self, edit_id: clock::Local, version: &clock::Global) -> bool {
|
pub fn was_undone(&self, edit_id: clock::Lamport, version: &clock::Global) -> bool {
|
||||||
let mut cursor = self.0.cursor::<UndoMapKey>();
|
let mut cursor = self.0.cursor::<UndoMapKey>();
|
||||||
cursor.seek(
|
cursor.seek(
|
||||||
&UndoMapKey {
|
&UndoMapKey {
|
||||||
|
@ -88,7 +88,7 @@ impl UndoMap {
|
||||||
undo_count % 2 == 1
|
undo_count % 2 == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn undo_count(&self, edit_id: clock::Local) -> u32 {
|
pub fn undo_count(&self, edit_id: clock::Lamport) -> u32 {
|
||||||
let mut cursor = self.0.cursor::<UndoMapKey>();
|
let mut cursor = self.0.cursor::<UndoMapKey>();
|
||||||
cursor.seek(
|
cursor.seek(
|
||||||
&UndoMapKey {
|
&UndoMapKey {
|
||||||
|
|
|
@ -88,8 +88,6 @@ pub struct Workspace {
|
||||||
pub dock: Dock,
|
pub dock: Dock,
|
||||||
pub status_bar: StatusBar,
|
pub status_bar: StatusBar,
|
||||||
pub toolbar: Toolbar,
|
pub toolbar: Toolbar,
|
||||||
pub breadcrumb_height: f32,
|
|
||||||
pub breadcrumbs: Interactive<ContainedText>,
|
|
||||||
pub disconnected_overlay: ContainedText,
|
pub disconnected_overlay: ContainedText,
|
||||||
pub modal: ContainerStyle,
|
pub modal: ContainerStyle,
|
||||||
pub zoomed_panel_foreground: ContainerStyle,
|
pub zoomed_panel_foreground: ContainerStyle,
|
||||||
|
@ -120,7 +118,6 @@ pub struct Titlebar {
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub menu: TitlebarMenu,
|
pub menu: TitlebarMenu,
|
||||||
pub project_menu_button: Toggleable<Interactive<ContainedText>>,
|
pub project_menu_button: Toggleable<Interactive<ContainedText>>,
|
||||||
pub project_name_divider: ContainedText,
|
|
||||||
pub git_menu_button: Toggleable<Interactive<ContainedText>>,
|
pub git_menu_button: Toggleable<Interactive<ContainedText>>,
|
||||||
pub item_spacing: f32,
|
pub item_spacing: f32,
|
||||||
pub face_pile_spacing: f32,
|
pub face_pile_spacing: f32,
|
||||||
|
@ -411,6 +408,8 @@ pub struct Toolbar {
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub item_spacing: f32,
|
pub item_spacing: f32,
|
||||||
pub toggleable_tool: Toggleable<Interactive<IconButton>>,
|
pub toggleable_tool: Toggleable<Interactive<IconButton>>,
|
||||||
|
pub breadcrumb_height: f32,
|
||||||
|
pub breadcrumbs: Interactive<ContainedText>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
@ -835,6 +834,9 @@ pub struct AutocompleteStyle {
|
||||||
pub selected_item: ContainerStyle,
|
pub selected_item: ContainerStyle,
|
||||||
pub hovered_item: ContainerStyle,
|
pub hovered_item: ContainerStyle,
|
||||||
pub match_highlight: HighlightStyle,
|
pub match_highlight: HighlightStyle,
|
||||||
|
pub server_name_container: ContainerStyle,
|
||||||
|
pub server_name_color: Color,
|
||||||
|
pub server_name_size_percent: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, Deserialize, JsonSchema)]
|
#[derive(Clone, Copy, Default, Deserialize, JsonSchema)]
|
||||||
|
@ -1150,6 +1152,17 @@ pub struct AssistantStyle {
|
||||||
pub api_key_editor: FieldEditor,
|
pub api_key_editor: FieldEditor,
|
||||||
pub api_key_prompt: ContainedText,
|
pub api_key_prompt: ContainedText,
|
||||||
pub saved_conversation: SavedConversation,
|
pub saved_conversation: SavedConversation,
|
||||||
|
pub inline: InlineAssistantStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct InlineAssistantStyle {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub container: ContainerStyle,
|
||||||
|
pub editor: FieldEditor,
|
||||||
|
pub disabled_editor: FieldEditor,
|
||||||
|
pub pending_edit_background: Color,
|
||||||
|
pub include_conversation: ToggleIconButtonStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
|
|
@ -590,12 +590,12 @@ pub(crate) fn next_word_start(
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
times: usize,
|
times: usize,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
let language = map.buffer_snapshot.language_at(point.to_point(map));
|
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
|
||||||
for _ in 0..times {
|
for _ in 0..times {
|
||||||
let mut crossed_newline = false;
|
let mut crossed_newline = false;
|
||||||
point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||||
let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
|
let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
|
||||||
let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
|
let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
|
||||||
let at_newline = right == '\n';
|
let at_newline = right == '\n';
|
||||||
|
|
||||||
let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
|
let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
|
||||||
|
@ -615,7 +615,7 @@ fn next_word_end(
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
times: usize,
|
times: usize,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
let language = map.buffer_snapshot.language_at(point.to_point(map));
|
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
|
||||||
for _ in 0..times {
|
for _ in 0..times {
|
||||||
if point.column() < map.line_len(point.row()) {
|
if point.column() < map.line_len(point.row()) {
|
||||||
*point.column_mut() += 1;
|
*point.column_mut() += 1;
|
||||||
|
@ -623,10 +623,9 @@ fn next_word_end(
|
||||||
*point.row_mut() += 1;
|
*point.row_mut() += 1;
|
||||||
*point.column_mut() = 0;
|
*point.column_mut() = 0;
|
||||||
}
|
}
|
||||||
// *point.column_mut() += 1;
|
|
||||||
point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||||
let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
|
let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
|
||||||
let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
|
let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
|
||||||
|
|
||||||
left_kind != right_kind && left_kind != CharKind::Whitespace
|
left_kind != right_kind && left_kind != CharKind::Whitespace
|
||||||
});
|
});
|
||||||
|
@ -652,14 +651,14 @@ fn previous_word_start(
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
times: usize,
|
times: usize,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
let language = map.buffer_snapshot.language_at(point.to_point(map));
|
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
|
||||||
for _ in 0..times {
|
for _ in 0..times {
|
||||||
// This works even though find_preceding_boundary is called for every character in the line containing
|
// This works even though find_preceding_boundary is called for every character in the line containing
|
||||||
// cursor because the newline is checked only once.
|
// cursor because the newline is checked only once.
|
||||||
point =
|
point =
|
||||||
movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
|
movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||||
let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
|
let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
|
||||||
let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
|
let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
|
||||||
|
|
||||||
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
|
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
|
||||||
});
|
});
|
||||||
|
@ -673,7 +672,7 @@ fn first_non_whitespace(
|
||||||
from: DisplayPoint,
|
from: DisplayPoint,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
let mut last_point = start_of_line(map, display_lines, from);
|
let mut last_point = start_of_line(map, display_lines, from);
|
||||||
let language = map.buffer_snapshot.language_at(from.to_point(map));
|
let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
|
||||||
for (ch, point) in map.chars_at(last_point) {
|
for (ch, point) in map.chars_at(last_point) {
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
return from;
|
return from;
|
||||||
|
@ -681,7 +680,7 @@ fn first_non_whitespace(
|
||||||
|
|
||||||
last_point = point;
|
last_point = point;
|
||||||
|
|
||||||
if char_kind(language, ch) != CharKind::Whitespace {
|
if char_kind(&scope, ch) != CharKind::Whitespace {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,22 +89,21 @@ fn expand_changed_word_selection(
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if times.is_none() || times.unwrap() == 1 {
|
if times.is_none() || times.unwrap() == 1 {
|
||||||
let language = map
|
let scope = map
|
||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
.language_at(selection.start.to_point(map));
|
.language_scope_at(selection.start.to_point(map));
|
||||||
let in_word = map
|
let in_word = map
|
||||||
.chars_at(selection.head())
|
.chars_at(selection.head())
|
||||||
.next()
|
.next()
|
||||||
.map(|(c, _)| char_kind(language, c) != CharKind::Whitespace)
|
.map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if in_word {
|
if in_word {
|
||||||
selection.end =
|
selection.end =
|
||||||
movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| {
|
movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| {
|
||||||
let left_kind =
|
let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
|
||||||
char_kind(language, left).coerce_punctuation(ignore_punctuation);
|
|
||||||
let right_kind =
|
let right_kind =
|
||||||
char_kind(language, right).coerce_punctuation(ignore_punctuation);
|
char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
|
||||||
|
|
||||||
left_kind != right_kind && left_kind != CharKind::Whitespace
|
left_kind != right_kind && left_kind != CharKind::Whitespace
|
||||||
});
|
});
|
||||||
|
|
|
@ -67,7 +67,8 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
|
||||||
let top_anchor = editor.scroll_manager.anchor().anchor;
|
let top_anchor = editor.scroll_manager.anchor().anchor;
|
||||||
|
|
||||||
editor.change_selections(None, cx, |s| {
|
editor.change_selections(None, cx, |s| {
|
||||||
s.move_heads_with(|map, head, goal| {
|
s.move_with(|map, selection| {
|
||||||
|
let head = selection.head();
|
||||||
let top = top_anchor.to_display_point(map);
|
let top = top_anchor.to_display_point(map);
|
||||||
let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32;
|
let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32;
|
||||||
let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1;
|
let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1;
|
||||||
|
@ -79,7 +80,11 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
|
||||||
} else {
|
} else {
|
||||||
head
|
head
|
||||||
};
|
};
|
||||||
(new_head, goal)
|
if selection.is_empty() {
|
||||||
|
selection.collapse_to(new_head, selection.goal)
|
||||||
|
} else {
|
||||||
|
selection.set_head(new_head, selection.goal)
|
||||||
|
};
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -90,12 +95,35 @@ mod test {
|
||||||
use crate::{state::Mode, test::VimTestContext};
|
use crate::{state::Mode, test::VimTestContext};
|
||||||
use gpui::geometry::vector::vec2f;
|
use gpui::geometry::vector::vec2f;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
use language::Point;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_scroll(cx: &mut gpui::TestAppContext) {
|
async fn test_scroll(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
|
||||||
cx.set_state(indoc! {"ˇa\nb\nc\nd\ne\n"}, Mode::Normal);
|
let window = cx.window;
|
||||||
|
let line_height =
|
||||||
|
cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
|
||||||
|
window.simulate_resize(vec2f(1000., 8.0 * line_height - 1.0), &mut cx);
|
||||||
|
|
||||||
|
cx.set_state(
|
||||||
|
indoc!(
|
||||||
|
"ˇone
|
||||||
|
two
|
||||||
|
three
|
||||||
|
four
|
||||||
|
five
|
||||||
|
six
|
||||||
|
seven
|
||||||
|
eight
|
||||||
|
nine
|
||||||
|
ten
|
||||||
|
eleven
|
||||||
|
twelve
|
||||||
|
"
|
||||||
|
),
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
|
||||||
cx.update_editor(|editor, cx| {
|
cx.update_editor(|editor, cx| {
|
||||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
|
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
|
||||||
|
@ -112,5 +140,33 @@ mod test {
|
||||||
cx.update_editor(|editor, cx| {
|
cx.update_editor(|editor, cx| {
|
||||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.))
|
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// does not select in normal mode
|
||||||
|
cx.simulate_keystrokes(["g", "g"]);
|
||||||
|
cx.update_editor(|editor, cx| {
|
||||||
|
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
|
||||||
|
});
|
||||||
|
cx.simulate_keystrokes(["ctrl-d"]);
|
||||||
|
cx.update_editor(|editor, cx| {
|
||||||
|
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0));
|
||||||
|
assert_eq!(
|
||||||
|
editor.selections.newest(cx).range(),
|
||||||
|
Point::new(5, 0)..Point::new(5, 0)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// does select in visual mode
|
||||||
|
cx.simulate_keystrokes(["g", "g"]);
|
||||||
|
cx.update_editor(|editor, cx| {
|
||||||
|
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
|
||||||
|
});
|
||||||
|
cx.simulate_keystrokes(["v", "ctrl-d"]);
|
||||||
|
cx.update_editor(|editor, cx| {
|
||||||
|
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0));
|
||||||
|
assert_eq!(
|
||||||
|
editor.selections.newest(cx).range(),
|
||||||
|
Point::new(0, 0)..Point::new(5, 1)
|
||||||
|
)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,19 +182,22 @@ fn in_word(
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
) -> Option<Range<DisplayPoint>> {
|
) -> Option<Range<DisplayPoint>> {
|
||||||
// Use motion::right so that we consider the character under the cursor when looking for the start
|
// Use motion::right so that we consider the character under the cursor when looking for the start
|
||||||
let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
|
let scope = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.language_scope_at(relative_to.to_point(map));
|
||||||
let start = movement::find_preceding_boundary(
|
let start = movement::find_preceding_boundary(
|
||||||
map,
|
map,
|
||||||
right(map, relative_to, 1),
|
right(map, relative_to, 1),
|
||||||
movement::FindRange::SingleLine,
|
movement::FindRange::SingleLine,
|
||||||
|left, right| {
|
|left, right| {
|
||||||
char_kind(language, left).coerce_punctuation(ignore_punctuation)
|
char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
|
||||||
!= char_kind(language, right).coerce_punctuation(ignore_punctuation)
|
!= char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
|
let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
|
||||||
char_kind(language, left).coerce_punctuation(ignore_punctuation)
|
char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
|
||||||
!= char_kind(language, right).coerce_punctuation(ignore_punctuation)
|
!= char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(start..end)
|
Some(start..end)
|
||||||
|
@ -217,11 +220,13 @@ fn around_word(
|
||||||
relative_to: DisplayPoint,
|
relative_to: DisplayPoint,
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
) -> Option<Range<DisplayPoint>> {
|
) -> Option<Range<DisplayPoint>> {
|
||||||
let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
|
let scope = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.language_scope_at(relative_to.to_point(map));
|
||||||
let in_word = map
|
let in_word = map
|
||||||
.chars_at(relative_to)
|
.chars_at(relative_to)
|
||||||
.next()
|
.next()
|
||||||
.map(|(c, _)| char_kind(language, c) != CharKind::Whitespace)
|
.map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if in_word {
|
if in_word {
|
||||||
|
@ -245,22 +250,24 @@ fn around_next_word(
|
||||||
relative_to: DisplayPoint,
|
relative_to: DisplayPoint,
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
) -> Option<Range<DisplayPoint>> {
|
) -> Option<Range<DisplayPoint>> {
|
||||||
let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
|
let scope = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.language_scope_at(relative_to.to_point(map));
|
||||||
// Get the start of the word
|
// Get the start of the word
|
||||||
let start = movement::find_preceding_boundary(
|
let start = movement::find_preceding_boundary(
|
||||||
map,
|
map,
|
||||||
right(map, relative_to, 1),
|
right(map, relative_to, 1),
|
||||||
FindRange::SingleLine,
|
FindRange::SingleLine,
|
||||||
|left, right| {
|
|left, right| {
|
||||||
char_kind(language, left).coerce_punctuation(ignore_punctuation)
|
char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
|
||||||
!= char_kind(language, right).coerce_punctuation(ignore_punctuation)
|
!= char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut word_found = false;
|
let mut word_found = false;
|
||||||
let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
|
let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
|
||||||
let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
|
let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
|
||||||
let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
|
let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
|
||||||
|
|
||||||
let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
|
let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.102.0"
|
version = "0.103.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
|
@ -6,6 +6,7 @@ use std::{borrow::Cow, str, sync::Arc};
|
||||||
use util::asset_str;
|
use util::asset_str;
|
||||||
|
|
||||||
mod c;
|
mod c;
|
||||||
|
mod css;
|
||||||
mod elixir;
|
mod elixir;
|
||||||
mod go;
|
mod go;
|
||||||
mod html;
|
mod html;
|
||||||
|
@ -18,6 +19,7 @@ mod python;
|
||||||
mod ruby;
|
mod ruby;
|
||||||
mod rust;
|
mod rust;
|
||||||
mod svelte;
|
mod svelte;
|
||||||
|
mod tailwind;
|
||||||
mod typescript;
|
mod typescript;
|
||||||
mod yaml;
|
mod yaml;
|
||||||
|
|
||||||
|
@ -51,7 +53,14 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
|
||||||
tree_sitter_cpp::language(),
|
tree_sitter_cpp::language(),
|
||||||
vec![Arc::new(c::CLspAdapter)],
|
vec![Arc::new(c::CLspAdapter)],
|
||||||
);
|
);
|
||||||
language("css", tree_sitter_css::language(), vec![]);
|
language(
|
||||||
|
"css",
|
||||||
|
tree_sitter_css::language(),
|
||||||
|
vec![
|
||||||
|
Arc::new(css::CssLspAdapter::new(node_runtime.clone())),
|
||||||
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
|
],
|
||||||
|
);
|
||||||
language(
|
language(
|
||||||
"elixir",
|
"elixir",
|
||||||
tree_sitter_elixir::language(),
|
tree_sitter_elixir::language(),
|
||||||
|
@ -95,6 +104,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
|
||||||
vec![
|
vec![
|
||||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
|
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
|
||||||
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
language(
|
language(
|
||||||
|
@ -111,12 +121,16 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
|
||||||
vec![
|
vec![
|
||||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
|
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
|
||||||
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
language(
|
language(
|
||||||
"html",
|
"html",
|
||||||
tree_sitter_html::language(),
|
tree_sitter_html::language(),
|
||||||
vec![Arc::new(html::HtmlLspAdapter::new(node_runtime.clone()))],
|
vec![
|
||||||
|
Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())),
|
||||||
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
language(
|
language(
|
||||||
"ruby",
|
"ruby",
|
||||||
|
|
|
@ -19,6 +19,10 @@ impl super::LspAdapter for CLspAdapter {
|
||||||
LanguageServerName("clangd".into())
|
LanguageServerName("clangd".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"clangd"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
delegate: &dyn LspAdapterDelegate,
|
delegate: &dyn LspAdapterDelegate,
|
||||||
|
|
130
crates/zed/src/languages/css.rs
Normal file
130
crates/zed/src/languages/css.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||||
|
use lsp::LanguageServerBinary;
|
||||||
|
use node_runtime::NodeRuntime;
|
||||||
|
use serde_json::json;
|
||||||
|
use smol::fs;
|
||||||
|
use std::{
|
||||||
|
any::Any,
|
||||||
|
ffi::OsString,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
const SERVER_PATH: &'static str =
|
||||||
|
"node_modules/vscode-langservers-extracted/bin/vscode-css-language-server";
|
||||||
|
|
||||||
|
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||||
|
vec![server_path.into(), "--stdio".into()]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CssLspAdapter {
|
||||||
|
node: Arc<NodeRuntime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CssLspAdapter {
|
||||||
|
pub fn new(node: Arc<NodeRuntime>) -> Self {
|
||||||
|
CssLspAdapter { node }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl LspAdapter for CssLspAdapter {
|
||||||
|
async fn name(&self) -> LanguageServerName {
|
||||||
|
LanguageServerName("vscode-css-language-server".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"css"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_latest_server_version(
|
||||||
|
&self,
|
||||||
|
_: &dyn LspAdapterDelegate,
|
||||||
|
) -> Result<Box<dyn 'static + Any + Send>> {
|
||||||
|
Ok(Box::new(
|
||||||
|
self.node
|
||||||
|
.npm_package_latest_version("vscode-langservers-extracted")
|
||||||
|
.await?,
|
||||||
|
) as Box<_>)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_server_binary(
|
||||||
|
&self,
|
||||||
|
version: Box<dyn 'static + Send + Any>,
|
||||||
|
container_dir: PathBuf,
|
||||||
|
_: &dyn LspAdapterDelegate,
|
||||||
|
) -> Result<LanguageServerBinary> {
|
||||||
|
let version = version.downcast::<String>().unwrap();
|
||||||
|
let server_path = container_dir.join(SERVER_PATH);
|
||||||
|
|
||||||
|
if fs::metadata(&server_path).await.is_err() {
|
||||||
|
self.node
|
||||||
|
.npm_install_packages(
|
||||||
|
&container_dir,
|
||||||
|
[("vscode-langservers-extracted", version.as_str())],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(LanguageServerBinary {
|
||||||
|
path: self.node.binary_path().await?,
|
||||||
|
arguments: server_binary_arguments(&server_path),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cached_server_binary(
|
||||||
|
&self,
|
||||||
|
container_dir: PathBuf,
|
||||||
|
_: &dyn LspAdapterDelegate,
|
||||||
|
) -> Option<LanguageServerBinary> {
|
||||||
|
get_cached_server_binary(container_dir, &self.node).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn installation_test_binary(
|
||||||
|
&self,
|
||||||
|
container_dir: PathBuf,
|
||||||
|
) -> Option<LanguageServerBinary> {
|
||||||
|
get_cached_server_binary(container_dir, &self.node).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||||
|
Some(json!({
|
||||||
|
"provideFormatter": true
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_cached_server_binary(
|
||||||
|
container_dir: PathBuf,
|
||||||
|
node: &NodeRuntime,
|
||||||
|
) -> Option<LanguageServerBinary> {
|
||||||
|
(|| async move {
|
||||||
|
let mut last_version_dir = None;
|
||||||
|
let mut entries = fs::read_dir(&container_dir).await?;
|
||||||
|
while let Some(entry) = entries.next().await {
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.file_type().await?.is_dir() {
|
||||||
|
last_version_dir = Some(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||||
|
let server_path = last_version_dir.join(SERVER_PATH);
|
||||||
|
if server_path.exists() {
|
||||||
|
Ok(LanguageServerBinary {
|
||||||
|
path: node.binary_path().await?,
|
||||||
|
arguments: server_binary_arguments(&server_path),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"missing executable in directory {:?}",
|
||||||
|
last_version_dir
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.await
|
||||||
|
.log_err()
|
||||||
|
}
|
|
@ -8,3 +8,4 @@ brackets = [
|
||||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
|
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
|
||||||
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
|
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
|
||||||
]
|
]
|
||||||
|
word_characters = ["-"]
|
||||||
|
|
|
@ -27,6 +27,10 @@ impl LspAdapter for ElixirLspAdapter {
|
||||||
LanguageServerName("elixir-ls".into())
|
LanguageServerName("elixir-ls".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"elixir-ls"
|
||||||
|
}
|
||||||
|
|
||||||
fn will_start_server(
|
fn will_start_server(
|
||||||
&self,
|
&self,
|
||||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||||
|
|
|
@ -37,6 +37,10 @@ impl super::LspAdapter for GoLspAdapter {
|
||||||
LanguageServerName("gopls".into())
|
LanguageServerName("gopls".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"gopls"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
delegate: &dyn LspAdapterDelegate,
|
delegate: &dyn LspAdapterDelegate,
|
||||||
|
|
|
@ -37,6 +37,10 @@ impl LspAdapter for HtmlLspAdapter {
|
||||||
LanguageServerName("vscode-html-language-server".into())
|
LanguageServerName("vscode-html-language-server".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"html"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
_: &dyn LspAdapterDelegate,
|
_: &dyn LspAdapterDelegate,
|
||||||
|
|
|
@ -10,3 +10,4 @@ brackets = [
|
||||||
{ start = "<", end = ">", close = true, newline = true, not_in = ["comment", "string"] },
|
{ start = "<", end = ">", close = true, newline = true, not_in = ["comment", "string"] },
|
||||||
{ start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] },
|
{ start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] },
|
||||||
]
|
]
|
||||||
|
word_characters = ["-"]
|
||||||
|
|
|
@ -14,7 +14,12 @@ brackets = [
|
||||||
{ start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
|
{ start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
|
||||||
]
|
]
|
||||||
word_characters = ["$", "#"]
|
word_characters = ["$", "#"]
|
||||||
|
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||||
|
|
||||||
[overrides.element]
|
[overrides.element]
|
||||||
line_comment = { remove = true }
|
line_comment = { remove = true }
|
||||||
block_comment = ["{/* ", " */}"]
|
block_comment = ["{/* ", " */}"]
|
||||||
|
|
||||||
|
[overrides.string]
|
||||||
|
word_characters = ["-"]
|
||||||
|
opt_into_language_servers = ["tailwindcss-language-server"]
|
||||||
|
|
|
@ -43,6 +43,10 @@ impl LspAdapter for JsonLspAdapter {
|
||||||
LanguageServerName("json-language-server".into())
|
LanguageServerName("json-language-server".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"json"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
_: &dyn LspAdapterDelegate,
|
_: &dyn LspAdapterDelegate,
|
||||||
|
@ -102,7 +106,7 @@ impl LspAdapter for JsonLspAdapter {
|
||||||
fn workspace_configuration(
|
fn workspace_configuration(
|
||||||
&self,
|
&self,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Option<BoxFuture<'static, serde_json::Value>> {
|
) -> BoxFuture<'static, serde_json::Value> {
|
||||||
let action_names = cx.all_action_names().collect::<Vec<_>>();
|
let action_names = cx.all_action_names().collect::<Vec<_>>();
|
||||||
let staff_mode = cx.is_staff();
|
let staff_mode = cx.is_staff();
|
||||||
let language_names = &self.languages.language_names();
|
let language_names = &self.languages.language_names();
|
||||||
|
@ -113,7 +117,7 @@ impl LspAdapter for JsonLspAdapter {
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
Some(
|
|
||||||
future::ready(serde_json::json!({
|
future::ready(serde_json::json!({
|
||||||
"json": {
|
"json": {
|
||||||
"format": {
|
"format": {
|
||||||
|
@ -134,8 +138,7 @@ impl LspAdapter for JsonLspAdapter {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.boxed(),
|
.boxed()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn language_ids(&self) -> HashMap<String, String> {
|
async fn language_ids(&self) -> HashMap<String, String> {
|
||||||
|
|
|
@ -70,6 +70,10 @@ impl LspAdapter for PluginLspAdapter {
|
||||||
LanguageServerName(name.into())
|
LanguageServerName(name.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"PluginLspAdapter"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
_: &dyn LspAdapterDelegate,
|
_: &dyn LspAdapterDelegate,
|
||||||
|
|
|
@ -6,7 +6,7 @@ use futures::{io::BufReader, StreamExt};
|
||||||
use language::{LanguageServerName, LspAdapterDelegate};
|
use language::{LanguageServerName, LspAdapterDelegate};
|
||||||
use lsp::LanguageServerBinary;
|
use lsp::LanguageServerBinary;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use std::{any::Any, env::consts, ffi::OsString, path::PathBuf};
|
use std::{any::Any, env::consts, path::PathBuf};
|
||||||
use util::{
|
use util::{
|
||||||
async_iife,
|
async_iife,
|
||||||
github::{latest_github_release, GitHubLspBinaryVersion},
|
github::{latest_github_release, GitHubLspBinaryVersion},
|
||||||
|
@ -16,19 +16,16 @@ use util::{
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct LuaLspAdapter;
|
pub struct LuaLspAdapter;
|
||||||
|
|
||||||
fn server_binary_arguments() -> Vec<OsString> {
|
|
||||||
vec![
|
|
||||||
"--logpath=~/lua-language-server.log".into(),
|
|
||||||
"--loglevel=trace".into(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl super::LspAdapter for LuaLspAdapter {
|
impl super::LspAdapter for LuaLspAdapter {
|
||||||
async fn name(&self) -> LanguageServerName {
|
async fn name(&self) -> LanguageServerName {
|
||||||
LanguageServerName("lua-language-server".into())
|
LanguageServerName("lua-language-server".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"lua"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
delegate: &dyn LspAdapterDelegate,
|
delegate: &dyn LspAdapterDelegate,
|
||||||
|
@ -83,7 +80,7 @@ impl super::LspAdapter for LuaLspAdapter {
|
||||||
.await?;
|
.await?;
|
||||||
Ok(LanguageServerBinary {
|
Ok(LanguageServerBinary {
|
||||||
path: binary_path,
|
path: binary_path,
|
||||||
arguments: server_binary_arguments(),
|
arguments: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +124,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
|
||||||
if let Some(path) = last_binary_path {
|
if let Some(path) = last_binary_path {
|
||||||
Ok(LanguageServerBinary {
|
Ok(LanguageServerBinary {
|
||||||
path,
|
path,
|
||||||
arguments: server_binary_arguments(),
|
arguments: Vec::new(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("no cached binary"))
|
Err(anyhow!("no cached binary"))
|
||||||
|
|
|
@ -41,6 +41,10 @@ impl LspAdapter for IntelephenseLspAdapter {
|
||||||
LanguageServerName("intelephense".into())
|
LanguageServerName("intelephense".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"php"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
_delegate: &dyn LspAdapterDelegate,
|
_delegate: &dyn LspAdapterDelegate,
|
||||||
|
|
|
@ -35,6 +35,10 @@ impl LspAdapter for PythonLspAdapter {
|
||||||
LanguageServerName("pyright".into())
|
LanguageServerName("pyright".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"pyright"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
_: &dyn LspAdapterDelegate,
|
_: &dyn LspAdapterDelegate,
|
||||||
|
|
|
@ -12,6 +12,10 @@ impl LspAdapter for RubyLanguageServer {
|
||||||
LanguageServerName("solargraph".into())
|
LanguageServerName("solargraph".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"solargraph"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
_: &dyn LspAdapterDelegate,
|
_: &dyn LspAdapterDelegate,
|
||||||
|
|
|
@ -22,6 +22,10 @@ impl LspAdapter for RustLspAdapter {
|
||||||
LanguageServerName("rust-analyzer".into())
|
LanguageServerName("rust-analyzer".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"rust"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
delegate: &dyn LspAdapterDelegate,
|
delegate: &dyn LspAdapterDelegate,
|
||||||
|
|
|
@ -36,6 +36,10 @@ impl LspAdapter for SvelteLspAdapter {
|
||||||
LanguageServerName("svelte-language-server".into())
|
LanguageServerName("svelte-language-server".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"svelte"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
_: &dyn LspAdapterDelegate,
|
_: &dyn LspAdapterDelegate,
|
||||||
|
|
161
crates/zed/src/languages/tailwind.rs
Normal file
161
crates/zed/src/languages/tailwind.rs
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use collections::HashMap;
|
||||||
|
use futures::{
|
||||||
|
future::{self, BoxFuture},
|
||||||
|
FutureExt, StreamExt,
|
||||||
|
};
|
||||||
|
use gpui::AppContext;
|
||||||
|
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||||
|
use lsp::LanguageServerBinary;
|
||||||
|
use node_runtime::NodeRuntime;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use smol::fs;
|
||||||
|
use std::{
|
||||||
|
any::Any,
|
||||||
|
ffi::OsString,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
const SERVER_PATH: &'static str = "node_modules/.bin/tailwindcss-language-server";
|
||||||
|
|
||||||
|
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||||
|
vec![server_path.into(), "--stdio".into()]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TailwindLspAdapter {
|
||||||
|
node: Arc<NodeRuntime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TailwindLspAdapter {
|
||||||
|
pub fn new(node: Arc<NodeRuntime>) -> Self {
|
||||||
|
TailwindLspAdapter { node }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl LspAdapter for TailwindLspAdapter {
|
||||||
|
async fn name(&self) -> LanguageServerName {
|
||||||
|
LanguageServerName("tailwindcss-language-server".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"tailwind"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_latest_server_version(
|
||||||
|
&self,
|
||||||
|
_: &dyn LspAdapterDelegate,
|
||||||
|
) -> Result<Box<dyn 'static + Any + Send>> {
|
||||||
|
Ok(Box::new(
|
||||||
|
self.node
|
||||||
|
.npm_package_latest_version("@tailwindcss/language-server")
|
||||||
|
.await?,
|
||||||
|
) as Box<_>)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_server_binary(
|
||||||
|
&self,
|
||||||
|
version: Box<dyn 'static + Send + Any>,
|
||||||
|
container_dir: PathBuf,
|
||||||
|
_: &dyn LspAdapterDelegate,
|
||||||
|
) -> Result<LanguageServerBinary> {
|
||||||
|
let version = version.downcast::<String>().unwrap();
|
||||||
|
let server_path = container_dir.join(SERVER_PATH);
|
||||||
|
|
||||||
|
if fs::metadata(&server_path).await.is_err() {
|
||||||
|
self.node
|
||||||
|
.npm_install_packages(
|
||||||
|
&container_dir,
|
||||||
|
[("@tailwindcss/language-server", version.as_str())],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(LanguageServerBinary {
|
||||||
|
path: self.node.binary_path().await?,
|
||||||
|
arguments: server_binary_arguments(&server_path),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cached_server_binary(
|
||||||
|
&self,
|
||||||
|
container_dir: PathBuf,
|
||||||
|
_: &dyn LspAdapterDelegate,
|
||||||
|
) -> Option<LanguageServerBinary> {
|
||||||
|
get_cached_server_binary(container_dir, &self.node).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn installation_test_binary(
|
||||||
|
&self,
|
||||||
|
container_dir: PathBuf,
|
||||||
|
) -> Option<LanguageServerBinary> {
|
||||||
|
get_cached_server_binary(container_dir, &self.node).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||||
|
Some(json!({
|
||||||
|
"provideFormatter": true,
|
||||||
|
"userLanguages": {
|
||||||
|
"html": "html",
|
||||||
|
"css": "css",
|
||||||
|
"javascript": "javascript",
|
||||||
|
"typescriptreact": "typescriptreact",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
|
||||||
|
future::ready(json!({
|
||||||
|
"tailwindCSS": {
|
||||||
|
"emmetCompletions": true,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn language_ids(&self) -> HashMap<String, String> {
|
||||||
|
HashMap::from_iter(
|
||||||
|
[
|
||||||
|
("HTML".to_string(), "html".to_string()),
|
||||||
|
("CSS".to_string(), "css".to_string()),
|
||||||
|
("JavaScript".to_string(), "javascript".to_string()),
|
||||||
|
("TSX".to_string(), "typescriptreact".to_string()),
|
||||||
|
]
|
||||||
|
.into_iter(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_cached_server_binary(
|
||||||
|
container_dir: PathBuf,
|
||||||
|
node: &NodeRuntime,
|
||||||
|
) -> Option<LanguageServerBinary> {
|
||||||
|
(|| async move {
|
||||||
|
let mut last_version_dir = None;
|
||||||
|
let mut entries = fs::read_dir(&container_dir).await?;
|
||||||
|
while let Some(entry) = entries.next().await {
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.file_type().await?.is_dir() {
|
||||||
|
last_version_dir = Some(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||||
|
let server_path = last_version_dir.join(SERVER_PATH);
|
||||||
|
if server_path.exists() {
|
||||||
|
Ok(LanguageServerBinary {
|
||||||
|
path: node.binary_path().await?,
|
||||||
|
arguments: server_binary_arguments(&server_path),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"missing executable in directory {:?}",
|
||||||
|
last_version_dir
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.await
|
||||||
|
.log_err()
|
||||||
|
}
|
|
@ -13,7 +13,12 @@ brackets = [
|
||||||
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
|
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
|
||||||
]
|
]
|
||||||
word_characters = ["#", "$"]
|
word_characters = ["#", "$"]
|
||||||
|
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||||
|
|
||||||
[overrides.element]
|
[overrides.element]
|
||||||
line_comment = { remove = true }
|
line_comment = { remove = true }
|
||||||
block_comment = ["{/* ", " */}"]
|
block_comment = ["{/* ", " */}"]
|
||||||
|
|
||||||
|
[overrides.string]
|
||||||
|
word_characters = ["-"]
|
||||||
|
opt_into_language_servers = ["tailwindcss-language-server"]
|
||||||
|
|
|
@ -56,6 +56,10 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||||
LanguageServerName("typescript-language-server".into())
|
LanguageServerName("typescript-language-server".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"tsserver"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
_: &dyn LspAdapterDelegate,
|
_: &dyn LspAdapterDelegate,
|
||||||
|
@ -202,8 +206,7 @@ impl EsLintLspAdapter {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl LspAdapter for EsLintLspAdapter {
|
impl LspAdapter for EsLintLspAdapter {
|
||||||
fn workspace_configuration(&self, _: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
|
fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
|
||||||
Some(
|
|
||||||
future::ready(json!({
|
future::ready(json!({
|
||||||
"": {
|
"": {
|
||||||
"validate": "on",
|
"validate": "on",
|
||||||
|
@ -212,14 +215,17 @@ impl LspAdapter for EsLintLspAdapter {
|
||||||
"nodePath": null,
|
"nodePath": null,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.boxed(),
|
.boxed()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn name(&self) -> LanguageServerName {
|
async fn name(&self) -> LanguageServerName {
|
||||||
LanguageServerName("eslint".into())
|
LanguageServerName("eslint".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"eslint"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
delegate: &dyn LspAdapterDelegate,
|
delegate: &dyn LspAdapterDelegate,
|
||||||
|
|
|
@ -40,6 +40,10 @@ impl LspAdapter for YamlLspAdapter {
|
||||||
LanguageServerName("yaml-language-server".into())
|
LanguageServerName("yaml-language-server".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn short_name(&self) -> &'static str {
|
||||||
|
"yaml"
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_latest_server_version(
|
async fn fetch_latest_server_version(
|
||||||
&self,
|
&self,
|
||||||
_: &dyn LspAdapterDelegate,
|
_: &dyn LspAdapterDelegate,
|
||||||
|
@ -86,11 +90,11 @@ impl LspAdapter for YamlLspAdapter {
|
||||||
) -> Option<LanguageServerBinary> {
|
) -> Option<LanguageServerBinary> {
|
||||||
get_cached_server_binary(container_dir, &self.node).await
|
get_cached_server_binary(container_dir, &self.node).await
|
||||||
}
|
}
|
||||||
fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
|
fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
|
||||||
let tab_size = all_language_settings(None, cx)
|
let tab_size = all_language_settings(None, cx)
|
||||||
.language(Some("YAML"))
|
.language(Some("YAML"))
|
||||||
.tab_size;
|
.tab_size;
|
||||||
Some(
|
|
||||||
future::ready(serde_json::json!({
|
future::ready(serde_json::json!({
|
||||||
"yaml": {
|
"yaml": {
|
||||||
"keyOrdering": false
|
"keyOrdering": false
|
||||||
|
@ -99,8 +103,7 @@ impl LspAdapter for YamlLspAdapter {
|
||||||
"editor.tabSize": tab_size,
|
"editor.tabSize": tab_size,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.boxed(),
|
.boxed()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -264,8 +264,9 @@ pub fn initialize_workspace(
|
||||||
toolbar.add_item(breadcrumbs, cx);
|
toolbar.add_item(breadcrumbs, cx);
|
||||||
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
|
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
|
||||||
toolbar.add_item(buffer_search_bar.clone(), cx);
|
toolbar.add_item(buffer_search_bar.clone(), cx);
|
||||||
let quick_action_bar =
|
let quick_action_bar = cx.add_view(|_| {
|
||||||
cx.add_view(|_| QuickActionBar::new(buffer_search_bar));
|
QuickActionBar::new(buffer_search_bar, workspace)
|
||||||
|
});
|
||||||
toolbar.add_item(quick_action_bar, cx);
|
toolbar.add_item(quick_action_bar, cx);
|
||||||
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||||
toolbar.add_item(project_search_bar, cx);
|
toolbar.add_item(project_search_bar, cx);
|
||||||
|
|
|
@ -21,9 +21,7 @@ function clear_themes(theme_directory: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const all_themes: Theme[] = themes.map((theme) =>
|
const all_themes: Theme[] = themes.map((theme) => create_theme(theme))
|
||||||
create_theme(theme)
|
|
||||||
)
|
|
||||||
|
|
||||||
function write_themes(themes: Theme[], output_directory: string) {
|
function write_themes(themes: Theme[], output_directory: string) {
|
||||||
clear_themes(output_directory)
|
clear_themes(output_directory)
|
||||||
|
@ -34,10 +32,7 @@ function write_themes(themes: Theme[], output_directory: string) {
|
||||||
const style_tree = app()
|
const style_tree = app()
|
||||||
const style_tree_json = JSON.stringify(style_tree, null, 2)
|
const style_tree_json = JSON.stringify(style_tree, null, 2)
|
||||||
const temp_path = path.join(temp_directory, `${theme.name}.json`)
|
const temp_path = path.join(temp_directory, `${theme.name}.json`)
|
||||||
const out_path = path.join(
|
const out_path = path.join(output_directory, `${theme.name}.json`)
|
||||||
output_directory,
|
|
||||||
`${theme.name}.json`
|
|
||||||
)
|
|
||||||
fs.writeFileSync(temp_path, style_tree_json)
|
fs.writeFileSync(temp_path, style_tree_json)
|
||||||
fs.renameSync(temp_path, out_path)
|
fs.renameSync(temp_path, out_path)
|
||||||
console.log(`- ${out_path} created`)
|
console.log(`- ${out_path} created`)
|
||||||
|
|
|
@ -83,8 +83,6 @@ function write_tokens(themes: Theme[], tokens_directory: string) {
|
||||||
console.log(`- ${METADATA_FILE} created`)
|
console.log(`- ${METADATA_FILE} created`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const all_themes: Theme[] = themes.map((theme) =>
|
const all_themes: Theme[] = themes.map((theme) => create_theme(theme))
|
||||||
create_theme(theme)
|
|
||||||
)
|
|
||||||
|
|
||||||
write_tokens(all_themes, TOKENS_DIRECTORY)
|
write_tokens(all_themes, TOKENS_DIRECTORY)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { TextStyle, background } from "../style_tree/components"
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
export namespace Button {
|
export namespace Button {
|
||||||
export type Options = {
|
export type Options = {
|
||||||
layer: Layer,
|
layer: Layer
|
||||||
background: keyof Theme["lowest"]
|
background: keyof Theme["lowest"]
|
||||||
color: keyof Theme["lowest"]
|
color: keyof Theme["lowest"]
|
||||||
variant: Button.Variant
|
variant: Button.Variant
|
||||||
|
@ -16,13 +16,13 @@ export namespace Button {
|
||||||
bottom?: number
|
bottom?: number
|
||||||
left?: number
|
left?: number
|
||||||
right?: number
|
right?: number
|
||||||
},
|
}
|
||||||
states: {
|
states: {
|
||||||
enabled?: boolean,
|
enabled?: boolean
|
||||||
hovered?: boolean,
|
hovered?: boolean
|
||||||
pressed?: boolean,
|
pressed?: boolean
|
||||||
focused?: boolean,
|
focused?: boolean
|
||||||
disabled?: boolean,
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,26 +38,26 @@ export namespace Button {
|
||||||
export const CORNER_RADIUS = 6
|
export const CORNER_RADIUS = 6
|
||||||
|
|
||||||
export const variant = {
|
export const variant = {
|
||||||
Default: 'filled',
|
Default: "filled",
|
||||||
Outline: 'outline',
|
Outline: "outline",
|
||||||
Ghost: 'ghost'
|
Ghost: "ghost",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type Variant = typeof variant[keyof typeof variant]
|
export type Variant = (typeof variant)[keyof typeof variant]
|
||||||
|
|
||||||
export const shape = {
|
export const shape = {
|
||||||
Rectangle: 'rectangle',
|
Rectangle: "rectangle",
|
||||||
Square: 'square'
|
Square: "square",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type Shape = typeof shape[keyof typeof shape]
|
export type Shape = (typeof shape)[keyof typeof shape]
|
||||||
|
|
||||||
export const size = {
|
export const size = {
|
||||||
Small: "sm",
|
Small: "sm",
|
||||||
Medium: "md"
|
Medium: "md",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type Size = typeof size[keyof typeof size]
|
export type Size = (typeof size)[keyof typeof size]
|
||||||
|
|
||||||
export type BaseStyle = {
|
export type BaseStyle = {
|
||||||
corder_radius: number
|
corder_radius: number
|
||||||
|
@ -67,8 +67,8 @@ export namespace Button {
|
||||||
bottom: number
|
bottom: number
|
||||||
left: number
|
left: number
|
||||||
right: number
|
right: number
|
||||||
},
|
}
|
||||||
margin: Button.Options['margin']
|
margin: Button.Options["margin"]
|
||||||
button_height: number
|
button_height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,15 +81,18 @@ export namespace Button {
|
||||||
shape: Button.shape.Rectangle,
|
shape: Button.shape.Rectangle,
|
||||||
states: {
|
states: {
|
||||||
hovered: true,
|
hovered: true,
|
||||||
pressed: true
|
pressed: true,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
): BaseStyle => {
|
): BaseStyle => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const layer = options.layer ?? theme.middle
|
const layer = options.layer ?? theme.middle
|
||||||
const color = options.color ?? "base"
|
const color = options.color ?? "base"
|
||||||
const background_color = options.variant === Button.variant.Ghost ? null : background(layer, options.background ?? color)
|
const background_color =
|
||||||
|
options.variant === Button.variant.Ghost
|
||||||
|
? null
|
||||||
|
: background(layer, options.background ?? color)
|
||||||
|
|
||||||
const m = {
|
const m = {
|
||||||
top: options.margin?.top ?? 0,
|
top: options.margin?.top ?? 0,
|
||||||
|
@ -106,8 +109,14 @@ export namespace Button {
|
||||||
padding: {
|
padding: {
|
||||||
top: padding,
|
top: padding,
|
||||||
bottom: padding,
|
bottom: padding,
|
||||||
left: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding,
|
left:
|
||||||
right: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding
|
options.shape === Button.shape.Rectangle
|
||||||
|
? padding + Button.RECTANGLE_PADDING
|
||||||
|
: padding,
|
||||||
|
right:
|
||||||
|
options.shape === Button.shape.Rectangle
|
||||||
|
? padding + Button.RECTANGLE_PADDING
|
||||||
|
: padding,
|
||||||
},
|
},
|
||||||
margin: m,
|
margin: m,
|
||||||
button_height: 16,
|
button_height: 16,
|
||||||
|
|
|
@ -11,11 +11,9 @@ export type Margin = {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IconButtonOptions {
|
interface IconButtonOptions {
|
||||||
layer?:
|
layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"]
|
||||||
| Theme["lowest"]
|
|
||||||
| Theme["middle"]
|
|
||||||
| Theme["highest"]
|
|
||||||
color?: keyof Theme["lowest"]
|
color?: keyof Theme["lowest"]
|
||||||
|
background_color?: keyof Theme["lowest"]
|
||||||
margin?: Partial<Margin>
|
margin?: Partial<Margin>
|
||||||
variant?: Button.Variant
|
variant?: Button.Variant
|
||||||
size?: Button.Size
|
size?: Button.Size
|
||||||
|
@ -23,18 +21,25 @@ interface IconButtonOptions {
|
||||||
|
|
||||||
type ToggleableIconButtonOptions = IconButtonOptions & {
|
type ToggleableIconButtonOptions = IconButtonOptions & {
|
||||||
active_color?: keyof Theme["lowest"]
|
active_color?: keyof Theme["lowest"]
|
||||||
|
active_background_color?: keyof Theme["lowest"]
|
||||||
active_layer?: Layer
|
active_layer?: Layer
|
||||||
|
active_variant?: Button.Variant
|
||||||
}
|
}
|
||||||
|
|
||||||
export function icon_button({ color, margin, layer, variant, size }: IconButtonOptions = {
|
export function icon_button(
|
||||||
|
{ color, background_color, margin, layer, variant, size }: IconButtonOptions = {
|
||||||
variant: Button.variant.Default,
|
variant: Button.variant.Default,
|
||||||
size: Button.size.Medium,
|
size: Button.size.Medium,
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
if (!color) color = "base"
|
if (!color) color = "base"
|
||||||
|
|
||||||
const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color)
|
const default_background =
|
||||||
|
variant === Button.variant.Ghost
|
||||||
|
? null
|
||||||
|
: background(layer ?? theme.lowest, background_color ?? color)
|
||||||
|
|
||||||
const m = {
|
const m = {
|
||||||
top: margin?.top ?? 0,
|
top: margin?.top ?? 0,
|
||||||
|
@ -55,42 +60,51 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO
|
||||||
corner_radius: 6,
|
corner_radius: 6,
|
||||||
padding: padding,
|
padding: padding,
|
||||||
margin: m,
|
margin: m,
|
||||||
icon_width: 12,
|
icon_width: 14,
|
||||||
icon_height: 14,
|
icon_height: 14,
|
||||||
button_width: size === Button.size.Small ? 16 : 20,
|
button_width: size === Button.size.Small ? 16 : 20,
|
||||||
button_height: 14,
|
button_height: 14,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
default: {
|
default: {
|
||||||
background: background_color,
|
background: default_background,
|
||||||
color: foreground(layer ?? theme.lowest, color),
|
color: foreground(layer ?? theme.lowest, color),
|
||||||
},
|
},
|
||||||
hovered: {
|
hovered: {
|
||||||
background: background(layer ?? theme.lowest, color, "hovered"),
|
background: background(layer ?? theme.lowest, background_color ?? color, "hovered"),
|
||||||
color: foreground(layer ?? theme.lowest, color, "hovered"),
|
color: foreground(layer ?? theme.lowest, color, "hovered"),
|
||||||
},
|
},
|
||||||
clicked: {
|
clicked: {
|
||||||
background: background(layer ?? theme.lowest, color, "pressed"),
|
background: background(layer ?? theme.lowest, background_color ?? color, "pressed"),
|
||||||
color: foreground(layer ?? theme.lowest, color, "pressed"),
|
color: foreground(layer ?? theme.lowest, color, "pressed"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleable_icon_button(
|
export function toggleable_icon_button({
|
||||||
theme: Theme,
|
color,
|
||||||
{ color, active_color, margin, variant, size, active_layer }: ToggleableIconButtonOptions
|
background_color,
|
||||||
) {
|
active_color,
|
||||||
|
active_background_color,
|
||||||
|
active_variant,
|
||||||
|
margin,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
active_layer,
|
||||||
|
}: ToggleableIconButtonOptions) {
|
||||||
if (!color) color = "base"
|
if (!color) color = "base"
|
||||||
|
|
||||||
return toggleable({
|
return toggleable({
|
||||||
state: {
|
state: {
|
||||||
inactive: icon_button({ color, margin, variant, size }),
|
inactive: icon_button({ color, background_color, margin, variant, size }),
|
||||||
active: icon_button({
|
active: icon_button({
|
||||||
color: active_color ? active_color : color,
|
color: active_color ? active_color : color,
|
||||||
|
background_color: active_background_color ? active_background_color : background_color,
|
||||||
margin,
|
margin,
|
||||||
layer: active_layer,
|
layer: active_layer,
|
||||||
size
|
variant: active_variant || variant,
|
||||||
|
size,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
6
styles/src/component/index.ts
Normal file
6
styles/src/component/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export * from "./icon_button"
|
||||||
|
export * from "./indicator"
|
||||||
|
export * from "./input"
|
||||||
|
export * from "./tab"
|
||||||
|
export * from "./tab_bar_button"
|
||||||
|
export * from "./text_button"
|
|
@ -1,7 +1,13 @@
|
||||||
import { foreground } from "../style_tree/components"
|
import { foreground } from "../style_tree/components"
|
||||||
import { Layer, StyleSets } from "../theme"
|
import { Layer, StyleSets } from "../theme"
|
||||||
|
|
||||||
export const indicator = ({ layer, color }: { layer: Layer, color: StyleSets }) => ({
|
export const indicator = ({
|
||||||
|
layer,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
layer: Layer
|
||||||
|
color: StyleSets
|
||||||
|
}) => ({
|
||||||
corner_radius: 4,
|
corner_radius: 4,
|
||||||
padding: 4,
|
padding: 4,
|
||||||
margin: { top: 12, left: 12 },
|
margin: { top: 12, left: 12 },
|
||||||
|
|
|
@ -18,6 +18,6 @@ export const input = () => {
|
||||||
bottom: 3,
|
bottom: 3,
|
||||||
left: 12,
|
left: 12,
|
||||||
right: 8,
|
right: 8,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ type TabProps = {
|
||||||
export const tab = ({ layer }: TabProps) => {
|
export const tab = ({ layer }: TabProps) => {
|
||||||
const active_color = text(layer, "sans", "base").color
|
const active_color = text(layer, "sans", "base").color
|
||||||
const inactive_border: Border = {
|
const inactive_border: Border = {
|
||||||
color: '#FFFFFF00',
|
color: "#FFFFFF00",
|
||||||
width: 1,
|
width: 1,
|
||||||
bottom: true,
|
bottom: true,
|
||||||
left: false,
|
left: false,
|
||||||
|
@ -27,7 +27,7 @@ export const tab = ({ layer }: TabProps) => {
|
||||||
top: 8,
|
top: 8,
|
||||||
left: 8,
|
left: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
bottom: 6
|
bottom: 6,
|
||||||
},
|
},
|
||||||
border: inactive_border,
|
border: inactive_border,
|
||||||
}
|
}
|
||||||
|
@ -35,17 +35,17 @@ export const tab = ({ layer }: TabProps) => {
|
||||||
const i = interactive({
|
const i = interactive({
|
||||||
state: {
|
state: {
|
||||||
default: {
|
default: {
|
||||||
...base
|
...base,
|
||||||
},
|
},
|
||||||
hovered: {
|
hovered: {
|
||||||
...base,
|
...base,
|
||||||
...text(layer, "sans", "base", "hovered")
|
...text(layer, "sans", "base", "hovered"),
|
||||||
},
|
},
|
||||||
clicked: {
|
clicked: {
|
||||||
...base,
|
...base,
|
||||||
...text(layer, "sans", "base", "pressed")
|
...text(layer, "sans", "base", "pressed"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return toggleable({
|
return toggleable({
|
||||||
|
@ -60,14 +60,14 @@ export const tab = ({ layer }: TabProps) => {
|
||||||
hovered: {
|
hovered: {
|
||||||
...i,
|
...i,
|
||||||
...text(layer, "sans", "base", "hovered"),
|
...text(layer, "sans", "base", "hovered"),
|
||||||
border: active_border
|
border: active_border,
|
||||||
},
|
},
|
||||||
clicked: {
|
clicked: {
|
||||||
...i,
|
...i,
|
||||||
...text(layer, "sans", "base", "pressed"),
|
...text(layer, "sans", "base", "pressed"),
|
||||||
border: active_border
|
border: active_border,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,13 @@ type TabBarButtonProps = TabBarButtonOptions & {
|
||||||
state?: Partial<Record<InteractiveState, Partial<TabBarButtonOptions>>>
|
state?: Partial<Record<InteractiveState, Partial<TabBarButtonOptions>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) {
|
export function tab_bar_button(
|
||||||
|
theme: Theme,
|
||||||
|
{ icon, color = "base" }: TabBarButtonProps
|
||||||
|
) {
|
||||||
const button_spacing = 8
|
const button_spacing = 8
|
||||||
|
|
||||||
return (
|
return interactive({
|
||||||
interactive({
|
|
||||||
base: {
|
base: {
|
||||||
icon: {
|
icon: {
|
||||||
color: foreground(theme.middle, color),
|
color: foreground(theme.middle, color),
|
||||||
|
@ -29,7 +31,10 @@ export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarBut
|
||||||
container: {
|
container: {
|
||||||
corner_radius: 4,
|
corner_radius: 4,
|
||||||
padding: {
|
padding: {
|
||||||
top: 4, bottom: 4, left: 4, right: 4
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
},
|
},
|
||||||
margin: {
|
margin: {
|
||||||
left: button_spacing / 2,
|
left: button_spacing / 2,
|
||||||
|
@ -41,15 +46,13 @@ export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarBut
|
||||||
hovered: {
|
hovered: {
|
||||||
container: {
|
container: {
|
||||||
background: background(theme.middle, color, "hovered"),
|
background: background(theme.middle, color, "hovered"),
|
||||||
|
},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
clicked: {
|
clicked: {
|
||||||
container: {
|
container: {
|
||||||
background: background(theme.middle, color, "pressed"),
|
background: background(theme.middle, color, "pressed"),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,7 @@ import { Button } from "./button"
|
||||||
import { Margin } from "./icon_button"
|
import { Margin } from "./icon_button"
|
||||||
|
|
||||||
interface TextButtonOptions {
|
interface TextButtonOptions {
|
||||||
layer?:
|
layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"]
|
||||||
| Theme["lowest"]
|
|
||||||
| Theme["middle"]
|
|
||||||
| Theme["highest"]
|
|
||||||
variant?: Button.Variant
|
variant?: Button.Variant
|
||||||
color?: keyof Theme["lowest"]
|
color?: keyof Theme["lowest"]
|
||||||
margin?: Partial<Margin>
|
margin?: Partial<Margin>
|
||||||
|
@ -36,7 +33,10 @@ export function text_button({
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
if (!color) color = "base"
|
if (!color) color = "base"
|
||||||
|
|
||||||
const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color)
|
const background_color =
|
||||||
|
variant === Button.variant.Ghost
|
||||||
|
? null
|
||||||
|
: background(layer ?? theme.lowest, color)
|
||||||
|
|
||||||
const text_options: TextProperties = {
|
const text_options: TextProperties = {
|
||||||
size: "xs",
|
size: "xs",
|
||||||
|
@ -67,19 +67,37 @@ export function text_button({
|
||||||
state: {
|
state: {
|
||||||
default: {
|
default: {
|
||||||
background: background_color,
|
background: background_color,
|
||||||
color:
|
color: disabled
|
||||||
disabled
|
|
||||||
? foreground(layer ?? theme.lowest, "disabled")
|
? foreground(layer ?? theme.lowest, "disabled")
|
||||||
: foreground(layer ?? theme.lowest, color),
|
: foreground(layer ?? theme.lowest, color),
|
||||||
},
|
},
|
||||||
hovered:
|
hovered: disabled
|
||||||
disabled ? {} : {
|
? {}
|
||||||
background: background(layer ?? theme.lowest, color, "hovered"),
|
: {
|
||||||
color: foreground(layer ?? theme.lowest, color, "hovered"),
|
background: background(
|
||||||
|
layer ?? theme.lowest,
|
||||||
|
color,
|
||||||
|
"hovered"
|
||||||
|
),
|
||||||
|
color: foreground(
|
||||||
|
layer ?? theme.lowest,
|
||||||
|
color,
|
||||||
|
"hovered"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
clicked: disabled ? {} : {
|
clicked: disabled
|
||||||
background: background(layer ?? theme.lowest, color, "pressed"),
|
? {}
|
||||||
color: foreground(layer ?? theme.lowest, color, "pressed"),
|
: {
|
||||||
|
background: background(
|
||||||
|
layer ?? theme.lowest,
|
||||||
|
color,
|
||||||
|
"pressed"
|
||||||
|
),
|
||||||
|
color: foreground(
|
||||||
|
layer ?? theme.lowest,
|
||||||
|
color,
|
||||||
|
"pressed"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { interactive, Interactive } from "./interactive"
|
import { interactive, Interactive } from "./interactive"
|
||||||
import { toggleable, Toggleable } from "./toggle"
|
import { toggleable, Toggleable } from "./toggle"
|
||||||
|
|
||||||
|
export * from "./padding"
|
||||||
|
export * from "./margin"
|
||||||
export { interactive, Interactive, toggleable, Toggleable }
|
export { interactive, Interactive, toggleable, Toggleable }
|
||||||
|
|
|
@ -16,19 +16,26 @@ export type MarginStyle = {
|
||||||
export const margin_style = (options: MarginOptions): MarginStyle => {
|
export const margin_style = (options: MarginOptions): MarginStyle => {
|
||||||
const { all, top, bottom, left, right } = options
|
const { all, top, bottom, left, right } = options
|
||||||
|
|
||||||
if (all !== undefined) return {
|
if (all !== undefined)
|
||||||
|
return {
|
||||||
top: all,
|
top: all,
|
||||||
bottom: all,
|
bottom: all,
|
||||||
left: all,
|
left: all,
|
||||||
right: all
|
right: all,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Margin must have at least one value")
|
if (
|
||||||
|
top === undefined &&
|
||||||
|
bottom === undefined &&
|
||||||
|
left === undefined &&
|
||||||
|
right === undefined
|
||||||
|
)
|
||||||
|
throw new Error("Margin must have at least one value")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
top: top || 0,
|
top: top || 0,
|
||||||
bottom: bottom || 0,
|
bottom: bottom || 0,
|
||||||
left: left || 0,
|
left: left || 0,
|
||||||
right: right || 0
|
right: right || 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,19 +16,26 @@ export type PaddingStyle = {
|
||||||
export const padding_style = (options: PaddingOptions): PaddingStyle => {
|
export const padding_style = (options: PaddingOptions): PaddingStyle => {
|
||||||
const { all, top, bottom, left, right } = options
|
const { all, top, bottom, left, right } = options
|
||||||
|
|
||||||
if (all !== undefined) return {
|
if (all !== undefined)
|
||||||
|
return {
|
||||||
top: all,
|
top: all,
|
||||||
bottom: all,
|
bottom: all,
|
||||||
left: all,
|
left: all,
|
||||||
right: all
|
right: all,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Padding must have at least one value")
|
if (
|
||||||
|
top === undefined &&
|
||||||
|
bottom === undefined &&
|
||||||
|
left === undefined &&
|
||||||
|
right === undefined
|
||||||
|
)
|
||||||
|
throw new Error("Padding must have at least one value")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
top: top || 0,
|
top: top || 0,
|
||||||
bottom: bottom || 0,
|
bottom: bottom || 0,
|
||||||
left: left || 0,
|
left: left || 0,
|
||||||
right: right || 0
|
right: right || 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { text, border, background, foreground, TextStyle } from "./components"
|
import { text, border, background, foreground, TextStyle } from "./components"
|
||||||
import { Interactive, interactive } from "../element"
|
import { Interactive, interactive, toggleable } from "../element"
|
||||||
import { tab_bar_button } from "../component/tab_bar_button"
|
import { tab_bar_button } from "../component/tab_bar_button"
|
||||||
import { StyleSets, useTheme } from "../theme"
|
import { StyleSets, useTheme } from "../theme"
|
||||||
|
|
||||||
|
@ -8,23 +8,24 @@ type RoleCycleButton = TextStyle & {
|
||||||
}
|
}
|
||||||
// TODO: Replace these with zed types
|
// TODO: Replace these with zed types
|
||||||
type RemainingTokens = TextStyle & {
|
type RemainingTokens = TextStyle & {
|
||||||
background: string,
|
background: string
|
||||||
margin: { top: number, right: number },
|
margin: { top: number; right: number }
|
||||||
padding: {
|
padding: {
|
||||||
right: number,
|
right: number
|
||||||
left: number,
|
left: number
|
||||||
top: number,
|
top: number
|
||||||
bottom: number,
|
bottom: number
|
||||||
},
|
}
|
||||||
corner_radius: number,
|
corner_radius: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function assistant(): any {
|
export default function assistant(): any {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const interactive_role = (color: StyleSets): Interactive<RoleCycleButton> => {
|
const interactive_role = (
|
||||||
return (
|
color: StyleSets
|
||||||
interactive({
|
): Interactive<RoleCycleButton> => {
|
||||||
|
return interactive({
|
||||||
base: {
|
base: {
|
||||||
...text(theme.highest, "sans", color, { size: "sm" }),
|
...text(theme.highest, "sans", color, { size: "sm" }),
|
||||||
},
|
},
|
||||||
|
@ -36,22 +37,19 @@ export default function assistant(): any {
|
||||||
clicked: {
|
clicked: {
|
||||||
...text(theme.highest, "sans", color, { size: "sm" }),
|
...text(theme.highest, "sans", color, { size: "sm" }),
|
||||||
background: background(theme.highest, color, "pressed"),
|
background: background(theme.highest, color, "pressed"),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens_remaining = (color: StyleSets): RemainingTokens => {
|
const tokens_remaining = (color: StyleSets): RemainingTokens => {
|
||||||
return (
|
return {
|
||||||
{
|
|
||||||
...text(theme.highest, "mono", color, { size: "xs" }),
|
...text(theme.highest, "mono", color, { size: "xs" }),
|
||||||
background: background(theme.highest, "on", "default"),
|
background: background(theme.highest, "on", "default"),
|
||||||
margin: { top: 12, right: 20 },
|
margin: { top: 12, right: 20 },
|
||||||
padding: { right: 4, left: 4, top: 1, bottom: 1 },
|
padding: { right: 4, left: 4, top: 1, bottom: 1 },
|
||||||
corner_radius: 6,
|
corner_radius: 6,
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -59,6 +57,85 @@ export default function assistant(): any {
|
||||||
background: background(theme.highest),
|
background: background(theme.highest),
|
||||||
padding: { left: 12 },
|
padding: { left: 12 },
|
||||||
},
|
},
|
||||||
|
inline: {
|
||||||
|
background: background(theme.highest),
|
||||||
|
margin: { top: 3, bottom: 3 },
|
||||||
|
border: border(theme.lowest, "on", {
|
||||||
|
top: true,
|
||||||
|
bottom: true,
|
||||||
|
overlay: true,
|
||||||
|
}),
|
||||||
|
editor: {
|
||||||
|
text: text(theme.highest, "mono", "default", { size: "sm" }),
|
||||||
|
placeholder_text: text(theme.highest, "sans", "on", "disabled"),
|
||||||
|
selection: theme.players[0],
|
||||||
|
},
|
||||||
|
disabled_editor: {
|
||||||
|
text: text(theme.highest, "mono", "disabled", { size: "sm" }),
|
||||||
|
placeholder_text: text(theme.highest, "sans", "on", "disabled"),
|
||||||
|
selection: {
|
||||||
|
cursor: text(theme.highest, "mono", "disabled").color,
|
||||||
|
selection: theme.players[0].selection,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pending_edit_background: background(theme.highest, "positive"),
|
||||||
|
include_conversation: toggleable({
|
||||||
|
base: interactive({
|
||||||
|
base: {
|
||||||
|
icon_size: 12,
|
||||||
|
color: foreground(theme.highest, "variant"),
|
||||||
|
|
||||||
|
button_width: 12,
|
||||||
|
background: background(theme.highest, "on"),
|
||||||
|
corner_radius: 2,
|
||||||
|
border: {
|
||||||
|
width: 1., color: background(theme.highest, "on")
|
||||||
|
},
|
||||||
|
padding: {
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
hovered: {
|
||||||
|
...text(theme.highest, "mono", "variant", "hovered"),
|
||||||
|
background: background(theme.highest, "on", "hovered"),
|
||||||
|
border: {
|
||||||
|
width: 1., color: background(theme.highest, "on", "hovered")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clicked: {
|
||||||
|
...text(theme.highest, "mono", "variant", "pressed"),
|
||||||
|
background: background(theme.highest, "on", "pressed"),
|
||||||
|
border: {
|
||||||
|
width: 1., color: background(theme.highest, "on", "pressed")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
state: {
|
||||||
|
active: {
|
||||||
|
default: {
|
||||||
|
icon_size: 12,
|
||||||
|
button_width: 12,
|
||||||
|
color: foreground(theme.highest, "variant"),
|
||||||
|
background: background(theme.highest, "accent"),
|
||||||
|
border: border(theme.highest, "accent"),
|
||||||
|
},
|
||||||
|
hovered: {
|
||||||
|
background: background(theme.highest, "accent", "hovered"),
|
||||||
|
border: border(theme.highest, "accent", "hovered"),
|
||||||
|
},
|
||||||
|
clicked: {
|
||||||
|
background: background(theme.highest, "accent", "pressed"),
|
||||||
|
border: border(theme.highest, "accent", "pressed"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
message_header: {
|
message_header: {
|
||||||
margin: { bottom: 4, top: 4 },
|
margin: { bottom: 4, top: 4 },
|
||||||
background: background(theme.highest),
|
background: background(theme.highest),
|
||||||
|
@ -93,7 +170,10 @@ export default function assistant(): any {
|
||||||
base: {
|
base: {
|
||||||
background: background(theme.middle),
|
background: background(theme.middle),
|
||||||
padding: { top: 4, bottom: 4 },
|
padding: { top: 4, bottom: 4 },
|
||||||
border: border(theme.middle, "default", { top: true, overlay: true }),
|
border: border(theme.middle, "default", {
|
||||||
|
top: true,
|
||||||
|
overlay: true,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
hovered: {
|
hovered: {
|
||||||
|
@ -101,7 +181,7 @@ export default function assistant(): any {
|
||||||
},
|
},
|
||||||
clicked: {
|
clicked: {
|
||||||
background: background(theme.middle, "pressed"),
|
background: background(theme.middle, "pressed"),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
saved_at: {
|
saved_at: {
|
||||||
|
|
|
@ -39,7 +39,12 @@ export default function channel_modal(): any {
|
||||||
row_height: ITEM_HEIGHT,
|
row_height: ITEM_HEIGHT,
|
||||||
header: {
|
header: {
|
||||||
background: background(theme.lowest),
|
background: background(theme.lowest),
|
||||||
border: border(theme.middle, { "bottom": true, "top": false, left: false, right: false }),
|
border: border(theme.middle, {
|
||||||
|
bottom: true,
|
||||||
|
top: false,
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
}),
|
||||||
padding: {
|
padding: {
|
||||||
top: SPACING,
|
top: SPACING,
|
||||||
left: SPACING - BUTTON_OFFSET,
|
left: SPACING - BUTTON_OFFSET,
|
||||||
|
@ -48,7 +53,7 @@ export default function channel_modal(): any {
|
||||||
corner_radii: {
|
corner_radii: {
|
||||||
top_right: 12,
|
top_right: 12,
|
||||||
top_left: 12,
|
top_left: 12,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
background: background(theme.middle),
|
background: background(theme.middle),
|
||||||
|
@ -57,12 +62,11 @@ export default function channel_modal(): any {
|
||||||
left: SPACING,
|
left: SPACING,
|
||||||
right: SPACING,
|
right: SPACING,
|
||||||
bottom: SPACING,
|
bottom: SPACING,
|
||||||
|
|
||||||
},
|
},
|
||||||
corner_radii: {
|
corner_radii: {
|
||||||
bottom_right: 12,
|
bottom_right: 12,
|
||||||
bottom_left: 12,
|
bottom_left: 12,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
background: background(theme.middle),
|
background: background(theme.middle),
|
||||||
|
@ -74,7 +78,6 @@ export default function channel_modal(): any {
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
// FIXME: due to a bug in the picker's size calculation, this must be 600
|
// FIXME: due to a bug in the picker's size calculation, this must be 600
|
||||||
max_height: 600,
|
max_height: 600,
|
||||||
|
@ -83,7 +86,7 @@ export default function channel_modal(): any {
|
||||||
...text(theme.middle, "sans", "on", { size: "lg" }),
|
...text(theme.middle, "sans", "on", { size: "lg" }),
|
||||||
padding: {
|
padding: {
|
||||||
left: BUTTON_OFFSET,
|
left: BUTTON_OFFSET,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
picker: {
|
picker: {
|
||||||
empty_container: {},
|
empty_container: {},
|
||||||
|
@ -108,8 +111,8 @@ export default function channel_modal(): any {
|
||||||
background: background(theme.middle),
|
background: background(theme.middle),
|
||||||
padding: {
|
padding: {
|
||||||
left: 7,
|
left: 7,
|
||||||
right: 7
|
right: 7,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
cancel_invite_button: {
|
cancel_invite_button: {
|
||||||
...text(theme.middle, "sans", { size: "xs" }),
|
...text(theme.middle, "sans", { size: "xs" }),
|
||||||
|
@ -125,7 +128,7 @@ export default function channel_modal(): any {
|
||||||
padding: {
|
padding: {
|
||||||
left: 4,
|
left: 4,
|
||||||
right: 4,
|
right: 4,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
contact_avatar: {
|
contact_avatar: {
|
||||||
corner_radius: 10,
|
corner_radius: 10,
|
||||||
|
@ -147,6 +150,6 @@ export default function channel_modal(): any {
|
||||||
background: background(theme.middle, "disabled"),
|
background: background(theme.middle, "disabled"),
|
||||||
color: foreground(theme.middle, "disabled"),
|
color: foreground(theme.middle, "disabled"),
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default function contacts_panel(): any {
|
||||||
color: foreground(layer, "on"),
|
color: foreground(layer, "on"),
|
||||||
icon_width: 14,
|
icon_width: 14,
|
||||||
button_width: 16,
|
button_width: 16,
|
||||||
corner_radius: 8
|
corner_radius: 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
const project_row = {
|
const project_row = {
|
||||||
|
@ -61,7 +61,7 @@ export default function contacts_panel(): any {
|
||||||
width: 14,
|
width: 14,
|
||||||
}
|
}
|
||||||
|
|
||||||
const header_icon_button = toggleable_icon_button(theme, {
|
const header_icon_button = toggleable_icon_button({
|
||||||
variant: "ghost",
|
variant: "ghost",
|
||||||
size: "sm",
|
size: "sm",
|
||||||
active_layer: theme.lowest,
|
active_layer: theme.lowest,
|
||||||
|
@ -275,7 +275,7 @@ export default function contacts_panel(): any {
|
||||||
list_empty_label_container: {
|
list_empty_label_container: {
|
||||||
margin: {
|
margin: {
|
||||||
left: NAME_MARGIN,
|
left: NAME_MARGIN,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
list_empty_icon: {
|
list_empty_icon: {
|
||||||
color: foreground(layer, "variant"),
|
color: foreground(layer, "variant"),
|
||||||
|
@ -289,7 +289,7 @@ export default function contacts_panel(): any {
|
||||||
top: SPACING / 2,
|
top: SPACING / 2,
|
||||||
bottom: SPACING / 2,
|
bottom: SPACING / 2,
|
||||||
left: SPACING,
|
left: SPACING,
|
||||||
right: SPACING
|
right: SPACING,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
|
@ -330,7 +330,7 @@ export default function contacts_panel(): any {
|
||||||
right: 4,
|
right: 4,
|
||||||
},
|
},
|
||||||
background: background(layer, "hovered"),
|
background: background(layer, "hovered"),
|
||||||
...text(layer, "sans", "hovered", { size: "xs" })
|
...text(layer, "sans", "hovered", { size: "xs" }),
|
||||||
},
|
},
|
||||||
contact_status_free: indicator({ layer, color: "positive" }),
|
contact_status_free: indicator({ layer, color: "positive" }),
|
||||||
contact_status_busy: indicator({ layer, color: "negative" }),
|
contact_status_busy: indicator({ layer, color: "negative" }),
|
||||||
|
@ -404,7 +404,7 @@ export default function contacts_panel(): any {
|
||||||
channel_editor: {
|
channel_editor: {
|
||||||
padding: {
|
padding: {
|
||||||
left: NAME_MARGIN,
|
left: NAME_MARGIN,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import { useTheme } from "../common"
|
import { useTheme } from "../common"
|
||||||
import { text_button } from "../component/text_button"
|
import { text_button } from "../component/text_button"
|
||||||
import { icon_button } from "../component/icon_button"
|
import { icon_button } from "../component/icon_button"
|
||||||
|
@ -14,14 +13,14 @@ export default function contacts_panel(): any {
|
||||||
base: text_button({}),
|
base: text_button({}),
|
||||||
state: {
|
state: {
|
||||||
active: {
|
active: {
|
||||||
...text_button({ color: "accent" })
|
...text_button({ color: "accent" }),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
disclosure: {
|
disclosure: {
|
||||||
...text(theme.lowest, "sans", "base"),
|
...text(theme.lowest, "sans", "base"),
|
||||||
button: icon_button({ variant: "ghost" }),
|
button: icon_button({ variant: "ghost" }),
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,4 @@ import { background, border } from "./components"
|
||||||
|
|
||||||
export default function contacts_popover(): any {
|
export default function contacts_popover(): any {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,6 +206,9 @@ export default function editor(): any {
|
||||||
match_highlight: foreground(theme.middle, "accent", "active"),
|
match_highlight: foreground(theme.middle, "accent", "active"),
|
||||||
background: background(theme.middle, "active"),
|
background: background(theme.middle, "active"),
|
||||||
},
|
},
|
||||||
|
server_name_container: { padding: { left: 40 } },
|
||||||
|
server_name_color: text(theme.middle, "sans", "disabled", {}).color,
|
||||||
|
server_name_size_percent: 0.75,
|
||||||
},
|
},
|
||||||
diagnostic_header: {
|
diagnostic_header: {
|
||||||
background: background(theme.middle),
|
background: background(theme.middle),
|
||||||
|
@ -307,7 +310,7 @@ export default function editor(): any {
|
||||||
? with_opacity(theme.ramps.green(0.5).hex(), 0.8)
|
? with_opacity(theme.ramps.green(0.5).hex(), 0.8)
|
||||||
: with_opacity(theme.ramps.green(0.4).hex(), 0.8),
|
: with_opacity(theme.ramps.green(0.4).hex(), 0.8),
|
||||||
},
|
},
|
||||||
selections: foreground(layer, "accent")
|
selections: foreground(layer, "accent"),
|
||||||
},
|
},
|
||||||
composition_mark: {
|
composition_mark: {
|
||||||
underline: {
|
underline: {
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default function feedback(): any {
|
||||||
...text(theme.highest, "mono", "on", "disabled"),
|
...text(theme.highest, "mono", "on", "disabled"),
|
||||||
background: background(theme.highest, "on", "disabled"),
|
background: background(theme.highest, "on", "disabled"),
|
||||||
border: border(theme.highest, "on", "disabled"),
|
border: border(theme.highest, "on", "disabled"),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
button_margin: 8,
|
button_margin: 8,
|
||||||
|
|
|
@ -152,7 +152,7 @@ export default function picker(): any {
|
||||||
0.5
|
0.5
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue