mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-21 03:18:47 +03:00
merged main
This commit is contained in:
commit
3f5667b101
1669
Cargo.lock
generated
1669
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@
|
|||||||
members = [
|
members = [
|
||||||
"crates/activity_indicator",
|
"crates/activity_indicator",
|
||||||
"crates/ai",
|
"crates/ai",
|
||||||
|
"crates/audio",
|
||||||
"crates/auto_update",
|
"crates/auto_update",
|
||||||
"crates/breadcrumbs",
|
"crates/breadcrumbs",
|
||||||
"crates/call",
|
"crates/call",
|
||||||
@ -102,6 +103,7 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
|||||||
toml = { version = "0.5" }
|
toml = { version = "0.5" }
|
||||||
tree-sitter = "0.20"
|
tree-sitter = "0.20"
|
||||||
unindent = { version = "0.1.7" }
|
unindent = { version = "0.1.7" }
|
||||||
|
pretty_assertions = "1.3.0"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }
|
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }
|
||||||
|
@ -24,9 +24,7 @@
|
|||||||
],
|
],
|
||||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
|
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
|
||||||
"cmd-shift-enter": "editor::NewlineAbove",
|
|
||||||
"cmd-enter": "editor::NewlineBelow"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -24,9 +24,7 @@
|
|||||||
"ctrl-.": "editor::GoToHunk",
|
"ctrl-.": "editor::GoToHunk",
|
||||||
"ctrl-,": "editor::GoToPrevHunk",
|
"ctrl-,": "editor::GoToPrevHunk",
|
||||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||||
"cmd-shift-enter": "editor::NewlineAbove",
|
|
||||||
"cmd-enter": "editor::NewlineBelow"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ctrl-shift-d": "editor::DuplicateLine",
|
"ctrl-shift-d": "editor::DuplicateLine",
|
||||||
"cmd-b": "editor::GoToDefinition",
|
"cmd-b": "editor::GoToDefinition",
|
||||||
"cmd-j": "editor::ScrollCursorCenter",
|
"cmd-j": "editor::ScrollCursorCenter",
|
||||||
"cmd-alt-enter": "editor::NewlineAbove",
|
|
||||||
"cmd-enter": "editor::NewlineBelow",
|
|
||||||
"cmd-shift-l": "editor::SelectLine",
|
"cmd-shift-l": "editor::SelectLine",
|
||||||
"cmd-shift-t": "outline::Toggle",
|
"cmd-shift-t": "outline::Toggle",
|
||||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||||
@ -56,7 +54,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
"bindings": {}
|
"bindings": {
|
||||||
|
"cmd-alt-enter": "editor::NewlineAbove"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar",
|
"context": "BufferSearchBar",
|
||||||
|
@ -71,15 +71,17 @@
|
|||||||
// "never"
|
// "never"
|
||||||
"show": "auto",
|
"show": "auto",
|
||||||
// Whether to show git diff indicators in the scrollbar.
|
// Whether to show git diff indicators in the scrollbar.
|
||||||
"git_diff": true
|
"git_diff": true,
|
||||||
|
// Whether to show selections in the scrollbar.
|
||||||
|
"selections": true
|
||||||
},
|
},
|
||||||
// Inlay hint related settings
|
// Inlay hint related settings
|
||||||
"inlay_hints": {
|
"inlay_hints": {
|
||||||
// Global switch to toggle hints on and off, switched off by default.
|
// Global switch to toggle hints on and off, switched off by default.
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
// Toggle certain types of hints on and off, all switched on by default.
|
// Toggle certain types of hints on and off, all switched on by default.
|
||||||
"show_type_hints": true,
|
"show_type_hints": true,
|
||||||
"show_parameter_hints": true,
|
"show_parameter_hints": true,
|
||||||
// Corresponds to null/None LSP hint type value.
|
// Corresponds to null/None LSP hint type value.
|
||||||
"show_other_hints": true
|
"show_other_hints": true
|
||||||
},
|
},
|
||||||
|
BIN
assets/sounds/joined_call.wav
Normal file
BIN
assets/sounds/joined_call.wav
Normal file
Binary file not shown.
BIN
assets/sounds/leave_call.wav
Normal file
BIN
assets/sounds/leave_call.wav
Normal file
Binary file not shown.
BIN
assets/sounds/mute.wav
Normal file
BIN
assets/sounds/mute.wav
Normal file
Binary file not shown.
BIN
assets/sounds/start_screenshare.wav
Normal file
BIN
assets/sounds/start_screenshare.wav
Normal file
Binary file not shown.
BIN
assets/sounds/stop_screenshare.wav
Normal file
BIN
assets/sounds/stop_screenshare.wav
Normal file
Binary file not shown.
BIN
assets/sounds/unmute.wav
Normal file
BIN
assets/sounds/unmute.wav
Normal file
Binary file not shown.
@ -12,6 +12,7 @@ use regex::Regex;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Reverse,
|
cmp::Reverse,
|
||||||
|
ffi::OsStr,
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@ -80,6 +81,9 @@ impl SavedConversationMetadata {
|
|||||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||||
while let Some(path) = paths.next().await {
|
while let Some(path) = paths.next().await {
|
||||||
let path = path?;
|
let path = path?;
|
||||||
|
if path.extension() != Some(OsStr::new("json")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let pattern = r" - \d+.zed.json$";
|
let pattern = r" - \d+.zed.json$";
|
||||||
let re = Regex::new(pattern).unwrap();
|
let re = Regex::new(pattern).unwrap();
|
||||||
|
@ -147,8 +147,9 @@ impl AssistantPanel {
|
|||||||
.await
|
.await
|
||||||
.log_err()
|
.log_err()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
this.update(&mut cx, |this, _| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.saved_conversations = saved_conversations
|
this.saved_conversations = saved_conversations;
|
||||||
|
cx.notify();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@ -1911,7 +1912,7 @@ impl ConversationEditor {
|
|||||||
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
|
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::<Editor>()) else {
|
let Some(editor) = workspace.active_item(cx).and_then(|item| item.act_as::<Editor>(cx)) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
23
crates/audio/Cargo.toml
Normal file
23
crates/audio/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "audio"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/audio.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
|
||||||
|
rodio = "0.17.1"
|
||||||
|
|
||||||
|
log.workspace = true
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
44
crates/audio/src/assets.rs
Normal file
44
crates/audio/src/assets.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use std::{io::Cursor, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use collections::HashMap;
|
||||||
|
use gpui::{AppContext, AssetSource};
|
||||||
|
use rodio::{
|
||||||
|
source::{Buffered, SamplesConverter},
|
||||||
|
Decoder, Source,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Sound = Buffered<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
|
||||||
|
|
||||||
|
pub struct SoundRegistry {
|
||||||
|
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
|
||||||
|
assets: Box<dyn AssetSource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoundRegistry {
|
||||||
|
pub fn new(source: impl AssetSource) -> Arc<Self> {
|
||||||
|
Arc::new(Self {
|
||||||
|
cache: Default::default(),
|
||||||
|
assets: Box::new(source),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn global(cx: &AppContext) -> Arc<Self> {
|
||||||
|
cx.global::<Arc<Self>>().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, name: &str) -> Result<impl Source<Item = f32>> {
|
||||||
|
if let Some(wav) = self.cache.lock().get(name) {
|
||||||
|
return Ok(wav.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = format!("sounds/{}.wav", name);
|
||||||
|
let bytes = self.assets.load(&path)?.into_owned();
|
||||||
|
let cursor = Cursor::new(bytes);
|
||||||
|
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();
|
||||||
|
|
||||||
|
self.cache.lock().insert(name.to_string(), source.clone());
|
||||||
|
|
||||||
|
Ok(source)
|
||||||
|
}
|
||||||
|
}
|
67
crates/audio/src/audio.rs
Normal file
67
crates/audio/src/audio.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
use assets::SoundRegistry;
|
||||||
|
use gpui::{AppContext, AssetSource};
|
||||||
|
use rodio::{OutputStream, OutputStreamHandle};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
mod assets;
|
||||||
|
|
||||||
|
pub fn init(source: impl AssetSource, cx: &mut AppContext) {
|
||||||
|
cx.set_global(SoundRegistry::new(source));
|
||||||
|
cx.set_global(Audio::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Sound {
|
||||||
|
Joined,
|
||||||
|
Leave,
|
||||||
|
Mute,
|
||||||
|
Unmute,
|
||||||
|
StartScreenshare,
|
||||||
|
StopScreenshare,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sound {
|
||||||
|
fn file(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Joined => "joined_call",
|
||||||
|
Self::Leave => "leave_call",
|
||||||
|
Self::Mute => "mute",
|
||||||
|
Self::Unmute => "unmute",
|
||||||
|
Self::StartScreenshare => "start_screenshare",
|
||||||
|
Self::StopScreenshare => "stop_screenshare",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Audio {
|
||||||
|
_output_stream: Option<OutputStream>,
|
||||||
|
output_handle: Option<OutputStreamHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Audio {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
_output_stream,
|
||||||
|
output_handle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play_sound(sound: Sound, cx: &AppContext) {
|
||||||
|
if !cx.has_global::<Self>() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let this = cx.global::<Self>();
|
||||||
|
|
||||||
|
let Some(output_handle) = this.output_handle.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
output_handle.play_raw(source).log_err();
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ test-support = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
audio = { path = "../audio" }
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
@ -3,6 +3,7 @@ use crate::{
|
|||||||
IncomingCall,
|
IncomingCall,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use audio::{Audio, Sound};
|
||||||
use client::{
|
use client::{
|
||||||
proto::{self, PeerId},
|
proto::{self, PeerId},
|
||||||
Client, TypedEnvelope, User, UserStore,
|
Client, TypedEnvelope, User, UserStore,
|
||||||
@ -151,6 +152,7 @@ impl Room {
|
|||||||
let connect = room.connect(&connection_info.server_url, &connection_info.token);
|
let connect = room.connect(&connection_info.server_url, &connection_info.token);
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
connect.await?;
|
connect.await?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| this.share_microphone(cx))
|
this.update(&mut cx, |this, cx| this.share_microphone(cx))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -176,6 +178,8 @@ impl Room {
|
|||||||
let maintain_connection =
|
let maintain_connection =
|
||||||
cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err());
|
cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err());
|
||||||
|
|
||||||
|
Audio::play_sound(Sound::Joined, cx);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
live_kit: live_kit_room,
|
live_kit: live_kit_room,
|
||||||
@ -265,6 +269,7 @@ impl Room {
|
|||||||
room.apply_room_update(room_proto, cx)?;
|
room.apply_room_update(room_proto, cx)?;
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(room)
|
Ok(room)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -306,6 +311,8 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Audio::play_sound(Sound::Leave, cx);
|
||||||
|
|
||||||
self.status = RoomStatus::Offline;
|
self.status = RoomStatus::Offline;
|
||||||
self.remote_participants.clear();
|
self.remote_participants.clear();
|
||||||
self.pending_participants.clear();
|
self.pending_participants.clear();
|
||||||
@ -656,6 +663,8 @@ impl Room {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Audio::play_sound(Sound::Joined, cx);
|
||||||
|
|
||||||
if let Some(live_kit) = this.live_kit.as_ref() {
|
if let Some(live_kit) = this.live_kit.as_ref() {
|
||||||
let video_tracks =
|
let video_tracks =
|
||||||
live_kit.room.remote_video_tracks(&user.id.to_string());
|
live_kit.room.remote_video_tracks(&user.id.to_string());
|
||||||
@ -922,6 +931,7 @@ impl Room {
|
|||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let project =
|
let project =
|
||||||
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.joined_projects.retain(|project| {
|
this.joined_projects.retain(|project| {
|
||||||
if let Some(project) = project.upgrade(cx) {
|
if let Some(project) = project.upgrade(cx) {
|
||||||
@ -1212,6 +1222,9 @@ impl Room {
|
|||||||
};
|
};
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Audio::play_sound(Sound::StartScreenshare, cx);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@ -1227,38 +1240,20 @@ impl Room {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn set_mute(
|
|
||||||
live_kit: &mut LiveKitRoom,
|
|
||||||
should_mute: bool,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) -> Result<Task<Result<()>>> {
|
|
||||||
if !should_mute {
|
|
||||||
// clear user muting state.
|
|
||||||
live_kit.muted_by_user = false;
|
|
||||||
}
|
|
||||||
match &mut live_kit.microphone_track {
|
|
||||||
LocalTrack::None => Err(anyhow!("microphone was not shared")),
|
|
||||||
LocalTrack::Pending { muted, .. } => {
|
|
||||||
*muted = should_mute;
|
|
||||||
cx.notify();
|
|
||||||
Ok(Task::Ready(Some(Ok(()))))
|
|
||||||
}
|
|
||||||
LocalTrack::Published {
|
|
||||||
track_publication,
|
|
||||||
muted,
|
|
||||||
} => {
|
|
||||||
*muted = should_mute;
|
|
||||||
cx.notify();
|
|
||||||
Ok(cx.background().spawn(track_publication.set_mute(*muted)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
|
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
|
||||||
let should_mute = !self.is_muted();
|
let should_mute = !self.is_muted();
|
||||||
if let Some(live_kit) = self.live_kit.as_mut() {
|
if let Some(live_kit) = self.live_kit.as_mut() {
|
||||||
let ret = Self::set_mute(live_kit, should_mute, cx);
|
let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?;
|
||||||
live_kit.muted_by_user = should_mute;
|
live_kit.muted_by_user = should_mute;
|
||||||
ret
|
|
||||||
|
if old_muted == true && live_kit.deafened == true {
|
||||||
|
if let Some(task) = self.toggle_deafen(cx).ok() {
|
||||||
|
task.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ret_task)
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("LiveKit not started"))
|
Err(anyhow!("LiveKit not started"))
|
||||||
}
|
}
|
||||||
@ -1274,7 +1269,7 @@ impl Room {
|
|||||||
// When deafening, mute user's mic as well.
|
// When deafening, mute user's mic as well.
|
||||||
// When undeafening, unmute user's mic unless it was manually muted prior to deafening.
|
// When undeafening, unmute user's mic unless it was manually muted prior to deafening.
|
||||||
if live_kit.deafened || !live_kit.muted_by_user {
|
if live_kit.deafened || !live_kit.muted_by_user {
|
||||||
mute_task = Some(Self::set_mute(live_kit, live_kit.deafened, cx)?);
|
mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0);
|
||||||
};
|
};
|
||||||
for participant in self.remote_participants.values() {
|
for participant in self.remote_participants.values() {
|
||||||
for track in live_kit
|
for track in live_kit
|
||||||
@ -1319,6 +1314,8 @@ impl Room {
|
|||||||
} => {
|
} => {
|
||||||
live_kit.room.unpublish_track(track_publication);
|
live_kit.room.unpublish_track(track_publication);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
|
Audio::play_sound(Sound::StopScreenshare, cx);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1347,6 +1344,51 @@ struct LiveKitRoom {
|
|||||||
_maintain_tracks: [Task<()>; 2],
|
_maintain_tracks: [Task<()>; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LiveKitRoom {
|
||||||
|
fn set_mute(
|
||||||
|
self: &mut LiveKitRoom,
|
||||||
|
should_mute: bool,
|
||||||
|
cx: &mut ModelContext<Room>,
|
||||||
|
) -> Result<(Task<Result<()>>, bool)> {
|
||||||
|
if !should_mute {
|
||||||
|
// clear user muting state.
|
||||||
|
self.muted_by_user = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (result, old_muted) = match &mut self.microphone_track {
|
||||||
|
LocalTrack::None => Err(anyhow!("microphone was not shared")),
|
||||||
|
LocalTrack::Pending { muted, .. } => {
|
||||||
|
let old_muted = *muted;
|
||||||
|
*muted = should_mute;
|
||||||
|
cx.notify();
|
||||||
|
Ok((Task::Ready(Some(Ok(()))), old_muted))
|
||||||
|
}
|
||||||
|
LocalTrack::Published {
|
||||||
|
track_publication,
|
||||||
|
muted,
|
||||||
|
} => {
|
||||||
|
let old_muted = *muted;
|
||||||
|
*muted = should_mute;
|
||||||
|
cx.notify();
|
||||||
|
Ok((
|
||||||
|
cx.background().spawn(track_publication.set_mute(*muted)),
|
||||||
|
old_muted,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
|
if old_muted != should_mute {
|
||||||
|
if should_mute {
|
||||||
|
Audio::play_sound(Sound::Mute, cx);
|
||||||
|
} else {
|
||||||
|
Audio::play_sound(Sound::Unmute, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((result, old_muted))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum LocalTrack {
|
enum LocalTrack {
|
||||||
None,
|
None,
|
||||||
Pending {
|
Pending {
|
||||||
|
@ -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.15.0"
|
version = "0.16.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
@ -57,6 +57,7 @@ tracing-log = "0.1.3"
|
|||||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
audio = { path = "../audio" }
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
call = { path = "../call", features = ["test-support"] }
|
call = { path = "../call", features = ["test-support"] }
|
||||||
@ -67,7 +68,7 @@ fs = { path = "../fs", features = ["test-support"] }
|
|||||||
git = { path = "../git", features = ["test-support"] }
|
git = { path = "../git", features = ["test-support"] }
|
||||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions.workspace = true
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
|
@ -3517,7 +3517,6 @@ pub use test::*;
|
|||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::executor::Background;
|
use gpui::executor::Background;
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use sea_orm::ConnectionTrait;
|
use sea_orm::ConnectionTrait;
|
||||||
use sqlx::migrate::MigrateDatabase;
|
use sqlx::migrate::MigrateDatabase;
|
||||||
@ -3566,9 +3565,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn postgres(background: Arc<Background>) -> Self {
|
pub fn postgres(background: Arc<Background>) -> Self {
|
||||||
lazy_static! {
|
static LOCK: Mutex<()> = Mutex::new(());
|
||||||
static ref LOCK: Mutex<()> = Mutex::new(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let _guard = LOCK.lock();
|
let _guard = LOCK.lock();
|
||||||
let mut rng = StdRng::from_entropy();
|
let mut rng = StdRng::from_entropy();
|
||||||
|
@ -203,6 +203,7 @@ impl TestServer {
|
|||||||
language::init(cx);
|
language::init(cx);
|
||||||
editor::init_settings(cx);
|
editor::init_settings(cx);
|
||||||
workspace::init(app_state.clone(), cx);
|
workspace::init(app_state.clone(), cx);
|
||||||
|
audio::init((), cx);
|
||||||
call::init(client.clone(), user_store.clone(), cx);
|
call::init(client.clone(), user_store.clone(), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, Formatter, InlayHintKind, 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, OffsetRangeExt, Point, Rope,
|
||||||
};
|
};
|
||||||
@ -7843,7 +7843,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
|
|
||||||
|
|
||||||
let mut language = Language::new(
|
let mut language = Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
@ -7955,10 +7954,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
|||||||
"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!(
|
|
||||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
|
||||||
"Cache should use editor settings to get the allowed hint kinds"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, edits_made,
|
inlay_cache.version, edits_made,
|
||||||
"Host editor update the cache version after every cache/view change",
|
"Host editor update the cache version after every cache/view change",
|
||||||
@ -7982,10 +7977,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
|||||||
"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!(
|
|
||||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
|
||||||
"Cache should use editor settings to get the allowed hint kinds"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, edits_made,
|
inlay_cache.version, edits_made,
|
||||||
"Guest editor update the cache version after every cache/view change"
|
"Guest editor update the cache version after every cache/view change"
|
||||||
@ -8007,10 +7998,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
|||||||
"Host should get hints from the 1st edit and 1st LSP query"
|
"Host should get hints from the 1st edit and 1st LSP query"
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(
|
|
||||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
|
||||||
"Inlay kinds settings never change during the test"
|
|
||||||
);
|
|
||||||
assert_eq!(inlay_cache.version, edits_made);
|
assert_eq!(inlay_cache.version, edits_made);
|
||||||
});
|
});
|
||||||
editor_b.update(cx_b, |editor, _| {
|
editor_b.update(cx_b, |editor, _| {
|
||||||
@ -8025,10 +8012,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
|||||||
"Guest should get hints the 1st edit and 2nd LSP query"
|
"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.allowed_hint_kinds, allowed_hint_kinds,
|
|
||||||
"Inlay kinds settings never change during the test"
|
|
||||||
);
|
|
||||||
assert_eq!(inlay_cache.version, edits_made);
|
assert_eq!(inlay_cache.version, edits_made);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -8054,10 +8037,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
|||||||
4th query was made by guest (but not applied) due to cache invalidation logic"
|
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.allowed_hint_kinds, allowed_hint_kinds,
|
|
||||||
"Inlay kinds settings never change during the test"
|
|
||||||
);
|
|
||||||
assert_eq!(inlay_cache.version, edits_made);
|
assert_eq!(inlay_cache.version, edits_made);
|
||||||
});
|
});
|
||||||
editor_b.update(cx_b, |editor, _| {
|
editor_b.update(cx_b, |editor, _| {
|
||||||
@ -8074,10 +8053,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
|||||||
"Guest should get hints from 3rd edit, 6th LSP query"
|
"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.allowed_hint_kinds, allowed_hint_kinds,
|
|
||||||
"Inlay kinds settings never change during the test"
|
|
||||||
);
|
|
||||||
assert_eq!(inlay_cache.version, edits_made);
|
assert_eq!(inlay_cache.version, edits_made);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -8103,10 +8078,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
|||||||
"Host should react to /refresh LSP request and get new hints from 7th LSP query"
|
"Host should react to /refresh LSP request and get new hints from 7th LSP query"
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(
|
|
||||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
|
||||||
"Inlay kinds settings never change during the test"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, edits_made,
|
inlay_cache.version, edits_made,
|
||||||
"Host should accepted all edits and bump its cache version every time"
|
"Host should accepted all edits and bump its cache version every time"
|
||||||
@ -8128,10 +8099,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
|||||||
"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 and get new hints from 8th LSP query"
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(
|
|
||||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
|
||||||
"Inlay kinds settings never change during the test"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version,
|
inlay_cache.version,
|
||||||
edits_made,
|
edits_made,
|
||||||
@ -8164,9 +8131,9 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
|||||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
show_type_hints: true,
|
show_type_hints: false,
|
||||||
show_parameter_hints: false,
|
show_parameter_hints: false,
|
||||||
show_other_hints: true,
|
show_other_hints: false,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -8177,13 +8144,12 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
|||||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
show_type_hints: true,
|
show_type_hints: true,
|
||||||
show_parameter_hints: false,
|
show_parameter_hints: true,
|
||||||
show_other_hints: true,
|
show_other_hints: true,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
|
|
||||||
|
|
||||||
let mut language = Language::new(
|
let mut language = Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
@ -8299,10 +8265,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
|||||||
"Host should get no hints due to them turned off"
|
"Host should get no hints due to them turned off"
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(
|
|
||||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
|
||||||
"Host should have allowed hint kinds set despite hints are off"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, 0,
|
inlay_cache.version, 0,
|
||||||
"Host should not increment its cache version due to no changes",
|
"Host should not increment its cache version due to no changes",
|
||||||
@ -8318,10 +8280,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
|||||||
"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!(
|
|
||||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
|
||||||
"Cache should use editor settings to get the allowed hint kinds"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, edits_made,
|
inlay_cache.version, edits_made,
|
||||||
"Guest editor update the cache version after every cache/view change"
|
"Guest editor update the cache version after every cache/view change"
|
||||||
@ -8339,7 +8297,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
|||||||
"Host should get nop hints due to them turned off, even after the /refresh"
|
"Host should get nop hints due to them turned off, even after the /refresh"
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, 0,
|
inlay_cache.version, 0,
|
||||||
"Host should not increment its cache version due to no changes",
|
"Host should not increment its cache version due to no changes",
|
||||||
@ -8355,10 +8312,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
|||||||
"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!(
|
|
||||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
|
||||||
"Inlay kinds settings never change during the test"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, edits_made,
|
inlay_cache.version, edits_made,
|
||||||
"Guest should accepted all edits and bump its cache version every time"
|
"Guest should accepted all edits and bump its cache version every time"
|
||||||
|
@ -37,9 +37,9 @@ use util::ResultExt;
|
|||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref PLAN_LOAD_PATH: Option<PathBuf> = path_env_var("LOAD_PLAN");
|
static ref PLAN_LOAD_PATH: Option<PathBuf> = path_env_var("LOAD_PLAN");
|
||||||
static ref PLAN_SAVE_PATH: Option<PathBuf> = path_env_var("SAVE_PLAN");
|
static ref PLAN_SAVE_PATH: Option<PathBuf> = path_env_var("SAVE_PLAN");
|
||||||
static ref LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Default::default();
|
|
||||||
static ref PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Default::default();
|
|
||||||
}
|
}
|
||||||
|
static LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Mutex::new(None);
|
||||||
|
static PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Mutex::new(None);
|
||||||
|
|
||||||
#[gpui::test(iterations = 100, on_failure = "on_failure")]
|
#[gpui::test(iterations = 100, on_failure = "on_failure")]
|
||||||
async fn test_random_collaboration(
|
async fn test_random_collaboration(
|
||||||
|
@ -35,6 +35,7 @@ gpui = { path = "../gpui" }
|
|||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
picker = { path = "../picker" }
|
picker = { path = "../picker" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
|
recent_projects = {path = "../recent_projects"}
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
theme_selector = { path = "../theme_selector" }
|
theme_selector = { path = "../theme_selector" }
|
||||||
@ -42,6 +43,7 @@ util = { path = "../util" }
|
|||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
zed-actions = {path = "../zed-actions"}
|
zed-actions = {path = "../zed-actions"}
|
||||||
|
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
238
crates/collab_ui/src/branch_list.rs
Normal file
238
crates/collab_ui/src/branch_list.rs
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
use anyhow::{anyhow, bail};
|
||||||
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
|
use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
|
||||||
|
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||||
|
use std::{ops::Not, sync::Arc};
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::{Toast, Workspace};
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
Picker::<BranchListDelegate>::init(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type BranchList = Picker<BranchListDelegate>;
|
||||||
|
|
||||||
|
pub fn build_branch_list(
|
||||||
|
workspace: ViewHandle<Workspace>,
|
||||||
|
cx: &mut ViewContext<BranchList>,
|
||||||
|
) -> BranchList {
|
||||||
|
Picker::new(
|
||||||
|
BranchListDelegate {
|
||||||
|
matches: vec![],
|
||||||
|
workspace,
|
||||||
|
selected_index: 0,
|
||||||
|
last_query: String::default(),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.with_theme(|theme| theme.picker.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BranchListDelegate {
|
||||||
|
matches: Vec<StringMatch>,
|
||||||
|
workspace: ViewHandle<Workspace>,
|
||||||
|
selected_index: usize,
|
||||||
|
last_query: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for BranchListDelegate {
|
||||||
|
fn placeholder_text(&self) -> Arc<str> {
|
||||||
|
"Select branch...".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||||
|
cx.spawn(move |picker, mut cx| async move {
|
||||||
|
let Some(candidates) = picker
|
||||||
|
.read_with(&mut cx, |view, cx| {
|
||||||
|
let delegate = view.delegate();
|
||||||
|
let project = delegate.workspace.read(cx).project().read(&cx);
|
||||||
|
let mut cwd =
|
||||||
|
project
|
||||||
|
.visible_worktrees(cx)
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.read(cx)
|
||||||
|
.abs_path()
|
||||||
|
.to_path_buf();
|
||||||
|
cwd.push(".git");
|
||||||
|
let Some(repo) = project.fs().open_repo(&cwd) else {bail!("Project does not have associated git repository.")};
|
||||||
|
let mut branches = repo
|
||||||
|
.lock()
|
||||||
|
.branches()?;
|
||||||
|
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||||
|
if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
|
||||||
|
// Truncate list of recent branches
|
||||||
|
// Do a partial sort to show recent-ish branches first.
|
||||||
|
branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
|
||||||
|
rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
|
||||||
|
});
|
||||||
|
branches.truncate(RECENT_BRANCHES_COUNT);
|
||||||
|
branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
|
||||||
|
}
|
||||||
|
Ok(branches
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, command)| StringMatchCandidate {
|
||||||
|
id: ix,
|
||||||
|
char_bag: command.name.chars().collect(),
|
||||||
|
string: command.name.into(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>())
|
||||||
|
})
|
||||||
|
.log_err() else { return; };
|
||||||
|
let Some(candidates) = candidates.log_err() else {return;};
|
||||||
|
let matches = if query.is_empty() {
|
||||||
|
candidates
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, candidate)| StringMatch {
|
||||||
|
candidate_id: index,
|
||||||
|
string: candidate.string,
|
||||||
|
positions: Vec::new(),
|
||||||
|
score: 0.0,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
fuzzy::match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
true,
|
||||||
|
10000,
|
||||||
|
&Default::default(),
|
||||||
|
cx.background(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
picker
|
||||||
|
.update(&mut cx, |picker, _| {
|
||||||
|
let delegate = picker.delegate_mut();
|
||||||
|
delegate.matches = matches;
|
||||||
|
if delegate.matches.is_empty() {
|
||||||
|
delegate.selected_index = 0;
|
||||||
|
} else {
|
||||||
|
delegate.selected_index =
|
||||||
|
core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
|
||||||
|
}
|
||||||
|
delegate.last_query = query;
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
let current_pick = self.selected_index();
|
||||||
|
let current_pick = self.matches[current_pick].string.clone();
|
||||||
|
cx.spawn(|picker, mut cx| async move {
|
||||||
|
picker.update(&mut cx, |this, cx| {
|
||||||
|
let project = this.delegate().workspace.read(cx).project().read(cx);
|
||||||
|
let mut cwd = project
|
||||||
|
.visible_worktrees(cx)
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow!("There are no visisible worktrees."))?
|
||||||
|
.read(cx)
|
||||||
|
.abs_path()
|
||||||
|
.to_path_buf();
|
||||||
|
cwd.push(".git");
|
||||||
|
let status = project
|
||||||
|
.fs()
|
||||||
|
.open_repo(&cwd)
|
||||||
|
.ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?
|
||||||
|
.lock()
|
||||||
|
.change_branch(¤t_pick);
|
||||||
|
if status.is_err() {
|
||||||
|
const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
|
||||||
|
this.delegate().workspace.update(cx, |model, ctx| {
|
||||||
|
model.show_toast(
|
||||||
|
Toast::new(
|
||||||
|
GIT_CHECKOUT_FAILURE_ID,
|
||||||
|
format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"),
|
||||||
|
),
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
status?;
|
||||||
|
}
|
||||||
|
cx.emit(PickerEvent::Dismiss);
|
||||||
|
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
}).log_err();
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
cx.emit(PickerEvent::Dismiss);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
mouse_state: &mut MouseState,
|
||||||
|
selected: bool,
|
||||||
|
cx: &gpui::AppContext,
|
||||||
|
) -> AnyElement<Picker<Self>> {
|
||||||
|
const DISPLAYED_MATCH_LEN: usize = 29;
|
||||||
|
let theme = &theme::current(cx);
|
||||||
|
let hit = &self.matches[ix];
|
||||||
|
let shortened_branch_name = util::truncate_and_trailoff(&hit.string, DISPLAYED_MATCH_LEN);
|
||||||
|
let highlights = hit
|
||||||
|
.positions
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|index| index < &DISPLAYED_MATCH_LEN)
|
||||||
|
.collect();
|
||||||
|
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
||||||
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
Label::new(shortened_branch_name.clone(), style.label.clone())
|
||||||
|
.with_highlights(highlights)
|
||||||
|
.contained()
|
||||||
|
.aligned()
|
||||||
|
.left(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.constrained()
|
||||||
|
.with_height(theme.contact_finder.row_height)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
fn render_header(
|
||||||
|
&self,
|
||||||
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
|
) -> Option<AnyElement<Picker<Self>>> {
|
||||||
|
let theme = &theme::current(cx);
|
||||||
|
let style = theme.picker.header.clone();
|
||||||
|
let label = if self.last_query.is_empty() {
|
||||||
|
Flex::row()
|
||||||
|
.with_child(Label::new("Recent branches", style.label.clone()))
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
} else {
|
||||||
|
Flex::row()
|
||||||
|
.with_child(Label::new("Branches", style.label.clone()))
|
||||||
|
.with_children(self.matches.is_empty().not().then(|| {
|
||||||
|
let suffix = if self.matches.len() == 1 { "" } else { "es" };
|
||||||
|
Label::new(
|
||||||
|
format!("{} match{}", self.matches.len(), suffix),
|
||||||
|
style.label,
|
||||||
|
)
|
||||||
|
.flex_float()
|
||||||
|
}))
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
};
|
||||||
|
Some(label.into_any())
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
|
branch_list::{build_branch_list, BranchList},
|
||||||
|
contact_notification::ContactNotification,
|
||||||
|
contacts_popover,
|
||||||
|
face_pile::FacePile,
|
||||||
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
|
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
|
||||||
ToggleScreenSharing,
|
ToggleScreenSharing,
|
||||||
};
|
};
|
||||||
@ -18,19 +21,25 @@ use gpui::{
|
|||||||
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
|
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
|
||||||
ViewContext, ViewHandle, WeakViewHandle,
|
ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
|
use picker::PickerEvent;
|
||||||
use project::{Project, RepositoryEntry};
|
use project::{Project, RepositoryEntry};
|
||||||
|
use recent_projects::{build_recent_projects, RecentProjects};
|
||||||
use std::{ops::Range, sync::Arc};
|
use std::{ops::Range, sync::Arc};
|
||||||
use theme::{AvatarStyle, Theme};
|
use theme::{AvatarStyle, Theme};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{FollowNextCollaborator, Workspace};
|
use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
|
||||||
|
|
||||||
// const MAX_TITLE_LENGTH: usize = 75;
|
const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||||
|
const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
collab,
|
collab,
|
||||||
[
|
[
|
||||||
ToggleContactsMenu,
|
ToggleContactsMenu,
|
||||||
ToggleUserMenu,
|
ToggleUserMenu,
|
||||||
|
ToggleVcsMenu,
|
||||||
|
ToggleProjectMenu,
|
||||||
|
SwitchBranch,
|
||||||
ShareProject,
|
ShareProject,
|
||||||
UnshareProject,
|
UnshareProject,
|
||||||
]
|
]
|
||||||
@ -41,6 +50,8 @@ pub fn init(cx: &mut AppContext) {
|
|||||||
cx.add_action(CollabTitlebarItem::share_project);
|
cx.add_action(CollabTitlebarItem::share_project);
|
||||||
cx.add_action(CollabTitlebarItem::unshare_project);
|
cx.add_action(CollabTitlebarItem::unshare_project);
|
||||||
cx.add_action(CollabTitlebarItem::toggle_user_menu);
|
cx.add_action(CollabTitlebarItem::toggle_user_menu);
|
||||||
|
cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
|
||||||
|
cx.add_action(CollabTitlebarItem::toggle_project_menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CollabTitlebarItem {
|
pub struct CollabTitlebarItem {
|
||||||
@ -49,6 +60,8 @@ pub struct CollabTitlebarItem {
|
|||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
||||||
|
branch_popover: Option<ViewHandle<BranchList>>,
|
||||||
|
project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
|
||||||
user_menu: ViewHandle<ContextMenu>,
|
user_menu: ViewHandle<ContextMenu>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
@ -69,12 +82,11 @@ impl View for CollabTitlebarItem {
|
|||||||
return Empty::new().into_any();
|
return Empty::new().into_any();
|
||||||
};
|
};
|
||||||
|
|
||||||
let project = self.project.read(cx);
|
|
||||||
let theme = theme::current(cx).clone();
|
let theme = theme::current(cx).clone();
|
||||||
let mut left_container = Flex::row();
|
let mut left_container = Flex::row();
|
||||||
let mut right_container = Flex::row().align_children_center();
|
let mut right_container = Flex::row().align_children_center();
|
||||||
|
|
||||||
left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx));
|
left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
|
||||||
|
|
||||||
let user = self.user_store.read(cx).current_user();
|
let user = self.user_store.read(cx).current_user();
|
||||||
let peer_id = self.client.peer_id();
|
let peer_id = self.client.peer_id();
|
||||||
@ -182,52 +194,97 @@ impl CollabTitlebarItem {
|
|||||||
menu.set_position_mode(OverlayPositionMode::Local);
|
menu.set_position_mode(OverlayPositionMode::Local);
|
||||||
menu
|
menu
|
||||||
}),
|
}),
|
||||||
|
branch_popover: None,
|
||||||
|
project_popover: None,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_title_root_names(
|
fn collect_title_root_names(
|
||||||
&self,
|
&self,
|
||||||
project: &Project,
|
|
||||||
theme: Arc<Theme>,
|
theme: Arc<Theme>,
|
||||||
cx: &ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> AnyElement<Self> {
|
) -> AnyElement<Self> {
|
||||||
let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
|
let project = self.project.read(cx);
|
||||||
let worktree = worktree.read(cx);
|
|
||||||
(worktree.root_name(), worktree.root_git_entry())
|
|
||||||
});
|
|
||||||
|
|
||||||
let (name, entry) = names_and_branches.next().unwrap_or(("", None));
|
let (name, entry) = {
|
||||||
|
let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
(worktree.root_name(), worktree.root_git_entry())
|
||||||
|
});
|
||||||
|
|
||||||
|
names_and_branches.next().unwrap_or(("", None))
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
|
||||||
let branch_prepended = entry
|
let branch_prepended = entry
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(RepositoryEntry::branch)
|
.and_then(RepositoryEntry::branch)
|
||||||
.map(|branch| format!("/{branch}"));
|
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
|
||||||
let text_style = theme.titlebar.title.clone();
|
let project_style = theme.titlebar.project_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 highlight = text_style.clone();
|
|
||||||
highlight.color = theme.titlebar.highlight_color;
|
|
||||||
|
|
||||||
let style = LabelStyle {
|
|
||||||
text: text_style,
|
|
||||||
highlight_text: Some(highlight),
|
|
||||||
};
|
|
||||||
let mut ret = Flex::row().with_child(
|
let mut ret = Flex::row().with_child(
|
||||||
Label::new(name.to_owned(), style.clone())
|
Stack::new()
|
||||||
.with_highlights((0..name.len()).into_iter().collect())
|
.with_child(
|
||||||
.contained()
|
MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, _| {
|
||||||
.aligned()
|
let style = project_style
|
||||||
.left()
|
.in_state(self.project_popover.is_some())
|
||||||
.into_any_named("title-project-name"),
|
.style_for(mouse_state);
|
||||||
|
Label::new(name, style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.aligned()
|
||||||
|
.left()
|
||||||
|
.into_any_named("title-project-name")
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_down(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.toggle_project_menu(&Default::default(), cx)
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, _, _| {}),
|
||||||
|
)
|
||||||
|
.with_children(self.render_project_popover_host(&theme.titlebar, cx)),
|
||||||
);
|
);
|
||||||
if let Some(git_branch) = branch_prepended {
|
if let Some(git_branch) = branch_prepended {
|
||||||
ret = ret.with_child(
|
ret = ret.with_child(
|
||||||
Label::new(git_branch, style)
|
Flex::row()
|
||||||
.contained()
|
.with_child(
|
||||||
.with_margin_right(item_spacing)
|
Label::new("/", divider_style.text)
|
||||||
.aligned()
|
.contained()
|
||||||
.left()
|
.with_style(divider_style.container)
|
||||||
.into_any_named("title-project-branch"),
|
.aligned()
|
||||||
|
.left(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Stack::new()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<ToggleVcsMenu, Self>::new(
|
||||||
|
0,
|
||||||
|
cx,
|
||||||
|
|mouse_state, _| {
|
||||||
|
let style = git_style
|
||||||
|
.in_state(self.branch_popover.is_some())
|
||||||
|
.style_for(mouse_state);
|
||||||
|
Label::new(git_branch, style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container.clone())
|
||||||
|
.with_margin_right(item_spacing)
|
||||||
|
.aligned()
|
||||||
|
.left()
|
||||||
|
.into_any_named("title-project-branch")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_down(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.toggle_vcs_menu(&Default::default(), cx)
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, _, _| {}),
|
||||||
|
)
|
||||||
|
.with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ret.into_any()
|
ret.into_any()
|
||||||
@ -320,7 +377,135 @@ impl CollabTitlebarItem {
|
|||||||
user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
|
user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
fn render_branches_popover_host<'a>(
|
||||||
|
&'a self,
|
||||||
|
_theme: &'a theme::Titlebar,
|
||||||
|
cx: &'a mut ViewContext<Self>,
|
||||||
|
) -> Option<AnyElement<Self>> {
|
||||||
|
self.branch_popover.as_ref().map(|child| {
|
||||||
|
let theme = theme::current(cx).clone();
|
||||||
|
let child = ChildView::new(child, cx);
|
||||||
|
let child = MouseEventHandler::<BranchList, Self>::new(0, cx, |_, _| {
|
||||||
|
child
|
||||||
|
.flex(1., true)
|
||||||
|
.contained()
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.contacts_popover.width)
|
||||||
|
.with_height(theme.contacts_popover.height)
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, |_, _, _| {})
|
||||||
|
.on_down_out(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.branch_popover.take();
|
||||||
|
cx.emit(());
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.contained()
|
||||||
|
.into_any();
|
||||||
|
|
||||||
|
Overlay::new(child)
|
||||||
|
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||||
|
.with_anchor_corner(AnchorCorner::TopLeft)
|
||||||
|
.with_z_index(999)
|
||||||
|
.aligned()
|
||||||
|
.bottom()
|
||||||
|
.left()
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn render_project_popover_host<'a>(
|
||||||
|
&'a self,
|
||||||
|
_theme: &'a theme::Titlebar,
|
||||||
|
cx: &'a mut ViewContext<Self>,
|
||||||
|
) -> Option<AnyElement<Self>> {
|
||||||
|
self.project_popover.as_ref().map(|child| {
|
||||||
|
let theme = theme::current(cx).clone();
|
||||||
|
let child = ChildView::new(child, cx);
|
||||||
|
let child = MouseEventHandler::<RecentProjects, Self>::new(0, cx, |_, _| {
|
||||||
|
child
|
||||||
|
.flex(1., true)
|
||||||
|
.contained()
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.contacts_popover.width)
|
||||||
|
.with_height(theme.contacts_popover.height)
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, |_, _, _| {})
|
||||||
|
.on_down_out(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.project_popover.take();
|
||||||
|
cx.emit(());
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.into_any();
|
||||||
|
|
||||||
|
Overlay::new(child)
|
||||||
|
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||||
|
.with_anchor_corner(AnchorCorner::TopLeft)
|
||||||
|
.with_z_index(999)
|
||||||
|
.aligned()
|
||||||
|
.bottom()
|
||||||
|
.left()
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
|
||||||
|
if self.branch_popover.take().is_none() {
|
||||||
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
|
let view = cx.add_view(|cx| build_branch_list(workspace, cx));
|
||||||
|
cx.subscribe(&view, |this, _, event, cx| {
|
||||||
|
match event {
|
||||||
|
PickerEvent::Dismiss => {
|
||||||
|
this.branch_popover = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
self.project_popover.take();
|
||||||
|
cx.focus(&view);
|
||||||
|
self.branch_popover = Some(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
if self.project_popover.take().is_none() {
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let workspaces = WORKSPACE_DB
|
||||||
|
.recent_workspaces_on_disk()
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, location)| location)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
this.update(&mut cx, move |this, cx| {
|
||||||
|
let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
|
||||||
|
|
||||||
|
cx.subscribe(&view, |this, _, event, cx| {
|
||||||
|
match event {
|
||||||
|
PickerEvent::Dismiss => {
|
||||||
|
this.project_popover = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
cx.focus(&view);
|
||||||
|
this.branch_popover.take();
|
||||||
|
this.project_popover = Some(view);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
fn render_toggle_contacts_button(
|
fn render_toggle_contacts_button(
|
||||||
&self,
|
&self,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
@ -733,7 +918,7 @@ impl CollabTitlebarItem {
|
|||||||
self.contacts_popover.as_ref().map(|popover| {
|
self.contacts_popover.as_ref().map(|popover| {
|
||||||
Overlay::new(ChildView::new(popover, cx))
|
Overlay::new(ChildView::new(popover, cx))
|
||||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||||
.with_anchor_corner(AnchorCorner::TopRight)
|
.with_anchor_corner(AnchorCorner::TopLeft)
|
||||||
.with_z_index(999)
|
.with_z_index(999)
|
||||||
.aligned()
|
.aligned()
|
||||||
.bottom()
|
.bottom()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
mod branch_list;
|
||||||
mod collab_titlebar_item;
|
mod collab_titlebar_item;
|
||||||
mod contact_finder;
|
mod contact_finder;
|
||||||
mod contact_list;
|
mod contact_list;
|
||||||
@ -28,6 +29,7 @@ actions!(
|
|||||||
);
|
);
|
||||||
|
|
||||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
|
branch_list::init(cx);
|
||||||
collab_titlebar_item::init(cx);
|
collab_titlebar_item::init(cx);
|
||||||
contact_list::init(cx);
|
contact_list::init(cx);
|
||||||
contact_finder::init(cx);
|
contact_finder::init(cx);
|
||||||
|
@ -244,8 +244,7 @@ impl ContextMenu {
|
|||||||
let show_count = self.show_count;
|
let show_count = self.show_count;
|
||||||
cx.defer(move |this, cx| {
|
cx.defer(move |this, cx| {
|
||||||
if cx.handle().is_focused(cx) && this.show_count == show_count {
|
if cx.handle().is_focused(cx) && this.show_count == show_count {
|
||||||
let window_id = cx.window_id();
|
(**cx).focus(this.previously_focused_view_id.take());
|
||||||
(**cx).focus(window_id, this.previously_focused_view_id.take());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -43,10 +43,10 @@ const DB_FILE_NAME: &'static str = "db.sqlite";
|
|||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
|
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
|
||||||
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
||||||
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
|
|
||||||
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||||
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
|
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
|
||||||
}
|
}
|
||||||
|
static DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
/// Open or create a database at the given directory path.
|
/// Open or create a database at the given directory path.
|
||||||
/// This will retry a couple times if there are failures. If opening fails once, the db directory
|
/// This will retry a couple times if there are failures. If opening fails once, the db directory
|
||||||
|
@ -20,7 +20,6 @@ use language::{
|
|||||||
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
|
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
|
||||||
use sum_tree::{Bias, TreeMap};
|
use sum_tree::{Bias, TreeMap};
|
||||||
use tab_map::TabMap;
|
use tab_map::TabMap;
|
||||||
use text::Rope;
|
|
||||||
use wrap_map::WrapMap;
|
use wrap_map::WrapMap;
|
||||||
|
|
||||||
pub use block_map::{
|
pub use block_map::{
|
||||||
@ -28,7 +27,7 @@ pub use block_map::{
|
|||||||
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use self::inlay_map::{Inlay, InlayProperties};
|
pub use self::inlay_map::Inlay;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum FoldStatus {
|
pub enum FoldStatus {
|
||||||
@ -246,10 +245,10 @@ impl DisplayMap {
|
|||||||
self.inlay_map.current_inlays()
|
self.inlay_map.current_inlays()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn splice_inlays<T: Into<Rope>>(
|
pub fn splice_inlays(
|
||||||
&mut self,
|
&mut self,
|
||||||
to_remove: Vec<InlayId>,
|
to_remove: Vec<InlayId>,
|
||||||
to_insert: Vec<(InlayId, InlayProperties<T>)>,
|
to_insert: Vec<Inlay>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
if to_remove.is_empty() && to_insert.is_empty() {
|
if to_remove.is_empty() && to_insert.is_empty() {
|
||||||
|
@ -2,9 +2,9 @@ use crate::{
|
|||||||
multi_buffer::{MultiBufferChunks, MultiBufferRows},
|
multi_buffer::{MultiBufferChunks, MultiBufferRows},
|
||||||
Anchor, InlayId, MultiBufferSnapshot, ToOffset,
|
Anchor, InlayId, MultiBufferSnapshot, ToOffset,
|
||||||
};
|
};
|
||||||
use collections::{BTreeMap, BTreeSet, HashMap};
|
use collections::{BTreeMap, BTreeSet};
|
||||||
use gpui::fonts::HighlightStyle;
|
use gpui::fonts::HighlightStyle;
|
||||||
use language::{Chunk, Edit, Point, Rope, TextSummary};
|
use language::{Chunk, Edit, Point, TextSummary};
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
cmp,
|
cmp,
|
||||||
@ -13,13 +13,12 @@ use std::{
|
|||||||
vec,
|
vec,
|
||||||
};
|
};
|
||||||
use sum_tree::{Bias, Cursor, SumTree};
|
use sum_tree::{Bias, Cursor, SumTree};
|
||||||
use text::Patch;
|
use text::{Patch, Rope};
|
||||||
|
|
||||||
use super::TextHighlights;
|
use super::TextHighlights;
|
||||||
|
|
||||||
pub struct InlayMap {
|
pub struct InlayMap {
|
||||||
snapshot: InlaySnapshot,
|
snapshot: InlaySnapshot,
|
||||||
inlays_by_id: HashMap<InlayId, Inlay>,
|
|
||||||
inlays: Vec<Inlay>,
|
inlays: Vec<Inlay>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,10 +42,29 @@ pub struct Inlay {
|
|||||||
pub text: text::Rope,
|
pub text: text::Rope,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
impl Inlay {
|
||||||
pub struct InlayProperties<T> {
|
pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self {
|
||||||
pub position: Anchor,
|
let mut text = hint.text();
|
||||||
pub text: T,
|
if hint.padding_right && !text.ends_with(' ') {
|
||||||
|
text.push(' ');
|
||||||
|
}
|
||||||
|
if hint.padding_left && !text.starts_with(' ') {
|
||||||
|
text.insert(0, ' ');
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
id: InlayId::Hint(id),
|
||||||
|
position,
|
||||||
|
text: text.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn suggestion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
|
||||||
|
Self {
|
||||||
|
id: InlayId::Suggestion(id),
|
||||||
|
position,
|
||||||
|
text: text.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl sum_tree::Item for Transform {
|
impl sum_tree::Item for Transform {
|
||||||
@ -368,7 +386,6 @@ impl InlayMap {
|
|||||||
(
|
(
|
||||||
Self {
|
Self {
|
||||||
snapshot: snapshot.clone(),
|
snapshot: snapshot.clone(),
|
||||||
inlays_by_id: HashMap::default(),
|
|
||||||
inlays: Vec::new(),
|
inlays: Vec::new(),
|
||||||
},
|
},
|
||||||
snapshot,
|
snapshot,
|
||||||
@ -510,45 +527,40 @@ impl InlayMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn splice<T: Into<Rope>>(
|
pub fn splice(
|
||||||
&mut self,
|
&mut self,
|
||||||
to_remove: Vec<InlayId>,
|
to_remove: Vec<InlayId>,
|
||||||
to_insert: Vec<(InlayId, InlayProperties<T>)>,
|
to_insert: Vec<Inlay>,
|
||||||
) -> (InlaySnapshot, Vec<InlayEdit>) {
|
) -> (InlaySnapshot, Vec<InlayEdit>) {
|
||||||
let snapshot = &mut self.snapshot;
|
let snapshot = &mut self.snapshot;
|
||||||
let mut edits = BTreeSet::new();
|
let mut edits = BTreeSet::new();
|
||||||
|
|
||||||
self.inlays.retain(|inlay| !to_remove.contains(&inlay.id));
|
self.inlays.retain(|inlay| {
|
||||||
for inlay_id in to_remove {
|
let retain = !to_remove.contains(&inlay.id);
|
||||||
if let Some(inlay) = self.inlays_by_id.remove(&inlay_id) {
|
if !retain {
|
||||||
let offset = inlay.position.to_offset(&snapshot.buffer);
|
let offset = inlay.position.to_offset(&snapshot.buffer);
|
||||||
edits.insert(offset);
|
edits.insert(offset);
|
||||||
}
|
}
|
||||||
}
|
retain
|
||||||
|
});
|
||||||
for (existing_id, properties) in to_insert {
|
|
||||||
let inlay = Inlay {
|
|
||||||
id: existing_id,
|
|
||||||
position: properties.position,
|
|
||||||
text: properties.text.into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
for inlay_to_insert in to_insert {
|
||||||
// Avoid inserting empty inlays.
|
// Avoid inserting empty inlays.
|
||||||
if inlay.text.is_empty() {
|
if inlay_to_insert.text.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.inlays_by_id.insert(inlay.id, inlay.clone());
|
let offset = inlay_to_insert.position.to_offset(&snapshot.buffer);
|
||||||
match self
|
match self.inlays.binary_search_by(|probe| {
|
||||||
.inlays
|
probe
|
||||||
.binary_search_by(|probe| probe.position.cmp(&inlay.position, &snapshot.buffer))
|
.position
|
||||||
{
|
.cmp(&inlay_to_insert.position, &snapshot.buffer)
|
||||||
|
}) {
|
||||||
Ok(ix) | Err(ix) => {
|
Ok(ix) | Err(ix) => {
|
||||||
self.inlays.insert(ix, inlay.clone());
|
self.inlays.insert(ix, inlay_to_insert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = inlay.position.to_offset(&snapshot.buffer);
|
|
||||||
edits.insert(offset);
|
edits.insert(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -606,15 +618,19 @@ impl InlayMap {
|
|||||||
} else {
|
} else {
|
||||||
InlayId::Suggestion(post_inc(next_inlay_id))
|
InlayId::Suggestion(post_inc(next_inlay_id))
|
||||||
};
|
};
|
||||||
to_insert.push((
|
to_insert.push(Inlay {
|
||||||
inlay_id,
|
id: inlay_id,
|
||||||
InlayProperties {
|
position: snapshot.buffer.anchor_at(position, bias),
|
||||||
position: snapshot.buffer.anchor_at(position, bias),
|
text: text.into(),
|
||||||
text,
|
});
|
||||||
},
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
to_remove.push(*self.inlays_by_id.keys().choose(rng).unwrap());
|
to_remove.push(
|
||||||
|
self.inlays
|
||||||
|
.iter()
|
||||||
|
.choose(rng)
|
||||||
|
.map(|inlay| inlay.id)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log::info!("removing inlays: {:?}", to_remove);
|
log::info!("removing inlays: {:?}", to_remove);
|
||||||
@ -1095,6 +1111,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{InlayId, MultiBuffer};
|
use crate::{InlayId, MultiBuffer};
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
|
use project::{InlayHint, InlayHintLabel};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{cmp::Reverse, env, sync::Arc};
|
use std::{cmp::Reverse, env, sync::Arc};
|
||||||
@ -1102,6 +1119,89 @@ mod tests {
|
|||||||
use text::Patch;
|
use text::Patch;
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inlay_properties_label_padding() {
|
||||||
|
assert_eq!(
|
||||||
|
Inlay::hint(
|
||||||
|
0,
|
||||||
|
Anchor::min(),
|
||||||
|
&InlayHint {
|
||||||
|
label: InlayHintLabel::String("a".to_string()),
|
||||||
|
buffer_id: 0,
|
||||||
|
position: text::Anchor::default(),
|
||||||
|
padding_left: false,
|
||||||
|
padding_right: false,
|
||||||
|
tooltip: None,
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.text
|
||||||
|
.to_string(),
|
||||||
|
"a",
|
||||||
|
"Should not pad label if not requested"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Inlay::hint(
|
||||||
|
0,
|
||||||
|
Anchor::min(),
|
||||||
|
&InlayHint {
|
||||||
|
label: InlayHintLabel::String("a".to_string()),
|
||||||
|
buffer_id: 0,
|
||||||
|
position: text::Anchor::default(),
|
||||||
|
padding_left: true,
|
||||||
|
padding_right: true,
|
||||||
|
tooltip: None,
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.text
|
||||||
|
.to_string(),
|
||||||
|
" a ",
|
||||||
|
"Should pad label for every side requested"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Inlay::hint(
|
||||||
|
0,
|
||||||
|
Anchor::min(),
|
||||||
|
&InlayHint {
|
||||||
|
label: InlayHintLabel::String(" a ".to_string()),
|
||||||
|
buffer_id: 0,
|
||||||
|
position: text::Anchor::default(),
|
||||||
|
padding_left: false,
|
||||||
|
padding_right: false,
|
||||||
|
tooltip: None,
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.text
|
||||||
|
.to_string(),
|
||||||
|
" a ",
|
||||||
|
"Should not change already padded label"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Inlay::hint(
|
||||||
|
0,
|
||||||
|
Anchor::min(),
|
||||||
|
&InlayHint {
|
||||||
|
label: InlayHintLabel::String(" a ".to_string()),
|
||||||
|
buffer_id: 0,
|
||||||
|
position: text::Anchor::default(),
|
||||||
|
padding_left: true,
|
||||||
|
padding_right: true,
|
||||||
|
tooltip: None,
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.text
|
||||||
|
.to_string(),
|
||||||
|
" a ",
|
||||||
|
"Should not change already padded label"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_basic_inlays(cx: &mut AppContext) {
|
fn test_basic_inlays(cx: &mut AppContext) {
|
||||||
let buffer = MultiBuffer::build_simple("abcdefghi", cx);
|
let buffer = MultiBuffer::build_simple("abcdefghi", cx);
|
||||||
@ -1112,13 +1212,11 @@ mod tests {
|
|||||||
|
|
||||||
let (inlay_snapshot, _) = inlay_map.splice(
|
let (inlay_snapshot, _) = inlay_map.splice(
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![(
|
vec![Inlay {
|
||||||
InlayId::Hint(post_inc(&mut next_inlay_id)),
|
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
|
||||||
InlayProperties {
|
position: buffer.read(cx).snapshot(cx).anchor_after(3),
|
||||||
position: buffer.read(cx).snapshot(cx).anchor_after(3),
|
text: "|123|".into(),
|
||||||
text: "|123|",
|
}],
|
||||||
},
|
|
||||||
)],
|
|
||||||
);
|
);
|
||||||
assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
|
assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -1191,20 +1289,16 @@ mod tests {
|
|||||||
let (inlay_snapshot, _) = inlay_map.splice(
|
let (inlay_snapshot, _) = inlay_map.splice(
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
(
|
Inlay {
|
||||||
InlayId::Hint(post_inc(&mut next_inlay_id)),
|
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
|
||||||
InlayProperties {
|
position: buffer.read(cx).snapshot(cx).anchor_before(3),
|
||||||
position: buffer.read(cx).snapshot(cx).anchor_before(3),
|
text: "|123|".into(),
|
||||||
text: "|123|",
|
},
|
||||||
},
|
Inlay {
|
||||||
),
|
id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
|
||||||
(
|
position: buffer.read(cx).snapshot(cx).anchor_after(3),
|
||||||
InlayId::Suggestion(post_inc(&mut next_inlay_id)),
|
text: "|456|".into(),
|
||||||
InlayProperties {
|
},
|
||||||
position: buffer.read(cx).snapshot(cx).anchor_after(3),
|
|
||||||
text: "|456|",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
|
assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
|
||||||
@ -1389,8 +1483,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// The inlays can be manually removed.
|
// The inlays can be manually removed.
|
||||||
let (inlay_snapshot, _) = inlay_map
|
let (inlay_snapshot, _) = inlay_map.splice(
|
||||||
.splice::<String>(inlay_map.inlays_by_id.keys().copied().collect(), Vec::new());
|
inlay_map.inlays.iter().map(|inlay| inlay.id).collect(),
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
|
assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1404,27 +1500,21 @@ mod tests {
|
|||||||
let (inlay_snapshot, _) = inlay_map.splice(
|
let (inlay_snapshot, _) = inlay_map.splice(
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
(
|
Inlay {
|
||||||
InlayId::Hint(post_inc(&mut next_inlay_id)),
|
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
|
||||||
InlayProperties {
|
position: buffer.read(cx).snapshot(cx).anchor_before(0),
|
||||||
position: buffer.read(cx).snapshot(cx).anchor_before(0),
|
text: "|123|\n".into(),
|
||||||
text: "|123|\n",
|
},
|
||||||
},
|
Inlay {
|
||||||
),
|
id: InlayId::Hint(post_inc(&mut next_inlay_id)),
|
||||||
(
|
position: buffer.read(cx).snapshot(cx).anchor_before(4),
|
||||||
InlayId::Hint(post_inc(&mut next_inlay_id)),
|
text: "|456|".into(),
|
||||||
InlayProperties {
|
},
|
||||||
position: buffer.read(cx).snapshot(cx).anchor_before(4),
|
Inlay {
|
||||||
text: "|456|",
|
id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
|
||||||
},
|
position: buffer.read(cx).snapshot(cx).anchor_before(7),
|
||||||
),
|
text: "\n|567|\n".into(),
|
||||||
(
|
},
|
||||||
InlayId::Suggestion(post_inc(&mut next_inlay_id)),
|
|
||||||
InlayProperties {
|
|
||||||
position: buffer.read(cx).snapshot(cx).anchor_before(7),
|
|
||||||
text: "\n|567|\n",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
|
assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
|
||||||
@ -1514,7 +1604,7 @@ mod tests {
|
|||||||
(offset, inlay.clone())
|
(offset, inlay.clone())
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut expected_text = Rope::from(buffer_snapshot.text().as_str());
|
let mut expected_text = Rope::from(buffer_snapshot.text());
|
||||||
for (offset, inlay) in inlays.into_iter().rev() {
|
for (offset, inlay) in inlays.into_iter().rev() {
|
||||||
expected_text.replace(offset..offset, &inlay.text.to_string());
|
expected_text.replace(offset..offset, &inlay.text.to_string());
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ use aho_corasick::AhoCorasick;
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use blink_manager::BlinkManager;
|
use blink_manager::BlinkManager;
|
||||||
use client::{ClickhouseEvent, TelemetrySettings};
|
use client::{ClickhouseEvent, TelemetrySettings};
|
||||||
use clock::ReplicaId;
|
use clock::{Global, ReplicaId};
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||||
use copilot::Copilot;
|
use copilot::Copilot;
|
||||||
pub use display_map::DisplayPoint;
|
pub use display_map::DisplayPoint;
|
||||||
@ -190,6 +190,15 @@ pub enum InlayId {
|
|||||||
Hint(usize),
|
Hint(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl InlayId {
|
||||||
|
fn id(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Suggestion(id) => *id,
|
||||||
|
Self::Hint(id) => *id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
editor,
|
editor,
|
||||||
[
|
[
|
||||||
@ -1195,11 +1204,11 @@ enum GotoDefinitionKind {
|
|||||||
Type,
|
Type,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum InlayRefreshReason {
|
enum InlayRefreshReason {
|
||||||
SettingsChange(InlayHintSettings),
|
SettingsChange(InlayHintSettings),
|
||||||
NewLinesShown,
|
NewLinesShown,
|
||||||
ExcerptEdited,
|
BufferEdited(HashSet<Arc<Language>>),
|
||||||
RefreshRequested,
|
RefreshRequested,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2026,6 +2035,7 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let selections = self.selections.all_adjusted(cx);
|
let selections = self.selections.all_adjusted(cx);
|
||||||
|
let mut brace_inserted = false;
|
||||||
let mut edits = Vec::new();
|
let mut edits = Vec::new();
|
||||||
let mut new_selections = Vec::with_capacity(selections.len());
|
let mut new_selections = Vec::with_capacity(selections.len());
|
||||||
let mut new_autoclose_regions = Vec::new();
|
let mut new_autoclose_regions = Vec::new();
|
||||||
@ -2084,6 +2094,7 @@ impl Editor {
|
|||||||
selection.range(),
|
selection.range(),
|
||||||
format!("{}{}", text, bracket_pair.end).into(),
|
format!("{}{}", text, bracket_pair.end).into(),
|
||||||
));
|
));
|
||||||
|
brace_inserted = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2110,6 +2121,7 @@ impl Editor {
|
|||||||
selection.end..selection.end,
|
selection.end..selection.end,
|
||||||
bracket_pair.end.as_str().into(),
|
bracket_pair.end.as_str().into(),
|
||||||
));
|
));
|
||||||
|
brace_inserted = true;
|
||||||
new_selections.push((
|
new_selections.push((
|
||||||
Selection {
|
Selection {
|
||||||
id: selection.id,
|
id: selection.id,
|
||||||
@ -2177,8 +2189,7 @@ impl Editor {
|
|||||||
let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
|
let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||||
|
|
||||||
// When buffer contents is updated and caret is moved, try triggering on type formatting.
|
if !brace_inserted && settings::get::<EditorSettings>(cx).use_on_type_format {
|
||||||
if settings::get::<EditorSettings>(cx).use_on_type_format {
|
|
||||||
if let Some(on_type_format_task) =
|
if let Some(on_type_format_task) =
|
||||||
this.trigger_on_type_formatting(text.to_string(), cx)
|
this.trigger_on_type_formatting(text.to_string(), cx)
|
||||||
{
|
{
|
||||||
@ -2617,7 +2628,7 @@ impl Editor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let invalidate_cache = match reason {
|
let (invalidate_cache, required_languages) = match reason {
|
||||||
InlayRefreshReason::SettingsChange(new_settings) => {
|
InlayRefreshReason::SettingsChange(new_settings) => {
|
||||||
match self.inlay_hint_cache.update_settings(
|
match self.inlay_hint_cache.update_settings(
|
||||||
&self.buffer,
|
&self.buffer,
|
||||||
@ -2633,16 +2644,18 @@ impl Editor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ControlFlow::Break(None) => return,
|
ControlFlow::Break(None) => return,
|
||||||
ControlFlow::Continue(()) => InvalidationStrategy::RefreshRequested,
|
ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InlayRefreshReason::NewLinesShown => InvalidationStrategy::None,
|
InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
|
||||||
InlayRefreshReason::ExcerptEdited => InvalidationStrategy::ExcerptEdited,
|
InlayRefreshReason::BufferEdited(buffer_languages) => {
|
||||||
InlayRefreshReason::RefreshRequested => InvalidationStrategy::RefreshRequested,
|
(InvalidationStrategy::BufferEdited, Some(buffer_languages))
|
||||||
|
}
|
||||||
|
InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.inlay_hint_cache.refresh_inlay_hints(
|
self.inlay_hint_cache.refresh_inlay_hints(
|
||||||
self.excerpt_visible_offsets(cx),
|
self.excerpt_visible_offsets(required_languages.as_ref(), cx),
|
||||||
invalidate_cache,
|
invalidate_cache,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@ -2661,8 +2674,9 @@ impl Editor {
|
|||||||
|
|
||||||
fn excerpt_visible_offsets(
|
fn excerpt_visible_offsets(
|
||||||
&self,
|
&self,
|
||||||
|
restrict_to_languages: Option<&HashSet<Arc<Language>>>,
|
||||||
cx: &mut ViewContext<'_, '_, Editor>,
|
cx: &mut ViewContext<'_, '_, Editor>,
|
||||||
) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)> {
|
) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
|
||||||
let multi_buffer = self.buffer().read(cx);
|
let multi_buffer = self.buffer().read(cx);
|
||||||
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
||||||
let multi_buffer_visible_start = self
|
let multi_buffer_visible_start = self
|
||||||
@ -2680,8 +2694,22 @@ impl Editor {
|
|||||||
.range_to_buffer_ranges(multi_buffer_visible_range, cx)
|
.range_to_buffer_ranges(multi_buffer_visible_range, cx)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
|
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
|
||||||
.map(|(buffer, excerpt_visible_range, excerpt_id)| {
|
.filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
|
||||||
(excerpt_id, (buffer, excerpt_visible_range))
|
let buffer = buffer_handle.read(cx);
|
||||||
|
let language = buffer.language()?;
|
||||||
|
if let Some(restrict_to_languages) = restrict_to_languages {
|
||||||
|
if !restrict_to_languages.contains(language) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((
|
||||||
|
excerpt_id,
|
||||||
|
(
|
||||||
|
buffer_handle,
|
||||||
|
buffer.version().clone(),
|
||||||
|
excerpt_visible_range,
|
||||||
|
),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@ -2689,26 +2717,11 @@ impl Editor {
|
|||||||
fn splice_inlay_hints(
|
fn splice_inlay_hints(
|
||||||
&self,
|
&self,
|
||||||
to_remove: Vec<InlayId>,
|
to_remove: Vec<InlayId>,
|
||||||
to_insert: Vec<(Anchor, InlayId, project::InlayHint)>,
|
to_insert: Vec<Inlay>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let buffer = self.buffer.read(cx).read(cx);
|
|
||||||
let new_inlays = to_insert
|
|
||||||
.into_iter()
|
|
||||||
.map(|(position, id, hint)| {
|
|
||||||
let mut text = hint.text();
|
|
||||||
if hint.padding_right {
|
|
||||||
text.push(' ');
|
|
||||||
}
|
|
||||||
if hint.padding_left {
|
|
||||||
text.insert(0, ' ');
|
|
||||||
}
|
|
||||||
(id, InlayProperties { position, text })
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
drop(buffer);
|
|
||||||
self.display_map.update(cx, |display_map, cx| {
|
self.display_map.update(cx, |display_map, cx| {
|
||||||
display_map.splice_inlays(to_remove, new_inlays, cx);
|
display_map.splice_inlays(to_remove, to_insert, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3393,7 +3406,7 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.display_map.update(cx, |map, cx| {
|
self.display_map.update(cx, |map, cx| {
|
||||||
map.splice_inlays::<&str>(vec![suggestion.id], Vec::new(), cx)
|
map.splice_inlays(vec![suggestion.id], Vec::new(), cx)
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
true
|
true
|
||||||
@ -3426,7 +3439,7 @@ impl Editor {
|
|||||||
fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
|
fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
|
||||||
let suggestion = self.copilot_state.suggestion.take()?;
|
let suggestion = self.copilot_state.suggestion.take()?;
|
||||||
self.display_map.update(cx, |map, cx| {
|
self.display_map.update(cx, |map, cx| {
|
||||||
map.splice_inlays::<&str>(vec![suggestion.id], Default::default(), cx);
|
map.splice_inlays(vec![suggestion.id], Default::default(), cx);
|
||||||
});
|
});
|
||||||
let buffer = self.buffer.read(cx).read(cx);
|
let buffer = self.buffer.read(cx).read(cx);
|
||||||
|
|
||||||
@ -3457,21 +3470,11 @@ impl Editor {
|
|||||||
to_remove.push(suggestion.id);
|
to_remove.push(suggestion.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let suggestion_inlay_id = InlayId::Suggestion(post_inc(&mut self.next_inlay_id));
|
let suggestion_inlay =
|
||||||
let to_insert = vec![(
|
Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
|
||||||
suggestion_inlay_id,
|
self.copilot_state.suggestion = Some(suggestion_inlay.clone());
|
||||||
InlayProperties {
|
|
||||||
position: cursor,
|
|
||||||
text: text.clone(),
|
|
||||||
},
|
|
||||||
)];
|
|
||||||
self.display_map.update(cx, move |map, cx| {
|
self.display_map.update(cx, move |map, cx| {
|
||||||
map.splice_inlays(to_remove, to_insert, cx)
|
map.splice_inlays(to_remove, vec![suggestion_inlay], cx)
|
||||||
});
|
|
||||||
self.copilot_state.suggestion = Some(Inlay {
|
|
||||||
id: suggestion_inlay_id,
|
|
||||||
position: cursor,
|
|
||||||
text,
|
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
} else {
|
} else {
|
||||||
@ -7256,7 +7259,7 @@ impl Editor {
|
|||||||
|
|
||||||
fn on_buffer_event(
|
fn on_buffer_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: ModelHandle<MultiBuffer>,
|
multibuffer: ModelHandle<MultiBuffer>,
|
||||||
event: &multi_buffer::Event,
|
event: &multi_buffer::Event,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
@ -7268,7 +7271,33 @@ impl Editor {
|
|||||||
self.update_visible_copilot_suggestion(cx);
|
self.update_visible_copilot_suggestion(cx);
|
||||||
}
|
}
|
||||||
cx.emit(Event::BufferEdited);
|
cx.emit(Event::BufferEdited);
|
||||||
self.refresh_inlays(InlayRefreshReason::ExcerptEdited, cx);
|
|
||||||
|
if let Some(project) = &self.project {
|
||||||
|
let project = project.read(cx);
|
||||||
|
let languages_affected = multibuffer
|
||||||
|
.read(cx)
|
||||||
|
.all_buffers()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|buffer| {
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let language = buffer.language()?;
|
||||||
|
if project.is_local()
|
||||||
|
&& project.language_servers_for_buffer(buffer, cx).count() == 0
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(language)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
if !languages_affected.is_empty() {
|
||||||
|
self.refresh_inlays(
|
||||||
|
InlayRefreshReason::BufferEdited(languages_affected),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
multi_buffer::Event::ExcerptsAdded {
|
multi_buffer::Event::ExcerptsAdded {
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -15,6 +15,7 @@ pub struct EditorSettings {
|
|||||||
pub struct Scrollbar {
|
pub struct Scrollbar {
|
||||||
pub show: ShowScrollbar,
|
pub show: ShowScrollbar,
|
||||||
pub git_diff: bool,
|
pub git_diff: bool,
|
||||||
|
pub selections: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||||
@ -39,6 +40,7 @@ pub struct EditorSettingsContent {
|
|||||||
pub struct ScrollbarContent {
|
pub struct ScrollbarContent {
|
||||||
pub show: Option<ShowScrollbar>,
|
pub show: Option<ShowScrollbar>,
|
||||||
pub git_diff: Option<bool>,
|
pub git_diff: Option<bool>,
|
||||||
|
pub selections: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Setting for EditorSettings {
|
impl Setting for EditorSettings {
|
||||||
|
@ -6979,6 +6979,111 @@ async fn test_copilot_disabled_globs(
|
|||||||
assert!(copilot_requests.try_next().is_ok());
|
assert!(copilot_requests.try_next().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
|
||||||
|
let mut language = Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Rust".into(),
|
||||||
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
brackets: BracketPairConfig {
|
||||||
|
pairs: vec![BracketPair {
|
||||||
|
start: "{".to_string(),
|
||||||
|
end: "}".to_string(),
|
||||||
|
close: true,
|
||||||
|
newline: true,
|
||||||
|
}],
|
||||||
|
disabled_scopes_by_bracket_ix: Vec::new(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_rust::language()),
|
||||||
|
);
|
||||||
|
let mut fake_servers = language
|
||||||
|
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||||
|
capabilities: lsp::ServerCapabilities {
|
||||||
|
document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
|
||||||
|
first_trigger_character: "{".to_string(),
|
||||||
|
more_trigger_character: None,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/a",
|
||||||
|
json!({
|
||||||
|
"main.rs": "fn main() { let a = 5; }",
|
||||||
|
"other.rs": "// Test file",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||||||
|
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||||
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
let worktree_id = workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.project().read_with(cx, |project, cx| {
|
||||||
|
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_local_buffer("/a/main.rs", cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
cx.foreground().start_waiting();
|
||||||
|
let fake_server = fake_servers.next().await.unwrap();
|
||||||
|
let editor_handle = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
|
||||||
|
assert_eq!(
|
||||||
|
params.text_document_position.text_document.uri,
|
||||||
|
lsp::Url::from_file_path("/a/main.rs").unwrap(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
params.text_document_position.position,
|
||||||
|
lsp::Position::new(0, 21),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Some(vec![lsp::TextEdit {
|
||||||
|
new_text: "]".to_string(),
|
||||||
|
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
|
||||||
|
}]))
|
||||||
|
});
|
||||||
|
|
||||||
|
editor_handle.update(cx, |editor, cx| {
|
||||||
|
cx.focus(&editor_handle);
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
|
||||||
|
});
|
||||||
|
editor.handle_input("{", cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
|
buffer.read_with(cx, |buffer, _| {
|
||||||
|
assert_eq!(
|
||||||
|
buffer.text(),
|
||||||
|
"fn main() { let a = {5}; }",
|
||||||
|
"No extra braces from on type formatting should appear in the buffer"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
@ -1008,6 +1008,7 @@ impl EditorElement {
|
|||||||
bounds: RectF,
|
bounds: RectF,
|
||||||
layout: &mut LayoutState,
|
layout: &mut LayoutState,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
|
editor: &Editor,
|
||||||
) {
|
) {
|
||||||
enum ScrollbarMouseHandlers {}
|
enum ScrollbarMouseHandlers {}
|
||||||
if layout.mode != EditorMode::Full {
|
if layout.mode != EditorMode::Full {
|
||||||
@ -1050,9 +1051,74 @@ impl EditorElement {
|
|||||||
background: style.track.background_color,
|
background: style.track.background_color,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
let scrollbar_settings = settings::get::<EditorSettings>(cx).scrollbar;
|
||||||
|
let theme = theme::current(cx);
|
||||||
|
let scrollbar_theme = &theme.editor.scrollbar;
|
||||||
|
if layout.is_singleton && scrollbar_settings.selections {
|
||||||
|
let start_anchor = Anchor::min();
|
||||||
|
let end_anchor = Anchor::max();
|
||||||
|
let mut start_row = None;
|
||||||
|
let mut end_row = None;
|
||||||
|
let color = scrollbar_theme.selections;
|
||||||
|
let border = Border {
|
||||||
|
width: 1.,
|
||||||
|
color: style.thumb.border.color,
|
||||||
|
overlay: false,
|
||||||
|
top: false,
|
||||||
|
right: true,
|
||||||
|
bottom: false,
|
||||||
|
left: true,
|
||||||
|
};
|
||||||
|
let mut push_region = |start, end| {
|
||||||
|
if let (Some(start_display), Some(end_display)) = (start, end) {
|
||||||
|
let start_y = y_for_row(start_display as f32);
|
||||||
|
let mut end_y = y_for_row(end_display as f32);
|
||||||
|
if end_y - start_y < 1. {
|
||||||
|
end_y = start_y + 1.;
|
||||||
|
}
|
||||||
|
let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
|
||||||
|
|
||||||
if layout.is_singleton && settings::get::<EditorSettings>(cx).scrollbar.git_diff {
|
scene.push_quad(Quad {
|
||||||
let diff_style = theme::current(cx).editor.scrollbar.git.clone();
|
bounds,
|
||||||
|
background: Some(color),
|
||||||
|
border,
|
||||||
|
corner_radius: style.thumb.corner_radius,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (row, _) in &editor.background_highlights_in_range(
|
||||||
|
start_anchor..end_anchor,
|
||||||
|
&layout.position_map.snapshot,
|
||||||
|
&theme,
|
||||||
|
) {
|
||||||
|
let start_display = row.start;
|
||||||
|
let end_display = row.end;
|
||||||
|
|
||||||
|
if start_row.is_none() {
|
||||||
|
assert_eq!(end_row, None);
|
||||||
|
start_row = Some(start_display.row());
|
||||||
|
end_row = Some(end_display.row());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(current_end) = end_row.as_mut() {
|
||||||
|
if start_display.row() > *current_end + 1 {
|
||||||
|
push_region(start_row, end_row);
|
||||||
|
start_row = Some(start_display.row());
|
||||||
|
end_row = Some(end_display.row());
|
||||||
|
} else {
|
||||||
|
// Merge two hunks.
|
||||||
|
*current_end = end_display.row();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We might still have a hunk that was not rendered (if there was a search hit on the last line)
|
||||||
|
push_region(start_row, end_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if layout.is_singleton && scrollbar_settings.git_diff {
|
||||||
|
let diff_style = scrollbar_theme.git.clone();
|
||||||
for hunk in layout
|
for hunk in layout
|
||||||
.position_map
|
.position_map
|
||||||
.snapshot
|
.snapshot
|
||||||
@ -2368,7 +2434,7 @@ impl Element<Editor> for EditorElement {
|
|||||||
if !layout.blocks.is_empty() {
|
if !layout.blocks.is_empty() {
|
||||||
self.paint_blocks(scene, bounds, visible_bounds, layout, editor, cx);
|
self.paint_blocks(scene, bounds, visible_bounds, layout, editor, cx);
|
||||||
}
|
}
|
||||||
self.paint_scrollbar(scene, bounds, layout, cx);
|
self.paint_scrollbar(scene, bounds, layout, cx, &editor);
|
||||||
scene.pop_layer();
|
scene.pop_layer();
|
||||||
|
|
||||||
scene.pop_layer();
|
scene.pop_layer();
|
||||||
|
@ -38,14 +38,14 @@ pub struct CachedExcerptHints {
|
|||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum InvalidationStrategy {
|
pub enum InvalidationStrategy {
|
||||||
RefreshRequested,
|
RefreshRequested,
|
||||||
ExcerptEdited,
|
BufferEdited,
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct InlaySplice {
|
pub struct InlaySplice {
|
||||||
pub to_remove: Vec<InlayId>,
|
pub to_remove: Vec<InlayId>,
|
||||||
pub to_insert: Vec<(Anchor, InlayId, InlayHint)>,
|
pub to_insert: Vec<Inlay>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UpdateTask {
|
struct UpdateTask {
|
||||||
@ -94,7 +94,7 @@ impl InvalidationStrategy {
|
|||||||
fn should_invalidate(&self) -> bool {
|
fn should_invalidate(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
InvalidationStrategy::RefreshRequested | InvalidationStrategy::ExcerptEdited
|
InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,7 +197,7 @@ impl InlayHintCache {
|
|||||||
|
|
||||||
pub fn refresh_inlay_hints(
|
pub fn refresh_inlay_hints(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
|
mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
|
||||||
invalidate: InvalidationStrategy,
|
invalidate: InvalidationStrategy,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) {
|
) {
|
||||||
@ -285,13 +285,13 @@ impl InlayHintCache {
|
|||||||
if !old_kinds.contains(&cached_hint.kind)
|
if !old_kinds.contains(&cached_hint.kind)
|
||||||
&& new_kinds.contains(&cached_hint.kind)
|
&& new_kinds.contains(&cached_hint.kind)
|
||||||
{
|
{
|
||||||
to_insert.push((
|
to_insert.push(Inlay::hint(
|
||||||
|
cached_hint_id.id(),
|
||||||
multi_buffer_snapshot.anchor_in_excerpt(
|
multi_buffer_snapshot.anchor_in_excerpt(
|
||||||
*excerpt_id,
|
*excerpt_id,
|
||||||
cached_hint.position,
|
cached_hint.position,
|
||||||
),
|
),
|
||||||
*cached_hint_id,
|
&cached_hint,
|
||||||
cached_hint.clone(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
excerpt_cache.next();
|
excerpt_cache.next();
|
||||||
@ -307,11 +307,11 @@ impl InlayHintCache {
|
|||||||
for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
|
for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
|
||||||
let cached_hint_kind = maybe_missed_cached_hint.kind;
|
let cached_hint_kind = maybe_missed_cached_hint.kind;
|
||||||
if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
|
if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
|
||||||
to_insert.push((
|
to_insert.push(Inlay::hint(
|
||||||
|
cached_hint_id.id(),
|
||||||
multi_buffer_snapshot
|
multi_buffer_snapshot
|
||||||
.anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
|
.anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
|
||||||
*cached_hint_id,
|
&maybe_missed_cached_hint,
|
||||||
maybe_missed_cached_hint.clone(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -342,104 +342,113 @@ impl InlayHintCache {
|
|||||||
|
|
||||||
fn spawn_new_update_tasks(
|
fn spawn_new_update_tasks(
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
|
excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
|
||||||
invalidate: InvalidationStrategy,
|
invalidate: InvalidationStrategy,
|
||||||
update_cache_version: usize,
|
update_cache_version: usize,
|
||||||
cx: &mut ViewContext<'_, '_, Editor>,
|
cx: &mut ViewContext<'_, '_, Editor>,
|
||||||
) {
|
) {
|
||||||
let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
|
let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
|
||||||
for (excerpt_id, (buffer_handle, excerpt_visible_range)) in excerpts_to_query {
|
for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in
|
||||||
if !excerpt_visible_range.is_empty() {
|
excerpts_to_query
|
||||||
let buffer = buffer_handle.read(cx);
|
{
|
||||||
let buffer_snapshot = buffer.snapshot();
|
if excerpt_visible_range.is_empty() {
|
||||||
let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
|
continue;
|
||||||
if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
|
}
|
||||||
let new_task_buffer_version = buffer_snapshot.version();
|
let buffer = buffer_handle.read(cx);
|
||||||
let cached_excerpt_hints = cached_excerpt_hints.read();
|
let buffer_snapshot = buffer.snapshot();
|
||||||
let cached_buffer_version = &cached_excerpt_hints.buffer_version;
|
if buffer_snapshot
|
||||||
if cached_excerpt_hints.version > update_cache_version
|
.version()
|
||||||
|| cached_buffer_version.changed_since(new_task_buffer_version)
|
.changed_since(&new_task_buffer_version)
|
||||||
{
|
{
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
if !new_task_buffer_version.changed_since(&cached_buffer_version)
|
|
||||||
&& !matches!(invalidate, InvalidationStrategy::RefreshRequested)
|
let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
|
||||||
{
|
if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
|
||||||
return;
|
let cached_excerpt_hints = cached_excerpt_hints.read();
|
||||||
}
|
let cached_buffer_version = &cached_excerpt_hints.buffer_version;
|
||||||
|
if cached_excerpt_hints.version > update_cache_version
|
||||||
|
|| cached_buffer_version.changed_since(&new_task_buffer_version)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !new_task_buffer_version.changed_since(&cached_buffer_version)
|
||||||
|
&& !matches!(invalidate, InvalidationStrategy::RefreshRequested)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let buffer_id = buffer.remote_id();
|
||||||
|
let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
|
||||||
|
let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
|
||||||
|
|
||||||
|
let (multi_buffer_snapshot, full_excerpt_range) =
|
||||||
|
editor.buffer.update(cx, |multi_buffer, cx| {
|
||||||
|
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
||||||
|
(
|
||||||
|
multi_buffer_snapshot,
|
||||||
|
multi_buffer
|
||||||
|
.excerpts_for_buffer(&buffer_handle, cx)
|
||||||
|
.into_iter()
|
||||||
|
.find(|(id, _)| id == &excerpt_id)
|
||||||
|
.map(|(_, range)| range.context),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(full_excerpt_range) = full_excerpt_range {
|
||||||
|
let query = ExcerptQuery {
|
||||||
|
buffer_id,
|
||||||
|
excerpt_id,
|
||||||
|
dimensions: ExcerptDimensions {
|
||||||
|
excerpt_range_start: full_excerpt_range.start,
|
||||||
|
excerpt_range_end: full_excerpt_range.end,
|
||||||
|
excerpt_visible_range_start,
|
||||||
|
excerpt_visible_range_end,
|
||||||
|
},
|
||||||
|
cache_version: update_cache_version,
|
||||||
|
invalidate,
|
||||||
};
|
};
|
||||||
|
|
||||||
let buffer_id = buffer.remote_id();
|
let new_update_task = |is_refresh_after_regular_task| {
|
||||||
let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
|
new_update_task(
|
||||||
let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
|
query,
|
||||||
|
multi_buffer_snapshot,
|
||||||
let (multi_buffer_snapshot, full_excerpt_range) =
|
buffer_snapshot,
|
||||||
editor.buffer.update(cx, |multi_buffer, cx| {
|
Arc::clone(&visible_hints),
|
||||||
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
cached_excerpt_hints,
|
||||||
(
|
is_refresh_after_regular_task,
|
||||||
multi_buffer_snapshot,
|
cx,
|
||||||
multi_buffer
|
)
|
||||||
.excerpts_for_buffer(&buffer_handle, cx)
|
};
|
||||||
.into_iter()
|
match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
|
||||||
.find(|(id, _)| id == &excerpt_id)
|
hash_map::Entry::Occupied(mut o) => {
|
||||||
.map(|(_, range)| range.context),
|
let update_task = o.get_mut();
|
||||||
)
|
match (update_task.invalidate, invalidate) {
|
||||||
});
|
(_, InvalidationStrategy::None) => {}
|
||||||
|
(
|
||||||
if let Some(full_excerpt_range) = full_excerpt_range {
|
InvalidationStrategy::BufferEdited,
|
||||||
let query = ExcerptQuery {
|
InvalidationStrategy::RefreshRequested,
|
||||||
buffer_id,
|
) if !update_task.task.is_running_rx.is_closed() => {
|
||||||
excerpt_id,
|
update_task.pending_refresh = Some(query);
|
||||||
dimensions: ExcerptDimensions {
|
}
|
||||||
excerpt_range_start: full_excerpt_range.start,
|
_ => {
|
||||||
excerpt_range_end: full_excerpt_range.end,
|
o.insert(UpdateTask {
|
||||||
excerpt_visible_range_start,
|
invalidate,
|
||||||
excerpt_visible_range_end,
|
cache_version: query.cache_version,
|
||||||
},
|
task: new_update_task(false),
|
||||||
cache_version: update_cache_version,
|
pending_refresh: None,
|
||||||
invalidate,
|
});
|
||||||
};
|
|
||||||
|
|
||||||
let new_update_task = |is_refresh_after_regular_task| {
|
|
||||||
new_update_task(
|
|
||||||
query,
|
|
||||||
multi_buffer_snapshot,
|
|
||||||
buffer_snapshot,
|
|
||||||
Arc::clone(&visible_hints),
|
|
||||||
cached_excerpt_hints,
|
|
||||||
is_refresh_after_regular_task,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
|
|
||||||
hash_map::Entry::Occupied(mut o) => {
|
|
||||||
let update_task = o.get_mut();
|
|
||||||
match (update_task.invalidate, invalidate) {
|
|
||||||
(_, InvalidationStrategy::None) => {}
|
|
||||||
(
|
|
||||||
InvalidationStrategy::ExcerptEdited,
|
|
||||||
InvalidationStrategy::RefreshRequested,
|
|
||||||
) if !update_task.task.is_running_rx.is_closed() => {
|
|
||||||
update_task.pending_refresh = Some(query);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
o.insert(UpdateTask {
|
|
||||||
invalidate,
|
|
||||||
cache_version: query.cache_version,
|
|
||||||
task: new_update_task(false),
|
|
||||||
pending_refresh: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hash_map::Entry::Vacant(v) => {
|
}
|
||||||
v.insert(UpdateTask {
|
hash_map::Entry::Vacant(v) => {
|
||||||
invalidate,
|
v.insert(UpdateTask {
|
||||||
cache_version: query.cache_version,
|
invalidate,
|
||||||
task: new_update_task(false),
|
cache_version: query.cache_version,
|
||||||
pending_refresh: None,
|
task: new_update_task(false),
|
||||||
});
|
pending_refresh: None,
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -648,18 +657,22 @@ async fn fetch_and_update_hints(
|
|||||||
for new_hint in new_update.add_to_cache {
|
for new_hint in new_update.add_to_cache {
|
||||||
let new_hint_position = multi_buffer_snapshot
|
let new_hint_position = multi_buffer_snapshot
|
||||||
.anchor_in_excerpt(query.excerpt_id, new_hint.position);
|
.anchor_in_excerpt(query.excerpt_id, new_hint.position);
|
||||||
let new_inlay_id = InlayId::Hint(post_inc(&mut editor.next_inlay_id));
|
let new_inlay_id = post_inc(&mut editor.next_inlay_id);
|
||||||
if editor
|
if editor
|
||||||
.inlay_hint_cache
|
.inlay_hint_cache
|
||||||
.allowed_hint_kinds
|
.allowed_hint_kinds
|
||||||
.contains(&new_hint.kind)
|
.contains(&new_hint.kind)
|
||||||
{
|
{
|
||||||
splice
|
splice.to_insert.push(Inlay::hint(
|
||||||
.to_insert
|
new_inlay_id,
|
||||||
.push((new_hint_position, new_inlay_id, new_hint.clone()));
|
new_hint_position,
|
||||||
|
&new_hint,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
cached_excerpt_hints.hints.push((new_inlay_id, new_hint));
|
cached_excerpt_hints
|
||||||
|
.hints
|
||||||
|
.push((InlayId::Hint(new_inlay_id), new_hint));
|
||||||
}
|
}
|
||||||
|
|
||||||
cached_excerpt_hints
|
cached_excerpt_hints
|
||||||
@ -820,7 +833,7 @@ mod tests {
|
|||||||
use crate::{
|
use crate::{
|
||||||
scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
|
scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
|
||||||
serde_json::json,
|
serde_json::json,
|
||||||
ExcerptRange, InlayHintSettings,
|
ExcerptRange,
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
|
use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
|
||||||
@ -961,6 +974,348 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx, |settings| {
|
||||||
|
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||||
|
enabled: true,
|
||||||
|
show_type_hints: true,
|
||||||
|
show_parameter_hints: true,
|
||||||
|
show_other_hints: true,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
|
||||||
|
let lsp_request_count = Arc::new(AtomicU32::new(0));
|
||||||
|
fake_server
|
||||||
|
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
||||||
|
let task_lsp_request_count = Arc::clone(&lsp_request_count);
|
||||||
|
async move {
|
||||||
|
assert_eq!(
|
||||||
|
params.text_document.uri,
|
||||||
|
lsp::Url::from_file_path(file_with_hints).unwrap(),
|
||||||
|
);
|
||||||
|
let current_call_id =
|
||||||
|
Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
|
||||||
|
Ok(Some(vec![lsp::InlayHint {
|
||||||
|
position: lsp::Position::new(0, current_call_id),
|
||||||
|
label: lsp::InlayHintLabel::String(current_call_id.to_string()),
|
||||||
|
kind: None,
|
||||||
|
text_edits: None,
|
||||||
|
tooltip: None,
|
||||||
|
padding_left: None,
|
||||||
|
padding_right: None,
|
||||||
|
data: None,
|
||||||
|
}]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.await;
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
|
let mut edits_made = 1;
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
let expected_layers = vec!["0".to_string()];
|
||||||
|
assert_eq!(
|
||||||
|
expected_layers,
|
||||||
|
cached_hint_labels(editor),
|
||||||
|
"Should get its first hints when opening the editor"
|
||||||
|
);
|
||||||
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
|
assert_eq!(
|
||||||
|
inlay_cache.version, edits_made,
|
||||||
|
"The editor update the cache version after every cache/view change"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let progress_token = "test_progress_token";
|
||||||
|
fake_server
|
||||||
|
.request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
|
||||||
|
token: lsp::ProgressToken::String(progress_token.to_string()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("work done progress create request failed");
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||||
|
token: lsp::ProgressToken::String(progress_token.to_string()),
|
||||||
|
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
|
||||||
|
lsp::WorkDoneProgressBegin::default(),
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
let expected_layers = vec!["0".to_string()];
|
||||||
|
assert_eq!(
|
||||||
|
expected_layers,
|
||||||
|
cached_hint_labels(editor),
|
||||||
|
"Should not update hints while the work task is running"
|
||||||
|
);
|
||||||
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
|
assert_eq!(
|
||||||
|
inlay_cache.version, edits_made,
|
||||||
|
"Should not update the cache while the work task is running"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||||
|
token: lsp::ProgressToken::String(progress_token.to_string()),
|
||||||
|
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
|
||||||
|
lsp::WorkDoneProgressEnd::default(),
|
||||||
|
)),
|
||||||
|
});
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
|
edits_made += 1;
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
let expected_layers = vec!["1".to_string()];
|
||||||
|
assert_eq!(
|
||||||
|
expected_layers,
|
||||||
|
cached_hint_labels(editor),
|
||||||
|
"New hints should be queried after the work task is done"
|
||||||
|
);
|
||||||
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
|
assert_eq!(
|
||||||
|
inlay_cache.version, edits_made,
|
||||||
|
"Cache version should udpate once after the work task is done"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx, |settings| {
|
||||||
|
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||||
|
enabled: true,
|
||||||
|
show_type_hints: true,
|
||||||
|
show_parameter_hints: true,
|
||||||
|
show_other_hints: true,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/a",
|
||||||
|
json!({
|
||||||
|
"main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
|
||||||
|
"other.md": "Test md file with some text",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||||||
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
let worktree_id = workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.project().read_with(cx, |project, cx| {
|
||||||
|
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut rs_fake_servers = None;
|
||||||
|
let mut md_fake_servers = None;
|
||||||
|
for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
|
||||||
|
let mut language = Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: name.into(),
|
||||||
|
path_suffixes: vec![path_suffix.to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_rust::language()),
|
||||||
|
);
|
||||||
|
let fake_servers = language
|
||||||
|
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||||
|
name,
|
||||||
|
capabilities: lsp::ServerCapabilities {
|
||||||
|
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
match name {
|
||||||
|
"Rust" => rs_fake_servers = Some(fake_servers),
|
||||||
|
"Markdown" => md_fake_servers = Some(fake_servers),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
project.update(cx, |project, _| {
|
||||||
|
project.languages().add(Arc::new(language));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let _rs_buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_local_buffer("/a/main.rs", cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
cx.foreground().start_waiting();
|
||||||
|
let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
|
||||||
|
let rs_editor = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
|
||||||
|
rs_fake_server
|
||||||
|
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
||||||
|
let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
|
||||||
|
async move {
|
||||||
|
assert_eq!(
|
||||||
|
params.text_document.uri,
|
||||||
|
lsp::Url::from_file_path("/a/main.rs").unwrap(),
|
||||||
|
);
|
||||||
|
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
|
||||||
|
Ok(Some(vec![lsp::InlayHint {
|
||||||
|
position: lsp::Position::new(0, i),
|
||||||
|
label: lsp::InlayHintLabel::String(i.to_string()),
|
||||||
|
kind: None,
|
||||||
|
text_edits: None,
|
||||||
|
tooltip: None,
|
||||||
|
padding_left: None,
|
||||||
|
padding_right: None,
|
||||||
|
data: None,
|
||||||
|
}]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.await;
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
rs_editor.update(cx, |editor, cx| {
|
||||||
|
let expected_layers = vec!["0".to_string()];
|
||||||
|
assert_eq!(
|
||||||
|
expected_layers,
|
||||||
|
cached_hint_labels(editor),
|
||||||
|
"Should get its first hints when opening the editor"
|
||||||
|
);
|
||||||
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
|
assert_eq!(
|
||||||
|
inlay_cache.version, 1,
|
||||||
|
"Rust editor update the cache version after every cache/view change"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
let _md_buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_local_buffer("/a/other.md", cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
cx.foreground().start_waiting();
|
||||||
|
let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
|
||||||
|
let md_editor = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id, "other.md"), None, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
let md_lsp_request_count = Arc::new(AtomicU32::new(0));
|
||||||
|
md_fake_server
|
||||||
|
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
||||||
|
let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
|
||||||
|
async move {
|
||||||
|
assert_eq!(
|
||||||
|
params.text_document.uri,
|
||||||
|
lsp::Url::from_file_path("/a/other.md").unwrap(),
|
||||||
|
);
|
||||||
|
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
|
||||||
|
Ok(Some(vec![lsp::InlayHint {
|
||||||
|
position: lsp::Position::new(0, i),
|
||||||
|
label: lsp::InlayHintLabel::String(i.to_string()),
|
||||||
|
kind: None,
|
||||||
|
text_edits: None,
|
||||||
|
tooltip: None,
|
||||||
|
padding_left: None,
|
||||||
|
padding_right: None,
|
||||||
|
data: None,
|
||||||
|
}]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.await;
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
md_editor.update(cx, |editor, cx| {
|
||||||
|
let expected_layers = vec!["0".to_string()];
|
||||||
|
assert_eq!(
|
||||||
|
expected_layers,
|
||||||
|
cached_hint_labels(editor),
|
||||||
|
"Markdown editor should have a separate verison, repeating Rust editor rules"
|
||||||
|
);
|
||||||
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
|
assert_eq!(inlay_cache.version, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
rs_editor.update(cx, |editor, cx| {
|
||||||
|
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||||
|
editor.handle_input("some rs change", cx);
|
||||||
|
});
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
rs_editor.update(cx, |editor, cx| {
|
||||||
|
let expected_layers = vec!["1".to_string()];
|
||||||
|
assert_eq!(
|
||||||
|
expected_layers,
|
||||||
|
cached_hint_labels(editor),
|
||||||
|
"Rust inlay cache should change after the edit"
|
||||||
|
);
|
||||||
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
|
assert_eq!(
|
||||||
|
inlay_cache.version, 2,
|
||||||
|
"Every time hint cache changes, cache version should be incremented"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
md_editor.update(cx, |editor, cx| {
|
||||||
|
let expected_layers = vec!["0".to_string()];
|
||||||
|
assert_eq!(
|
||||||
|
expected_layers,
|
||||||
|
cached_hint_labels(editor),
|
||||||
|
"Markdown editor should not be affected by Rust editor changes"
|
||||||
|
);
|
||||||
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
|
assert_eq!(inlay_cache.version, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
md_editor.update(cx, |editor, cx| {
|
||||||
|
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||||
|
editor.handle_input("some md change", cx);
|
||||||
|
});
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
md_editor.update(cx, |editor, cx| {
|
||||||
|
let expected_layers = vec!["1".to_string()];
|
||||||
|
assert_eq!(
|
||||||
|
expected_layers,
|
||||||
|
cached_hint_labels(editor),
|
||||||
|
"Rust editor should not be affected by Markdown editor changes"
|
||||||
|
);
|
||||||
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
|
assert_eq!(inlay_cache.version, 2);
|
||||||
|
});
|
||||||
|
rs_editor.update(cx, |editor, cx| {
|
||||||
|
let expected_layers = vec!["1".to_string()];
|
||||||
|
assert_eq!(
|
||||||
|
expected_layers,
|
||||||
|
cached_hint_labels(editor),
|
||||||
|
"Markdown editor should also change independently"
|
||||||
|
);
|
||||||
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
|
assert_eq!(inlay_cache.version, 2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
|
async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
|
||||||
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
|
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
|
||||||
@ -1079,7 +1434,6 @@ mod tests {
|
|||||||
visible_hint_labels(editor, cx)
|
visible_hint_labels(editor, cx)
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, edits_made,
|
inlay_cache.version, edits_made,
|
||||||
"Should not update cache version due to new loaded hints being the same"
|
"Should not update cache version due to new loaded hints being the same"
|
||||||
@ -1215,7 +1569,6 @@ mod tests {
|
|||||||
assert!(cached_hint_labels(editor).is_empty());
|
assert!(cached_hint_labels(editor).is_empty());
|
||||||
assert!(visible_hint_labels(editor, cx).is_empty());
|
assert!(visible_hint_labels(editor, cx).is_empty());
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, edits_made,
|
inlay_cache.version, edits_made,
|
||||||
"The editor should not update the cache version after /refresh query without updates"
|
"The editor should not update the cache version after /refresh query without updates"
|
||||||
@ -1289,20 +1642,18 @@ mod tests {
|
|||||||
visible_hint_labels(editor, cx),
|
visible_hint_labels(editor, cx),
|
||||||
);
|
);
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds);
|
|
||||||
assert_eq!(inlay_cache.version, edits_made);
|
assert_eq!(inlay_cache.version, edits_made);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
|
async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
|
||||||
let allowed_hint_kinds = HashSet::from_iter([None]);
|
|
||||||
init_test(cx, |settings| {
|
init_test(cx, |settings| {
|
||||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
|
show_type_hints: true,
|
||||||
show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
|
show_parameter_hints: true,
|
||||||
show_other_hints: allowed_hint_kinds.contains(&None),
|
show_other_hints: true,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1370,7 +1721,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, 1,
|
inlay_cache.version, 1,
|
||||||
"Only one update should be registered in the cache after all cancellations"
|
"Only one update should be registered in the cache after all cancellations"
|
||||||
@ -1417,7 +1767,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, 2,
|
inlay_cache.version, 2,
|
||||||
"Should update the cache version once more, for the new change"
|
"Should update the cache version once more, for the new change"
|
||||||
@ -1427,13 +1776,12 @@ mod tests {
|
|||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
|
async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
|
||||||
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
|
|
||||||
init_test(cx, |settings| {
|
init_test(cx, |settings| {
|
||||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
|
show_type_hints: true,
|
||||||
show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
|
show_parameter_hints: true,
|
||||||
show_other_hints: allowed_hint_kinds.contains(&None),
|
show_other_hints: true,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1539,7 +1887,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inlay_cache.version, 2,
|
inlay_cache.version, 2,
|
||||||
"Both LSP queries should've bumped the cache version"
|
"Both LSP queries should've bumped the cache version"
|
||||||
@ -1572,7 +1919,6 @@ mod tests {
|
|||||||
"Should have hints from the new LSP response after edit");
|
"Should have hints from the new LSP response after edit");
|
||||||
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
|
||||||
assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
|
assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1582,13 +1928,12 @@ mod tests {
|
|||||||
deterministic: Arc<Deterministic>,
|
deterministic: Arc<Deterministic>,
|
||||||
cx: &mut gpui::TestAppContext,
|
cx: &mut gpui::TestAppContext,
|
||||||
) {
|
) {
|
||||||
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
|
|
||||||
init_test(cx, |settings| {
|
init_test(cx, |settings| {
|
||||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
|
show_type_hints: true,
|
||||||
show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
|
show_parameter_hints: true,
|
||||||
show_other_hints: allowed_hint_kinds.contains(&None),
|
show_other_hints: true,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1794,7 +2139,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
|
||||||
assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
|
assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1826,7 +2170,6 @@ mod tests {
|
|||||||
"With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
|
"With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
|
||||||
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
|
||||||
assert_eq!(inlay_cache.version, 9);
|
assert_eq!(inlay_cache.version, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1855,7 +2198,6 @@ mod tests {
|
|||||||
"After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
|
"After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
|
||||||
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
|
||||||
assert_eq!(inlay_cache.version, 12);
|
assert_eq!(inlay_cache.version, 12);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1884,7 +2226,6 @@ mod tests {
|
|||||||
"After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
|
"After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
|
||||||
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
|
||||||
assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
|
assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1911,7 +2252,6 @@ mod tests {
|
|||||||
unedited (2nd) buffer should have the same hint");
|
unedited (2nd) buffer should have the same hint");
|
||||||
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||||
let inlay_cache = editor.inlay_hint_cache();
|
let inlay_cache = editor.inlay_hint_cache();
|
||||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
|
||||||
assert_eq!(inlay_cache.version, 16);
|
assert_eq!(inlay_cache.version, 16);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -263,13 +263,13 @@ pub fn find_preceding_boundary(
|
|||||||
|
|
||||||
if let Some((prev_ch, prev_point)) = prev {
|
if let Some((prev_ch, prev_point)) = prev {
|
||||||
if is_boundary(ch, prev_ch) {
|
if is_boundary(ch, prev_ch) {
|
||||||
return prev_point;
|
return map.clip_point(prev_point, Bias::Left);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prev = Some((ch, point));
|
prev = Some((ch, point));
|
||||||
}
|
}
|
||||||
DisplayPoint::zero()
|
map.clip_point(DisplayPoint::zero(), Bias::Left)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
||||||
@ -292,7 +292,7 @@ pub fn find_preceding_boundary_in_line(
|
|||||||
for (ch, point) in map.reverse_chars_at(from) {
|
for (ch, point) in map.reverse_chars_at(from) {
|
||||||
if let Some((prev_ch, prev_point)) = prev {
|
if let Some((prev_ch, prev_point)) = prev {
|
||||||
if is_boundary(ch, prev_ch) {
|
if is_boundary(ch, prev_ch) {
|
||||||
return prev_point;
|
return map.clip_point(prev_point, Bias::Left);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,7 +303,7 @@ pub fn find_preceding_boundary_in_line(
|
|||||||
prev = Some((ch, point));
|
prev = Some((ch, point));
|
||||||
}
|
}
|
||||||
|
|
||||||
prev.map(|(_, point)| point).unwrap_or(from)
|
map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Left)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
||||||
@ -406,8 +406,12 @@ pub fn split_display_range_by_lines(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
|
use crate::{
|
||||||
|
display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange,
|
||||||
|
InlayId, MultiBuffer,
|
||||||
|
};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
use util::post_inc;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_previous_word_start(cx: &mut gpui::AppContext) {
|
fn test_previous_word_start(cx: &mut gpui::AppContext) {
|
||||||
@ -505,6 +509,80 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let input_text = "abcdefghijklmnopqrstuvwxys";
|
||||||
|
let family_id = cx
|
||||||
|
.font_cache()
|
||||||
|
.load_family(&["Helvetica"], &Default::default())
|
||||||
|
.unwrap();
|
||||||
|
let font_id = cx
|
||||||
|
.font_cache()
|
||||||
|
.select_font(family_id, &Default::default())
|
||||||
|
.unwrap();
|
||||||
|
let font_size = 14.0;
|
||||||
|
let buffer = MultiBuffer::build_simple(input_text, cx);
|
||||||
|
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||||
|
let display_map =
|
||||||
|
cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
|
||||||
|
|
||||||
|
// add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
|
||||||
|
let mut id = 0;
|
||||||
|
let inlays = (0..buffer_snapshot.len())
|
||||||
|
.map(|offset| {
|
||||||
|
[
|
||||||
|
Inlay {
|
||||||
|
id: InlayId::Suggestion(post_inc(&mut id)),
|
||||||
|
position: buffer_snapshot.anchor_at(offset, Bias::Left),
|
||||||
|
text: format!("test").into(),
|
||||||
|
},
|
||||||
|
Inlay {
|
||||||
|
id: InlayId::Suggestion(post_inc(&mut id)),
|
||||||
|
position: buffer_snapshot.anchor_at(offset, Bias::Right),
|
||||||
|
text: format!("test").into(),
|
||||||
|
},
|
||||||
|
Inlay {
|
||||||
|
id: InlayId::Hint(post_inc(&mut id)),
|
||||||
|
position: buffer_snapshot.anchor_at(offset, Bias::Left),
|
||||||
|
text: format!("test").into(),
|
||||||
|
},
|
||||||
|
Inlay {
|
||||||
|
id: InlayId::Hint(post_inc(&mut id)),
|
||||||
|
position: buffer_snapshot.anchor_at(offset, Bias::Right),
|
||||||
|
text: format!("test").into(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.collect();
|
||||||
|
let snapshot = display_map.update(cx, |map, cx| {
|
||||||
|
map.splice_inlays(Vec::new(), inlays, cx);
|
||||||
|
map.snapshot(cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
find_preceding_boundary(
|
||||||
|
&snapshot,
|
||||||
|
buffer_snapshot.len().to_display_point(&snapshot),
|
||||||
|
|left, _| left == 'a',
|
||||||
|
),
|
||||||
|
0.to_display_point(&snapshot),
|
||||||
|
"Should not stop at inlays when looking for boundaries"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
find_preceding_boundary_in_line(
|
||||||
|
&snapshot,
|
||||||
|
buffer_snapshot.len().to_display_point(&snapshot),
|
||||||
|
|left, _| left == 'a',
|
||||||
|
),
|
||||||
|
0.to_display_point(&snapshot),
|
||||||
|
"Should not stop at inlays when looking for boundaries in line"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_next_word_end(cx: &mut gpui::AppContext) {
|
fn test_next_word_end(cx: &mut gpui::AppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -31,6 +31,7 @@ serde_derive.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
time.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
@ -279,6 +279,9 @@ impl Fs for RealFs {
|
|||||||
|
|
||||||
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
|
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
|
||||||
let buffer_size = text.summary().len.min(10 * 1024);
|
let buffer_size = text.summary().len.min(10 * 1024);
|
||||||
|
if let Some(path) = path.parent() {
|
||||||
|
self.create_dir(path).await?;
|
||||||
|
}
|
||||||
let file = smol::fs::File::create(path).await?;
|
let file = smol::fs::File::create(path).await?;
|
||||||
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
|
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
|
||||||
for chunk in chunks(text, line_ending) {
|
for chunk in chunks(text, line_ending) {
|
||||||
@ -1077,6 +1080,9 @@ impl Fs for FakeFs {
|
|||||||
self.simulate_random_delay().await;
|
self.simulate_random_delay().await;
|
||||||
let path = normalize_path(path);
|
let path = normalize_path(path);
|
||||||
let content = chunks(text, line_ending).collect();
|
let content = chunks(text, line_ending).collect();
|
||||||
|
if let Some(path) = path.parent() {
|
||||||
|
self.create_dir(path).await?;
|
||||||
|
}
|
||||||
self.write_file_internal(path, content)?;
|
self.write_file_internal(path, content)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use git2::ErrorCode;
|
use git2::{BranchType, ErrorCode};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rpc::proto;
|
use rpc::proto;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
@ -16,6 +16,12 @@ use util::ResultExt;
|
|||||||
|
|
||||||
pub use git2::Repository as LibGitRepository;
|
pub use git2::Repository as LibGitRepository;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Hash, PartialEq)]
|
||||||
|
pub struct Branch {
|
||||||
|
pub name: Box<str>,
|
||||||
|
/// Timestamp of most recent commit, normalized to Unix Epoch format.
|
||||||
|
pub unix_timestamp: Option<i64>,
|
||||||
|
}
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait GitRepository: Send {
|
pub trait GitRepository: Send {
|
||||||
fn reload_index(&self);
|
fn reload_index(&self);
|
||||||
@ -27,6 +33,12 @@ pub trait GitRepository: Send {
|
|||||||
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
|
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
|
||||||
|
|
||||||
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
|
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
|
||||||
|
fn branches(&self) -> Result<Vec<Branch>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
fn change_branch(&self, _: &str) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for dyn GitRepository {
|
impl std::fmt::Debug for dyn GitRepository {
|
||||||
@ -106,6 +118,40 @@ impl GitRepository for LibGitRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn branches(&self) -> Result<Vec<Branch>> {
|
||||||
|
let local_branches = self.branches(Some(BranchType::Local))?;
|
||||||
|
let valid_branches = local_branches
|
||||||
|
.filter_map(|branch| {
|
||||||
|
branch.ok().and_then(|(branch, _)| {
|
||||||
|
let name = branch.name().ok().flatten().map(Box::from)?;
|
||||||
|
let timestamp = branch.get().peel_to_commit().ok()?.time();
|
||||||
|
let unix_timestamp = timestamp.seconds();
|
||||||
|
let timezone_offset = timestamp.offset_minutes();
|
||||||
|
let utc_offset =
|
||||||
|
time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?;
|
||||||
|
let unix_timestamp =
|
||||||
|
time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?;
|
||||||
|
Some(Branch {
|
||||||
|
name,
|
||||||
|
unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(valid_branches)
|
||||||
|
}
|
||||||
|
fn change_branch(&self, name: &str) -> Result<()> {
|
||||||
|
let revision = self.find_branch(name, BranchType::Local)?;
|
||||||
|
let revision = revision.get();
|
||||||
|
let as_tree = revision.peel_to_tree()?;
|
||||||
|
self.checkout_tree(as_tree.as_object(), None)?;
|
||||||
|
self.set_head(
|
||||||
|
revision
|
||||||
|
.name()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_status(status: git2::Status) -> Option<GitFileStatus> {
|
fn read_status(status: git2::Status) -> Option<GitFileStatus> {
|
||||||
|
@ -24,6 +24,7 @@ pub struct GoToLine {
|
|||||||
prev_scroll_position: Option<Vector2F>,
|
prev_scroll_position: Option<Vector2F>,
|
||||||
cursor_point: Point,
|
cursor_point: Point,
|
||||||
max_point: Point,
|
max_point: Point,
|
||||||
|
has_focus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
@ -57,6 +58,7 @@ impl GoToLine {
|
|||||||
prev_scroll_position: scroll_position,
|
prev_scroll_position: scroll_position,
|
||||||
cursor_point,
|
cursor_point,
|
||||||
max_point,
|
max_point,
|
||||||
|
has_focus: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,11 +180,20 @@ impl View for GoToLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
self.has_focus = true;
|
||||||
cx.focus(&self.line_editor);
|
cx.focus(&self.line_editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||||
|
self.has_focus = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Modal for GoToLine {
|
impl Modal for GoToLine {
|
||||||
|
fn has_focus(&self) -> bool {
|
||||||
|
self.has_focus
|
||||||
|
}
|
||||||
|
|
||||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||||
matches!(event, Event::Dismissed)
|
matches!(event, Event::Dismissed)
|
||||||
}
|
}
|
||||||
|
@ -2971,14 +2971,12 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn focus(&mut self, handle: &AnyViewHandle) {
|
pub fn focus(&mut self, handle: &AnyViewHandle) {
|
||||||
self.window_context
|
self.window_context.focus(Some(handle.view_id));
|
||||||
.focus(handle.window_id, Some(handle.view_id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focus_self(&mut self) {
|
pub fn focus_self(&mut self) {
|
||||||
let window_id = self.window_id;
|
|
||||||
let view_id = self.view_id;
|
let view_id = self.view_id;
|
||||||
self.window_context.focus(window_id, Some(view_id));
|
self.window_context.focus(Some(view_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_self_focused(&self) -> bool {
|
pub fn is_self_focused(&self) -> bool {
|
||||||
@ -2997,8 +2995,7 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn blur(&mut self) {
|
pub fn blur(&mut self) {
|
||||||
let window_id = self.window_id;
|
self.window_context.focus(None);
|
||||||
self.window_context.focus(window_id, None);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_window_should_close<F>(&mut self, mut callback: F)
|
pub fn on_window_should_close<F>(&mut self, mut callback: F)
|
||||||
@ -3304,11 +3301,15 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
|||||||
let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
|
let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
|
||||||
MouseState {
|
MouseState {
|
||||||
hovered: self.window.hovered_region_ids.contains(®ion_id),
|
hovered: self.window.hovered_region_ids.contains(®ion_id),
|
||||||
clicked: self
|
clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region {
|
||||||
.window
|
if region_id == clicked_region_id {
|
||||||
.clicked_region_ids
|
Some(button)
|
||||||
.get(®ion_id)
|
} else {
|
||||||
.and_then(|_| self.window.clicked_button),
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
accessed_hovered: false,
|
accessed_hovered: false,
|
||||||
accessed_clicked: false,
|
accessed_clicked: false,
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,8 @@ use crate::{
|
|||||||
text_layout::TextLayoutCache,
|
text_layout::TextLayoutCache,
|
||||||
util::post_inc,
|
util::post_inc,
|
||||||
Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
|
Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
|
||||||
Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription,
|
Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, NoAction, SceneBuilder,
|
||||||
View, ViewContext, ViewHandle, WindowInvalidation,
|
Subscription, View, ViewContext, ViewHandle, WindowInvalidation,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
@ -53,7 +53,7 @@ pub struct Window {
|
|||||||
last_mouse_moved_event: Option<Event>,
|
last_mouse_moved_event: Option<Event>,
|
||||||
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
|
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
|
||||||
pub(crate) clicked_region_ids: HashSet<MouseRegionId>,
|
pub(crate) clicked_region_ids: HashSet<MouseRegionId>,
|
||||||
pub(crate) clicked_button: Option<MouseButton>,
|
pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
|
||||||
mouse_position: Vector2F,
|
mouse_position: Vector2F,
|
||||||
text_layout_cache: TextLayoutCache,
|
text_layout_cache: TextLayoutCache,
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ impl Window {
|
|||||||
last_mouse_moved_event: None,
|
last_mouse_moved_event: None,
|
||||||
hovered_region_ids: Default::default(),
|
hovered_region_ids: Default::default(),
|
||||||
clicked_region_ids: Default::default(),
|
clicked_region_ids: Default::default(),
|
||||||
clicked_button: None,
|
clicked_region: None,
|
||||||
mouse_position: vec2f(0., 0.),
|
mouse_position: vec2f(0., 0.),
|
||||||
titlebar_height,
|
titlebar_height,
|
||||||
appearance,
|
appearance,
|
||||||
@ -434,7 +434,11 @@ impl<'a> WindowContext<'a> {
|
|||||||
MatchResult::None => false,
|
MatchResult::None => false,
|
||||||
MatchResult::Pending => true,
|
MatchResult::Pending => true,
|
||||||
MatchResult::Matches(matches) => {
|
MatchResult::Matches(matches) => {
|
||||||
|
let no_action_id = (NoAction {}).id();
|
||||||
for (view_id, action) in matches {
|
for (view_id, action) in matches {
|
||||||
|
if action.id() == no_action_id {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if self.dispatch_action(Some(*view_id), action.as_ref()) {
|
if self.dispatch_action(Some(*view_id), action.as_ref()) {
|
||||||
self.keystroke_matcher.clear_pending();
|
self.keystroke_matcher.clear_pending();
|
||||||
handled_by = Some(action.boxed_clone());
|
handled_by = Some(action.boxed_clone());
|
||||||
@ -480,8 +484,8 @@ impl<'a> WindowContext<'a> {
|
|||||||
// specific ancestor element that contained both [positions]'
|
// specific ancestor element that contained both [positions]'
|
||||||
// So we need to store the overlapping regions on mouse down.
|
// So we need to store the overlapping regions on mouse down.
|
||||||
|
|
||||||
// If there is already clicked_button stored, don't replace it.
|
// If there is already region being clicked, don't replace it.
|
||||||
if self.window.clicked_button.is_none() {
|
if self.window.clicked_region.is_none() {
|
||||||
self.window.clicked_region_ids = self
|
self.window.clicked_region_ids = self
|
||||||
.window
|
.window
|
||||||
.mouse_regions
|
.mouse_regions
|
||||||
@ -495,7 +499,17 @@ impl<'a> WindowContext<'a> {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.window.clicked_button = Some(e.button);
|
let mut highest_z_index = 0;
|
||||||
|
let mut clicked_region_id = None;
|
||||||
|
for (region, z_index) in self.window.mouse_regions.iter() {
|
||||||
|
if region.bounds.contains_point(e.position) && *z_index >= highest_z_index {
|
||||||
|
highest_z_index = *z_index;
|
||||||
|
clicked_region_id = Some(region.id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.window.clicked_region =
|
||||||
|
clicked_region_id.map(|region_id| (region_id, e.button));
|
||||||
}
|
}
|
||||||
|
|
||||||
mouse_events.push(MouseEvent::Down(MouseDown {
|
mouse_events.push(MouseEvent::Down(MouseDown {
|
||||||
@ -560,7 +574,7 @@ impl<'a> WindowContext<'a> {
|
|||||||
prev_mouse_position: self.window.mouse_position,
|
prev_mouse_position: self.window.mouse_position,
|
||||||
platform_event: e.clone(),
|
platform_event: e.clone(),
|
||||||
}));
|
}));
|
||||||
} else if let Some(clicked_button) = self.window.clicked_button {
|
} else if let Some((_, clicked_button)) = self.window.clicked_region {
|
||||||
// Mouse up event happened outside the current window. Simulate mouse up button event
|
// Mouse up event happened outside the current window. Simulate mouse up button event
|
||||||
let button_event = e.to_button_event(clicked_button);
|
let button_event = e.to_button_event(clicked_button);
|
||||||
mouse_events.push(MouseEvent::Up(MouseUp {
|
mouse_events.push(MouseEvent::Up(MouseUp {
|
||||||
@ -683,8 +697,8 @@ impl<'a> WindowContext<'a> {
|
|||||||
// Only raise click events if the released button is the same as the one stored
|
// Only raise click events if the released button is the same as the one stored
|
||||||
if self
|
if self
|
||||||
.window
|
.window
|
||||||
.clicked_button
|
.clicked_region
|
||||||
.map(|clicked_button| clicked_button == e.button)
|
.map(|(_, clicked_button)| clicked_button == e.button)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
// Clear clicked regions and clicked button
|
// Clear clicked regions and clicked button
|
||||||
@ -692,7 +706,7 @@ impl<'a> WindowContext<'a> {
|
|||||||
&mut self.window.clicked_region_ids,
|
&mut self.window.clicked_region_ids,
|
||||||
Default::default(),
|
Default::default(),
|
||||||
);
|
);
|
||||||
self.window.clicked_button = None;
|
self.window.clicked_region = None;
|
||||||
|
|
||||||
// Find regions which still overlap with the mouse since the last MouseDown happened
|
// Find regions which still overlap with the mouse since the last MouseDown happened
|
||||||
for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
|
for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
|
||||||
@ -867,18 +881,10 @@ impl<'a> WindowContext<'a> {
|
|||||||
}
|
}
|
||||||
for view_id in &invalidation.updated {
|
for view_id in &invalidation.updated {
|
||||||
let titlebar_height = self.window.titlebar_height;
|
let titlebar_height = self.window.titlebar_height;
|
||||||
let hovered_region_ids = self.window.hovered_region_ids.clone();
|
|
||||||
let clicked_region_ids = self
|
|
||||||
.window
|
|
||||||
.clicked_button
|
|
||||||
.map(|button| (self.window.clicked_region_ids.clone(), button));
|
|
||||||
|
|
||||||
let element = self
|
let element = self
|
||||||
.render_view(RenderParams {
|
.render_view(RenderParams {
|
||||||
view_id: *view_id,
|
view_id: *view_id,
|
||||||
titlebar_height,
|
titlebar_height,
|
||||||
hovered_region_ids,
|
|
||||||
clicked_region_ids,
|
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
appearance,
|
appearance,
|
||||||
})
|
})
|
||||||
@ -1092,6 +1098,10 @@ impl<'a> WindowContext<'a> {
|
|||||||
self.window.focused_view_id
|
self.window.focused_view_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn focus(&mut self, view_id: Option<usize>) {
|
||||||
|
self.app_context.focus(self.window_id, view_id);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn window_bounds(&self) -> WindowBounds {
|
pub fn window_bounds(&self) -> WindowBounds {
|
||||||
self.window.platform_window.bounds()
|
self.window.platform_window.bounds()
|
||||||
}
|
}
|
||||||
@ -1183,8 +1193,6 @@ impl<'a> WindowContext<'a> {
|
|||||||
pub struct RenderParams {
|
pub struct RenderParams {
|
||||||
pub view_id: usize,
|
pub view_id: usize,
|
||||||
pub titlebar_height: f32,
|
pub titlebar_height: f32,
|
||||||
pub hovered_region_ids: HashSet<MouseRegionId>,
|
|
||||||
pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
|
|
||||||
pub refreshing: bool,
|
pub refreshing: bool,
|
||||||
pub appearance: Appearance,
|
pub appearance: Appearance,
|
||||||
}
|
}
|
||||||
|
@ -31,3 +31,5 @@ pub use window::{Axis, SizeConstraint, Vector2FExt, WindowContext};
|
|||||||
|
|
||||||
pub use anyhow;
|
pub use anyhow;
|
||||||
pub use serde_json;
|
pub use serde_json;
|
||||||
|
|
||||||
|
actions!(zed, [NoAction]);
|
||||||
|
@ -4,7 +4,7 @@ use pathfinder_geometry::vector::vec2f;
|
|||||||
|
|
||||||
use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
|
use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct KeyDownEvent {
|
pub struct KeyDownEvent {
|
||||||
pub keystroke: Keystroke,
|
pub keystroke: Keystroke,
|
||||||
pub is_held: bool,
|
pub is_held: bool,
|
||||||
|
@ -232,10 +232,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
|
|||||||
sel!(canBecomeKeyWindow),
|
sel!(canBecomeKeyWindow),
|
||||||
yes as extern "C" fn(&Object, Sel) -> BOOL,
|
yes as extern "C" fn(&Object, Sel) -> BOOL,
|
||||||
);
|
);
|
||||||
decl.add_method(
|
|
||||||
sel!(sendEvent:),
|
|
||||||
send_event as extern "C" fn(&Object, Sel, id),
|
|
||||||
);
|
|
||||||
decl.add_method(
|
decl.add_method(
|
||||||
sel!(windowDidResize:),
|
sel!(windowDidResize:),
|
||||||
window_did_resize as extern "C" fn(&Object, Sel, id),
|
window_did_resize as extern "C" fn(&Object, Sel, id),
|
||||||
@ -299,7 +295,7 @@ struct WindowState {
|
|||||||
appearance_changed_callback: Option<Box<dyn FnMut()>>,
|
appearance_changed_callback: Option<Box<dyn FnMut()>>,
|
||||||
input_handler: Option<Box<dyn InputHandler>>,
|
input_handler: Option<Box<dyn InputHandler>>,
|
||||||
pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
|
pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
|
||||||
performed_key_equivalent: bool,
|
last_key_equivalent: Option<KeyDownEvent>,
|
||||||
synthetic_drag_counter: usize,
|
synthetic_drag_counter: usize,
|
||||||
executor: Rc<executor::Foreground>,
|
executor: Rc<executor::Foreground>,
|
||||||
scene_to_render: Option<Scene>,
|
scene_to_render: Option<Scene>,
|
||||||
@ -521,7 +517,7 @@ impl Window {
|
|||||||
appearance_changed_callback: None,
|
appearance_changed_callback: None,
|
||||||
input_handler: None,
|
input_handler: None,
|
||||||
pending_key_down: None,
|
pending_key_down: None,
|
||||||
performed_key_equivalent: false,
|
last_key_equivalent: None,
|
||||||
synthetic_drag_counter: 0,
|
synthetic_drag_counter: 0,
|
||||||
executor,
|
executor,
|
||||||
scene_to_render: Default::default(),
|
scene_to_render: Default::default(),
|
||||||
@ -965,36 +961,34 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
|||||||
let window_height = window_state_borrow.content_size().y();
|
let window_height = window_state_borrow.content_size().y();
|
||||||
let event = unsafe { Event::from_native(native_event, Some(window_height)) };
|
let event = unsafe { Event::from_native(native_event, Some(window_height)) };
|
||||||
|
|
||||||
if let Some(event) = event {
|
if let Some(Event::KeyDown(event)) = event {
|
||||||
|
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
|
||||||
|
// If that event isn't handled, it will then dispatch a "key down" event. GPUI
|
||||||
|
// makes no distinction between these two types of events, so we need to ignore
|
||||||
|
// the "key down" event if we've already just processed its "key equivalent" version.
|
||||||
if key_equivalent {
|
if key_equivalent {
|
||||||
window_state_borrow.performed_key_equivalent = true;
|
window_state_borrow.last_key_equivalent = Some(event.clone());
|
||||||
} else if window_state_borrow.performed_key_equivalent {
|
} else if window_state_borrow.last_key_equivalent.take().as_ref() == Some(&event) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
let function_is_held;
|
let keydown = event.keystroke.clone();
|
||||||
window_state_borrow.pending_key_down = match event {
|
let fn_modifier = keydown.function;
|
||||||
Event::KeyDown(event) => {
|
// Ignore events from held-down keys after some of the initially-pressed keys
|
||||||
let keydown = event.keystroke.clone();
|
// were released.
|
||||||
// Ignore events from held-down keys after some of the initially-pressed keys
|
if event.is_held {
|
||||||
// were released.
|
if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
|
||||||
if event.is_held {
|
return YES;
|
||||||
if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window_state_borrow.last_fresh_keydown = Some(keydown);
|
|
||||||
}
|
|
||||||
function_is_held = event.keystroke.function;
|
|
||||||
Some((event, None))
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
_ => return NO,
|
window_state_borrow.last_fresh_keydown = Some(keydown);
|
||||||
};
|
}
|
||||||
|
window_state_borrow.pending_key_down = Some((event, None));
|
||||||
drop(window_state_borrow);
|
drop(window_state_borrow);
|
||||||
|
|
||||||
if !function_is_held {
|
// Send the event to the input context for IME handling, unless the `fn` modifier is
|
||||||
|
// being pressed.
|
||||||
|
if !fn_modifier {
|
||||||
unsafe {
|
unsafe {
|
||||||
let input_context: id = msg_send![this, inputContext];
|
let input_context: id = msg_send![this, inputContext];
|
||||||
let _: BOOL = msg_send![input_context, handleEvent: native_event];
|
let _: BOOL = msg_send![input_context, handleEvent: native_event];
|
||||||
@ -1143,13 +1137,6 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
|
|
||||||
unsafe {
|
|
||||||
let _: () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];
|
|
||||||
get_window_state(this).borrow_mut().performed_key_equivalent = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
|
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
|
||||||
let window_state = unsafe { get_window_state(this) };
|
let window_state = unsafe { get_window_state(this) };
|
||||||
window_state.as_ref().borrow().move_traffic_light();
|
window_state.as_ref().borrow().move_traffic_light();
|
||||||
|
@ -4,7 +4,6 @@ mod syntax_map_tests;
|
|||||||
use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
|
use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
@ -25,9 +24,7 @@ thread_local! {
|
|||||||
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
|
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
static QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Mutex::new(vec![]);
|
||||||
static ref QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Default::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct SyntaxMap {
|
pub struct SyntaxMap {
|
||||||
|
@ -17,7 +17,6 @@ test-support = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"collections/test-support",
|
"collections/test-support",
|
||||||
"gpui/test-support",
|
"gpui/test-support",
|
||||||
"lazy_static",
|
|
||||||
"live_kit_server",
|
"live_kit_server",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
]
|
]
|
||||||
@ -38,7 +37,6 @@ parking_lot.workspace = true
|
|||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
|
|
||||||
async-trait = { workspace = true, optional = true }
|
async-trait = { workspace = true, optional = true }
|
||||||
lazy_static = { workspace = true, optional = true }
|
|
||||||
nanoid = { version ="0.4", optional = true}
|
nanoid = { version ="0.4", optional = true}
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
@ -60,7 +58,6 @@ foreign-types = "0.3"
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
jwt = "0.16"
|
jwt = "0.16"
|
||||||
lazy_static.workspace = true
|
|
||||||
objc = "0.2"
|
objc = "0.2"
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use collections::HashMap;
|
use collections::{BTreeMap, HashMap};
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use gpui::executor::Background;
|
use gpui::executor::Background;
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use live_kit_server::token;
|
use live_kit_server::token;
|
||||||
use media::core_video::CVImageBuffer;
|
use media::core_video::CVImageBuffer;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use std::{future::Future, mem, sync::Arc};
|
use std::{future::Future, mem, sync::Arc};
|
||||||
|
|
||||||
lazy_static! {
|
static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
|
||||||
static ref SERVERS: Mutex<HashMap<String, Arc<TestServer>>> = Default::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TestServer {
|
pub struct TestServer {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
@ -25,6 +25,7 @@ pub struct Picker<D: PickerDelegate> {
|
|||||||
theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
|
theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
|
||||||
confirmed: bool,
|
confirmed: bool,
|
||||||
pending_update_matches: Task<Option<()>>,
|
pending_update_matches: Task<Option<()>>,
|
||||||
|
has_focus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PickerDelegate: Sized + 'static {
|
pub trait PickerDelegate: Sized + 'static {
|
||||||
@ -45,10 +46,16 @@ pub trait PickerDelegate: Sized + 'static {
|
|||||||
fn center_selection_after_match_updates(&self) -> bool {
|
fn center_selection_after_match_updates(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
fn render_header(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
|
fn render_header(
|
||||||
|
&self,
|
||||||
|
_cx: &mut ViewContext<Picker<Self>>,
|
||||||
|
) -> Option<AnyElement<Picker<Self>>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
fn render_footer(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
|
fn render_footer(
|
||||||
|
&self,
|
||||||
|
_cx: &mut ViewContext<Picker<Self>>,
|
||||||
|
) -> Option<AnyElement<Picker<Self>>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,13 +147,22 @@ impl<D: PickerDelegate> View for Picker<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
self.has_focus = true;
|
||||||
if cx.is_self_focused() {
|
if cx.is_self_focused() {
|
||||||
cx.focus(&self.query_editor);
|
cx.focus(&self.query_editor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||||
|
self.has_focus = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: PickerDelegate> Modal for Picker<D> {
|
impl<D: PickerDelegate> Modal for Picker<D> {
|
||||||
|
fn has_focus(&self) -> bool {
|
||||||
|
self.has_focus
|
||||||
|
}
|
||||||
|
|
||||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||||
matches!(event, PickerEvent::Dismiss)
|
matches!(event, PickerEvent::Dismiss)
|
||||||
}
|
}
|
||||||
@ -191,6 +207,7 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
theme,
|
theme,
|
||||||
confirmed: false,
|
confirmed: false,
|
||||||
pending_update_matches: Task::ready(None),
|
pending_update_matches: Task::ready(None),
|
||||||
|
has_focus: false,
|
||||||
};
|
};
|
||||||
this.update_matches(String::new(), cx);
|
this.update_matches(String::new(), cx);
|
||||||
this
|
this
|
||||||
|
@ -64,7 +64,7 @@ itertools = "0.10"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions.workspace = true
|
||||||
client = { path = "../client", features = ["test-support"] }
|
client = { path = "../client", features = ["test-support"] }
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
db = { path = "../db", features = ["test-support"] }
|
db = { path = "../db", features = ["test-support"] }
|
||||||
|
@ -1822,11 +1822,21 @@ impl LspCommand for InlayHints {
|
|||||||
async fn response_from_lsp(
|
async fn response_from_lsp(
|
||||||
self,
|
self,
|
||||||
message: Option<Vec<lsp::InlayHint>>,
|
message: Option<Vec<lsp::InlayHint>>,
|
||||||
_: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
buffer: ModelHandle<Buffer>,
|
buffer: ModelHandle<Buffer>,
|
||||||
_: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Result<Vec<InlayHint>> {
|
) -> Result<Vec<InlayHint>> {
|
||||||
|
let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
|
||||||
|
// `typescript-language-server` adds padding to the left for type hints, turning
|
||||||
|
// `const foo: boolean` into `const foo : boolean` which looks odd.
|
||||||
|
// `rust-analyzer` does not have the padding for this case, and we have to accomodate both.
|
||||||
|
//
|
||||||
|
// We could trim the whole string, but being pessimistic on par with the situation above,
|
||||||
|
// there might be a hint with multiple whitespaces at the end(s) which we need to display properly.
|
||||||
|
// Hence let's use a heuristic first to handle the most awkward case and look for more.
|
||||||
|
let force_no_type_left_padding =
|
||||||
|
lsp_adapter.name.0.as_ref() == "typescript-language-server";
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
let origin_buffer = buffer.read(cx);
|
let origin_buffer = buffer.read(cx);
|
||||||
Ok(message
|
Ok(message
|
||||||
@ -1840,6 +1850,12 @@ impl LspCommand for InlayHints {
|
|||||||
});
|
});
|
||||||
let position = origin_buffer
|
let position = origin_buffer
|
||||||
.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
|
.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
|
||||||
|
let padding_left =
|
||||||
|
if force_no_type_left_padding && kind == Some(InlayHintKind::Type) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
lsp_hint.padding_left.unwrap_or(false)
|
||||||
|
};
|
||||||
InlayHint {
|
InlayHint {
|
||||||
buffer_id: origin_buffer.remote_id(),
|
buffer_id: origin_buffer.remote_id(),
|
||||||
position: if kind == Some(InlayHintKind::Parameter) {
|
position: if kind == Some(InlayHintKind::Parameter) {
|
||||||
@ -1847,7 +1863,7 @@ impl LspCommand for InlayHints {
|
|||||||
} else {
|
} else {
|
||||||
origin_buffer.anchor_after(position)
|
origin_buffer.anchor_after(position)
|
||||||
},
|
},
|
||||||
padding_left: lsp_hint.padding_left.unwrap_or(false),
|
padding_left,
|
||||||
padding_right: lsp_hint.padding_right.unwrap_or(false),
|
padding_right: lsp_hint.padding_right.unwrap_or(false),
|
||||||
label: match lsp_hint.label {
|
label: match lsp_hint.label {
|
||||||
lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),
|
lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),
|
||||||
|
@ -778,20 +778,32 @@ impl Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut language_servers_to_stop = Vec::new();
|
let mut language_servers_to_stop = Vec::new();
|
||||||
|
let mut language_servers_to_restart = Vec::new();
|
||||||
let languages = self.languages.to_vec();
|
let languages = self.languages.to_vec();
|
||||||
|
let project_settings = settings::get::<ProjectSettings>(cx).clone();
|
||||||
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
|
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
|
||||||
let language = languages.iter().find(|l| {
|
let language = languages.iter().find_map(|l| {
|
||||||
l.lsp_adapters()
|
let adapter = l
|
||||||
|
.lsp_adapters()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|adapter| &adapter.name == started_lsp_name)
|
.find(|adapter| &adapter.name == started_lsp_name)?;
|
||||||
|
Some((l, adapter))
|
||||||
});
|
});
|
||||||
if let Some(language) = language {
|
if let Some((language, adapter)) = language {
|
||||||
let worktree = self.worktree_for_id(*worktree_id, cx);
|
let worktree = self.worktree_for_id(*worktree_id, cx);
|
||||||
let file = worktree.and_then(|tree| {
|
let file = worktree.as_ref().and_then(|tree| {
|
||||||
tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
|
tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
|
||||||
});
|
});
|
||||||
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
|
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
|
||||||
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
|
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
|
||||||
|
} else if let Some(worktree) = worktree {
|
||||||
|
let new_lsp_settings = project_settings
|
||||||
|
.lsp
|
||||||
|
.get(&adapter.name.0)
|
||||||
|
.and_then(|s| s.initialization_options.as_ref());
|
||||||
|
if adapter.initialization_options.as_ref() != new_lsp_settings {
|
||||||
|
language_servers_to_restart.push((worktree, Arc::clone(language)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -808,6 +820,11 @@ impl Project {
|
|||||||
self.start_language_servers(&worktree, worktree_path, language, cx);
|
self.start_language_servers(&worktree, worktree_path, language, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restart all language servers with changed initialization options.
|
||||||
|
for (worktree, language) in language_servers_to_restart {
|
||||||
|
self.restart_language_servers(worktree, language, cx);
|
||||||
|
}
|
||||||
|
|
||||||
if !self.copilot_enabled && Copilot::global(cx).is_some() {
|
if !self.copilot_enabled && Copilot::global(cx).is_some() {
|
||||||
self.copilot_enabled = true;
|
self.copilot_enabled = true;
|
||||||
for buffer in self.opened_buffers.values() {
|
for buffer in self.opened_buffers.values() {
|
||||||
@ -3398,6 +3415,7 @@ impl Project {
|
|||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
|
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
|
||||||
|
cx.emit(Event::RefreshInlays);
|
||||||
status.pending_work.remove(&token);
|
status.pending_work.remove(&token);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -981,6 +981,19 @@ impl LocalWorktree {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the lowest path in the worktree's datastructures that is an ancestor
|
||||||
|
fn lowest_ancestor(&self, path: &Path) -> PathBuf {
|
||||||
|
let mut lowest_ancestor = None;
|
||||||
|
for path in path.ancestors() {
|
||||||
|
if self.entry_for_path(path).is_some() {
|
||||||
|
lowest_ancestor = Some(path.to_path_buf());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lowest_ancestor.unwrap_or_else(|| PathBuf::from(""))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_entry(
|
pub fn create_entry(
|
||||||
&self,
|
&self,
|
||||||
path: impl Into<Arc<Path>>,
|
path: impl Into<Arc<Path>>,
|
||||||
@ -988,6 +1001,7 @@ impl LocalWorktree {
|
|||||||
cx: &mut ModelContext<Worktree>,
|
cx: &mut ModelContext<Worktree>,
|
||||||
) -> Task<Result<Entry>> {
|
) -> Task<Result<Entry>> {
|
||||||
let path = path.into();
|
let path = path.into();
|
||||||
|
let lowest_ancestor = self.lowest_ancestor(&path);
|
||||||
let abs_path = self.absolutize(&path);
|
let abs_path = self.absolutize(&path);
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
let write = cx.background().spawn(async move {
|
let write = cx.background().spawn(async move {
|
||||||
@ -1001,10 +1015,31 @@ impl LocalWorktree {
|
|||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
write.await?;
|
write.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
let (result, refreshes) = this.update(&mut cx, |this, cx| {
|
||||||
this.as_local_mut().unwrap().refresh_entry(path, None, cx)
|
let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
|
||||||
})
|
let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
|
||||||
.await
|
for refresh_path in refresh_paths.ancestors() {
|
||||||
|
if refresh_path == Path::new("") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let refresh_full_path = lowest_ancestor.join(refresh_path);
|
||||||
|
|
||||||
|
refreshes.push(this.as_local_mut().unwrap().refresh_entry(
|
||||||
|
refresh_full_path.into(),
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(
|
||||||
|
this.as_local_mut().unwrap().refresh_entry(path, None, cx),
|
||||||
|
refreshes,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
for refresh in refreshes {
|
||||||
|
refresh.await.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.await
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2140,6 +2175,7 @@ impl LocalSnapshot {
|
|||||||
impl BackgroundScannerState {
|
impl BackgroundScannerState {
|
||||||
fn should_scan_directory(&self, entry: &Entry) -> bool {
|
fn should_scan_directory(&self, entry: &Entry) -> bool {
|
||||||
(!entry.is_external && !entry.is_ignored)
|
(!entry.is_external && !entry.is_ignored)
|
||||||
|
|| entry.path.file_name() == Some(&*DOT_GIT)
|
||||||
|| self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
|
|| self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
|
||||||
|| self
|
|| self
|
||||||
.paths_to_scan
|
.paths_to_scan
|
||||||
@ -2319,6 +2355,7 @@ impl BackgroundScannerState {
|
|||||||
.entry_for_id(entry_id)
|
.entry_for_id(entry_id)
|
||||||
.map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue };
|
.map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue };
|
||||||
|
|
||||||
|
log::info!("reload git repository {:?}", dot_git_dir);
|
||||||
let repository = repository.repo_ptr.lock();
|
let repository = repository.repo_ptr.lock();
|
||||||
let branch = repository.branch_name();
|
let branch = repository.branch_name();
|
||||||
repository.reload_index();
|
repository.reload_index();
|
||||||
@ -2359,6 +2396,8 @@ impl BackgroundScannerState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_repository(&mut self, dot_git_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
|
fn build_repository(&mut self, dot_git_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
|
||||||
|
log::info!("build git repository {:?}", dot_git_path);
|
||||||
|
|
||||||
let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into();
|
let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into();
|
||||||
|
|
||||||
// Guard against repositories inside the repository metadata
|
// Guard against repositories inside the repository metadata
|
||||||
@ -3138,8 +3177,6 @@ impl BackgroundScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) {
|
async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) {
|
||||||
log::debug!("received fs events {:?}", abs_paths);
|
|
||||||
|
|
||||||
let root_path = self.state.lock().snapshot.abs_path.clone();
|
let root_path = self.state.lock().snapshot.abs_path.clone();
|
||||||
let root_canonical_path = match self.fs.canonicalize(&root_path).await {
|
let root_canonical_path = match self.fs.canonicalize(&root_path).await {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
@ -3150,7 +3187,6 @@ impl BackgroundScanner {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut relative_paths = Vec::with_capacity(abs_paths.len());
|
let mut relative_paths = Vec::with_capacity(abs_paths.len());
|
||||||
let mut unloaded_relative_paths = Vec::new();
|
|
||||||
abs_paths.sort_unstable();
|
abs_paths.sort_unstable();
|
||||||
abs_paths.dedup_by(|a, b| a.starts_with(&b));
|
abs_paths.dedup_by(|a, b| a.starts_with(&b));
|
||||||
abs_paths.retain(|abs_path| {
|
abs_paths.retain(|abs_path| {
|
||||||
@ -3173,7 +3209,6 @@ impl BackgroundScanner {
|
|||||||
});
|
});
|
||||||
if !parent_dir_is_loaded {
|
if !parent_dir_is_loaded {
|
||||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||||
unloaded_relative_paths.push(relative_path);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3182,27 +3217,30 @@ impl BackgroundScanner {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if !relative_paths.is_empty() {
|
if relative_paths.is_empty() {
|
||||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
return;
|
||||||
self.reload_entries_for_paths(
|
|
||||||
root_path,
|
|
||||||
root_canonical_path,
|
|
||||||
&relative_paths,
|
|
||||||
abs_paths,
|
|
||||||
Some(scan_job_tx.clone()),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
drop(scan_job_tx);
|
|
||||||
self.scan_dirs(false, scan_job_rx).await;
|
|
||||||
|
|
||||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
|
||||||
self.update_ignore_statuses(scan_job_tx).await;
|
|
||||||
self.scan_dirs(false, scan_job_rx).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("received fs events {:?}", relative_paths);
|
||||||
|
|
||||||
|
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||||
|
self.reload_entries_for_paths(
|
||||||
|
root_path,
|
||||||
|
root_canonical_path,
|
||||||
|
&relative_paths,
|
||||||
|
abs_paths,
|
||||||
|
Some(scan_job_tx.clone()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
drop(scan_job_tx);
|
||||||
|
self.scan_dirs(false, scan_job_rx).await;
|
||||||
|
|
||||||
|
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||||
|
self.update_ignore_statuses(scan_job_tx).await;
|
||||||
|
self.scan_dirs(false, scan_job_rx).await;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
relative_paths.extend(unloaded_relative_paths);
|
|
||||||
state.reload_repositories(&relative_paths, self.fs.as_ref());
|
state.reload_repositories(&relative_paths, self.fs.as_ref());
|
||||||
state.snapshot.completed_scan_id = state.snapshot.scan_id;
|
state.snapshot.completed_scan_id = state.snapshot.scan_id;
|
||||||
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
|
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
|
||||||
@ -3610,23 +3648,28 @@ impl BackgroundScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fs_entry = state.insert_entry(fs_entry, self.fs.as_ref());
|
if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) {
|
||||||
|
if state.should_scan_directory(&fs_entry) {
|
||||||
if let Some(scan_queue_tx) = &scan_queue_tx {
|
let mut ancestor_inodes =
|
||||||
let mut ancestor_inodes = state.snapshot.ancestor_inodes_for_path(&path);
|
state.snapshot.ancestor_inodes_for_path(&path);
|
||||||
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
|
if !ancestor_inodes.contains(&metadata.inode) {
|
||||||
ancestor_inodes.insert(metadata.inode);
|
ancestor_inodes.insert(metadata.inode);
|
||||||
smol::block_on(scan_queue_tx.send(ScanJob {
|
smol::block_on(scan_queue_tx.send(ScanJob {
|
||||||
abs_path,
|
abs_path,
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
ignore_stack,
|
ignore_stack,
|
||||||
ancestor_inodes,
|
ancestor_inodes,
|
||||||
is_external: fs_entry.is_external,
|
is_external: fs_entry.is_external,
|
||||||
scan_queue: scan_queue_tx.clone(),
|
scan_queue: scan_queue_tx.clone(),
|
||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs_entry.kind = EntryKind::UnloadedDir;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.insert_entry(fs_entry, self.fs.as_ref());
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
self.remove_repo_path(&path, &mut state.snapshot);
|
self.remove_repo_path(&path, &mut state.snapshot);
|
||||||
|
@ -936,6 +936,119 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||||
|
let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
|
|
||||||
|
let fs_fake = FakeFs::new(cx.background());
|
||||||
|
fs_fake
|
||||||
|
.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"a": {},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let tree_fake = Worktree::local(
|
||||||
|
client_fake,
|
||||||
|
"/root".as_ref(),
|
||||||
|
true,
|
||||||
|
fs_fake,
|
||||||
|
Default::default(),
|
||||||
|
&mut cx.to_async(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let entry = tree_fake
|
||||||
|
.update(cx, |tree, cx| {
|
||||||
|
tree.as_local_mut()
|
||||||
|
.unwrap()
|
||||||
|
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(entry.is_file());
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
tree_fake.read_with(cx, |tree, _| {
|
||||||
|
assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
|
||||||
|
assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
|
||||||
|
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
|
||||||
|
});
|
||||||
|
|
||||||
|
let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
|
|
||||||
|
let fs_real = Arc::new(RealFs);
|
||||||
|
let temp_root = temp_tree(json!({
|
||||||
|
"a": {}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let tree_real = Worktree::local(
|
||||||
|
client_real,
|
||||||
|
temp_root.path(),
|
||||||
|
true,
|
||||||
|
fs_real,
|
||||||
|
Default::default(),
|
||||||
|
&mut cx.to_async(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let entry = tree_real
|
||||||
|
.update(cx, |tree, cx| {
|
||||||
|
tree.as_local_mut()
|
||||||
|
.unwrap()
|
||||||
|
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(entry.is_file());
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
tree_real.read_with(cx, |tree, _| {
|
||||||
|
assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
|
||||||
|
assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
|
||||||
|
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test smallest change
|
||||||
|
let entry = tree_real
|
||||||
|
.update(cx, |tree, cx| {
|
||||||
|
tree.as_local_mut()
|
||||||
|
.unwrap()
|
||||||
|
.create_entry("a/b/c/e.txt".as_ref(), false, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(entry.is_file());
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
tree_real.read_with(cx, |tree, _| {
|
||||||
|
assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test largest change
|
||||||
|
let entry = tree_real
|
||||||
|
.update(cx, |tree, cx| {
|
||||||
|
tree.as_local_mut()
|
||||||
|
.unwrap()
|
||||||
|
.create_entry("d/e/f/g.txt".as_ref(), false, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(entry.is_file());
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
tree_real.read_with(cx, |tree, _| {
|
||||||
|
assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
|
||||||
|
assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
|
||||||
|
assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
|
||||||
|
assert!(tree.entry_for_path("d/").unwrap().is_dir());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 100)]
|
#[gpui::test(iterations = 100)]
|
||||||
async fn test_random_worktree_operations_during_initial_scan(
|
async fn test_random_worktree_operations_during_initial_scan(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
@ -1654,6 +1767,23 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
|
|||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const A_TXT: &'static str = "a.txt";
|
||||||
|
const B_TXT: &'static str = "b.txt";
|
||||||
|
const E_TXT: &'static str = "c/d/e.txt";
|
||||||
|
const F_TXT: &'static str = "f.txt";
|
||||||
|
const DOTGITIGNORE: &'static str = ".gitignore";
|
||||||
|
const BUILD_FILE: &'static str = "target/build_file";
|
||||||
|
let project_path = Path::new("project");
|
||||||
|
|
||||||
|
// Set up git repository before creating the worktree.
|
||||||
|
let work_dir = root.path().join("project");
|
||||||
|
let mut repo = git_init(work_dir.as_path());
|
||||||
|
repo.add_ignore_rule(IGNORE_RULE).unwrap();
|
||||||
|
git_add(A_TXT, &repo);
|
||||||
|
git_add(E_TXT, &repo);
|
||||||
|
git_add(DOTGITIGNORE, &repo);
|
||||||
|
git_commit("Initial commit", &repo);
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
build_client(cx),
|
build_client(cx),
|
||||||
root.path(),
|
root.path(),
|
||||||
@ -1665,26 +1795,9 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
tree.flush_fs_events(cx).await;
|
||||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
const A_TXT: &'static str = "a.txt";
|
|
||||||
const B_TXT: &'static str = "b.txt";
|
|
||||||
const E_TXT: &'static str = "c/d/e.txt";
|
|
||||||
const F_TXT: &'static str = "f.txt";
|
|
||||||
const DOTGITIGNORE: &'static str = ".gitignore";
|
|
||||||
const BUILD_FILE: &'static str = "target/build_file";
|
|
||||||
let project_path: &Path = &Path::new("project");
|
|
||||||
|
|
||||||
let work_dir = root.path().join("project");
|
|
||||||
let mut repo = git_init(work_dir.as_path());
|
|
||||||
repo.add_ignore_rule(IGNORE_RULE).unwrap();
|
|
||||||
git_add(Path::new(A_TXT), &repo);
|
|
||||||
git_add(Path::new(E_TXT), &repo);
|
|
||||||
git_add(Path::new(DOTGITIGNORE), &repo);
|
|
||||||
git_commit("Initial commit", &repo);
|
|
||||||
|
|
||||||
tree.flush_fs_events(cx).await;
|
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
// Check that the right git state is observed on startup
|
// Check that the right git state is observed on startup
|
||||||
@ -1704,39 +1817,39 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Modify a file in the working copy.
|
||||||
std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
|
std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
|
||||||
|
|
||||||
tree.flush_fs_events(cx).await;
|
tree.flush_fs_events(cx).await;
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// The worktree detects that the file's git status has changed.
|
||||||
tree.read_with(cx, |tree, _cx| {
|
tree.read_with(cx, |tree, _cx| {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(A_TXT)),
|
snapshot.status_for_file(project_path.join(A_TXT)),
|
||||||
Some(GitFileStatus::Modified)
|
Some(GitFileStatus::Modified)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
git_add(Path::new(A_TXT), &repo);
|
// Create a commit in the git repository.
|
||||||
git_add(Path::new(B_TXT), &repo);
|
git_add(A_TXT, &repo);
|
||||||
|
git_add(B_TXT, &repo);
|
||||||
git_commit("Committing modified and added", &repo);
|
git_commit("Committing modified and added", &repo);
|
||||||
tree.flush_fs_events(cx).await;
|
tree.flush_fs_events(cx).await;
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
// Check that repo only changes are tracked
|
// The worktree detects that the files' git status have changed.
|
||||||
tree.read_with(cx, |tree, _cx| {
|
tree.read_with(cx, |tree, _cx| {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(F_TXT)),
|
snapshot.status_for_file(project_path.join(F_TXT)),
|
||||||
Some(GitFileStatus::Added)
|
Some(GitFileStatus::Added)
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
|
assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
|
||||||
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
|
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Modify files in the working copy and perform git operations on other files.
|
||||||
git_reset(0, &repo);
|
git_reset(0, &repo);
|
||||||
git_remove_index(Path::new(B_TXT), &repo);
|
git_remove_index(Path::new(B_TXT), &repo);
|
||||||
git_stash(&mut repo);
|
git_stash(&mut repo);
|
||||||
|
@ -27,6 +27,7 @@ serde_derive.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
|
pretty_assertions.workspace = true
|
||||||
unicase = "2.6"
|
unicase = "2.6"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -64,7 +64,7 @@ pub struct ProjectPanel {
|
|||||||
pending_serialization: Task<Option<()>>,
|
pending_serialization: Task<Option<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
struct Selection {
|
struct Selection {
|
||||||
worktree_id: WorktreeId,
|
worktree_id: WorktreeId,
|
||||||
entry_id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
@ -547,7 +547,7 @@ impl ProjectPanel {
|
|||||||
worktree_id,
|
worktree_id,
|
||||||
entry_id: NEW_ENTRY_ID,
|
entry_id: NEW_ENTRY_ID,
|
||||||
});
|
});
|
||||||
let new_path = entry.path.join(&filename);
|
let new_path = entry.path.join(&filename.trim_start_matches("/"));
|
||||||
if path_already_exists(new_path.as_path()) {
|
if path_already_exists(new_path.as_path()) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@ -588,6 +588,7 @@ impl ProjectPanel {
|
|||||||
if selection.entry_id == edited_entry_id {
|
if selection.entry_id == edited_entry_id {
|
||||||
selection.worktree_id = worktree_id;
|
selection.worktree_id = worktree_id;
|
||||||
selection.entry_id = new_entry.id;
|
selection.entry_id = new_entry.id;
|
||||||
|
this.expand_to_selection(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.update_visible_entries(None, cx);
|
this.update_visible_entries(None, cx);
|
||||||
@ -965,6 +966,24 @@ impl ProjectPanel {
|
|||||||
Some((worktree, entry))
|
Some((worktree, entry))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn expand_to_selection(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||||
|
let (worktree, entry) = self.selected_entry(cx)?;
|
||||||
|
let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
|
||||||
|
|
||||||
|
for path in entry.path.ancestors() {
|
||||||
|
let Some(entry) = worktree.entry_for_path(path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if entry.is_dir() {
|
||||||
|
if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
|
||||||
|
expanded_dir_ids.insert(idx, entry.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
fn update_visible_entries(
|
fn update_visible_entries(
|
||||||
&mut self,
|
&mut self,
|
||||||
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
||||||
@ -1592,6 +1611,7 @@ impl ClipboardEntry {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{TestAppContext, ViewHandle};
|
use gpui::{TestAppContext, ViewHandle};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use project::FakeFs;
|
use project::FakeFs;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
@ -2002,6 +2022,133 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 30)]
|
||||||
|
async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root1",
|
||||||
|
json!({
|
||||||
|
".dockerignore": "",
|
||||||
|
".git": {
|
||||||
|
"HEAD": "",
|
||||||
|
},
|
||||||
|
"a": {
|
||||||
|
"0": { "q": "", "r": "", "s": "" },
|
||||||
|
"1": { "t": "", "u": "" },
|
||||||
|
"2": { "v": "", "w": "", "x": "", "y": "" },
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
"3": { "Q": "" },
|
||||||
|
"4": { "R": "", "S": "", "T": "", "U": "" },
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"5": {},
|
||||||
|
"6": { "V": "", "W": "" },
|
||||||
|
"7": { "X": "" },
|
||||||
|
"8": { "Y": {}, "Z": "" }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root2",
|
||||||
|
json!({
|
||||||
|
"d": {
|
||||||
|
"9": ""
|
||||||
|
},
|
||||||
|
"e": {}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||||
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
|
||||||
|
|
||||||
|
select_path(&panel, "root1", cx);
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..10, cx),
|
||||||
|
&[
|
||||||
|
"v root1 <== selected",
|
||||||
|
" > .git",
|
||||||
|
" > a",
|
||||||
|
" > b",
|
||||||
|
" > C",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" > d",
|
||||||
|
" > e",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add a file with the root folder selected. The filename editor is placed
|
||||||
|
// before the first file in the root folder.
|
||||||
|
panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
|
||||||
|
cx.read_window(window_id, |cx| {
|
||||||
|
let panel = panel.read(cx);
|
||||||
|
assert!(panel.filename_editor.is_focused(cx));
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..10, cx),
|
||||||
|
&[
|
||||||
|
"v root1",
|
||||||
|
" > .git",
|
||||||
|
" > a",
|
||||||
|
" > b",
|
||||||
|
" > C",
|
||||||
|
" [EDITOR: ''] <== selected",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" > d",
|
||||||
|
" > e",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let confirm = panel.update(cx, |panel, cx| {
|
||||||
|
panel.filename_editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_text("/bdir1/dir2/the-new-filename", cx)
|
||||||
|
});
|
||||||
|
panel.confirm(&Confirm, cx).unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..10, cx),
|
||||||
|
&[
|
||||||
|
"v root1",
|
||||||
|
" > .git",
|
||||||
|
" > a",
|
||||||
|
" > b",
|
||||||
|
" > C",
|
||||||
|
" [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" > d",
|
||||||
|
" > e",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
confirm.await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..13, cx),
|
||||||
|
&[
|
||||||
|
"v root1",
|
||||||
|
" > .git",
|
||||||
|
" > a",
|
||||||
|
" > b",
|
||||||
|
" v bdir1",
|
||||||
|
" v dir2",
|
||||||
|
" the-new-filename <== selected",
|
||||||
|
" > C",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" > d",
|
||||||
|
" > e",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
|
async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -21,6 +21,7 @@ util = { path = "../util"}
|
|||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
|
|
||||||
|
futures.workspace = true
|
||||||
ordered-float.workspace = true
|
ordered-float.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
@ -48,7 +48,7 @@ fn toggle(
|
|||||||
let workspace = cx.weak_handle();
|
let workspace = cx.weak_handle();
|
||||||
cx.add_view(|cx| {
|
cx.add_view(|cx| {
|
||||||
RecentProjects::new(
|
RecentProjects::new(
|
||||||
RecentProjectsDelegate::new(workspace, workspace_locations),
|
RecentProjectsDelegate::new(workspace, workspace_locations, true),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.with_max_size(800., 1200.)
|
.with_max_size(800., 1200.)
|
||||||
@ -64,25 +64,40 @@ fn toggle(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecentProjects = Picker<RecentProjectsDelegate>;
|
pub fn build_recent_projects(
|
||||||
|
workspace: WeakViewHandle<Workspace>,
|
||||||
|
workspaces: Vec<WorkspaceLocation>,
|
||||||
|
cx: &mut ViewContext<RecentProjects>,
|
||||||
|
) -> RecentProjects {
|
||||||
|
Picker::new(
|
||||||
|
RecentProjectsDelegate::new(workspace, workspaces, false),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.with_theme(|theme| theme.picker.clone())
|
||||||
|
}
|
||||||
|
|
||||||
struct RecentProjectsDelegate {
|
pub type RecentProjects = Picker<RecentProjectsDelegate>;
|
||||||
|
|
||||||
|
pub struct RecentProjectsDelegate {
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
workspace_locations: Vec<WorkspaceLocation>,
|
workspace_locations: Vec<WorkspaceLocation>,
|
||||||
selected_match_index: usize,
|
selected_match_index: usize,
|
||||||
matches: Vec<StringMatch>,
|
matches: Vec<StringMatch>,
|
||||||
|
render_paths: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecentProjectsDelegate {
|
impl RecentProjectsDelegate {
|
||||||
fn new(
|
fn new(
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
workspace_locations: Vec<WorkspaceLocation>,
|
workspace_locations: Vec<WorkspaceLocation>,
|
||||||
|
render_paths: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
workspace,
|
workspace,
|
||||||
workspace_locations,
|
workspace_locations,
|
||||||
selected_match_index: 0,
|
selected_match_index: 0,
|
||||||
matches: Default::default(),
|
matches: Default::default(),
|
||||||
|
render_paths,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,6 +203,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
highlighted_location
|
highlighted_location
|
||||||
.paths
|
.paths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter(|_| self.render_paths)
|
||||||
.map(|highlighted_path| highlighted_path.render(style.label.clone())),
|
.map(|highlighted_path| highlighted_path.render(style.label.clone())),
|
||||||
)
|
)
|
||||||
.flex(1., false)
|
.flex(1., false)
|
||||||
|
@ -38,5 +38,5 @@ tree-sitter-json = "*"
|
|||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
fs = { path = "../fs", features = ["test-support"] }
|
fs = { path = "../fs", features = ["test-support"] }
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions.workspace = true
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
|
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use gpui::{keymap_matcher::Binding, AppContext};
|
use gpui::{keymap_matcher::Binding, AppContext, NoAction};
|
||||||
use schemars::{
|
use schemars::{
|
||||||
gen::{SchemaGenerator, SchemaSettings},
|
gen::{SchemaGenerator, SchemaSettings},
|
||||||
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
|
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
|
||||||
@ -11,18 +11,18 @@ use serde::Deserialize;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use util::{asset_str, ResultExt};
|
use util::{asset_str, ResultExt};
|
||||||
|
|
||||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct KeymapFile(Vec<KeymapBlock>);
|
pub struct KeymapFile(Vec<KeymapBlock>);
|
||||||
|
|
||||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
|
||||||
pub struct KeymapBlock {
|
pub struct KeymapBlock {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
context: Option<String>,
|
context: Option<String>,
|
||||||
bindings: BTreeMap<String, KeymapAction>,
|
bindings: BTreeMap<String, KeymapAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default, Clone)]
|
#[derive(Debug, Deserialize, Default, Clone)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct KeymapAction(Value);
|
pub struct KeymapAction(Value);
|
||||||
|
|
||||||
@ -61,21 +61,22 @@ impl KeymapFile {
|
|||||||
// We want to deserialize the action data as a `RawValue` so that we can
|
// We want to deserialize the action data as a `RawValue` so that we can
|
||||||
// deserialize the action itself dynamically directly from the JSON
|
// deserialize the action itself dynamically directly from the JSON
|
||||||
// string. But `RawValue` currently does not work inside of an untagged enum.
|
// string. But `RawValue` currently does not work inside of an untagged enum.
|
||||||
if let Value::Array(items) = action {
|
match action {
|
||||||
let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
|
Value::Array(items) => {
|
||||||
return Some(Err(anyhow!("Expected array of length 2")));
|
let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
|
||||||
};
|
return Some(Err(anyhow!("Expected array of length 2")));
|
||||||
let serde_json::Value::String(name) = name else {
|
};
|
||||||
return Some(Err(anyhow!("Expected first item in array to be a string.")))
|
let serde_json::Value::String(name) = name else {
|
||||||
};
|
return Some(Err(anyhow!("Expected first item in array to be a string.")))
|
||||||
cx.deserialize_action(
|
};
|
||||||
&name,
|
cx.deserialize_action(
|
||||||
Some(data),
|
&name,
|
||||||
)
|
Some(data),
|
||||||
} else if let Value::String(name) = action {
|
)
|
||||||
cx.deserialize_action(&name, None)
|
},
|
||||||
} else {
|
Value::String(name) => cx.deserialize_action(&name, None),
|
||||||
return Some(Err(anyhow!("Expected two-element array, got {:?}", action)));
|
Value::Null => Ok(no_action()),
|
||||||
|
_ => return Some(Err(anyhow!("Expected two-element array, got {action:?}"))),
|
||||||
}
|
}
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
@ -115,6 +116,10 @@ impl KeymapFile {
|
|||||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
|
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
|
Schema::Object(SchemaObject {
|
||||||
|
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})),
|
})),
|
||||||
@ -129,6 +134,10 @@ impl KeymapFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn no_action() -> Box<dyn gpui::Action> {
|
||||||
|
Box::new(NoAction {})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::KeymapFile;
|
use crate::KeymapFile;
|
||||||
|
@ -2489,7 +2489,12 @@ impl ToOffset for Point {
|
|||||||
|
|
||||||
impl ToOffset for usize {
|
impl ToOffset for usize {
|
||||||
fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
|
fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
|
||||||
assert!(*self <= snapshot.len(), "offset {self} is out of range");
|
assert!(
|
||||||
|
*self <= snapshot.len(),
|
||||||
|
"offset {} is out of range, max allowed is {}",
|
||||||
|
self,
|
||||||
|
snapshot.len()
|
||||||
|
);
|
||||||
*self
|
*self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,6 @@ pub struct Theme {
|
|||||||
pub assistant: AssistantStyle,
|
pub assistant: AssistantStyle,
|
||||||
pub feedback: FeedbackStyle,
|
pub feedback: FeedbackStyle,
|
||||||
pub welcome: WelcomeStyle,
|
pub welcome: WelcomeStyle,
|
||||||
pub color_scheme: ColorScheme,
|
|
||||||
pub titlebar: Titlebar,
|
pub titlebar: Titlebar,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,8 +117,9 @@ pub struct Titlebar {
|
|||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub title: TextStyle,
|
pub project_menu_button: Toggleable<Interactive<ContainedText>>,
|
||||||
pub highlight_color: Color,
|
pub project_name_divider: 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,
|
||||||
pub avatar_ribbon: AvatarRibbon,
|
pub avatar_ribbon: AvatarRibbon,
|
||||||
@ -585,6 +585,8 @@ pub struct Picker {
|
|||||||
pub empty_input_editor: FieldEditor,
|
pub empty_input_editor: FieldEditor,
|
||||||
pub no_matches: ContainedLabel,
|
pub no_matches: ContainedLabel,
|
||||||
pub item: Toggleable<Interactive<ContainedLabel>>,
|
pub item: Toggleable<Interactive<ContainedLabel>>,
|
||||||
|
pub header: ContainedLabel,
|
||||||
|
pub footer: ContainedLabel,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
||||||
@ -720,6 +722,7 @@ pub struct Scrollbar {
|
|||||||
pub width: f32,
|
pub width: f32,
|
||||||
pub min_height_factor: f32,
|
pub min_height_factor: f32,
|
||||||
pub git: GitDiffColors,
|
pub git: GitDiffColors,
|
||||||
|
pub selections: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
@ -36,7 +36,6 @@ workspace = { path = "../workspace" }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
lazy_static.workspace = true
|
|
||||||
|
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
@ -11,8 +11,6 @@ use gpui::keymap_matcher::Keystroke;
|
|||||||
|
|
||||||
use language::Point;
|
use language::Point;
|
||||||
|
|
||||||
#[cfg(feature = "neovim")]
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
use nvim_rs::{
|
use nvim_rs::{
|
||||||
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
|
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
|
||||||
@ -32,9 +30,7 @@ use collections::VecDeque;
|
|||||||
// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock
|
// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock
|
||||||
// to ensure we are only constructing one neovim connection at a time.
|
// to ensure we are only constructing one neovim connection at a time.
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
lazy_static! {
|
static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
|
||||||
static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum NeovimData {
|
pub enum NeovimData {
|
||||||
|
@ -97,9 +97,25 @@ lazy_static! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait Modal: View {
|
pub trait Modal: View {
|
||||||
|
fn has_focus(&self) -> bool;
|
||||||
fn dismiss_on_event(event: &Self::Event) -> bool;
|
fn dismiss_on_event(event: &Self::Event) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait ModalHandle {
|
||||||
|
fn as_any(&self) -> &AnyViewHandle;
|
||||||
|
fn has_focus(&self, cx: &WindowContext) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Modal> ModalHandle for ViewHandle<T> {
|
||||||
|
fn as_any(&self) -> &AnyViewHandle {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_focus(&self, cx: &WindowContext) -> bool {
|
||||||
|
self.read(cx).has_focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct RemoveWorktreeFromProject(pub WorktreeId);
|
pub struct RemoveWorktreeFromProject(pub WorktreeId);
|
||||||
|
|
||||||
@ -466,7 +482,7 @@ pub enum Event {
|
|||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
weak_self: WeakViewHandle<Self>,
|
weak_self: WeakViewHandle<Self>,
|
||||||
remote_entity_subscription: Option<client::Subscription>,
|
remote_entity_subscription: Option<client::Subscription>,
|
||||||
modal: Option<AnyViewHandle>,
|
modal: Option<ActiveModal>,
|
||||||
zoomed: Option<AnyWeakViewHandle>,
|
zoomed: Option<AnyWeakViewHandle>,
|
||||||
zoomed_position: Option<DockPosition>,
|
zoomed_position: Option<DockPosition>,
|
||||||
center: PaneGroup,
|
center: PaneGroup,
|
||||||
@ -495,6 +511,11 @@ pub struct Workspace {
|
|||||||
pane_history_timestamp: Arc<AtomicUsize>,
|
pane_history_timestamp: Arc<AtomicUsize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ActiveModal {
|
||||||
|
view: Box<dyn ModalHandle>,
|
||||||
|
previously_focused_view_id: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct ViewId {
|
pub struct ViewId {
|
||||||
pub creator: PeerId,
|
pub creator: PeerId,
|
||||||
@ -1482,8 +1503,10 @@ impl Workspace {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
// Whatever modal was visible is getting clobbered. If its the same type as V, then return
|
// Whatever modal was visible is getting clobbered. If its the same type as V, then return
|
||||||
// it. Otherwise, create a new modal and set it as active.
|
// it. Otherwise, create a new modal and set it as active.
|
||||||
let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
|
if let Some(already_open_modal) = self
|
||||||
if let Some(already_open_modal) = already_open_modal {
|
.dismiss_modal(cx)
|
||||||
|
.and_then(|modal| modal.downcast::<V>())
|
||||||
|
{
|
||||||
cx.focus_self();
|
cx.focus_self();
|
||||||
Some(already_open_modal)
|
Some(already_open_modal)
|
||||||
} else {
|
} else {
|
||||||
@ -1494,8 +1517,12 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
let previously_focused_view_id = cx.focused_view_id();
|
||||||
cx.focus(&modal);
|
cx.focus(&modal);
|
||||||
self.modal = Some(modal.into_any());
|
self.modal = Some(ActiveModal {
|
||||||
|
view: Box::new(modal),
|
||||||
|
previously_focused_view_id,
|
||||||
|
});
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1503,13 +1530,20 @@ impl Workspace {
|
|||||||
pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
|
pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
|
||||||
self.modal
|
self.modal
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|modal| modal.clone().downcast::<V>())
|
.and_then(|modal| modal.view.as_any().clone().downcast::<V>())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyViewHandle> {
|
||||||
if self.modal.take().is_some() {
|
if let Some(modal) = self.modal.take() {
|
||||||
cx.focus(&self.active_pane);
|
if let Some(previously_focused_view_id) = modal.previously_focused_view_id {
|
||||||
|
if modal.view.has_focus(cx) {
|
||||||
|
cx.window_context().focus(Some(previously_focused_view_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
Some(modal.view.as_any().clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3496,7 +3530,7 @@ impl View for Workspace {
|
|||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
.with_children(self.modal.as_ref().map(|modal| {
|
.with_children(self.modal.as_ref().map(|modal| {
|
||||||
ChildView::new(modal, cx)
|
ChildView::new(modal.view.as_any(), cx)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.workspace.modal)
|
.with_style(theme.workspace.modal)
|
||||||
.aligned()
|
.aligned()
|
||||||
@ -4775,6 +4809,7 @@ mod tests {
|
|||||||
theme::init((), cx);
|
theme::init((), cx);
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
crate::init_settings(cx);
|
crate::init_settings(cx);
|
||||||
|
Project::init_settings(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.94.0"
|
version = "0.95.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
@ -16,6 +16,7 @@ name = "Zed"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
audio = { path = "../audio" }
|
||||||
activity_indicator = { path = "../activity_indicator" }
|
activity_indicator = { path = "../activity_indicator" }
|
||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update" }
|
||||||
breadcrumbs = { path = "../breadcrumbs" }
|
breadcrumbs = { path = "../breadcrumbs" }
|
||||||
|
@ -7,6 +7,7 @@ use rust_embed::RustEmbed;
|
|||||||
#[include = "fonts/**/*"]
|
#[include = "fonts/**/*"]
|
||||||
#[include = "icons/**/*"]
|
#[include = "icons/**/*"]
|
||||||
#[include = "themes/**/*"]
|
#[include = "themes/**/*"]
|
||||||
|
#[include = "sounds/**/*"]
|
||||||
#[include = "*.md"]
|
#[include = "*.md"]
|
||||||
#[exclude = "*.DS_Store"]
|
#[exclude = "*.DS_Store"]
|
||||||
pub struct Assets;
|
pub struct Assets;
|
||||||
|
@ -181,6 +181,8 @@ fn main() {
|
|||||||
background_actions,
|
background_actions,
|
||||||
});
|
});
|
||||||
cx.set_global(Arc::downgrade(&app_state));
|
cx.set_global(Arc::downgrade(&app_state));
|
||||||
|
|
||||||
|
audio::init(Assets, cx);
|
||||||
auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
|
auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
|
||||||
|
|
||||||
workspace::init(app_state.clone(), cx);
|
workspace::init(app_state.clone(), cx);
|
||||||
|
@ -2074,6 +2074,167 @@ mod tests {
|
|||||||
line!(),
|
line!(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_key_bindings_for<'a>(
|
||||||
|
window_id: usize,
|
||||||
|
cx: &TestAppContext,
|
||||||
|
actions: Vec<(&'static str, &'a dyn Action)>,
|
||||||
|
line: u32,
|
||||||
|
) {
|
||||||
|
for (key, action) in actions {
|
||||||
|
// assert that...
|
||||||
|
assert!(
|
||||||
|
cx.available_actions(window_id, 0)
|
||||||
|
.into_iter()
|
||||||
|
.any(|(_, bound_action, b)| {
|
||||||
|
// action names match...
|
||||||
|
bound_action.name() == action.name()
|
||||||
|
&& bound_action.namespace() == action.namespace()
|
||||||
|
// and key strokes contain the given key
|
||||||
|
&& b.iter()
|
||||||
|
.any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
|
||||||
|
}),
|
||||||
|
"On {} Failed to find {} with key binding {}",
|
||||||
|
line,
|
||||||
|
action.name(),
|
||||||
|
key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
|
||||||
|
struct TestView;
|
||||||
|
|
||||||
|
impl Entity for TestView {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for TestView {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"TestView"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
Empty::new().into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let executor = cx.background();
|
||||||
|
let fs = FakeFs::new(executor.clone());
|
||||||
|
|
||||||
|
actions!(test, [A, B]);
|
||||||
|
// From the Atom keymap
|
||||||
|
actions!(workspace, [ActivatePreviousPane]);
|
||||||
|
// From the JetBrains keymap
|
||||||
|
actions!(pane, [ActivatePrevItem]);
|
||||||
|
|
||||||
|
fs.save(
|
||||||
|
"/settings.json".as_ref(),
|
||||||
|
&r#"
|
||||||
|
{
|
||||||
|
"base_keymap": "Atom"
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
fs.save(
|
||||||
|
"/keymap.json".as_ref(),
|
||||||
|
&r#"
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"bindings": {
|
||||||
|
"backspace": "test::A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"#
|
||||||
|
.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.set_global(SettingsStore::test(cx));
|
||||||
|
theme::init(Assets, cx);
|
||||||
|
welcome::init(cx);
|
||||||
|
|
||||||
|
cx.add_global_action(|_: &A, _cx| {});
|
||||||
|
cx.add_global_action(|_: &B, _cx| {});
|
||||||
|
cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
|
||||||
|
cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
|
||||||
|
|
||||||
|
let settings_rx = watch_config_file(
|
||||||
|
executor.clone(),
|
||||||
|
fs.clone(),
|
||||||
|
PathBuf::from("/settings.json"),
|
||||||
|
);
|
||||||
|
let keymap_rx =
|
||||||
|
watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
|
||||||
|
|
||||||
|
handle_keymap_file_changes(keymap_rx, cx);
|
||||||
|
handle_settings_file_changes(settings_rx, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
|
let (window_id, _view) = cx.add_window(|_| TestView);
|
||||||
|
|
||||||
|
// Test loading the keymap base at all
|
||||||
|
assert_key_bindings_for(
|
||||||
|
window_id,
|
||||||
|
cx,
|
||||||
|
vec![("backspace", &A), ("k", &ActivatePreviousPane)],
|
||||||
|
line!(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test disabling the key binding for the base keymap
|
||||||
|
fs.save(
|
||||||
|
"/keymap.json".as_ref(),
|
||||||
|
&r#"
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"bindings": {
|
||||||
|
"backspace": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"#
|
||||||
|
.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
|
assert_key_bindings_for(window_id, cx, vec![("k", &ActivatePreviousPane)], line!());
|
||||||
|
|
||||||
|
// Test modifying the base, while retaining the users keymap
|
||||||
|
fs.save(
|
||||||
|
"/settings.json".as_ref(),
|
||||||
|
&r#"
|
||||||
|
{
|
||||||
|
"base_keymap": "JetBrains"
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
|
assert_key_bindings_for(window_id, cx, vec![("[", &ActivatePrevItem)], line!());
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
fn assert_key_bindings_for<'a>(
|
fn assert_key_bindings_for<'a>(
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
cx: &TestAppContext,
|
cx: &TestAppContext,
|
||||||
@ -2160,6 +2321,7 @@ mod tests {
|
|||||||
state.initialize_workspace = initialize_workspace;
|
state.initialize_workspace = initialize_workspace;
|
||||||
state.build_window_options = build_window_options;
|
state.build_window_options = build_window_options;
|
||||||
theme::init((), cx);
|
theme::init((), cx);
|
||||||
|
audio::init((), cx);
|
||||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
workspace::init(app_state.clone(), cx);
|
workspace::init(app_state.clone(), cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
|
@ -35,7 +35,7 @@ Match a property identifier and highlight it using the identifier `@property`. I
|
|||||||
```
|
```
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
function buildDefaultSyntax(colorScheme: ColorScheme): Partial<Syntax> {
|
function buildDefaultSyntax(colorScheme: Theme): Partial<Syntax> {
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
64
styles/package-lock.json
generated
64
styles/package-lock.json
generated
@ -27,7 +27,8 @@
|
|||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.1.5",
|
"typescript": "^5.1.5",
|
||||||
"utility-types": "^3.10.0",
|
"utility-types": "^3.10.0",
|
||||||
"vitest": "^0.32.0"
|
"vitest": "^0.32.0",
|
||||||
|
"zustand": "^4.3.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
@ -2595,6 +2596,12 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
@ -2706,6 +2713,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/loupe": {
|
"node_modules/loupe": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
|
||||||
@ -3292,6 +3311,18 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "18.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||||
|
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
@ -4025,6 +4056,14 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/utility-types": {
|
"node_modules/utility-types": {
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
|
||||||
@ -4305,6 +4344,29 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "4.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz",
|
||||||
|
"integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==",
|
||||||
|
"dependencies": {
|
||||||
|
"use-sync-external-store": "1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"immer": ">=9.0",
|
||||||
|
"react": ">=16.8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,21 +16,22 @@
|
|||||||
"@tokens-studio/types": "^0.2.3",
|
"@tokens-studio/types": "^0.2.3",
|
||||||
"@types/chroma-js": "^2.4.0",
|
"@types/chroma-js": "^2.4.0",
|
||||||
"@types/node": "^18.14.1",
|
"@types/node": "^18.14.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||||
|
"@typescript-eslint/parser": "^5.60.1",
|
||||||
|
"@vitest/coverage-v8": "^0.32.0",
|
||||||
"ayu": "^8.0.1",
|
"ayu": "^8.0.1",
|
||||||
"chroma-js": "^2.4.2",
|
"chroma-js": "^2.4.2",
|
||||||
"deepmerge": "^4.3.0",
|
"deepmerge": "^4.3.0",
|
||||||
|
"eslint": "^8.43.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.5.5",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"json-schema-to-typescript": "^13.0.2",
|
"json-schema-to-typescript": "^13.0.2",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"ts-deepmerge": "^6.0.3",
|
"ts-deepmerge": "^6.0.3",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
|
"typescript": "^5.1.5",
|
||||||
"utility-types": "^3.10.0",
|
"utility-types": "^3.10.0",
|
||||||
"vitest": "^0.32.0",
|
"vitest": "^0.32.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
"zustand": "^4.3.8"
|
||||||
"@typescript-eslint/parser": "^5.60.1",
|
|
||||||
"@vitest/coverage-v8": "^0.32.0",
|
|
||||||
"eslint": "^8.43.0",
|
|
||||||
"eslint-import-resolver-typescript": "^3.5.5",
|
|
||||||
"eslint-plugin-import": "^2.27.5",
|
|
||||||
"typescript": "^5.1.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,9 @@ import * as fs from "fs"
|
|||||||
import { tmpdir } from "os"
|
import { tmpdir } from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import app from "./style_tree/app"
|
import app from "./style_tree/app"
|
||||||
import { ColorScheme, create_color_scheme } from "./theme/color_scheme"
|
import { Theme, create_theme } from "./theme/create_theme"
|
||||||
import { themes } from "./themes"
|
import { themes } from "./themes"
|
||||||
|
import { useThemeStore } from "./theme"
|
||||||
|
|
||||||
const assets_directory = `${__dirname}/../../assets`
|
const assets_directory = `${__dirname}/../../assets`
|
||||||
const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
|
const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
|
||||||
@ -20,15 +21,22 @@ function clear_themes(theme_directory: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function write_themes(themes: ColorScheme[], output_directory: string) {
|
const all_themes: Theme[] = themes.map((theme) =>
|
||||||
|
create_theme(theme)
|
||||||
|
)
|
||||||
|
|
||||||
|
function write_themes(themes: Theme[], output_directory: string) {
|
||||||
clear_themes(output_directory)
|
clear_themes(output_directory)
|
||||||
for (const color_scheme of themes) {
|
for (const theme of themes) {
|
||||||
const style_tree = app(color_scheme)
|
const { setTheme } = useThemeStore.getState()
|
||||||
|
setTheme(theme)
|
||||||
|
|
||||||
|
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, `${color_scheme.name}.json`)
|
const temp_path = path.join(temp_directory, `${theme.name}.json`)
|
||||||
const out_path = path.join(
|
const out_path = path.join(
|
||||||
output_directory,
|
output_directory,
|
||||||
`${color_scheme.name}.json`
|
`${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)
|
||||||
@ -36,8 +44,4 @@ function write_themes(themes: ColorScheme[], output_directory: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const all_themes: ColorScheme[] = themes.map((theme) =>
|
|
||||||
create_color_scheme(theme)
|
|
||||||
)
|
|
||||||
|
|
||||||
write_themes(all_themes, `${assets_directory}/themes`)
|
write_themes(all_themes, `${assets_directory}/themes`)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { ColorScheme, create_color_scheme } from "./common"
|
import { Theme, create_theme, useThemeStore } from "./common"
|
||||||
import { themes } from "./themes"
|
import { themes } from "./themes"
|
||||||
import { slugify } from "./utils/slugify"
|
import { slugify } from "./utils/slugify"
|
||||||
import { theme_tokens } from "./theme/tokens/color_scheme"
|
import { theme_tokens } from "./theme/tokens/theme"
|
||||||
|
|
||||||
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
|
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
|
||||||
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
|
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
|
||||||
@ -27,7 +27,7 @@ type TokenSet = {
|
|||||||
selected_token_sets: { [key: string]: "enabled" }
|
selected_token_sets: { [key: string]: "enabled" }
|
||||||
}
|
}
|
||||||
|
|
||||||
function build_token_set_order(theme: ColorScheme[]): {
|
function build_token_set_order(theme: Theme[]): {
|
||||||
token_set_order: string[]
|
token_set_order: string[]
|
||||||
} {
|
} {
|
||||||
const token_set_order: string[] = theme.map((scheme) =>
|
const token_set_order: string[] = theme.map((scheme) =>
|
||||||
@ -36,7 +36,7 @@ function build_token_set_order(theme: ColorScheme[]): {
|
|||||||
return { token_set_order }
|
return { token_set_order }
|
||||||
}
|
}
|
||||||
|
|
||||||
function build_themes_index(theme: ColorScheme[]): TokenSet[] {
|
function build_themes_index(theme: Theme[]): TokenSet[] {
|
||||||
const themes_index: TokenSet[] = theme.map((scheme, index) => {
|
const themes_index: TokenSet[] = theme.map((scheme, index) => {
|
||||||
const id = `${scheme.is_light ? "light" : "dark"}_${scheme.name
|
const id = `${scheme.is_light ? "light" : "dark"}_${scheme.name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@ -55,12 +55,15 @@ function build_themes_index(theme: ColorScheme[]): TokenSet[] {
|
|||||||
return themes_index
|
return themes_index
|
||||||
}
|
}
|
||||||
|
|
||||||
function write_tokens(themes: ColorScheme[], tokens_directory: string) {
|
function write_tokens(themes: Theme[], tokens_directory: string) {
|
||||||
clear_tokens(tokens_directory)
|
clear_tokens(tokens_directory)
|
||||||
|
|
||||||
for (const theme of themes) {
|
for (const theme of themes) {
|
||||||
|
const { setTheme } = useThemeStore.getState()
|
||||||
|
setTheme(theme)
|
||||||
|
|
||||||
const file_name = slugify(theme.name) + ".json"
|
const file_name = slugify(theme.name) + ".json"
|
||||||
const tokens = theme_tokens(theme)
|
const tokens = theme_tokens()
|
||||||
const tokens_json = JSON.stringify(tokens, null, 2)
|
const tokens_json = JSON.stringify(tokens, null, 2)
|
||||||
const out_path = path.join(tokens_directory, file_name)
|
const out_path = path.join(tokens_directory, file_name)
|
||||||
fs.writeFileSync(out_path, tokens_json, { mode: 0o644 })
|
fs.writeFileSync(out_path, tokens_json, { mode: 0o644 })
|
||||||
@ -80,8 +83,8 @@ function write_tokens(themes: ColorScheme[], tokens_directory: string) {
|
|||||||
console.log(`- ${METADATA_FILE} created`)
|
console.log(`- ${METADATA_FILE} created`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const all_themes: ColorScheme[] = themes.map((theme) =>
|
const all_themes: Theme[] = themes.map((theme) =>
|
||||||
create_color_scheme(theme)
|
create_theme(theme)
|
||||||
)
|
)
|
||||||
|
|
||||||
write_tokens(all_themes, TOKENS_DIRECTORY)
|
write_tokens(all_themes, TOKENS_DIRECTORY)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { interactive, toggleable } from "../element"
|
import { interactive, toggleable } from "../element"
|
||||||
import { background, foreground } from "../style_tree/components"
|
import { background, foreground } from "../style_tree/components"
|
||||||
import { ColorScheme } from "../theme/color_scheme"
|
import { useTheme, Theme } from "../theme"
|
||||||
|
|
||||||
export type Margin = {
|
export type Margin = {
|
||||||
top: number
|
top: number
|
||||||
@ -11,21 +11,20 @@ export type Margin = {
|
|||||||
|
|
||||||
interface IconButtonOptions {
|
interface IconButtonOptions {
|
||||||
layer?:
|
layer?:
|
||||||
| ColorScheme["lowest"]
|
| Theme["lowest"]
|
||||||
| ColorScheme["middle"]
|
| Theme["middle"]
|
||||||
| ColorScheme["highest"]
|
| Theme["highest"]
|
||||||
color?: keyof ColorScheme["lowest"]
|
color?: keyof Theme["lowest"]
|
||||||
margin?: Partial<Margin>
|
margin?: Partial<Margin>
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToggleableIconButtonOptions = IconButtonOptions & {
|
type ToggleableIconButtonOptions = IconButtonOptions & {
|
||||||
active_color?: keyof ColorScheme["lowest"]
|
active_color?: keyof Theme["lowest"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function icon_button(
|
export function icon_button({ color, margin, layer }: IconButtonOptions) {
|
||||||
theme: ColorScheme,
|
const theme = useTheme()
|
||||||
{ color, margin, layer }: IconButtonOptions
|
|
||||||
) {
|
|
||||||
if (!color) color = "base"
|
if (!color) color = "base"
|
||||||
|
|
||||||
const m = {
|
const m = {
|
||||||
@ -68,15 +67,15 @@ export function icon_button(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toggleable_icon_button(
|
export function toggleable_icon_button(
|
||||||
theme: ColorScheme,
|
theme: Theme,
|
||||||
{ color, active_color, margin }: ToggleableIconButtonOptions
|
{ color, active_color, margin }: ToggleableIconButtonOptions
|
||||||
) {
|
) {
|
||||||
if (!color) color = "base"
|
if (!color) color = "base"
|
||||||
|
|
||||||
return toggleable({
|
return toggleable({
|
||||||
state: {
|
state: {
|
||||||
inactive: icon_button(theme, { color, margin }),
|
inactive: icon_button({ color, margin }),
|
||||||
active: icon_button(theme, {
|
active: icon_button({
|
||||||
color: active_color ? active_color : color,
|
color: active_color ? active_color : color,
|
||||||
margin,
|
margin,
|
||||||
layer: theme.middle,
|
layer: theme.middle,
|
||||||
|
@ -5,27 +5,30 @@ import {
|
|||||||
foreground,
|
foreground,
|
||||||
text,
|
text,
|
||||||
} from "../style_tree/components"
|
} from "../style_tree/components"
|
||||||
import { ColorScheme } from "../theme/color_scheme"
|
import { useTheme, Theme } from "../theme"
|
||||||
import { Margin } from "./icon_button"
|
import { Margin } from "./icon_button"
|
||||||
|
|
||||||
interface TextButtonOptions {
|
interface TextButtonOptions {
|
||||||
layer?:
|
layer?:
|
||||||
| ColorScheme["lowest"]
|
| Theme["lowest"]
|
||||||
| ColorScheme["middle"]
|
| Theme["middle"]
|
||||||
| ColorScheme["highest"]
|
| Theme["highest"]
|
||||||
color?: keyof ColorScheme["lowest"]
|
color?: keyof Theme["lowest"]
|
||||||
margin?: Partial<Margin>
|
margin?: Partial<Margin>
|
||||||
text_properties?: TextProperties
|
text_properties?: TextProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToggleableTextButtonOptions = TextButtonOptions & {
|
type ToggleableTextButtonOptions = TextButtonOptions & {
|
||||||
active_color?: keyof ColorScheme["lowest"]
|
active_color?: keyof Theme["lowest"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function text_button(
|
export function text_button({
|
||||||
theme: ColorScheme,
|
color,
|
||||||
{ color, layer, margin, text_properties }: TextButtonOptions
|
layer,
|
||||||
) {
|
margin,
|
||||||
|
text_properties,
|
||||||
|
}: TextButtonOptions) {
|
||||||
|
const theme = useTheme()
|
||||||
if (!color) color = "base"
|
if (!color) color = "base"
|
||||||
|
|
||||||
const text_options: TextProperties = {
|
const text_options: TextProperties = {
|
||||||
@ -72,15 +75,15 @@ export function text_button(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toggleable_text_button(
|
export function toggleable_text_button(
|
||||||
theme: ColorScheme,
|
theme: Theme,
|
||||||
{ color, active_color, margin }: ToggleableTextButtonOptions
|
{ color, active_color, margin }: ToggleableTextButtonOptions
|
||||||
) {
|
) {
|
||||||
if (!color) color = "base"
|
if (!color) color = "base"
|
||||||
|
|
||||||
return toggleable({
|
return toggleable({
|
||||||
state: {
|
state: {
|
||||||
inactive: text_button(theme, { color, margin }),
|
inactive: text_button({ color, margin }),
|
||||||
active: text_button(theme, {
|
active: text_button({
|
||||||
color: active_color ? active_color : color,
|
color: active_color ? active_color : color,
|
||||||
margin,
|
margin,
|
||||||
layer: theme.middle,
|
layer: theme.middle,
|
||||||
|
@ -17,59 +17,46 @@ import terminal from "./terminal"
|
|||||||
import contact_list from "./contact_list"
|
import contact_list from "./contact_list"
|
||||||
import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
|
import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
|
||||||
import incoming_call_notification from "./incoming_call_notification"
|
import incoming_call_notification from "./incoming_call_notification"
|
||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import welcome from "./welcome"
|
import welcome from "./welcome"
|
||||||
import copilot from "./copilot"
|
import copilot from "./copilot"
|
||||||
import assistant from "./assistant"
|
import assistant from "./assistant"
|
||||||
import { titlebar } from "./titlebar"
|
import { titlebar } from "./titlebar"
|
||||||
import editor from "./editor"
|
import editor from "./editor"
|
||||||
import feedback from "./feedback"
|
import feedback from "./feedback"
|
||||||
|
import { useTheme } from "../common"
|
||||||
|
|
||||||
|
export default function app(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
export default function app(theme: ColorScheme): any {
|
|
||||||
return {
|
return {
|
||||||
meta: {
|
meta: {
|
||||||
name: theme.name,
|
name: theme.name,
|
||||||
is_light: theme.is_light,
|
is_light: theme.is_light,
|
||||||
},
|
},
|
||||||
command_palette: command_palette(theme),
|
command_palette: command_palette(),
|
||||||
contact_notification: contact_notification(theme),
|
contact_notification: contact_notification(),
|
||||||
project_shared_notification: project_shared_notification(theme),
|
project_shared_notification: project_shared_notification(),
|
||||||
incoming_call_notification: incoming_call_notification(theme),
|
incoming_call_notification: incoming_call_notification(),
|
||||||
picker: picker(theme),
|
picker: picker(),
|
||||||
workspace: workspace(theme),
|
workspace: workspace(),
|
||||||
titlebar: titlebar(theme),
|
titlebar: titlebar(),
|
||||||
copilot: copilot(theme),
|
copilot: copilot(),
|
||||||
welcome: welcome(theme),
|
welcome: welcome(),
|
||||||
context_menu: context_menu(theme),
|
context_menu: context_menu(),
|
||||||
editor: editor(theme),
|
editor: editor(),
|
||||||
project_diagnostics: project_diagnostics(theme),
|
project_diagnostics: project_diagnostics(),
|
||||||
project_panel: project_panel(theme),
|
project_panel: project_panel(),
|
||||||
contacts_popover: contacts_popover(theme),
|
contacts_popover: contacts_popover(),
|
||||||
contact_finder: contact_finder(theme),
|
contact_finder: contact_finder(),
|
||||||
contact_list: contact_list(theme),
|
contact_list: contact_list(),
|
||||||
toolbar_dropdown_menu: toolbar_dropdown_menu(theme),
|
toolbar_dropdown_menu: toolbar_dropdown_menu(),
|
||||||
search: search(theme),
|
search: search(),
|
||||||
shared_screen: shared_screen(theme),
|
shared_screen: shared_screen(),
|
||||||
update_notification: update_notification(theme),
|
update_notification: update_notification(),
|
||||||
simple_message_notification: simple_message_notification(theme),
|
simple_message_notification: simple_message_notification(),
|
||||||
tooltip: tooltip(theme),
|
tooltip: tooltip(),
|
||||||
terminal: terminal(theme),
|
terminal: terminal(),
|
||||||
assistant: assistant(theme),
|
assistant: assistant(),
|
||||||
feedback: feedback(theme),
|
feedback: feedback()
|
||||||
color_scheme: {
|
|
||||||
...theme,
|
|
||||||
players: Object.values(theme.players),
|
|
||||||
ramps: {
|
|
||||||
neutral: theme.ramps.neutral.colors(100, "hex"),
|
|
||||||
red: theme.ramps.red.colors(100, "hex"),
|
|
||||||
orange: theme.ramps.orange.colors(100, "hex"),
|
|
||||||
yellow: theme.ramps.yellow.colors(100, "hex"),
|
|
||||||
green: theme.ramps.green.colors(100, "hex"),
|
|
||||||
cyan: theme.ramps.cyan.colors(100, "hex"),
|
|
||||||
blue: theme.ramps.blue.colors(100, "hex"),
|
|
||||||
violet: theme.ramps.violet.colors(100, "hex"),
|
|
||||||
magenta: theme.ramps.magenta.colors(100, "hex"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import { text, border, background, foreground } from "./components"
|
import { text, border, background, foreground } from "./components"
|
||||||
import { interactive } from "../element"
|
import { interactive } from "../element"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
|
||||||
|
export default function assistant(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
export default function assistant(theme: ColorScheme): any {
|
|
||||||
return {
|
return {
|
||||||
container: {
|
container: {
|
||||||
background: background(theme.highest),
|
background: background(theme.highest),
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import { with_opacity } from "../theme/color"
|
import { with_opacity } from "../theme/color"
|
||||||
import { text, background } from "./components"
|
import { text, background } from "./components"
|
||||||
import { toggleable } from "../element"
|
import { toggleable } from "../element"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
|
||||||
|
export default function command_palette(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
export default function command_palette(theme: ColorScheme): any {
|
|
||||||
const key = toggleable({
|
const key = toggleable({
|
||||||
base: {
|
base: {
|
||||||
text: text(theme.highest, "mono", "variant", "default", {
|
text: text(theme.highest, "mono", "variant", "default", {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { font_families, font_sizes, FontWeight } from "../common"
|
import { font_families, font_sizes, FontWeight } from "../common"
|
||||||
import { Layer, Styles, StyleSets, Style } from "../theme/color_scheme"
|
import { Layer, Styles, StyleSets, Style } from "../theme/create_theme"
|
||||||
|
|
||||||
function is_style_set(key: any): key is StyleSets {
|
function is_style_set(key: any): key is StyleSets {
|
||||||
return [
|
return [
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import picker from "./picker"
|
import picker from "./picker"
|
||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import { background, border, foreground, text } from "./components"
|
import { background, border, foreground, text } from "./components"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
|
||||||
|
export default function contact_finder(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
export default function contact_finder(theme: ColorScheme): any {
|
|
||||||
const side_margin = 6
|
const side_margin = 6
|
||||||
const contact_button = {
|
const contact_button = {
|
||||||
background: background(theme.middle, "variant"),
|
background: background(theme.middle, "variant"),
|
||||||
@ -12,7 +14,7 @@ export default function contact_finder(theme: ColorScheme): any {
|
|||||||
corner_radius: 8,
|
corner_radius: 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
const picker_style = picker(theme)
|
const picker_style = picker()
|
||||||
const picker_input = {
|
const picker_input = {
|
||||||
background: background(theme.middle, "on"),
|
background: background(theme.middle, "on"),
|
||||||
corner_radius: 6,
|
corner_radius: 6,
|
||||||
@ -44,6 +46,8 @@ export default function contact_finder(theme: ColorScheme): any {
|
|||||||
no_matches: picker_style.no_matches,
|
no_matches: picker_style.no_matches,
|
||||||
input_editor: picker_input,
|
input_editor: picker_input,
|
||||||
empty_input_editor: picker_input,
|
empty_input_editor: picker_input,
|
||||||
|
header: picker_style.header,
|
||||||
|
footer: picker_style.footer,
|
||||||
},
|
},
|
||||||
row_height: 28,
|
row_height: 28,
|
||||||
contact_avatar: {
|
contact_avatar: {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import {
|
import {
|
||||||
background,
|
background,
|
||||||
border,
|
border,
|
||||||
@ -7,7 +6,10 @@ import {
|
|||||||
text,
|
text,
|
||||||
} from "./components"
|
} from "./components"
|
||||||
import { interactive, toggleable } from "../element"
|
import { interactive, toggleable } from "../element"
|
||||||
export default function contacts_panel(theme: ColorScheme): any {
|
import { useTheme } from "../theme"
|
||||||
|
export default function contacts_panel(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
const name_margin = 8
|
const name_margin = 8
|
||||||
const side_padding = 12
|
const side_padding = 12
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import { background, foreground, text } from "./components"
|
import { background, foreground, text } from "./components"
|
||||||
import { interactive } from "../element"
|
import { interactive } from "../element"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
|
||||||
|
export default function contact_notification(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
export default function contact_notification(theme: ColorScheme): any {
|
|
||||||
const avatar_size = 12
|
const avatar_size = 12
|
||||||
const header_padding = 8
|
const header_padding = 8
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
import { useTheme } from "../theme"
|
||||||
import { background, border } from "./components"
|
import { background, border } from "./components"
|
||||||
|
|
||||||
export default function contacts_popover(theme: ColorScheme): any {
|
export default function contacts_popover(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
background: background(theme.middle),
|
background: background(theme.middle),
|
||||||
corner_radius: 6,
|
corner_radius: 6,
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import { background, border, border_color, text } from "./components"
|
import { background, border, border_color, text } from "./components"
|
||||||
import { interactive, toggleable } from "../element"
|
import { interactive, toggleable } from "../element"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
|
||||||
|
export default function context_menu(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
export default function context_menu(theme: ColorScheme): any {
|
|
||||||
return {
|
return {
|
||||||
background: background(theme.middle),
|
background: background(theme.middle),
|
||||||
corner_radius: 10,
|
corner_radius: 10,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import { background, border, foreground, svg, text } from "./components"
|
import { background, border, foreground, svg, text } from "./components"
|
||||||
import { interactive } from "../element"
|
import { interactive } from "../element"
|
||||||
export default function copilot(theme: ColorScheme): any {
|
import { useTheme } from "../theme"
|
||||||
|
export default function copilot(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
const content_width = 264
|
const content_width = 264
|
||||||
|
|
||||||
const cta_button =
|
const cta_button =
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { with_opacity } from "../theme/color"
|
import { with_opacity } from "../theme/color"
|
||||||
import { ColorScheme, Layer, StyleSets } from "../theme/color_scheme"
|
import { Layer, StyleSets } from "../theme/create_theme"
|
||||||
import {
|
import {
|
||||||
background,
|
background,
|
||||||
border,
|
border,
|
||||||
@ -11,8 +11,11 @@ import hover_popover from "./hover_popover"
|
|||||||
|
|
||||||
import { build_syntax } from "../theme/syntax"
|
import { build_syntax } from "../theme/syntax"
|
||||||
import { interactive, toggleable } from "../element"
|
import { interactive, toggleable } from "../element"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
|
||||||
|
export default function editor(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
export default function editor(theme: ColorScheme): any {
|
|
||||||
const { is_light } = theme
|
const { is_light } = theme
|
||||||
|
|
||||||
const layer = theme.highest
|
const layer = theme.highest
|
||||||
@ -45,7 +48,7 @@ export default function editor(theme: ColorScheme): any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const syntax = build_syntax(theme)
|
const syntax = build_syntax()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text_color: syntax.primary.color,
|
text_color: syntax.primary.color,
|
||||||
@ -248,7 +251,7 @@ export default function editor(theme: ColorScheme): any {
|
|||||||
invalid_hint_diagnostic: diagnostic(theme.middle, "base"),
|
invalid_hint_diagnostic: diagnostic(theme.middle, "base"),
|
||||||
invalid_information_diagnostic: diagnostic(theme.middle, "base"),
|
invalid_information_diagnostic: diagnostic(theme.middle, "base"),
|
||||||
invalid_warning_diagnostic: diagnostic(theme.middle, "base"),
|
invalid_warning_diagnostic: diagnostic(theme.middle, "base"),
|
||||||
hover_popover: hover_popover(theme),
|
hover_popover: hover_popover(),
|
||||||
link_definition: {
|
link_definition: {
|
||||||
color: syntax.link_uri.color,
|
color: syntax.link_uri.color,
|
||||||
underline: syntax.link_uri.underline,
|
underline: syntax.link_uri.underline,
|
||||||
@ -301,6 +304,7 @@ export default function editor(theme: ColorScheme): 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")
|
||||||
},
|
},
|
||||||
composition_mark: {
|
composition_mark: {
|
||||||
underline: {
|
underline: {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import { background, border, text } from "./components"
|
import { background, border, text } from "./components"
|
||||||
import { interactive } from "../element"
|
import { interactive } from "../element"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
|
||||||
|
export default function feedback(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
export default function feedback(theme: ColorScheme): any {
|
|
||||||
return {
|
return {
|
||||||
submit_button: interactive({
|
submit_button: interactive({
|
||||||
base: {
|
base: {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
import { useTheme } from "../theme"
|
||||||
import { background, border, foreground, text } from "./components"
|
import { background, border, foreground, text } from "./components"
|
||||||
|
|
||||||
export default function hover_popover(theme: ColorScheme): any {
|
export default function hover_popover(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
const base_container = {
|
const base_container = {
|
||||||
background: background(theme.middle),
|
background: background(theme.middle),
|
||||||
corner_radius: 8,
|
corner_radius: 8,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
import { useTheme } from "../theme"
|
||||||
import { background, border, text } from "./components"
|
import { background, border, text } from "./components"
|
||||||
|
|
||||||
export default function incoming_call_notification(
|
export default function incoming_call_notification(): unknown {
|
||||||
theme: ColorScheme
|
const theme = useTheme()
|
||||||
): unknown {
|
|
||||||
const avatar_size = 48
|
const avatar_size = 48
|
||||||
return {
|
return {
|
||||||
window_height: 74,
|
window_height: 74,
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import { with_opacity } from "../theme/color"
|
import { with_opacity } from "../theme/color"
|
||||||
import { background, border, text } from "./components"
|
import { background, border, text } from "./components"
|
||||||
import { interactive, toggleable } from "../element"
|
import { interactive, toggleable } from "../element"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
|
||||||
|
export default function picker(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
export default function picker(theme: ColorScheme): any {
|
|
||||||
const container = {
|
const container = {
|
||||||
background: background(theme.lowest),
|
background: background(theme.lowest),
|
||||||
border: border(theme.lowest),
|
border: border(theme.lowest),
|
||||||
@ -108,5 +110,23 @@ export default function picker(theme: ColorScheme): any {
|
|||||||
top: 8,
|
top: 8,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
header: {
|
||||||
|
text: text(theme.lowest, "sans", "variant", { size: "xs" }),
|
||||||
|
|
||||||
|
margin: {
|
||||||
|
top: 1,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
text: text(theme.lowest, "sans", "variant", { size: "xs" }),
|
||||||
|
margin: {
|
||||||
|
top: 1,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
import { useTheme } from "../theme"
|
||||||
import { background, text } from "./components"
|
import { background, text } from "./components"
|
||||||
|
|
||||||
export default function project_diagnostics(theme: ColorScheme): any {
|
export default function project_diagnostics(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
background: background(theme.highest),
|
background: background(theme.highest),
|
||||||
tab_icon_spacing: 4,
|
tab_icon_spacing: 4,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import { with_opacity } from "../theme/color"
|
import { with_opacity } from "../theme/color"
|
||||||
import {
|
import {
|
||||||
Border,
|
Border,
|
||||||
@ -10,7 +9,10 @@ import {
|
|||||||
} from "./components"
|
} from "./components"
|
||||||
import { interactive, toggleable } from "../element"
|
import { interactive, toggleable } from "../element"
|
||||||
import merge from "ts-deepmerge"
|
import merge from "ts-deepmerge"
|
||||||
export default function project_panel(theme: ColorScheme): any {
|
import { useTheme } from "../theme"
|
||||||
|
export default function project_panel(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
const { is_light } = theme
|
const { is_light } = theme
|
||||||
|
|
||||||
type EntryStateProps = {
|
type EntryStateProps = {
|
||||||
@ -65,13 +67,12 @@ export default function project_panel(theme: ColorScheme): any {
|
|||||||
const unselected_hovered_style = merge(
|
const unselected_hovered_style = merge(
|
||||||
base_properties,
|
base_properties,
|
||||||
{ background: background(theme.middle, "hovered") },
|
{ background: background(theme.middle, "hovered") },
|
||||||
unselected?.hovered ?? {},
|
unselected?.hovered ?? {}
|
||||||
)
|
)
|
||||||
const unselected_clicked_style = merge(
|
const unselected_clicked_style = merge(
|
||||||
base_properties,
|
base_properties,
|
||||||
{ background: background(theme.middle, "pressed"), }
|
{ background: background(theme.middle, "pressed") },
|
||||||
,
|
unselected?.clicked ?? {}
|
||||||
unselected?.clicked ?? {},
|
|
||||||
)
|
)
|
||||||
const selected_default_style = merge(
|
const selected_default_style = merge(
|
||||||
base_properties,
|
base_properties,
|
||||||
@ -79,18 +80,15 @@ export default function project_panel(theme: ColorScheme): any {
|
|||||||
background: background(theme.lowest),
|
background: background(theme.lowest),
|
||||||
text: text(theme.lowest, "sans", { size: "sm" }),
|
text: text(theme.lowest, "sans", { size: "sm" }),
|
||||||
},
|
},
|
||||||
selected_style?.default ?? {},
|
selected_style?.default ?? {}
|
||||||
|
|
||||||
)
|
)
|
||||||
const selected_hovered_style = merge(
|
const selected_hovered_style = merge(
|
||||||
base_properties,
|
base_properties,
|
||||||
{
|
{
|
||||||
background: background(theme.lowest, "hovered"),
|
background: background(theme.lowest, "hovered"),
|
||||||
text: text(theme.lowest, "sans", { size: "sm" }),
|
text: text(theme.lowest, "sans", { size: "sm" }),
|
||||||
|
|
||||||
},
|
},
|
||||||
selected_style?.hovered ?? {},
|
selected_style?.hovered ?? {}
|
||||||
|
|
||||||
)
|
)
|
||||||
const selected_clicked_style = merge(
|
const selected_clicked_style = merge(
|
||||||
base_properties,
|
base_properties,
|
||||||
@ -98,8 +96,7 @@ export default function project_panel(theme: ColorScheme): any {
|
|||||||
background: background(theme.lowest, "pressed"),
|
background: background(theme.lowest, "pressed"),
|
||||||
text: text(theme.lowest, "sans", { size: "sm" }),
|
text: text(theme.lowest, "sans", { size: "sm" }),
|
||||||
},
|
},
|
||||||
selected_style?.clicked ?? {},
|
selected_style?.clicked ?? {}
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return toggleable({
|
return toggleable({
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
import { useTheme } from "../theme"
|
||||||
import { background, border, text } from "./components"
|
import { background, border, text } from "./components"
|
||||||
|
|
||||||
export default function project_shared_notification(
|
export default function project_shared_notification(): unknown {
|
||||||
theme: ColorScheme
|
const theme = useTheme()
|
||||||
): unknown {
|
|
||||||
const avatar_size = 48
|
const avatar_size = 48
|
||||||
return {
|
return {
|
||||||
window_height: 74,
|
window_height: 74,
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import { with_opacity } from "../theme/color"
|
import { with_opacity } from "../theme/color"
|
||||||
import { background, border, foreground, text } from "./components"
|
import { background, border, foreground, text } from "./components"
|
||||||
import { interactive, toggleable } from "../element"
|
import { interactive, toggleable } from "../element"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
|
||||||
|
export default function search(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
export default function search(theme: ColorScheme): any {
|
|
||||||
// Search input
|
// Search input
|
||||||
const editor = {
|
const editor = {
|
||||||
background: background(theme.highest),
|
background: background(theme.highest),
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
import { useTheme } from "../theme"
|
||||||
import { background } from "./components"
|
import { background } from "./components"
|
||||||
|
|
||||||
export default function sharedScreen(theme: ColorScheme) {
|
export default function sharedScreen() {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
background: background(theme.highest),
|
background: background(theme.highest),
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { ColorScheme } from "../theme/color_scheme"
|
|
||||||
import { background, border, foreground, text } from "./components"
|
import { background, border, foreground, text } from "./components"
|
||||||
import { interactive } from "../element"
|
import { interactive } from "../element"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
|
||||||
|
export default function simple_message_notification(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
export default function simple_message_notification(theme: ColorScheme): any {
|
|
||||||
const header_padding = 8
|
const header_padding = 8
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user