Merge branch 'main' into update-pickers
This commit is contained in:
commit
9e6103f863
95 changed files with 10906 additions and 3606 deletions
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
@ -11,6 +11,11 @@ on:
|
|||
branches:
|
||||
- "**"
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
|
3
.github/workflows/release_nightly.yml
vendored
3
.github/workflows/release_nightly.yml
vendored
|
@ -81,11 +81,12 @@ jobs:
|
|||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Set release channel to nightly
|
||||
- name: Set release channel to nightly, add nightly prefix to the final version
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
sed -i '' "s/version = \"\(.*\)\"/version = \"\1-nightly\"/" crates/zed2/Cargo.toml
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Generate license file
|
||||
|
|
81
Cargo.lock
generated
81
Cargo.lock
generated
|
@ -376,6 +376,47 @@ dependencies = [
|
|||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ai2",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"client2",
|
||||
"collections",
|
||||
"ctor",
|
||||
"editor2",
|
||||
"env_logger 0.9.3",
|
||||
"fs2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"indoc",
|
||||
"isahc",
|
||||
"language2",
|
||||
"log",
|
||||
"menu2",
|
||||
"multi_buffer2",
|
||||
"ordered-float 2.10.0",
|
||||
"parking_lot 0.11.2",
|
||||
"project2",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"schemars",
|
||||
"search2",
|
||||
"semantic_index2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smol",
|
||||
"theme2",
|
||||
"tiktoken-rs",
|
||||
"ui2",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.4.1"
|
||||
|
@ -413,9 +454,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.3.15"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a"
|
||||
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"futures-core",
|
||||
|
@ -1702,7 +1743,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.29.1"
|
||||
version = "0.30.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
@ -1789,7 +1830,7 @@ dependencies = [
|
|||
"clap 3.2.25",
|
||||
"client2",
|
||||
"clock",
|
||||
"collab_ui",
|
||||
"collab_ui2",
|
||||
"collections",
|
||||
"ctor",
|
||||
"dashmap",
|
||||
|
@ -1918,6 +1959,7 @@ dependencies = [
|
|||
"postage",
|
||||
"pretty_assertions",
|
||||
"project2",
|
||||
"recent_projects2",
|
||||
"rich_text2",
|
||||
"rpc2",
|
||||
"schemars",
|
||||
|
@ -7076,6 +7118,7 @@ dependencies = [
|
|||
name = "quick_action_bar2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"assistant2",
|
||||
"editor2",
|
||||
"gpui2",
|
||||
"search2",
|
||||
|
@ -7265,6 +7308,28 @@ dependencies = [
|
|||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "recent_projects2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"db",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"ordered-float 2.10.0",
|
||||
"picker2",
|
||||
"postage",
|
||||
"settings2",
|
||||
"smol",
|
||||
"text2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
|
@ -9490,6 +9555,7 @@ dependencies = [
|
|||
"terminal2",
|
||||
"theme2",
|
||||
"thiserror",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
@ -11706,7 +11772,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.116.0"
|
||||
version = "0.117.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
|
@ -11847,11 +11913,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed2"
|
||||
version = "0.109.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"activity_indicator2",
|
||||
"ai2",
|
||||
"anyhow",
|
||||
"assistant2",
|
||||
"async-compression",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tar",
|
||||
|
@ -11904,6 +11971,7 @@ dependencies = [
|
|||
"project_panel2",
|
||||
"quick_action_bar2",
|
||||
"rand 0.8.5",
|
||||
"recent_projects2",
|
||||
"regex",
|
||||
"rope2",
|
||||
"rpc2",
|
||||
|
@ -11911,6 +11979,7 @@ dependencies = [
|
|||
"rust-embed",
|
||||
"schemars",
|
||||
"search2",
|
||||
"semantic_index2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
|
|
|
@ -4,6 +4,7 @@ members = [
|
|||
"crates/activity_indicator2",
|
||||
"crates/ai",
|
||||
"crates/assistant",
|
||||
"crates/assistant2",
|
||||
"crates/audio",
|
||||
"crates/audio2",
|
||||
"crates/auto_update",
|
||||
|
@ -91,6 +92,7 @@ members = [
|
|||
"crates/project_symbols",
|
||||
"crates/quick_action_bar2",
|
||||
"crates/recent_projects",
|
||||
"crates/recent_projects2",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rpc2",
|
||||
|
@ -135,6 +137,7 @@ resolver = "2"
|
|||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.57" }
|
||||
async-trait = { version = "0.1" }
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
# TODO: Switch back to the published version of `ctor` once:
|
||||
# 1. A new version of `ctor` is published with this change: https://github.com/mmastrac/rust-ctor/pull/295
|
||||
# 2. We've confirmed it's fine to update to the latest version of `ctor` (we're currently on v0.1.20).
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-o": "workspace::Open",
|
||||
"cmd-=": "zed::IncreaseBufferFontSize",
|
||||
"cmd-+": "zed::IncreaseBufferFontSize",
|
||||
|
@ -388,7 +389,6 @@
|
|||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-1": ["workspace::ActivatePane", 0],
|
||||
"cmd-2": ["workspace::ActivatePane", 1],
|
||||
"cmd-3": ["workspace::ActivatePane", 2],
|
||||
|
|
|
@ -104,7 +104,7 @@ pub struct OpenAIResponseStreamEvent {
|
|||
|
||||
pub async fn stream_completion(
|
||||
credential: ProviderCredential,
|
||||
executor: Arc<BackgroundExecutor>,
|
||||
executor: BackgroundExecutor,
|
||||
request: Box<dyn CompletionRequest>,
|
||||
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||
let api_key = match credential {
|
||||
|
@ -197,11 +197,11 @@ pub async fn stream_completion(
|
|||
pub struct OpenAICompletionProvider {
|
||||
model: OpenAILanguageModel,
|
||||
credential: Arc<RwLock<ProviderCredential>>,
|
||||
executor: Arc<BackgroundExecutor>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
impl OpenAICompletionProvider {
|
||||
pub fn new(model_name: &str, executor: Arc<BackgroundExecutor>) -> Self {
|
||||
pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self {
|
||||
let model = OpenAILanguageModel::load(model_name);
|
||||
let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
|
||||
Self {
|
||||
|
|
54
crates/assistant2/Cargo.toml
Normal file
54
crates/assistant2/Cargo.toml
Normal file
|
@ -0,0 +1,54 @@
|
|||
[package]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ai = { package = "ai2", path = "../ai2" }
|
||||
client = { package = "client2", path = "../client2" }
|
||||
collections = { path = "../collections"}
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fs = { package = "fs2", path = "../fs2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
||||
uuid.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
futures.workspace = true
|
||||
indoc.workspace = true
|
||||
isahc.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ai = { package = "ai2", path = "../ai2", features = ["test-support"]}
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
63
crates/assistant2/README.zmd
Normal file
63
crates/assistant2/README.zmd
Normal file
|
@ -0,0 +1,63 @@
|
|||
> Hello, I'm Nathan Sobo.
|
||||
I'm currently your sole creator and the leader of Zed Industries.
|
||||
Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation.
|
||||
|
||||
< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development.
|
||||
|
||||
As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world.
|
||||
|
||||
> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish.
|
||||
|
||||
We've decided that our next milestone will be open-sourcing Zed on the Zed platform.
|
||||
|
||||
< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further.
|
||||
|
||||
Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation.
|
||||
|
||||
Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future!
|
||||
|
||||
@nathan> #zed, can you describe the conversation UI as you currently understand it.
|
||||
|
||||
< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary.
|
||||
|
||||
Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered.
|
||||
|
||||
@nathan> What details around the conversation view are worth exploring?
|
||||
|
||||
< @nathan, there are several aspects of the conversation view that are worth exploring and refining:
|
||||
|
||||
1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context.
|
||||
2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history.
|
||||
3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials.
|
||||
4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants.
|
||||
5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view.
|
||||
|
||||
Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed.
|
||||
|
||||
@nathan> @nate, which of these would you like to explore?
|
||||
|
||||
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
|
||||
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots).
|
||||
|
||||
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...
|
||||
|
||||
#zed describe a hypothetical conversation UI that meets the description.
|
||||
|
||||
< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements:
|
||||
|
||||
1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space.
|
||||
|
||||
2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions.
|
||||
|
||||
3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view.
|
||||
|
||||
4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly.
|
||||
|
||||
5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow.
|
||||
|
||||
6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view.
|
||||
|
||||
7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context.
|
||||
|
||||
This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs.
|
3
crates/assistant2/features.zmd
Normal file
3
crates/assistant2/features.zmd
Normal file
|
@ -0,0 +1,3 @@
|
|||
Push content to a deeper layer.
|
||||
A context can have multiple sublayers.
|
||||
You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document.
|
126
crates/assistant2/src/assistant.rs
Normal file
126
crates/assistant2/src/assistant.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
pub mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod codegen;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
|
||||
use ai::providers::open_ai::Role;
|
||||
use anyhow::Result;
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
use assistant_settings::OpenAIModel;
|
||||
use chrono::{DateTime, Local};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{actions, AppContext};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
actions!(
|
||||
NewConversation,
|
||||
Assist,
|
||||
Split,
|
||||
CycleMessageRole,
|
||||
QuoteSelection,
|
||||
ToggleFocus,
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
ToggleIncludeConversation,
|
||||
ToggleRetrieveContext,
|
||||
);
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
struct MessageId(usize);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct MessageMetadata {
|
||||
role: Role,
|
||||
sent_at: DateTime<Local>,
|
||||
status: MessageStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum MessageStatus {
|
||||
Pending,
|
||||
Done,
|
||||
Error(Arc<str>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedMessage {
|
||||
id: MessageId,
|
||||
start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversation {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
model: OpenAIModel,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
const VERSION: &'static str = "0.1.0";
|
||||
}
|
||||
|
||||
struct SavedConversationMetadata {
|
||||
title: String,
|
||||
path: PathBuf,
|
||||
mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
3486
crates/assistant2/src/assistant_panel.rs
Normal file
3486
crates/assistant2/src/assistant_panel.rs
Normal file
File diff suppressed because it is too large
Load diff
80
crates/assistant2/src/assistant_settings.rs
Normal file
80
crates/assistant2/src/assistant_settings.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum OpenAIModel {
|
||||
#[serde(rename = "gpt-3.5-turbo-0613")]
|
||||
ThreePointFiveTurbo,
|
||||
#[serde(rename = "gpt-4-0613")]
|
||||
Four,
|
||||
#[serde(rename = "gpt-4-1106-preview")]
|
||||
FourTurbo,
|
||||
}
|
||||
|
||||
impl OpenAIModel {
|
||||
pub fn full_name(&self) -> &'static str {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613",
|
||||
OpenAIModel::Four => "gpt-4-0613",
|
||||
OpenAIModel::FourTurbo => "gpt-4-1106-preview",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_name(&self) -> &'static str {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo",
|
||||
OpenAIModel::Four => "gpt-4",
|
||||
OpenAIModel::FourTurbo => "gpt-4-turbo",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(&self) -> Self {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four,
|
||||
OpenAIModel::Four => OpenAIModel::FourTurbo,
|
||||
OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AssistantSettings {
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: f32,
|
||||
pub default_height: f32,
|
||||
pub default_open_ai_model: OpenAIModel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContent {
|
||||
pub button: Option<bool>,
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
pub default_height: Option<f32>,
|
||||
pub default_open_ai_model: Option<OpenAIModel>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
688
crates/assistant2/src/codegen.rs
Normal file
688
crates/assistant2/src/codegen.rs
Normal file
|
@ -0,0 +1,688 @@
|
|||
use crate::streaming_diff::{Hunk, StreamingDiff};
|
||||
use ai::completion::{CompletionProvider, CompletionRequest};
|
||||
use anyhow::Result;
|
||||
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{EventEmitter, Model, ModelContext, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
use multi_buffer;
|
||||
use std::{cmp, future, ops::Range, sync::Arc};
|
||||
|
||||
pub enum Event {
|
||||
Finished,
|
||||
Undone,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CodegenKind {
|
||||
Transform { range: Range<Anchor> },
|
||||
Generate { position: Anchor },
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
buffer: Model<MultiBuffer>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
kind: CodegenKind,
|
||||
last_equal_ranges: Vec<Range<Anchor>>,
|
||||
transaction_id: Option<TransactionId>,
|
||||
error: Option<anyhow::Error>,
|
||||
generation: Task<()>,
|
||||
idle: bool,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Codegen {}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
kind: CodegenKind,
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
Self {
|
||||
provider,
|
||||
buffer: buffer.clone(),
|
||||
snapshot,
|
||||
kind,
|
||||
last_equal_ranges: Default::default(),
|
||||
transaction_id: Default::default(),
|
||||
error: Default::default(),
|
||||
idle: true,
|
||||
generation: Task::ready(()),
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
_buffer: Model<MultiBuffer>,
|
||||
event: &multi_buffer::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
|
||||
if self.transaction_id == Some(*transaction_id) {
|
||||
self.transaction_id = None;
|
||||
self.generation = Task::ready(());
|
||||
cx.emit(Event::Undone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range(&self) -> Range<Anchor> {
|
||||
match &self.kind {
|
||||
CodegenKind::Transform { range } => range.clone(),
|
||||
CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &CodegenKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
|
||||
&self.last_equal_ranges
|
||||
}
|
||||
|
||||
pub fn idle(&self) -> bool {
|
||||
self.idle
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<&anyhow::Error> {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: Box<dyn CompletionRequest>, cx: &mut ModelContext<Self>) {
|
||||
let range = self.range();
|
||||
let snapshot = self.snapshot.clone();
|
||||
let selected_text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
.collect::<Rope>();
|
||||
|
||||
let selection_start = range.start.to_point(&snapshot);
|
||||
let suggested_line_indent = snapshot
|
||||
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
|
||||
.into_values()
|
||||
.next()
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row));
|
||||
|
||||
let response = self.provider.complete(prompt);
|
||||
self.generation = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let generate = async {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
let diff = cx.background_executor().spawn(async move {
|
||||
let chunks = strip_invalid_spans_from_codeblock(response.await?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
let mut new_text = String::new();
|
||||
let mut base_indent = None;
|
||||
let mut line_indent = None;
|
||||
let mut first_line = true;
|
||||
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
let chunk = chunk?;
|
||||
|
||||
let mut lines = chunk.split('\n').peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
new_text.push_str(line);
|
||||
if line_indent.is_none() {
|
||||
if let Some(non_whitespace_ch_ix) =
|
||||
new_text.find(|ch: char| !ch.is_whitespace())
|
||||
{
|
||||
line_indent = Some(non_whitespace_ch_ix);
|
||||
base_indent = base_indent.or(line_indent);
|
||||
|
||||
let line_indent = line_indent.unwrap();
|
||||
let base_indent = base_indent.unwrap();
|
||||
let indent_delta = line_indent as i32 - base_indent as i32;
|
||||
let mut corrected_indent_len = cmp::max(
|
||||
0,
|
||||
suggested_line_indent.len as i32 + indent_delta,
|
||||
)
|
||||
as usize;
|
||||
if first_line {
|
||||
corrected_indent_len = corrected_indent_len
|
||||
.saturating_sub(selection_start.column as usize);
|
||||
}
|
||||
|
||||
let indent_char = suggested_line_indent.char();
|
||||
let mut indent_buffer = [0; 4];
|
||||
let indent_str =
|
||||
indent_char.encode_utf8(&mut indent_buffer);
|
||||
new_text.replace_range(
|
||||
..line_indent,
|
||||
&indent_str.repeat(corrected_indent_len),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if line_indent.is_some() {
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
new_text.clear();
|
||||
}
|
||||
|
||||
if lines.peek().is_some() {
|
||||
hunks_tx.send(diff.push_new("\n")).await?;
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
hunks_tx.send(diff.finish()).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
while let Some(hunks) = hunks_rx.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
|
||||
let transaction = this.buffer.update(cx, |buffer, cx| {
|
||||
// Avoid grouping assistant edits with user edits.
|
||||
buffer.finalize_last_transaction(cx);
|
||||
|
||||
buffer.start_transaction(cx);
|
||||
buffer.edit(
|
||||
hunks.into_iter().filter_map(|hunk| match hunk {
|
||||
Hunk::Insert { text } => {
|
||||
let edit_start = snapshot.anchor_after(edit_start);
|
||||
Some((edit_start..edit_start, text))
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
Some((edit_range, String::new()))
|
||||
}
|
||||
Hunk::Keep { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
this.last_equal_ranges.push(edit_range);
|
||||
None
|
||||
}
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
buffer.end_transaction(cx)
|
||||
});
|
||||
|
||||
if let Some(transaction) = transaction {
|
||||
if let Some(first_transaction) = this.transaction_id {
|
||||
// Group all assistant edits into the first transaction.
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_transactions(
|
||||
transaction,
|
||||
first_transaction,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.transaction_id = Some(transaction);
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
|
||||
diff.await?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = generate.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
this.idle = true;
|
||||
if let Err(error) = result {
|
||||
this.error = Some(error);
|
||||
}
|
||||
cx.emit(Event::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
self.error.take();
|
||||
self.idle = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction_id) = self.transaction_id {
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_invalid_spans_from_codeblock(
|
||||
stream: impl Stream<Item = Result<String>>,
|
||||
) -> impl Stream<Item = Result<String>> {
|
||||
let mut first_line = true;
|
||||
let mut buffer = String::new();
|
||||
let mut starts_with_markdown_codeblock = false;
|
||||
let mut includes_start_or_end_span = false;
|
||||
stream.filter_map(move |chunk| {
|
||||
let chunk = match chunk {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => return future::ready(Some(Err(err))),
|
||||
};
|
||||
buffer.push_str(&chunk);
|
||||
|
||||
if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
|
||||
includes_start_or_end_span = true;
|
||||
|
||||
buffer = buffer
|
||||
.strip_prefix("<|S|>")
|
||||
.or_else(|| buffer.strip_prefix("<|S|"))
|
||||
.unwrap_or(&buffer)
|
||||
.to_string();
|
||||
} else if buffer.ends_with("|E|>") {
|
||||
includes_start_or_end_span = true;
|
||||
} else if buffer.starts_with("<|")
|
||||
|| buffer.starts_with("<|S")
|
||||
|| buffer.starts_with("<|S|")
|
||||
|| buffer.ends_with("|")
|
||||
|| buffer.ends_with("|E")
|
||||
|| buffer.ends_with("|E|")
|
||||
{
|
||||
return future::ready(None);
|
||||
}
|
||||
|
||||
if first_line {
|
||||
if buffer == "" || buffer == "`" || buffer == "``" {
|
||||
return future::ready(None);
|
||||
} else if buffer.starts_with("```") {
|
||||
starts_with_markdown_codeblock = true;
|
||||
if let Some(newline_ix) = buffer.find('\n') {
|
||||
buffer.replace_range(..newline_ix + 1, "");
|
||||
first_line = false;
|
||||
} else {
|
||||
return future::ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut text = buffer.to_string();
|
||||
if starts_with_markdown_codeblock {
|
||||
text = text
|
||||
.strip_suffix("\n```\n")
|
||||
.or_else(|| text.strip_suffix("\n```"))
|
||||
.or_else(|| text.strip_suffix("\n``"))
|
||||
.or_else(|| text.strip_suffix("\n`"))
|
||||
.or_else(|| text.strip_suffix('\n'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
}
|
||||
|
||||
if includes_start_or_end_span {
|
||||
text = text
|
||||
.strip_suffix("|E|>")
|
||||
.or_else(|| text.strip_suffix("E|>"))
|
||||
.or_else(|| text.strip_prefix("|>"))
|
||||
.or_else(|| text.strip_prefix(">"))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
};
|
||||
|
||||
if text.contains('\n') {
|
||||
first_line = false;
|
||||
}
|
||||
|
||||
let remainder = buffer.split_off(text.len());
|
||||
let result = if buffer.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Ok(buffer.clone()))
|
||||
};
|
||||
|
||||
buffer = remainder;
|
||||
future::ready(result)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use ai::test::FakeCompletionProvider;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DummyCompletionRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl CompletionRequest for DummyCompletionRequest {
|
||||
fn data(&self) -> serde_json::Result<String> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
let x = 0;
|
||||
for _ in 0..10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Transform { range },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
" let mut x = 0;\n",
|
||||
" while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
" }",
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("CHUNK: {:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_past_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
le
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 6))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"t mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_before_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = concat!(
|
||||
"fn main() {\n",
|
||||
" \n",
|
||||
"}\n" //
|
||||
);
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 2))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"let mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("{:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks(
|
||||
"```html\n```js\nLorem ipsum dolor\n```\n```",
|
||||
2
|
||||
))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"```js\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"``\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
|
||||
stream::iter(
|
||||
text.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(size)
|
||||
.map(|chunk| Ok(chunk.iter().collect::<String>()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
389
crates/assistant2/src/prompts.rs
Normal file
389
crates/assistant2/src/prompts.rs
Normal file
|
@ -0,0 +1,389 @@
|
|||
use ai::models::LanguageModel;
|
||||
use ai::prompts::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate};
|
||||
use ai::prompts::file_context::FileContext;
|
||||
use ai::prompts::generate::GenerateInlineContent;
|
||||
use ai::prompts::preamble::EngineerPreamble;
|
||||
use ai::prompts::repository_context::{PromptCodeSnippet, RepositoryContext};
|
||||
use ai::providers::open_ai::OpenAILanguageModel;
|
||||
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
|
||||
use std::cmp::{self, Reverse};
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||
#[derive(Debug)]
|
||||
struct Match {
|
||||
collapse: Range<usize>,
|
||||
keep: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
let selected_range = selected_range.to_offset(buffer);
|
||||
let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| {
|
||||
Some(&grammar.embedding_config.as_ref()?.query)
|
||||
});
|
||||
let configs = ts_matches
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|g| g.embedding_config.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut matches = Vec::new();
|
||||
while let Some(mat) = ts_matches.peek() {
|
||||
let config = &configs[mat.grammar_index];
|
||||
if let Some(collapse) = mat.captures.iter().find_map(|cap| {
|
||||
if Some(cap.index) == config.collapse_capture_ix {
|
||||
Some(cap.node.byte_range())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
let mut keep = Vec::new();
|
||||
for capture in mat.captures.iter() {
|
||||
if Some(capture.index) == config.keep_capture_ix {
|
||||
keep.push(capture.node.byte_range());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ts_matches.advance();
|
||||
matches.push(Match { collapse, keep });
|
||||
} else {
|
||||
ts_matches.advance();
|
||||
}
|
||||
}
|
||||
matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end)));
|
||||
let mut matches = matches.into_iter().peekable();
|
||||
|
||||
let mut summary = String::new();
|
||||
let mut offset = 0;
|
||||
let mut flushed_selection = false;
|
||||
while let Some(mat) = matches.next() {
|
||||
// Keep extending the collapsed range if the next match surrounds
|
||||
// the current one.
|
||||
while let Some(next_mat) = matches.peek() {
|
||||
if mat.collapse.start <= next_mat.collapse.start
|
||||
&& mat.collapse.end >= next_mat.collapse.end
|
||||
{
|
||||
matches.next().unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if offset > mat.collapse.start {
|
||||
// Skip collapsed nodes that have already been summarized.
|
||||
offset = cmp::max(offset, mat.collapse.end);
|
||||
continue;
|
||||
}
|
||||
|
||||
if offset <= selected_range.start && selected_range.start <= mat.collapse.end {
|
||||
if !flushed_selection {
|
||||
// The collapsed node ends after the selection starts, so we'll flush the selection first.
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
flushed_selection = true;
|
||||
}
|
||||
|
||||
// If the selection intersects the collapsed node, we won't collapse it.
|
||||
if selected_range.end >= mat.collapse.start {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..mat.collapse.start));
|
||||
for keep in mat.keep {
|
||||
summary.extend(buffer.text_for_range(keep));
|
||||
}
|
||||
offset = mat.collapse.end;
|
||||
}
|
||||
|
||||
// Flush selection if we haven't already done so.
|
||||
if !flushed_selection && offset <= selected_range.start {
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..buffer.len()));
|
||||
summary
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
search_results: Vec<PromptCodeSnippet>,
|
||||
model: &str,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
// Using new Prompt Templates
|
||||
let openai_model: Arc<dyn LanguageModel> = Arc::new(OpenAILanguageModel::load(model));
|
||||
let lang_name = if let Some(language_name) = language_name {
|
||||
Some(language_name.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let args = PromptArguments {
|
||||
model: openai_model,
|
||||
language_name: lang_name.clone(),
|
||||
project_name,
|
||||
snippets: search_results.clone(),
|
||||
reserved_tokens: 1000,
|
||||
buffer: Some(buffer),
|
||||
selected_range: Some(range),
|
||||
user_prompt: Some(user_prompt.clone()),
|
||||
};
|
||||
|
||||
let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
|
||||
(PromptPriority::Mandatory, Box::new(EngineerPreamble {})),
|
||||
(
|
||||
PromptPriority::Ordered { order: 1 },
|
||||
Box::new(RepositoryContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 0 },
|
||||
Box::new(FileContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Mandatory,
|
||||
Box::new(GenerateInlineContent {}),
|
||||
),
|
||||
];
|
||||
let chain = PromptChain::new(args, templates);
|
||||
let (prompt, _) = chain.generate(true)?;
|
||||
|
||||
anyhow::Ok(prompt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{AppContext, Context};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use settings::SettingsStore;
|
||||
|
||||
pub(crate) fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_embedding_query(
|
||||
r#"
|
||||
(
|
||||
[(line_comment) (attribute_item)]* @context
|
||||
.
|
||||
[
|
||||
(struct_item
|
||||
name: (_) @name)
|
||||
|
||||
(enum_item
|
||||
name: (_) @name)
|
||||
|
||||
(impl_item
|
||||
trait: (_)? @name
|
||||
"for"? @name
|
||||
type: (_) @name)
|
||||
|
||||
(trait_item
|
||||
name: (_) @name)
|
||||
|
||||
(function_item
|
||||
name: (_) @name
|
||||
body: (block
|
||||
"{" @keep
|
||||
"}" @keep) @collapse)
|
||||
|
||||
(macro_definition
|
||||
name: (_) @name)
|
||||
] @item
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_outline_for_prompt(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language_settings::init(cx);
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
self.a
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
<|S|>a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let <|S|a |E|>= 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
<|S|>
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
<|S|>"}
|
||||
);
|
||||
|
||||
// Ensure nested functions get collapsed properly.
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
let a = 30;
|
||||
fn nested() -> usize {
|
||||
3
|
||||
}
|
||||
self.a + nested()
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)),
|
||||
indoc! {"
|
||||
<|S|>struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
}
|
293
crates/assistant2/src/streaming_diff.rs
Normal file
293
crates/assistant2/src/streaming_diff.rs
Normal file
|
@ -0,0 +1,293 @@
|
|||
use collections::HashMap;
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::{self, Debug},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
struct Matrix {
|
||||
cells: Vec<f64>,
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cells: Vec::new(),
|
||||
rows: 0,
|
||||
cols: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn resize(&mut self, rows: usize, cols: usize) {
|
||||
self.cells.resize(rows * cols, 0.);
|
||||
self.rows = rows;
|
||||
self.cols = cols;
|
||||
}
|
||||
|
||||
fn get(&self, row: usize, col: usize) -> f64 {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
self.cells[col * self.rows + row]
|
||||
}
|
||||
|
||||
fn set(&mut self, row: usize, col: usize, value: f64) {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
|
||||
self.cells[col * self.rows + row] = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Matrix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f)?;
|
||||
for i in 0..self.rows {
|
||||
for j in 0..self.cols {
|
||||
write!(f, "{:5}", self.get(i, j))?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Hunk {
|
||||
Insert { text: String },
|
||||
Remove { len: usize },
|
||||
Keep { len: usize },
|
||||
}
|
||||
|
||||
pub struct StreamingDiff {
|
||||
old: Vec<char>,
|
||||
new: Vec<char>,
|
||||
scores: Matrix,
|
||||
old_text_ix: usize,
|
||||
new_text_ix: usize,
|
||||
equal_runs: HashMap<(usize, usize), u32>,
|
||||
}
|
||||
|
||||
impl StreamingDiff {
|
||||
const INSERTION_SCORE: f64 = -1.;
|
||||
const DELETION_SCORE: f64 = -20.;
|
||||
const EQUALITY_BASE: f64 = 1.8;
|
||||
const MAX_EQUALITY_EXPONENT: i32 = 16;
|
||||
|
||||
pub fn new(old: String) -> Self {
|
||||
let old = old.chars().collect::<Vec<_>>();
|
||||
let mut scores = Matrix::new();
|
||||
scores.resize(old.len() + 1, 1);
|
||||
for i in 0..=old.len() {
|
||||
scores.set(i, 0, i as f64 * Self::DELETION_SCORE);
|
||||
}
|
||||
Self {
|
||||
old,
|
||||
new: Vec::new(),
|
||||
scores,
|
||||
old_text_ix: 0,
|
||||
new_text_ix: 0,
|
||||
equal_runs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_new(&mut self, text: &str) -> Vec<Hunk> {
|
||||
self.new.extend(text.chars());
|
||||
self.scores.resize(self.old.len() + 1, self.new.len() + 1);
|
||||
|
||||
for j in self.new_text_ix + 1..=self.new.len() {
|
||||
self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE);
|
||||
for i in 1..=self.old.len() {
|
||||
let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE;
|
||||
let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE;
|
||||
let equality_score = if self.old[i - 1] == self.new[j - 1] {
|
||||
let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0);
|
||||
equal_run += 1;
|
||||
self.equal_runs.insert((i, j), equal_run);
|
||||
|
||||
let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT);
|
||||
self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent)
|
||||
} else {
|
||||
f64::NEG_INFINITY
|
||||
};
|
||||
|
||||
let score = insertion_score.max(deletion_score).max(equality_score);
|
||||
self.scores.set(i, j, score);
|
||||
}
|
||||
}
|
||||
|
||||
let mut max_score = f64::NEG_INFINITY;
|
||||
let mut next_old_text_ix = self.old_text_ix;
|
||||
let next_new_text_ix = self.new.len();
|
||||
for i in self.old_text_ix..=self.old.len() {
|
||||
let score = self.scores.get(i, next_new_text_ix);
|
||||
if score > max_score {
|
||||
max_score = score;
|
||||
next_old_text_ix = i;
|
||||
}
|
||||
}
|
||||
|
||||
let hunks = self.backtrack(next_old_text_ix, next_new_text_ix);
|
||||
self.old_text_ix = next_old_text_ix;
|
||||
self.new_text_ix = next_new_text_ix;
|
||||
hunks
|
||||
}
|
||||
|
||||
fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<Hunk> {
|
||||
let mut pending_insert: Option<Range<usize>> = None;
|
||||
let mut hunks = Vec::new();
|
||||
let mut i = old_text_ix;
|
||||
let mut j = new_text_ix;
|
||||
while (i, j) != (self.old_text_ix, self.new_text_ix) {
|
||||
let insertion_score = if j > self.new_text_ix {
|
||||
Some((i, j - 1))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let deletion_score = if i > self.old_text_ix {
|
||||
Some((i - 1, j))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let equality_score = if i > self.old_text_ix && j > self.new_text_ix {
|
||||
if self.old[i - 1] == self.new[j - 1] {
|
||||
Some((i - 1, j - 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score]
|
||||
.iter()
|
||||
.max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j))))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
if prev_i == i && prev_j == j - 1 {
|
||||
if let Some(pending_insert) = pending_insert.as_mut() {
|
||||
pending_insert.start = prev_j;
|
||||
} else {
|
||||
pending_insert = Some(prev_j..j);
|
||||
}
|
||||
} else {
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
let char_len = self.old[i - 1].len_utf8();
|
||||
if prev_i == i - 1 && prev_j == j {
|
||||
if let Some(Hunk::Remove { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Remove { len: char_len })
|
||||
}
|
||||
} else {
|
||||
if let Some(Hunk::Keep { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Keep { len: char_len })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i = prev_i;
|
||||
j = prev_j;
|
||||
}
|
||||
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
hunks.reverse();
|
||||
hunks
|
||||
}
|
||||
|
||||
pub fn finish(self) -> Vec<Hunk> {
|
||||
self.backtrack(self.old.len(), self.new.len())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_diffs(mut rng: StdRng) {
|
||||
let old_text_len = env::var("OLD_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
let new_text_len = env::var("NEW_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let old = util::RandomCharIter::new(&mut rng)
|
||||
.take(old_text_len)
|
||||
.collect::<String>();
|
||||
log::info!("old text: {:?}", old);
|
||||
|
||||
let mut diff = StreamingDiff::new(old.clone());
|
||||
let mut hunks = Vec::new();
|
||||
let mut new_len = 0;
|
||||
let mut new = String::new();
|
||||
while new_len < new_text_len {
|
||||
let new_chunk_len = rng.gen_range(1..=new_text_len - new_len);
|
||||
let new_chunk = util::RandomCharIter::new(&mut rng)
|
||||
.take(new_len)
|
||||
.collect::<String>();
|
||||
log::info!("new chunk: {:?}", new_chunk);
|
||||
new_len += new_chunk_len;
|
||||
new.push_str(&new_chunk);
|
||||
let new_hunks = diff.push_new(&new_chunk);
|
||||
log::info!("hunks: {:?}", new_hunks);
|
||||
hunks.extend(new_hunks);
|
||||
}
|
||||
let final_hunks = diff.finish();
|
||||
log::info!("final hunks: {:?}", final_hunks);
|
||||
hunks.extend(final_hunks);
|
||||
|
||||
log::info!("new text: {:?}", new);
|
||||
let mut old_ix = 0;
|
||||
let mut new_ix = 0;
|
||||
let mut patched = String::new();
|
||||
for hunk in hunks {
|
||||
match hunk {
|
||||
Hunk::Keep { len } => {
|
||||
assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]);
|
||||
patched.push_str(&old[old_ix..old_ix + len]);
|
||||
old_ix += len;
|
||||
new_ix += len;
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
old_ix += len;
|
||||
}
|
||||
Hunk::Insert { text } => {
|
||||
assert_eq!(text, &new[new_ix..new_ix + text.len()]);
|
||||
patched.push_str(&text);
|
||||
new_ix += text.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(patched, new);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
|||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.29.1"
|
||||
version = "0.30.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -81,7 +81,7 @@ settings = { package = "settings2", path = "../settings2", features = ["test-sup
|
|||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
|
||||
collab_ui = { path = "../collab_ui", features = ["test-support"] }
|
||||
collab_ui = { path = "../collab_ui2", package = "collab_ui2", features = ["test-support"] }
|
||||
|
||||
async-trait.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -13,7 +13,7 @@ use client::{
|
|||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext, WindowHandle};
|
||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
|
||||
|
@ -602,14 +602,12 @@ impl TestClient {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
//todo(workspace)
|
||||
#[allow(dead_code)]
|
||||
pub fn build_workspace(
|
||||
&self,
|
||||
pub fn build_workspace<'a>(
|
||||
&'a self,
|
||||
project: &Model<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> WindowHandle<Workspace> {
|
||||
cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ notifications = { package = "notifications2", path = "../notifications2" }
|
|||
rich_text = { package = "rich_text2", path = "../rich_text2" }
|
||||
picker = { package = "picker2", path = "../picker2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
# recent_projects = { path = "../recent_projects" }
|
||||
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
|
||||
rpc = { package ="rpc2", path = "../rpc2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
|
||||
|
|
|
@ -1,454 +1,444 @@
|
|||
// use anyhow::{anyhow, Result};
|
||||
// use call::report_call_event_for_channel;
|
||||
// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||
// use client::{
|
||||
// proto::{self, PeerId},
|
||||
// Collaborator, ParticipantIndex,
|
||||
// };
|
||||
// use collections::HashMap;
|
||||
// use editor::{CollaborationHub, Editor};
|
||||
// use gpui::{
|
||||
// actions,
|
||||
// elements::{ChildView, Label},
|
||||
// geometry::vector::Vector2F,
|
||||
// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
|
||||
// ViewContext, ViewHandle,
|
||||
// };
|
||||
// use project::Project;
|
||||
// use smallvec::SmallVec;
|
||||
// use std::{
|
||||
// any::{Any, TypeId},
|
||||
// sync::Arc,
|
||||
// };
|
||||
// use util::ResultExt;
|
||||
// use workspace::{
|
||||
// item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
// register_followable_item,
|
||||
// searchable::SearchableItemHandle,
|
||||
// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
// };
|
||||
use anyhow::Result;
|
||||
use call::report_call_event_for_channel;
|
||||
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Collaborator, ParticipantIndex,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::{CollaborationHub, Editor, EditorEvent};
|
||||
use gpui::{
|
||||
actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
|
||||
IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
};
|
||||
use project::Project;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::Label;
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
register_followable_item,
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
// actions!(channel_view, [Deploy]);
|
||||
actions!(Deploy);
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// register_followable_item::<ChannelView>(cx)
|
||||
// }
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
register_followable_item::<ChannelView>(cx)
|
||||
}
|
||||
|
||||
// pub struct ChannelView {
|
||||
// pub editor: ViewHandle<Editor>,
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// remote_id: Option<ViewId>,
|
||||
// _editor_event_subscription: Subscription,
|
||||
// }
|
||||
pub struct ChannelView {
|
||||
pub editor: View<Editor>,
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
remote_id: Option<ViewId>,
|
||||
_editor_event_subscription: Subscription,
|
||||
}
|
||||
|
||||
// impl ChannelView {
|
||||
// pub fn open(
|
||||
// channel_id: ChannelId,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let pane = workspace.read(cx).active_pane().clone();
|
||||
// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_view = channel_view.await?;
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// report_call_event_for_channel(
|
||||
// "open channel notes",
|
||||
// channel_id,
|
||||
// &workspace.read(cx).app_state().client,
|
||||
// cx,
|
||||
// );
|
||||
// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
// });
|
||||
// anyhow::Ok(channel_view)
|
||||
// })
|
||||
// }
|
||||
impl ChannelView {
|
||||
pub fn open(
|
||||
channel_id: ChannelId,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let pane = workspace.read(cx).active_pane().clone();
|
||||
let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
report_call_event_for_channel(
|
||||
"open channel notes",
|
||||
channel_id,
|
||||
&workspace.read(cx).app_state().client,
|
||||
cx,
|
||||
);
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
})?;
|
||||
anyhow::Ok(channel_view)
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn open_in_pane(
|
||||
// channel_id: ChannelId,
|
||||
// pane: ViewHandle<Pane>,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let workspace = workspace.read(cx);
|
||||
// let project = workspace.project().to_owned();
|
||||
// let channel_store = ChannelStore::global(cx);
|
||||
// let language_registry = workspace.app_state().languages.clone();
|
||||
// let markdown = language_registry.language_for_name("Markdown");
|
||||
// let channel_buffer =
|
||||
// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
pub fn open_in_pane(
|
||||
channel_id: ChannelId,
|
||||
pane: View<Pane>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().to_owned();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
let language_registry = workspace.app_state().languages.clone();
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
let channel_buffer =
|
||||
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_buffer = channel_buffer.await?;
|
||||
// let markdown = markdown.await.log_err();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_buffer = channel_buffer.await?;
|
||||
let markdown = markdown.await.log_err();
|
||||
|
||||
// channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
// buffer.buffer().update(cx, |buffer, cx| {
|
||||
// buffer.set_language_registry(language_registry);
|
||||
// if let Some(markdown) = markdown {
|
||||
// buffer.set_language(Some(markdown), cx);
|
||||
// }
|
||||
// })
|
||||
// });
|
||||
channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_language_registry(language_registry);
|
||||
if let Some(markdown) = markdown {
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
// let existing_view = pane
|
||||
// .items_of_type::<Self>()
|
||||
// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
let existing_view = pane
|
||||
.items_of_type::<Self>()
|
||||
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
|
||||
// // If this channel buffer is already open in this pane, just return it.
|
||||
// if let Some(existing_view) = existing_view.clone() {
|
||||
// if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
// return existing_view;
|
||||
// }
|
||||
// }
|
||||
// If this channel buffer is already open in this pane, just return it.
|
||||
if let Some(existing_view) = existing_view.clone() {
|
||||
if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
return existing_view;
|
||||
}
|
||||
}
|
||||
|
||||
// let view = cx.add_view(|cx| {
|
||||
// let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
// this.acknowledge_buffer_version(cx);
|
||||
// this
|
||||
// });
|
||||
let view = cx.build_view(|cx| {
|
||||
let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
});
|
||||
|
||||
// // If the pane contained a disconnected view for this channel buffer,
|
||||
// // replace that.
|
||||
// if let Some(existing_item) = existing_view {
|
||||
// if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
|
||||
// .detach();
|
||||
// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
// }
|
||||
// }
|
||||
// If the pane contained a disconnected view for this channel buffer,
|
||||
// replace that.
|
||||
if let Some(existing_item) = existing_view {
|
||||
if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
|
||||
.detach();
|
||||
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
}
|
||||
}
|
||||
|
||||
// view
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("pane was dropped"))
|
||||
// })
|
||||
// }
|
||||
view
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn new(
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> Self {
|
||||
// let buffer = channel_buffer.read(cx).buffer();
|
||||
// let editor = cx.add_view(|cx| {
|
||||
// let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
// channel_buffer.clone(),
|
||||
// )));
|
||||
// editor.set_read_only(
|
||||
// !channel_buffer
|
||||
// .read(cx)
|
||||
// .channel(cx)
|
||||
// .is_some_and(|c| c.can_edit_notes()),
|
||||
// );
|
||||
// editor
|
||||
// });
|
||||
// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
||||
pub fn new(
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let buffer = channel_buffer.read(cx).buffer();
|
||||
let editor = cx.build_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
channel_buffer.clone(),
|
||||
)));
|
||||
editor.set_read_only(
|
||||
!channel_buffer
|
||||
.read(cx)
|
||||
.channel(cx)
|
||||
.is_some_and(|c| c.can_edit_notes()),
|
||||
);
|
||||
editor
|
||||
});
|
||||
let _editor_event_subscription =
|
||||
cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
|
||||
|
||||
// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
// .detach();
|
||||
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
.detach();
|
||||
|
||||
// Self {
|
||||
// editor,
|
||||
// project,
|
||||
// channel_store,
|
||||
// channel_buffer,
|
||||
// remote_id: None,
|
||||
// _editor_event_subscription,
|
||||
// }
|
||||
// }
|
||||
Self {
|
||||
editor,
|
||||
project,
|
||||
channel_store,
|
||||
channel_buffer,
|
||||
remote_id: None,
|
||||
_editor_event_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
// self.channel_buffer.read(cx).channel(cx)
|
||||
// }
|
||||
pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
self.channel_buffer.read(cx).channel(cx)
|
||||
}
|
||||
|
||||
// fn handle_channel_buffer_event(
|
||||
// &mut self,
|
||||
// _: ModelHandle<ChannelBuffer>,
|
||||
// event: &ChannelBufferEvent,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// match event {
|
||||
// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(true);
|
||||
// cx.notify();
|
||||
// }),
|
||||
// ChannelBufferEvent::ChannelChanged => {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||
// cx.emit(editor::Event::TitleChanged);
|
||||
// cx.notify()
|
||||
// });
|
||||
// }
|
||||
// ChannelBufferEvent::BufferEdited => {
|
||||
// if cx.is_self_focused() || self.editor.is_focused(cx) {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// } else {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.notes_changed(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// ChannelBufferEvent::CollaboratorsChanged => {}
|
||||
// }
|
||||
// }
|
||||
fn handle_channel_buffer_event(
|
||||
&mut self,
|
||||
_: Model<ChannelBuffer>,
|
||||
event: &ChannelBufferEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(true);
|
||||
cx.notify();
|
||||
}),
|
||||
ChannelBufferEvent::ChannelChanged => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||
cx.emit(editor::EditorEvent::TitleChanged);
|
||||
cx.notify()
|
||||
});
|
||||
}
|
||||
ChannelBufferEvent::BufferEdited => {
|
||||
if self.editor.read(cx).is_focused(cx) {
|
||||
self.acknowledge_buffer_version(cx);
|
||||
} else {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.notes_changed(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
ChannelBufferEvent::CollaboratorsChanged => {}
|
||||
}
|
||||
}
|
||||
|
||||
// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.acknowledge_notes_version(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// self.channel_buffer.update(cx, |buffer, cx| {
|
||||
// buffer.acknowledge_buffer_version(cx);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.acknowledge_notes_version(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.channel_buffer.update(cx, |buffer, cx| {
|
||||
buffer.acknowledge_buffer_version(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// impl Entity for ChannelView {
|
||||
// type Event = editor::Event;
|
||||
// }
|
||||
impl EventEmitter<EditorEvent> for ChannelView {}
|
||||
|
||||
// impl View for ChannelView {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "ChannelView"
|
||||
// }
|
||||
impl Render for ChannelView {
|
||||
type Element = AnyView;
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// ChildView::new(self.editor.as_any(), cx).into_any()
|
||||
// }
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
self.editor.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// if cx.is_self_focused() {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// cx.focus(self.editor.as_any())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
impl FocusableView for ChannelView {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
// impl Item for ChannelView {
|
||||
// fn act_as_type<'a>(
|
||||
// &'a self,
|
||||
// type_id: TypeId,
|
||||
// self_handle: &'a ViewHandle<Self>,
|
||||
// _: &'a AppContext,
|
||||
// ) -> Option<&'a AnyViewHandle> {
|
||||
// if type_id == TypeId::of::<Self>() {
|
||||
// Some(self_handle)
|
||||
// } else if type_id == TypeId::of::<Editor>() {
|
||||
// Some(&self.editor)
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
impl Item for ChannelView {
|
||||
type Event = EditorEvent;
|
||||
|
||||
// fn tab_content<V: 'static>(
|
||||
// &self,
|
||||
// _: Option<usize>,
|
||||
// style: &theme::Tab,
|
||||
// cx: &gpui::AppContext,
|
||||
// ) -> AnyElement<V> {
|
||||
// let label = if let Some(channel) = self.channel(cx) {
|
||||
// match (
|
||||
// channel.can_edit_notes(),
|
||||
// self.channel_buffer.read(cx).is_connected(),
|
||||
// ) {
|
||||
// (true, true) => format!("#{}", channel.name),
|
||||
// (false, true) => format!("#{} (read-only)", channel.name),
|
||||
// (_, false) => format!("#{} (disconnected)", channel.name),
|
||||
// }
|
||||
// } else {
|
||||
// format!("channel notes (disconnected)")
|
||||
// };
|
||||
// Label::new(label, style.label.to_owned()).into_any()
|
||||
// }
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a View<Self>,
|
||||
_: &'a AppContext,
|
||||
) -> Option<AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
|
||||
// Some(Self::new(
|
||||
// self.project.clone(),
|
||||
// self.channel_store.clone(),
|
||||
// self.channel_buffer.clone(),
|
||||
// cx,
|
||||
// ))
|
||||
// }
|
||||
fn tab_content(&self, _: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let label = if let Some(channel) = self.channel(cx) {
|
||||
match (
|
||||
channel.can_edit_notes(),
|
||||
self.channel_buffer.read(cx).is_connected(),
|
||||
) {
|
||||
(true, true) => format!("#{}", channel.name),
|
||||
(false, true) => format!("#{} (read-only)", channel.name),
|
||||
(_, false) => format!("#{} (disconnected)", channel.name),
|
||||
}
|
||||
} else {
|
||||
format!("channel notes (disconnected)")
|
||||
};
|
||||
Label::new(label).into_any_element()
|
||||
}
|
||||
|
||||
// fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
|
||||
Some(cx.build_view(|cx| {
|
||||
Self::new(
|
||||
self.project.clone(),
|
||||
self.channel_store.clone(),
|
||||
self.channel_buffer.clone(),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
// fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
// }
|
||||
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
// }
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
}
|
||||
|
||||
// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
// }
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
}
|
||||
|
||||
// fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
// Some(Box::new(self.editor.clone()))
|
||||
// }
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
}
|
||||
|
||||
// fn show_toolbar(&self) -> bool {
|
||||
// true
|
||||
// }
|
||||
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
|
||||
// self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||
// }
|
||||
fn show_toolbar(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
// editor::Editor::to_item_events(event)
|
||||
// }
|
||||
// }
|
||||
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
|
||||
self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||
}
|
||||
|
||||
// impl FollowableItem for ChannelView {
|
||||
// fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
// self.remote_id
|
||||
// }
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
}
|
||||
|
||||
// fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// if !channel_buffer.is_connected() {
|
||||
// return None;
|
||||
// }
|
||||
impl FollowableItem for ChannelView {
|
||||
fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
self.remote_id
|
||||
}
|
||||
|
||||
// Some(proto::view::Variant::ChannelView(
|
||||
// proto::view::ChannelView {
|
||||
// channel_id: channel_buffer.channel_id,
|
||||
// editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
// self.editor.read(cx).to_state_proto(cx)
|
||||
// {
|
||||
// Some(proto)
|
||||
// } else {
|
||||
// None
|
||||
// },
|
||||
// },
|
||||
// ))
|
||||
// }
|
||||
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
if !channel_buffer.is_connected() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// fn from_state_proto(
|
||||
// pane: ViewHandle<workspace::Pane>,
|
||||
// workspace: ViewHandle<workspace::Workspace>,
|
||||
// remote_id: workspace::ViewId,
|
||||
// state: &mut Option<proto::view::Variant>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
|
||||
// let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
// return None;
|
||||
// };
|
||||
// let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||
// unreachable!()
|
||||
// };
|
||||
Some(proto::view::Variant::ChannelView(
|
||||
proto::view::ChannelView {
|
||||
channel_id: channel_buffer.channel_id,
|
||||
editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
self.editor.read(cx).to_state_proto(cx)
|
||||
{
|
||||
Some(proto)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
fn from_state_proto(
|
||||
pane: View<workspace::Pane>,
|
||||
workspace: View<workspace::Workspace>,
|
||||
remote_id: workspace::ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
|
||||
let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
return None;
|
||||
};
|
||||
let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
// Some(cx.spawn(|mut cx| async move {
|
||||
// let this = open.await?;
|
||||
let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
|
||||
// let task = this
|
||||
// .update(&mut cx, |this, cx| {
|
||||
// this.remote_id = Some(remote_id);
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let this = open.await?;
|
||||
|
||||
// if let Some(state) = state.editor {
|
||||
// Some(this.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(
|
||||
// &this.project,
|
||||
// proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
// selections: state.selections,
|
||||
// pending_selection: state.pending_selection,
|
||||
// scroll_top_anchor: state.scroll_top_anchor,
|
||||
// scroll_x: state.scroll_x,
|
||||
// scroll_y: state.scroll_y,
|
||||
// ..Default::default()
|
||||
// }),
|
||||
// cx,
|
||||
// )
|
||||
// }))
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("window was closed"))?;
|
||||
let task = this.update(&mut cx, |this, cx| {
|
||||
this.remote_id = Some(remote_id);
|
||||
|
||||
// if let Some(task) = task {
|
||||
// task.await?;
|
||||
// }
|
||||
if let Some(state) = state.editor {
|
||||
Some(this.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(
|
||||
&this.project,
|
||||
proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
selections: state.selections,
|
||||
pending_selection: state.pending_selection,
|
||||
scroll_top_anchor: state.scroll_top_anchor,
|
||||
scroll_x: state.scroll_x,
|
||||
scroll_y: state.scroll_y,
|
||||
..Default::default()
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
// Ok(this)
|
||||
// }))
|
||||
// }
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
}
|
||||
|
||||
// fn add_event_to_update_proto(
|
||||
// &self,
|
||||
// event: &Self::Event,
|
||||
// update: &mut Option<proto::update_view::Variant>,
|
||||
// cx: &AppContext,
|
||||
// ) -> bool {
|
||||
// self.editor
|
||||
// .read(cx)
|
||||
// .add_event_to_update_proto(event, update, cx)
|
||||
// }
|
||||
Ok(this)
|
||||
}))
|
||||
}
|
||||
|
||||
// fn apply_update_proto(
|
||||
// &mut self,
|
||||
// project: &ModelHandle<Project>,
|
||||
// message: proto::update_view::Variant,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> gpui::Task<anyhow::Result<()>> {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(project, message, cx)
|
||||
// })
|
||||
// }
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &EditorEvent,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &WindowContext,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.read(cx)
|
||||
.add_event_to_update_proto(event, update, cx)
|
||||
}
|
||||
|
||||
// fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
// })
|
||||
// }
|
||||
fn apply_update_proto(
|
||||
&mut self,
|
||||
project: &Model<Project>,
|
||||
message: proto::update_view::Variant,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<anyhow::Result<()>> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(project, message, cx)
|
||||
})
|
||||
}
|
||||
|
||||
// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
||||
// Editor::should_unfollow_on_event(event, cx)
|
||||
// }
|
||||
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
})
|
||||
}
|
||||
|
||||
// fn is_project_item(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
// }
|
||||
fn is_project_item(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
|
||||
fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
|
||||
Editor::to_follow_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
// impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||
// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||
// self.0.read(cx).collaborators()
|
||||
// }
|
||||
struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
|
||||
|
||||
// fn user_participant_indices<'a>(
|
||||
// &self,
|
||||
// cx: &'a AppContext,
|
||||
// ) -> &'a HashMap<u64, ParticipantIndex> {
|
||||
// self.0.read(cx).user_store().read(cx).participant_indices()
|
||||
// }
|
||||
// }
|
||||
impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||
self.0.read(cx).collaborators()
|
||||
}
|
||||
|
||||
fn user_participant_indices<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a HashMap<u64, ParticipantIndex> {
|
||||
self.0.read(cx).user_store().read(cx).participant_indices()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -191,6 +191,7 @@ use workspace::{
|
|||
Workspace,
|
||||
};
|
||||
|
||||
use crate::channel_view::ChannelView;
|
||||
use crate::{face_pile::FacePile, CollaborationPanelSettings};
|
||||
|
||||
use self::channel_modal::ChannelModal;
|
||||
|
@ -1935,8 +1936,7 @@ impl CollabPanel {
|
|||
|
||||
fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
todo!();
|
||||
// ChannelView::open(action.channel_id, workspace, cx).detach();
|
||||
ChannelView::open(channel_id, workspace, cx).detach();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2619,6 +2619,9 @@ impl CollabPanel {
|
|||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| {
|
||||
Tooltip::text("Open channel notes", cx)
|
||||
}),
|
||||
|
@ -3204,10 +3207,6 @@ impl Panel for CollabPanel {
|
|||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn has_focus(&self, cx: &gpui::WindowContext) -> bool {
|
||||
self.focus_handle.contains_focused(cx)
|
||||
}
|
||||
|
||||
fn persistent_name() -> &'static str {
|
||||
"CollabPanel"
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||
// vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
collab_panel::init(cx);
|
||||
channel_view::init(cx);
|
||||
// chat_panel::init(cx);
|
||||
notifications::init(&app_state, cx);
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ theme = { path = "../theme" }
|
|||
lsp = { path = "../lsp" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
|
|
@ -29,7 +29,7 @@ lsp = { package = "lsp2", path = "../lsp2" }
|
|||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
|
|
@ -5,7 +5,7 @@ use gpui::{
|
|||
WindowBounds, WindowHandle, WindowKind, WindowOptions,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, v_stack, Button, Clickable, Color, Icon, IconElement, Label};
|
||||
use ui::{prelude::*, Button, Icon, IconElement, Label};
|
||||
|
||||
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ use std::{
|
|||
};
|
||||
use theme::ActiveTheme;
|
||||
pub use toolbar_controls::ToolbarControls;
|
||||
use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label};
|
||||
use ui::{h_stack, prelude::*, HighlightedLabel, Icon, IconElement, Label};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||
|
|
|
@ -24,7 +24,7 @@ use lsp::DiagnosticSeverity;
|
|||
use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
|
||||
use sum_tree::{Bias, TreeMap};
|
||||
use tab_map::TabMap;
|
||||
use theme::{SyntaxTheme, Theme};
|
||||
use theme::{StatusColors, SyntaxTheme, Theme};
|
||||
use wrap_map::WrapMap;
|
||||
|
||||
pub use block_map::{
|
||||
|
@ -513,8 +513,8 @@ impl DisplaySnapshot {
|
|||
self.chunks(
|
||||
display_rows,
|
||||
language_aware,
|
||||
Some(editor_style.syntax.inlay_style),
|
||||
Some(editor_style.syntax.suggestion_style),
|
||||
Some(editor_style.inlays_style),
|
||||
Some(editor_style.suggestions_style),
|
||||
)
|
||||
.map(|chunk| {
|
||||
let mut highlight_style = chunk
|
||||
|
|
|
@ -993,7 +993,7 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::display_map::inlay_map::InlayMap;
|
||||
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
|
||||
use gpui::{div, font, px, Element, Platform as _};
|
||||
use gpui::{div, font, px, Element};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
|
@ -1185,11 +1185,7 @@ mod tests {
|
|||
fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| init_test(cx));
|
||||
|
||||
let font_id = cx
|
||||
.test_platform
|
||||
.text_system()
|
||||
.font_id(&font("Helvetica"))
|
||||
.unwrap();
|
||||
let font_id = cx.text_system().font_id(&font("Helvetica")).unwrap();
|
||||
|
||||
let text = "one two three\nfour five six\nseven eight";
|
||||
|
||||
|
|
|
@ -1032,7 +1032,7 @@ mod tests {
|
|||
display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
|
||||
MultiBuffer,
|
||||
};
|
||||
use gpui::{font, px, test::observe, Platform};
|
||||
use gpui::{font, px, test::observe};
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt;
|
||||
|
|
|
@ -92,6 +92,7 @@ use std::{
|
|||
ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
sync::Weak,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
pub use sum_tree::Bias;
|
||||
|
@ -420,6 +421,25 @@ pub fn init(cx: &mut AppContext) {
|
|||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.on_action(move |_: &workspace::NewFile, cx| {
|
||||
let app_state = cx.global::<Weak<workspace::AppState>>();
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::open_new(&app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
cx.on_action(move |_: &workspace::NewWindow, cx| {
|
||||
let app_state = cx.global::<Weak<workspace::AppState>>();
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::open_new(&app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
trait InvalidationRegion {
|
||||
|
@ -479,6 +499,8 @@ pub struct EditorStyle {
|
|||
pub scrollbar_width: Pixels,
|
||||
pub syntax: Arc<SyntaxTheme>,
|
||||
pub diagnostic_style: DiagnosticStyle,
|
||||
pub inlays_style: HighlightStyle,
|
||||
pub suggestions_style: HighlightStyle,
|
||||
}
|
||||
|
||||
type CompletionId = usize;
|
||||
|
@ -1960,14 +1982,14 @@ impl Editor {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
// pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
|
||||
// self.cursor_shape = cursor_shape;
|
||||
// cx.notify();
|
||||
// }
|
||||
pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
|
||||
self.cursor_shape = cursor_shape;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
|
||||
// self.collapse_matches = collapse_matches;
|
||||
// }
|
||||
pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
|
||||
self.collapse_matches = collapse_matches;
|
||||
}
|
||||
|
||||
pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
|
||||
if self.collapse_matches {
|
||||
|
@ -1976,56 +1998,47 @@ impl Editor {
|
|||
range.clone()
|
||||
}
|
||||
|
||||
// pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
|
||||
// if self.display_map.read(cx).clip_at_line_ends != clip {
|
||||
// self.display_map
|
||||
// .update(cx, |map, _| map.clip_at_line_ends = clip);
|
||||
// }
|
||||
// }
|
||||
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
|
||||
if self.display_map.read(cx).clip_at_line_ends != clip {
|
||||
self.display_map
|
||||
.update(cx, |map, _| map.clip_at_line_ends = clip);
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn set_keymap_context_layer<Tag: 'static>(
|
||||
// &mut self,
|
||||
// context: KeymapContext,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// self.keymap_context_layers
|
||||
// .insert(TypeId::of::<Tag>(), context);
|
||||
// cx.notify();
|
||||
// }
|
||||
pub fn set_keymap_context_layer<Tag: 'static>(
|
||||
&mut self,
|
||||
context: KeyContext,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.keymap_context_layers
|
||||
.insert(TypeId::of::<Tag>(), context);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// self.keymap_context_layers.remove(&TypeId::of::<Tag>());
|
||||
// cx.notify();
|
||||
// }
|
||||
pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.keymap_context_layers.remove(&TypeId::of::<Tag>());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// pub fn set_input_enabled(&mut self, input_enabled: bool) {
|
||||
// self.input_enabled = input_enabled;
|
||||
// }
|
||||
pub fn set_input_enabled(&mut self, input_enabled: bool) {
|
||||
self.input_enabled = input_enabled;
|
||||
}
|
||||
|
||||
// pub fn set_autoindent(&mut self, autoindent: bool) {
|
||||
// if autoindent {
|
||||
// self.autoindent_mode = Some(AutoindentMode::EachLine);
|
||||
// } else {
|
||||
// self.autoindent_mode = None;
|
||||
// }
|
||||
// }
|
||||
pub fn set_autoindent(&mut self, autoindent: bool) {
|
||||
if autoindent {
|
||||
self.autoindent_mode = Some(AutoindentMode::EachLine);
|
||||
} else {
|
||||
self.autoindent_mode = None;
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn read_only(&self) -> bool {
|
||||
// self.read_only
|
||||
// }
|
||||
pub fn read_only(&self) -> bool {
|
||||
self.read_only
|
||||
}
|
||||
|
||||
// pub fn set_read_only(&mut self, read_only: bool) {
|
||||
// self.read_only = read_only;
|
||||
// }
|
||||
|
||||
// pub fn set_field_editor_style(
|
||||
// &mut self,
|
||||
// style: Option<Arc<GetFieldEditorTheme>>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// self.get_field_editor_theme = style;
|
||||
// cx.notify();
|
||||
// }
|
||||
pub fn set_read_only(&mut self, read_only: bool) {
|
||||
self.read_only = read_only;
|
||||
}
|
||||
|
||||
fn selections_did_change(
|
||||
&mut self,
|
||||
|
@ -7629,6 +7642,18 @@ impl Editor {
|
|||
.editor_style
|
||||
.diagnostic_style
|
||||
.clone(),
|
||||
// todo!("what about the rest of the highlight style parts for inlays and suggestions?")
|
||||
inlays_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().hint),
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
fade_out: Some(0.6),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
suggestions_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().predictive),
|
||||
fade_out: Some(0.6),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
},
|
||||
))
|
||||
.into_any_element()
|
||||
|
@ -9266,7 +9291,7 @@ impl Render for Editor {
|
|||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features,
|
||||
font_size: settings.buffer_font_size.into(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
|
@ -9291,6 +9316,19 @@ impl Render for Editor {
|
|||
scrollbar_width: px(12.),
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
diagnostic_style: cx.theme().diagnostic_style(),
|
||||
// TODO kb find `HighlightStyle` usages
|
||||
// todo!("what about the rest of the highlight style parts?")
|
||||
inlays_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().hint),
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
fade_out: Some(0.6),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
suggestions_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().predictive),
|
||||
fade_out: Some(0.6),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use futures::StreamExt;
|
|||
use gpui::{
|
||||
div,
|
||||
serde_json::{self, json},
|
||||
Div, Flatten, Platform, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
|
||||
Div, Flatten, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
|
@ -345,7 +345,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
|
|||
);
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(3, 3),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
|
@ -356,7 +361,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
|
|||
);
|
||||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(1, 1),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
|
@ -368,7 +378,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
|
|||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.end_selection(cx);
|
||||
view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(3, 3),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
|
@ -380,7 +395,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
|
|||
|
||||
editor.update(cx, |view, cx| {
|
||||
view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx);
|
||||
view.update_selection(DisplayPoint::new(0, 0), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(0, 0),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
|
@ -423,7 +443,12 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) {
|
|||
});
|
||||
|
||||
view.update(cx, |view, cx| {
|
||||
view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(3, 3),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
[DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
|
||||
|
@ -432,7 +457,12 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) {
|
|||
|
||||
view.update(cx, |view, cx| {
|
||||
view.cancel(&Cancel, cx);
|
||||
view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(1, 1),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
[DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
|
||||
|
@ -643,11 +673,21 @@ fn test_cancel(cx: &mut TestAppContext) {
|
|||
|
||||
view.update(cx, |view, cx| {
|
||||
view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
|
||||
view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(1, 1),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
view.end_selection(cx);
|
||||
|
||||
view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
|
||||
view.update_selection(DisplayPoint::new(0, 3), 0, gpui::Point::<f32>::zero(), cx);
|
||||
view.update_selection(
|
||||
DisplayPoint::new(0, 3),
|
||||
0,
|
||||
gpui::Point::<f32>::default(),
|
||||
cx,
|
||||
);
|
||||
view.end_selection(cx);
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
|
@ -3238,9 +3278,7 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
|
|||
the lazy dog"});
|
||||
cx.update_editor(|e, cx| e.copy(&Copy, cx));
|
||||
assert_eq!(
|
||||
cx.test_platform
|
||||
.read_from_clipboard()
|
||||
.map(|item| item.text().to_owned()),
|
||||
cx.read_from_clipboard().map(|item| item.text().to_owned()),
|
||||
Some("fox jumps over\n".to_owned())
|
||||
);
|
||||
|
||||
|
|
|
@ -485,7 +485,7 @@ impl EditorElement {
|
|||
let modifiers = event.modifiers;
|
||||
if editor.has_pending_selection() && event.pressed_button == Some(MouseButton::Left) {
|
||||
let point_for_position = position_map.point_for_position(text_bounds, event.position);
|
||||
let mut scroll_delta = gpui::Point::<f32>::zero();
|
||||
let mut scroll_delta = gpui::Point::<f32>::default();
|
||||
let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
|
||||
let top = text_bounds.origin.y + vertical_margin;
|
||||
let bottom = text_bounds.lower_left().y - vertical_margin;
|
||||
|
@ -511,7 +511,7 @@ impl EditorElement {
|
|||
position: point_for_position.previous_valid,
|
||||
goal_column: point_for_position.exact_unclipped.column(),
|
||||
scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
|
||||
.clamp(&gpui::Point::zero(), &position_map.scroll_max),
|
||||
.clamp(&gpui::Point::default(), &position_map.scroll_max),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
@ -2835,8 +2835,10 @@ impl Element for EditorElement {
|
|||
self.paint_text(text_bounds, &mut layout, cx);
|
||||
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.with_element_id(Some("editor_blocks"), |cx| {
|
||||
self.paint_blocks(bounds, &mut layout, cx);
|
||||
cx.with_z_index(1, |cx| {
|
||||
cx.with_element_id(Some("editor_blocks"), |cx| {
|
||||
self.paint_blocks(bounds, &mut layout, cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
@ -3459,7 +3461,6 @@ mod tests {
|
|||
DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0)
|
||||
);
|
||||
assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0));
|
||||
dbg!("Hi");
|
||||
// moves cursor on buffer boundary back two lines
|
||||
// and doesn't allow selection to bleed through
|
||||
assert_eq!(
|
||||
|
|
|
@ -32,7 +32,7 @@ use std::{
|
|||
};
|
||||
use text::Selection;
|
||||
use theme::{ActiveTheme, Theme};
|
||||
use ui::{h_stack, Color, Label};
|
||||
use ui::{h_stack, prelude::*, Label};
|
||||
use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, FollowEvent, FollowableItemHandle},
|
||||
|
@ -939,7 +939,7 @@ impl SearchableItem for Editor {
|
|||
fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
|
||||
self.highlight_background::<BufferSearchHighlights>(
|
||||
matches,
|
||||
|theme| theme.title_bar_background, // todo: update theme
|
||||
|theme| theme.search_match_background,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use gpui::{
|
|||
};
|
||||
use text::{Bias, Point};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, v_stack, Color, Label, StyledExt};
|
||||
use ui::{h_stack, prelude::*, v_stack, Label};
|
||||
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
||||
|
||||
actions!(Toggle);
|
||||
|
|
|
@ -15,10 +15,10 @@ use smol::future::FutureExt;
|
|||
pub use test_context::*;
|
||||
|
||||
use crate::{
|
||||
current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView,
|
||||
AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
|
||||
AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
|
||||
ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
|
||||
ForegroundExecutor, KeyBinding, Keymap, LayoutId, Menu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
|
||||
TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
|
||||
WindowHandle, WindowId,
|
||||
|
@ -39,7 +39,10 @@ use std::{
|
|||
sync::{atomic::Ordering::SeqCst, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
use util::http::{self, HttpClient};
|
||||
use util::{
|
||||
http::{self, HttpClient},
|
||||
ResultExt,
|
||||
};
|
||||
|
||||
/// Temporary(?) wrapper around RefCell<AppContext> to help us debug any double borrows.
|
||||
/// Strongly consider removing after stabilization.
|
||||
|
@ -275,6 +278,8 @@ impl AppContext {
|
|||
}),
|
||||
});
|
||||
|
||||
init_app_menus(platform.as_ref(), &mut *app.borrow_mut());
|
||||
|
||||
platform.on_quit(Box::new({
|
||||
let cx = app.clone();
|
||||
move || {
|
||||
|
@ -425,6 +430,10 @@ impl AppContext {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
self.platform.active_window()
|
||||
}
|
||||
|
||||
/// Opens a new window with the given option and the root view returned by the given function.
|
||||
/// The function is invoked with a `WindowContext`, which can be used to interact with window-specific
|
||||
/// functionality.
|
||||
|
@ -851,7 +860,6 @@ impl AppContext {
|
|||
}
|
||||
|
||||
/// Remove the global of the given type from the app context. Does not notify global observers.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn remove_global<G: Any>(&mut self) -> G {
|
||||
let global_type = TypeId::of::<G>();
|
||||
*self
|
||||
|
@ -1015,6 +1023,90 @@ impl AppContext {
|
|||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
pub(crate) fn clear_pending_keystrokes(&mut self) {
|
||||
for window in self.windows() {
|
||||
window
|
||||
.update(self, |_, cx| {
|
||||
cx.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.clear_pending_keystrokes()
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_action_available(&mut self, action: &dyn Action) -> bool {
|
||||
if let Some(window) = self.active_window() {
|
||||
if let Ok(window_action_available) =
|
||||
window.update(self, |_, cx| cx.is_action_available(action))
|
||||
{
|
||||
return window_action_available;
|
||||
}
|
||||
}
|
||||
|
||||
self.global_action_listeners
|
||||
.contains_key(&action.as_any().type_id())
|
||||
}
|
||||
|
||||
pub fn set_menus(&mut self, menus: Vec<Menu>) {
|
||||
self.platform.set_menus(menus, &self.keymap.lock());
|
||||
}
|
||||
|
||||
pub fn dispatch_action(&mut self, action: &dyn Action) {
|
||||
if let Some(active_window) = self.active_window() {
|
||||
active_window
|
||||
.update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
|
||||
.log_err();
|
||||
} else {
|
||||
self.propagate_event = true;
|
||||
|
||||
if let Some(mut global_listeners) = self
|
||||
.global_action_listeners
|
||||
.remove(&action.as_any().type_id())
|
||||
{
|
||||
for listener in &global_listeners {
|
||||
listener(action.as_any(), DispatchPhase::Capture, self);
|
||||
if !self.propagate_event {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
global_listeners.extend(
|
||||
self.global_action_listeners
|
||||
.remove(&action.as_any().type_id())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
self.global_action_listeners
|
||||
.insert(action.as_any().type_id(), global_listeners);
|
||||
}
|
||||
|
||||
if self.propagate_event {
|
||||
if let Some(mut global_listeners) = self
|
||||
.global_action_listeners
|
||||
.remove(&action.as_any().type_id())
|
||||
{
|
||||
for listener in global_listeners.iter().rev() {
|
||||
listener(action.as_any(), DispatchPhase::Bubble, self);
|
||||
if !self.propagate_event {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
global_listeners.extend(
|
||||
self.global_action_listeners
|
||||
.remove(&action.as_any().type_id())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
self.global_action_listeners
|
||||
.insert(action.as_any().type_id(), global_listeners);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AppContext {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use crate::{
|
||||
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||
BackgroundExecutor, Bounds, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
|
||||
KeyDownEvent, Keystroke, Model, ModelContext, Pixels, PlatformWindow, Point, Render, Result,
|
||||
Size, Task, TestDispatcher, TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext,
|
||||
VisualContext, WindowBounds, WindowContext, WindowHandle, WindowOptions,
|
||||
BackgroundExecutor, Bounds, ClipboardItem, Context, Div, Entity, EventEmitter,
|
||||
ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, Pixels, Platform,
|
||||
PlatformWindow, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow,
|
||||
TestWindowHandlers, TextSystem, View, ViewContext, VisualContext, WindowBounds, WindowContext,
|
||||
WindowHandle, WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt};
|
||||
|
@ -16,6 +17,7 @@ pub struct TestAppContext {
|
|||
pub foreground_executor: ForegroundExecutor,
|
||||
pub dispatcher: TestDispatcher,
|
||||
pub test_platform: Rc<TestPlatform>,
|
||||
text_system: Arc<TextSystem>,
|
||||
}
|
||||
|
||||
impl Context for TestAppContext {
|
||||
|
@ -82,6 +84,7 @@ impl TestAppContext {
|
|||
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
|
||||
let asset_source = Arc::new(());
|
||||
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
|
||||
Self {
|
||||
app: AppContext::new(platform.clone(), asset_source, http_client),
|
||||
|
@ -89,6 +92,7 @@ impl TestAppContext {
|
|||
foreground_executor,
|
||||
dispatcher: dispatcher.clone(),
|
||||
test_platform: platform,
|
||||
text_system,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,6 +159,18 @@ impl TestAppContext {
|
|||
(view, Box::leak(cx))
|
||||
}
|
||||
|
||||
pub fn text_system(&self) -> &Arc<TextSystem> {
|
||||
&self.text_system
|
||||
}
|
||||
|
||||
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
self.test_platform.write_to_clipboard(item)
|
||||
}
|
||||
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.test_platform.read_from_clipboard()
|
||||
}
|
||||
|
||||
pub fn simulate_new_path_selection(
|
||||
&self,
|
||||
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
|
||||
|
@ -529,6 +545,10 @@ pub struct VisualTestContext<'a> {
|
|||
}
|
||||
|
||||
impl<'a> VisualTestContext<'a> {
|
||||
pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
|
||||
self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
|
||||
}
|
||||
|
||||
pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self {
|
||||
Self { cx, window }
|
||||
}
|
||||
|
|
|
@ -370,39 +370,6 @@ impl<E: Element> DrawableElement<E> {
|
|||
}
|
||||
}
|
||||
|
||||
// impl<V: 'static, E: Element> Element for DrawableElement<V, E> {
|
||||
// type State = <E::Element as Element>::State;
|
||||
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// element_state: Option<Self::State>,
|
||||
// cx: &mut WindowContext,
|
||||
// ) -> (LayoutId, Self::State) {
|
||||
|
||||
// }
|
||||
|
||||
// fn paint(
|
||||
// self,
|
||||
// bounds: Bounds<Pixels>,
|
||||
// element_state: &mut Self::State,
|
||||
// cx: &mut WindowContext,
|
||||
// ) {
|
||||
// todo!()
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl<V: 'static, E: 'static + Element> RenderOnce for DrawableElement<V, E> {
|
||||
// type Element = Self;
|
||||
|
||||
// fn element_id(&self) -> Option<ElementId> {
|
||||
// self.element.as_ref()?.element_id()
|
||||
// }
|
||||
|
||||
// fn render_once(self) -> Self::Element {
|
||||
// self
|
||||
// }
|
||||
// }
|
||||
|
||||
impl<E> ElementObject for Option<DrawableElement<E>>
|
||||
where
|
||||
E: Element,
|
||||
|
|
|
@ -559,6 +559,8 @@ pub type KeyDownListener = Box<dyn Fn(&KeyDownEvent, DispatchPhase, &mut WindowC
|
|||
|
||||
pub type KeyUpListener = Box<dyn Fn(&KeyUpEvent, DispatchPhase, &mut WindowContext) + 'static>;
|
||||
|
||||
pub type DragEventListener = Box<dyn Fn(&MouseMoveEvent, &mut WindowContext) + 'static>;
|
||||
|
||||
pub type ActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
|
||||
|
||||
pub fn div() -> Div {
|
||||
|
@ -751,7 +753,7 @@ pub struct Interactivity {
|
|||
pub tooltip_builder: Option<TooltipBuilder>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct InteractiveBounds {
|
||||
pub bounds: Bounds<Pixels>,
|
||||
pub stacking_order: StackingOrder,
|
||||
|
@ -899,11 +901,14 @@ impl Interactivity {
|
|||
.active_drag
|
||||
.take()
|
||||
.expect("checked for type drag state type above");
|
||||
|
||||
listener(drag.view.clone(), cx);
|
||||
cx.notify();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cx.active_drag = None;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1324,7 +1329,7 @@ impl GroupBounds {
|
|||
}
|
||||
|
||||
pub struct Focusable<E> {
|
||||
element: E,
|
||||
pub element: E,
|
||||
}
|
||||
|
||||
impl<E: InteractiveElement> FocusableElement for Focusable<E> {}
|
||||
|
|
|
@ -102,7 +102,7 @@ impl Element for Overlay {
|
|||
|
||||
let mut desired = self.anchor_corner.get_bounds(origin, size);
|
||||
let limits = Bounds {
|
||||
origin: Point::zero(),
|
||||
origin: Point::default(),
|
||||
size: cx.viewport_size(),
|
||||
};
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -28,7 +28,7 @@ pub(crate) struct DispatchTree {
|
|||
pub(crate) struct DispatchNode {
|
||||
pub key_listeners: SmallVec<[KeyListener; 2]>,
|
||||
pub action_listeners: SmallVec<[DispatchActionListener; 16]>,
|
||||
pub context: KeyContext,
|
||||
pub context: Option<KeyContext>,
|
||||
parent: Option<DispatchNodeId>,
|
||||
}
|
||||
|
||||
|
@ -70,33 +70,33 @@ impl DispatchTree {
|
|||
});
|
||||
self.node_stack.push(node_id);
|
||||
if let Some(context) = context {
|
||||
self.active_node().context = context.clone();
|
||||
self.active_node().context = Some(context.clone());
|
||||
self.context_stack.push(context);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_node(&mut self) {
|
||||
let node_id = self.node_stack.pop().unwrap();
|
||||
if !self.nodes[node_id.0].context.is_empty() {
|
||||
if self.nodes[node_id.0].context.is_some() {
|
||||
self.context_stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_keystroke_matchers(&mut self) {
|
||||
pub fn clear_pending_keystrokes(&mut self) {
|
||||
self.keystroke_matchers.clear();
|
||||
}
|
||||
|
||||
/// Preserve keystroke matchers from previous frames to support multi-stroke
|
||||
/// bindings across multiple frames.
|
||||
pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
|
||||
pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
|
||||
if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) {
|
||||
let dispatch_path = self.dispatch_path(node_id);
|
||||
|
||||
self.context_stack.clear();
|
||||
for node_id in dispatch_path {
|
||||
let node = self.node(node_id);
|
||||
if !node.context.is_empty() {
|
||||
self.context_stack.push(node.context.clone());
|
||||
if let Some(context) = node.context.clone() {
|
||||
self.context_stack.push(context);
|
||||
}
|
||||
|
||||
if let Some((context_stack, matcher)) = old_tree
|
||||
|
@ -161,6 +161,20 @@ impl DispatchTree {
|
|||
actions
|
||||
}
|
||||
|
||||
pub fn is_action_available(&self, action: &dyn Action, target: DispatchNodeId) -> bool {
|
||||
for node_id in self.dispatch_path(target) {
|
||||
let node = &self.nodes[node_id.0];
|
||||
if node
|
||||
.action_listeners
|
||||
.iter()
|
||||
.any(|listener| listener.action_type == action.as_any().type_id())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn bindings_for_action(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
mod app_menu;
|
||||
mod keystroke;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
|
@ -5,10 +6,10 @@ mod mac;
|
|||
mod test;
|
||||
|
||||
use crate::{
|
||||
point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
|
||||
FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout,
|
||||
Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene,
|
||||
SharedString, Size, TaskLabel,
|
||||
point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
|
||||
FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, Keymap,
|
||||
LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result,
|
||||
Scene, SharedString, Size, TaskLabel,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use async_task::Runnable;
|
||||
|
@ -32,6 +33,7 @@ use std::{
|
|||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use app_menu::*;
|
||||
pub use keystroke::*;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use mac::*;
|
||||
|
@ -44,7 +46,7 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
|||
Rc::new(MacPlatform::new())
|
||||
}
|
||||
|
||||
pub trait Platform: 'static {
|
||||
pub(crate) trait Platform: 'static {
|
||||
fn background_executor(&self) -> BackgroundExecutor;
|
||||
fn foreground_executor(&self) -> ForegroundExecutor;
|
||||
fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
|
||||
|
@ -59,7 +61,7 @@ pub trait Platform: 'static {
|
|||
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn main_window(&self) -> Option<AnyWindowHandle>;
|
||||
fn active_window(&self) -> Option<AnyWindowHandle>;
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
|
@ -90,6 +92,11 @@ pub trait Platform: 'static {
|
|||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||
|
||||
fn os_name(&self) -> &'static str;
|
||||
fn os_version(&self) -> Result<SemanticVersion>;
|
||||
fn app_version(&self) -> Result<SemanticVersion>;
|
||||
|
@ -138,6 +145,7 @@ pub trait PlatformWindow {
|
|||
fn mouse_position(&self) -> Point<Pixels>;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
|
||||
fn clear_input_handler(&mut self);
|
||||
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
|
||||
fn activate(&self);
|
||||
fn set_title(&mut self, title: &str);
|
||||
|
|
77
crates/gpui2/src/platform/app_menu.rs
Normal file
77
crates/gpui2/src/platform/app_menu.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use crate::{Action, AppContext, Platform};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Menu<'a> {
|
||||
pub name: &'a str,
|
||||
pub items: Vec<MenuItem<'a>>,
|
||||
}
|
||||
|
||||
pub enum MenuItem<'a> {
|
||||
Separator,
|
||||
Submenu(Menu<'a>),
|
||||
Action {
|
||||
name: &'a str,
|
||||
action: Box<dyn Action>,
|
||||
os_action: Option<OsAction>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> MenuItem<'a> {
|
||||
pub fn separator() -> Self {
|
||||
Self::Separator
|
||||
}
|
||||
|
||||
pub fn submenu(menu: Menu<'a>) -> Self {
|
||||
Self::Submenu(menu)
|
||||
}
|
||||
|
||||
pub fn action(name: &'a str, action: impl Action) -> Self {
|
||||
Self::Action {
|
||||
name,
|
||||
action: Box::new(action),
|
||||
os_action: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
|
||||
Self::Action {
|
||||
name,
|
||||
action: Box::new(action),
|
||||
os_action: Some(os_action),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum OsAction {
|
||||
Cut,
|
||||
Copy,
|
||||
Paste,
|
||||
SelectAll,
|
||||
Undo,
|
||||
Redo,
|
||||
}
|
||||
|
||||
pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &mut AppContext) {
|
||||
platform.on_will_open_app_menu(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move || {
|
||||
cx.update(|cx| cx.clear_pending_keystrokes()).ok();
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_validate_app_menu_command(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
cx.update(|cx| cx.is_action_available(action))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_app_menu_action(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
cx.update(|cx| cx.dispatch_action(action)).log_err();
|
||||
}
|
||||
}));
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
use super::BoolExt;
|
||||
use super::{events::key_to_native, BoolExt};
|
||||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
|
||||
InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow,
|
||||
PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result,
|
||||
SemanticVersion, VideoTimestamp, WindowOptions,
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker,
|
||||
MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
|
||||
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
||||
NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, NSSavePanel, NSWindow,
|
||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
||||
NSPasteboardTypeString, NSSavePanel, NSWindow,
|
||||
},
|
||||
base::{id, nil, BOOL, YES},
|
||||
base::{id, nil, selector, BOOL, YES},
|
||||
foundation::{
|
||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
|
||||
NSUInteger, NSURL,
|
||||
|
@ -155,12 +156,12 @@ pub struct MacPlatformState {
|
|||
reopen: Option<Box<dyn FnMut()>>,
|
||||
quit: Option<Box<dyn FnMut()>>,
|
||||
event: Option<Box<dyn FnMut(InputEvent) -> bool>>,
|
||||
// menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
|
||||
// validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
|
||||
validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
will_open_menu: Option<Box<dyn FnMut()>>,
|
||||
menu_actions: Vec<Box<dyn Action>>,
|
||||
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
|
||||
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||
// menu_actions: Vec<Box<dyn Action>>,
|
||||
}
|
||||
|
||||
impl MacPlatform {
|
||||
|
@ -179,12 +180,12 @@ impl MacPlatform {
|
|||
reopen: None,
|
||||
quit: None,
|
||||
event: None,
|
||||
menu_command: None,
|
||||
validate_menu_command: None,
|
||||
will_open_menu: None,
|
||||
menu_actions: Default::default(),
|
||||
open_urls: None,
|
||||
finish_launching: None,
|
||||
// menu_command: None,
|
||||
// validate_menu_command: None,
|
||||
// menu_actions: Default::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -200,151 +201,153 @@ impl MacPlatform {
|
|||
}
|
||||
}
|
||||
|
||||
// unsafe fn create_menu_bar(
|
||||
// &self,
|
||||
// menus: Vec<Menu>,
|
||||
// delegate: id,
|
||||
// actions: &mut Vec<Box<dyn Action>>,
|
||||
// keystroke_matcher: &KeymapMatcher,
|
||||
// ) -> id {
|
||||
// let application_menu = NSMenu::new(nil).autorelease();
|
||||
// application_menu.setDelegate_(delegate);
|
||||
unsafe fn create_menu_bar(
|
||||
&self,
|
||||
menus: Vec<Menu>,
|
||||
delegate: id,
|
||||
actions: &mut Vec<Box<dyn Action>>,
|
||||
keymap: &Keymap,
|
||||
) -> id {
|
||||
let application_menu = NSMenu::new(nil).autorelease();
|
||||
application_menu.setDelegate_(delegate);
|
||||
|
||||
// for menu_config in menus {
|
||||
// let menu = NSMenu::new(nil).autorelease();
|
||||
// menu.setTitle_(ns_string(menu_config.name));
|
||||
// menu.setDelegate_(delegate);
|
||||
for menu_config in menus {
|
||||
let menu = NSMenu::new(nil).autorelease();
|
||||
menu.setTitle_(ns_string(menu_config.name));
|
||||
menu.setDelegate_(delegate);
|
||||
|
||||
// for item_config in menu_config.items {
|
||||
// menu.addItem_(self.create_menu_item(
|
||||
// item_config,
|
||||
// delegate,
|
||||
// actions,
|
||||
// keystroke_matcher,
|
||||
// ));
|
||||
// }
|
||||
for item_config in menu_config.items {
|
||||
menu.addItem_(self.create_menu_item(item_config, delegate, actions, keymap));
|
||||
}
|
||||
|
||||
// let menu_item = NSMenuItem::new(nil).autorelease();
|
||||
// menu_item.setSubmenu_(menu);
|
||||
// application_menu.addItem_(menu_item);
|
||||
let menu_item = NSMenuItem::new(nil).autorelease();
|
||||
menu_item.setSubmenu_(menu);
|
||||
application_menu.addItem_(menu_item);
|
||||
|
||||
// if menu_config.name == "Window" {
|
||||
// let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
// app.setWindowsMenu_(menu);
|
||||
// }
|
||||
// }
|
||||
if menu_config.name == "Window" {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
app.setWindowsMenu_(menu);
|
||||
}
|
||||
}
|
||||
|
||||
// application_menu
|
||||
// }
|
||||
application_menu
|
||||
}
|
||||
|
||||
// unsafe fn create_menu_item(
|
||||
// &self,
|
||||
// item: MenuItem,
|
||||
// delegate: id,
|
||||
// actions: &mut Vec<Box<dyn Action>>,
|
||||
// keystroke_matcher: &KeymapMatcher,
|
||||
// ) -> id {
|
||||
// match item {
|
||||
// MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
||||
// MenuItem::Action {
|
||||
// name,
|
||||
// action,
|
||||
// os_action,
|
||||
// } => {
|
||||
// // TODO
|
||||
// let keystrokes = keystroke_matcher
|
||||
// .bindings_for_action(action.id())
|
||||
// .find(|binding| binding.action().eq(action.as_ref()))
|
||||
// .map(|binding| binding.keystrokes());
|
||||
// let selector = match os_action {
|
||||
// Some(crate::OsAction::Cut) => selector("cut:"),
|
||||
// Some(crate::OsAction::Copy) => selector("copy:"),
|
||||
// Some(crate::OsAction::Paste) => selector("paste:"),
|
||||
// Some(crate::OsAction::SelectAll) => selector("selectAll:"),
|
||||
// Some(crate::OsAction::Undo) => selector("undo:"),
|
||||
// Some(crate::OsAction::Redo) => selector("redo:"),
|
||||
// None => selector("handleGPUIMenuItem:"),
|
||||
// };
|
||||
unsafe fn create_menu_item(
|
||||
&self,
|
||||
item: MenuItem,
|
||||
delegate: id,
|
||||
actions: &mut Vec<Box<dyn Action>>,
|
||||
keymap: &Keymap,
|
||||
) -> id {
|
||||
match item {
|
||||
MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
||||
MenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
} => {
|
||||
let keystrokes = keymap
|
||||
.bindings_for_action(action.type_id())
|
||||
.find(|binding| binding.action().partial_eq(action.as_ref()))
|
||||
.map(|binding| binding.keystrokes());
|
||||
|
||||
// let item;
|
||||
// if let Some(keystrokes) = keystrokes {
|
||||
// if keystrokes.len() == 1 {
|
||||
// let keystroke = &keystrokes[0];
|
||||
// let mut mask = NSEventModifierFlags::empty();
|
||||
// for (modifier, flag) in &[
|
||||
// (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
|
||||
// (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
|
||||
// (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
|
||||
// (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask),
|
||||
// ] {
|
||||
// if *modifier {
|
||||
// mask |= *flag;
|
||||
// }
|
||||
// }
|
||||
let selector = match os_action {
|
||||
Some(crate::OsAction::Cut) => selector("cut:"),
|
||||
Some(crate::OsAction::Copy) => selector("copy:"),
|
||||
Some(crate::OsAction::Paste) => selector("paste:"),
|
||||
Some(crate::OsAction::SelectAll) => selector("selectAll:"),
|
||||
Some(crate::OsAction::Undo) => selector("undo:"),
|
||||
Some(crate::OsAction::Redo) => selector("redo:"),
|
||||
None => selector("handleGPUIMenuItem:"),
|
||||
};
|
||||
|
||||
// item = NSMenuItem::alloc(nil)
|
||||
// .initWithTitle_action_keyEquivalent_(
|
||||
// ns_string(name),
|
||||
// selector,
|
||||
// ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||
// )
|
||||
// .autorelease();
|
||||
// item.setKeyEquivalentModifierMask_(mask);
|
||||
// }
|
||||
// // For multi-keystroke bindings, render the keystroke as part of the title.
|
||||
// else {
|
||||
// use std::fmt::Write;
|
||||
let item;
|
||||
if let Some(keystrokes) = keystrokes {
|
||||
if keystrokes.len() == 1 {
|
||||
let keystroke = &keystrokes[0];
|
||||
let mut mask = NSEventModifierFlags::empty();
|
||||
for (modifier, flag) in &[
|
||||
(
|
||||
keystroke.modifiers.command,
|
||||
NSEventModifierFlags::NSCommandKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.control,
|
||||
NSEventModifierFlags::NSControlKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.alt,
|
||||
NSEventModifierFlags::NSAlternateKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.shift,
|
||||
NSEventModifierFlags::NSShiftKeyMask,
|
||||
),
|
||||
] {
|
||||
if *modifier {
|
||||
mask |= *flag;
|
||||
}
|
||||
}
|
||||
|
||||
// let mut name = format!("{name} [");
|
||||
// for (i, keystroke) in keystrokes.iter().enumerate() {
|
||||
// if i > 0 {
|
||||
// name.push(' ');
|
||||
// }
|
||||
// write!(&mut name, "{}", keystroke).unwrap();
|
||||
// }
|
||||
// name.push(']');
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector,
|
||||
ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||
)
|
||||
.autorelease();
|
||||
item.setKeyEquivalentModifierMask_(mask);
|
||||
}
|
||||
// For multi-keystroke bindings, render the keystroke as part of the title.
|
||||
else {
|
||||
use std::fmt::Write;
|
||||
|
||||
// item = NSMenuItem::alloc(nil)
|
||||
// .initWithTitle_action_keyEquivalent_(
|
||||
// ns_string(&name),
|
||||
// selector,
|
||||
// ns_string(""),
|
||||
// )
|
||||
// .autorelease();
|
||||
// }
|
||||
// } else {
|
||||
// item = NSMenuItem::alloc(nil)
|
||||
// .initWithTitle_action_keyEquivalent_(
|
||||
// ns_string(name),
|
||||
// selector,
|
||||
// ns_string(""),
|
||||
// )
|
||||
// .autorelease();
|
||||
// }
|
||||
let mut name = format!("{name} [");
|
||||
for (i, keystroke) in keystrokes.iter().enumerate() {
|
||||
if i > 0 {
|
||||
name.push(' ');
|
||||
}
|
||||
write!(&mut name, "{}", keystroke).unwrap();
|
||||
}
|
||||
name.push(']');
|
||||
|
||||
// let tag = actions.len() as NSInteger;
|
||||
// let _: () = msg_send![item, setTag: tag];
|
||||
// actions.push(action);
|
||||
// item
|
||||
// }
|
||||
// MenuItem::Submenu(Menu { name, items }) => {
|
||||
// let item = NSMenuItem::new(nil).autorelease();
|
||||
// let submenu = NSMenu::new(nil).autorelease();
|
||||
// submenu.setDelegate_(delegate);
|
||||
// for item in items {
|
||||
// submenu.addItem_(self.create_menu_item(
|
||||
// item,
|
||||
// delegate,
|
||||
// actions,
|
||||
// keystroke_matcher,
|
||||
// ));
|
||||
// }
|
||||
// item.setSubmenu_(submenu);
|
||||
// item.setTitle_(ns_string(name));
|
||||
// item
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(&name),
|
||||
selector,
|
||||
ns_string(""),
|
||||
)
|
||||
.autorelease();
|
||||
}
|
||||
} else {
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector,
|
||||
ns_string(""),
|
||||
)
|
||||
.autorelease();
|
||||
}
|
||||
|
||||
let tag = actions.len() as NSInteger;
|
||||
let _: () = msg_send![item, setTag: tag];
|
||||
actions.push(action);
|
||||
item
|
||||
}
|
||||
MenuItem::Submenu(Menu { name, items }) => {
|
||||
let item = NSMenuItem::new(nil).autorelease();
|
||||
let submenu = NSMenu::new(nil).autorelease();
|
||||
submenu.setDelegate_(delegate);
|
||||
for item in items {
|
||||
submenu.addItem_(self.create_menu_item(item, delegate, actions, keymap));
|
||||
}
|
||||
item.setSubmenu_(submenu);
|
||||
item.setTitle_(ns_string(name));
|
||||
item
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform for MacPlatform {
|
||||
|
@ -479,8 +482,8 @@ impl Platform for MacPlatform {
|
|||
MacDisplay::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>)
|
||||
}
|
||||
|
||||
fn main_window(&self) -> Option<AnyWindowHandle> {
|
||||
MacWindow::main_window()
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
MacWindow::active_window()
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
|
@ -631,6 +634,18 @@ impl Platform for MacPlatform {
|
|||
self.0.lock().event = Some(callback);
|
||||
}
|
||||
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||
self.0.lock().menu_command = Some(callback);
|
||||
}
|
||||
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.lock().will_open_menu = Some(callback);
|
||||
}
|
||||
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
|
||||
self.0.lock().validate_menu_command = Some(callback);
|
||||
}
|
||||
|
||||
fn os_name(&self) -> &'static str {
|
||||
"macOS"
|
||||
}
|
||||
|
@ -673,6 +688,15 @@ impl Platform for MacPlatform {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {
|
||||
unsafe {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
let mut state = self.0.lock();
|
||||
let actions = &mut state.menu_actions;
|
||||
app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), actions, keymap));
|
||||
}
|
||||
}
|
||||
|
||||
fn local_timezone(&self) -> UtcOffset {
|
||||
unsafe {
|
||||
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
|
||||
|
@ -681,32 +705,6 @@ impl Platform for MacPlatform {
|
|||
}
|
||||
}
|
||||
|
||||
// fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||
// self.0.lock().menu_command = Some(callback);
|
||||
// }
|
||||
|
||||
// fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
|
||||
// self.0.lock().will_open_menu = Some(callback);
|
||||
// }
|
||||
|
||||
// fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
|
||||
// self.0.lock().validate_menu_command = Some(callback);
|
||||
// }
|
||||
|
||||
// fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
|
||||
// unsafe {
|
||||
// let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
// let mut state = self.0.lock();
|
||||
// let actions = &mut state.menu_actions;
|
||||
// app.setMainMenu_(self.create_menu_bar(
|
||||
// menus,
|
||||
// app.delegate(),
|
||||
// actions,
|
||||
// keystroke_matcher,
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||
unsafe {
|
||||
let bundle: id = NSBundle::mainBundle();
|
||||
|
@ -956,7 +954,7 @@ unsafe fn path_from_objc(path: id) -> PathBuf {
|
|||
PathBuf::from(path)
|
||||
}
|
||||
|
||||
unsafe fn get_foreground_platform(object: &mut Object) -> &MacPlatform {
|
||||
unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform {
|
||||
let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
|
||||
assert!(!platform_ptr.is_null());
|
||||
&*(platform_ptr as *const MacPlatform)
|
||||
|
@ -965,7 +963,7 @@ unsafe fn get_foreground_platform(object: &mut Object) -> &MacPlatform {
|
|||
extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
|
||||
unsafe {
|
||||
if let Some(event) = InputEvent::from_native(native_event, None) {
|
||||
let platform = get_foreground_platform(this);
|
||||
let platform = get_mac_platform(this);
|
||||
if let Some(callback) = platform.0.lock().event.as_mut() {
|
||||
if !callback(event) {
|
||||
return;
|
||||
|
@ -981,7 +979,7 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
|
|||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
|
||||
|
||||
let platform = get_foreground_platform(this);
|
||||
let platform = get_mac_platform(this);
|
||||
let callback = platform.0.lock().finish_launching.take();
|
||||
if let Some(callback) = callback {
|
||||
callback();
|
||||
|
@ -991,7 +989,7 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
|
|||
|
||||
extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {
|
||||
if !has_open_windows {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
if let Some(callback) = platform.0.lock().reopen.as_mut() {
|
||||
callback();
|
||||
}
|
||||
|
@ -999,21 +997,21 @@ extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_wi
|
|||
}
|
||||
|
||||
extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
if let Some(callback) = platform.0.lock().become_active.as_mut() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
if let Some(callback) = platform.0.lock().resign_active.as_mut() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
if let Some(callback) = platform.0.lock().quit.as_mut() {
|
||||
callback();
|
||||
}
|
||||
|
@ -1035,49 +1033,47 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
if let Some(callback) = platform.0.lock().open_urls.as_mut() {
|
||||
callback(urls);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handle_menu_item(__this: &mut Object, _: Sel, __item: id) {
|
||||
todo!()
|
||||
// unsafe {
|
||||
// let platform = get_foreground_platform(this);
|
||||
// let mut platform = platform.0.lock();
|
||||
// if let Some(mut callback) = platform.menu_command.take() {
|
||||
// let tag: NSInteger = msg_send![item, tag];
|
||||
// let index = tag as usize;
|
||||
// if let Some(action) = platform.menu_actions.get(index) {
|
||||
// callback(action.as_ref());
|
||||
// }
|
||||
// platform.menu_command = Some(callback);
|
||||
// }
|
||||
// }
|
||||
extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
|
||||
unsafe {
|
||||
let platform = get_mac_platform(this);
|
||||
let mut platform = platform.0.lock();
|
||||
if let Some(mut callback) = platform.menu_command.take() {
|
||||
let tag: NSInteger = msg_send![item, tag];
|
||||
let index = tag as usize;
|
||||
if let Some(action) = platform.menu_actions.get(index) {
|
||||
callback(action.as_ref());
|
||||
}
|
||||
platform.menu_command = Some(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn validate_menu_item(__this: &mut Object, _: Sel, __item: id) -> bool {
|
||||
todo!()
|
||||
// unsafe {
|
||||
// let mut result = false;
|
||||
// let platform = get_foreground_platform(this);
|
||||
// let mut platform = platform.0.lock();
|
||||
// if let Some(mut callback) = platform.validate_menu_command.take() {
|
||||
// let tag: NSInteger = msg_send![item, tag];
|
||||
// let index = tag as usize;
|
||||
// if let Some(action) = platform.menu_actions.get(index) {
|
||||
// result = callback(action.as_ref());
|
||||
// }
|
||||
// platform.validate_menu_command = Some(callback);
|
||||
// }
|
||||
// result
|
||||
// }
|
||||
extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool {
|
||||
unsafe {
|
||||
let mut result = false;
|
||||
let platform = get_mac_platform(this);
|
||||
let mut platform = platform.0.lock();
|
||||
if let Some(mut callback) = platform.validate_menu_command.take() {
|
||||
let tag: NSInteger = msg_send![item, tag];
|
||||
let index = tag as usize;
|
||||
if let Some(action) = platform.menu_actions.get(index) {
|
||||
result = callback(action.as_ref());
|
||||
}
|
||||
platform.validate_menu_command = Some(callback);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
|
||||
unsafe {
|
||||
let platform = get_foreground_platform(this);
|
||||
let platform = get_mac_platform(this);
|
||||
let mut platform = platform.0.lock();
|
||||
if let Some(mut callback) = platform.will_open_menu.take() {
|
||||
callback();
|
||||
|
|
|
@ -662,7 +662,7 @@ impl MacWindow {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn main_window() -> Option<AnyWindowHandle> {
|
||||
pub fn active_window() -> Option<AnyWindowHandle> {
|
||||
unsafe {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let main_window: id = msg_send![app, mainWindow];
|
||||
|
@ -750,6 +750,10 @@ impl PlatformWindow for MacWindow {
|
|||
self.0.as_ref().lock().input_handler = Some(input_handler);
|
||||
}
|
||||
|
||||
fn clear_input_handler(&mut self) {
|
||||
self.0.as_ref().lock().input_handler = None;
|
||||
}
|
||||
|
||||
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize> {
|
||||
// macOs applies overrides to modal window buttons after they are added.
|
||||
// Two most important for this logic are:
|
||||
|
|
|
@ -15,7 +15,7 @@ impl TestDisplay {
|
|||
id: DisplayId(1),
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
bounds: Bounds::from_corners(
|
||||
Point::zero(),
|
||||
Point::default(),
|
||||
Point::new(GlobalPixels(1920.), GlobalPixels(1080.)),
|
||||
),
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
|
||||
Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
|
||||
Keymap, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::VecDeque;
|
||||
|
@ -127,7 +127,7 @@ impl Platform for TestPlatform {
|
|||
self.displays().iter().find(|d| d.id() == id).cloned()
|
||||
}
|
||||
|
||||
fn main_window(&self) -> Option<crate::AnyWindowHandle> {
|
||||
fn active_window(&self) -> Option<crate::AnyWindowHandle> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
@ -212,6 +212,14 @@ impl Platform for TestPlatform {
|
|||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
|
||||
|
||||
fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
|
||||
|
||||
fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
|
||||
|
||||
fn os_name(&self) -> &'static str {
|
||||
"test"
|
||||
}
|
||||
|
|
|
@ -66,11 +66,11 @@ impl PlatformWindow for TestWindow {
|
|||
}
|
||||
|
||||
fn titlebar_height(&self) -> Pixels {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn appearance(&self) -> WindowAppearance {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn display(&self) -> std::rc::Rc<dyn crate::PlatformDisplay> {
|
||||
|
@ -78,7 +78,7 @@ impl PlatformWindow for TestWindow {
|
|||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
Point::zero()
|
||||
Point::default()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
|
@ -89,6 +89,10 @@ impl PlatformWindow for TestWindow {
|
|||
self.input_handler = Some(Arc::new(Mutex::new(input_handler)));
|
||||
}
|
||||
|
||||
fn clear_input_handler(&mut self) {
|
||||
self.input_handler = None;
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_level: crate::PromptLevel,
|
||||
|
@ -99,7 +103,7 @@ impl PlatformWindow for TestWindow {
|
|||
}
|
||||
|
||||
fn activate(&self) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
|
@ -107,23 +111,23 @@ impl PlatformWindow for TestWindow {
|
|||
}
|
||||
|
||||
fn set_edited(&mut self, _edited: bool) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn show_character_palette(&self) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn zoom(&self) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn toggle_full_screen(&self) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_input(&self, callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
|
||||
|
@ -139,7 +143,7 @@ impl PlatformWindow for TestWindow {
|
|||
}
|
||||
|
||||
fn on_fullscreen(&self, _callback: Box<dyn FnMut(bool)>) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_moved(&self, callback: Box<dyn FnMut()>) {
|
||||
|
@ -147,19 +151,19 @@ impl PlatformWindow for TestWindow {
|
|||
}
|
||||
|
||||
fn on_should_close(&self, _callback: Box<dyn FnMut() -> bool>) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_close(&self, _callback: Box<dyn FnOnce()>) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_topmost_for_position(&self, _position: crate::Point<Pixels>) -> bool {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn draw(&self, scene: crate::Scene) {
|
||||
|
@ -223,7 +227,7 @@ impl PlatformAtlas for TestAtlas {
|
|||
},
|
||||
tile_id: TileId(tile_id),
|
||||
bounds: crate::Bounds {
|
||||
origin: Point::zero(),
|
||||
origin: Point::default(),
|
||||
size,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -385,7 +385,7 @@ impl Default for Style {
|
|||
min_size: Size::auto(),
|
||||
max_size: Size::auto(),
|
||||
aspect_ratio: None,
|
||||
gap: Size::zero(),
|
||||
gap: Size::default(),
|
||||
// Aligment
|
||||
align_items: None,
|
||||
align_self: None,
|
||||
|
|
|
@ -430,7 +430,7 @@ impl<'a> WindowContext<'a> {
|
|||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.clear_keystroke_matchers();
|
||||
.clear_pending_keystrokes();
|
||||
self.app.push_effect(Effect::FocusChanged {
|
||||
window_handle: self.window.handle,
|
||||
focused: Some(focus_id),
|
||||
|
@ -804,6 +804,22 @@ impl<'a> WindowContext<'a> {
|
|||
);
|
||||
}
|
||||
|
||||
pub fn is_action_available(&self, action: &dyn Action) -> bool {
|
||||
let target = self
|
||||
.focused()
|
||||
.and_then(|focused_handle| {
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(focused_handle.id)
|
||||
})
|
||||
.unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id());
|
||||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.is_action_available(action, target)
|
||||
}
|
||||
|
||||
/// The position of the mouse relative to the window.
|
||||
pub fn mouse_position(&self) -> Point<Pixels> {
|
||||
self.window.mouse_position
|
||||
|
@ -1167,7 +1183,7 @@ impl<'a> WindowContext<'a> {
|
|||
}
|
||||
|
||||
let available_space = cx.window.viewport_size.map(Into::into);
|
||||
root_view.draw(Point::zero(), available_space, cx);
|
||||
root_view.draw(Point::default(), available_space, cx);
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -1176,8 +1192,8 @@ impl<'a> WindowContext<'a> {
|
|||
let offset = cx.mouse_position() - active_drag.cursor_offset;
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
active_drag.view.draw(offset, available_space, cx);
|
||||
cx.active_drag = Some(active_drag);
|
||||
});
|
||||
self.active_drag = Some(active_drag);
|
||||
} else if let Some(active_tooltip) = self.app.active_tooltip.take() {
|
||||
self.with_z_index(1, |cx| {
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
|
@ -1190,7 +1206,7 @@ impl<'a> WindowContext<'a> {
|
|||
self.window
|
||||
.current_frame
|
||||
.dispatch_tree
|
||||
.preserve_keystroke_matchers(
|
||||
.preserve_pending_keystrokes(
|
||||
&mut self.window.previous_frame.dispatch_tree,
|
||||
self.window.focus,
|
||||
);
|
||||
|
@ -1212,6 +1228,7 @@ impl<'a> WindowContext<'a> {
|
|||
/// Rotate the current frame and the previous frame, then clear the current frame.
|
||||
/// We repopulate all state in the current frame during each paint.
|
||||
fn start_frame(&mut self) {
|
||||
self.window.platform_window.clear_input_handler();
|
||||
self.text_system().start_frame();
|
||||
|
||||
let window = &mut *self.window;
|
||||
|
@ -1377,8 +1394,8 @@ impl<'a> WindowContext<'a> {
|
|||
for node_id in &dispatch_path {
|
||||
let node = self.window.current_frame.dispatch_tree.node(*node_id);
|
||||
|
||||
if !node.context.is_empty() {
|
||||
context_stack.push(node.context.clone());
|
||||
if let Some(context) = node.context.clone() {
|
||||
context_stack.push(context);
|
||||
}
|
||||
|
||||
for key_listener in node.key_listeners.clone() {
|
||||
|
@ -1402,7 +1419,7 @@ impl<'a> WindowContext<'a> {
|
|||
|
||||
// Match keystrokes
|
||||
let node = self.window.current_frame.dispatch_tree.node(*node_id);
|
||||
if !node.context.is_empty() {
|
||||
if node.context.is_some() {
|
||||
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
|
||||
if let Some(found) = self
|
||||
.window
|
||||
|
@ -1547,7 +1564,7 @@ impl<'a> WindowContext<'a> {
|
|||
let context_stack = dispatch_tree
|
||||
.dispatch_path(node_id)
|
||||
.into_iter()
|
||||
.map(|node_id| dispatch_tree.node(node_id).context.clone())
|
||||
.filter_map(|node_id| dispatch_tree.node(node_id).context.clone())
|
||||
.collect();
|
||||
dispatch_tree.bindings_for_action(action, &context_stack)
|
||||
}
|
||||
|
|
|
@ -95,8 +95,6 @@ mod tests {
|
|||
.iter()
|
||||
.map(|(name, color)| (name.to_string(), (*color).into()))
|
||||
.collect(),
|
||||
inlay_style: HighlightStyle::default(),
|
||||
suggestion_style: HighlightStyle::default(),
|
||||
};
|
||||
|
||||
let capture_names = &[
|
||||
|
|
|
@ -10,7 +10,7 @@ doctest = false
|
|||
|
||||
[dependencies]
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
futures.workspace = true
|
||||
async-trait.workspace = true
|
||||
|
|
|
@ -55,7 +55,6 @@ pub struct ProjectPanel {
|
|||
clipboard_entry: Option<ClipboardEntry>,
|
||||
_dragged_entry_destination: Option<Arc<Path>>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
has_focus: bool,
|
||||
width: Option<f32>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
}
|
||||
|
@ -172,7 +171,6 @@ impl ProjectPanel {
|
|||
let focus_handle = cx.focus_handle();
|
||||
|
||||
cx.on_focus(&focus_handle, Self::focus_in).detach();
|
||||
cx.on_blur(&focus_handle, Self::focus_out).detach();
|
||||
|
||||
cx.subscribe(&project, |this, project, event, cx| match event {
|
||||
project::Event::ActiveEntryChanged(Some(entry_id)) => {
|
||||
|
@ -238,7 +236,6 @@ impl ProjectPanel {
|
|||
// context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
|
||||
_dragged_entry_destination: None,
|
||||
_workspace: workspace.weak_handle(),
|
||||
has_focus: false,
|
||||
width: None,
|
||||
pending_serialization: Task::ready(None),
|
||||
};
|
||||
|
@ -356,16 +353,11 @@ impl ProjectPanel {
|
|||
}
|
||||
|
||||
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.has_focus {
|
||||
self.has_focus = true;
|
||||
if !self.focus_handle.contains_focused(cx) {
|
||||
cx.emit(Event::Focus);
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
|
||||
fn deploy_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
|
@ -1557,10 +1549,6 @@ impl Panel for ProjectPanel {
|
|||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn has_focus(&self, _: &WindowContext) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn persistent_name() -> &'static str {
|
||||
"Project Panel"
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspac
|
|||
pub struct QuickActionBar {
|
||||
buffer_search_bar: ViewHandle<BufferSearchBar>,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
_inlay_hints_enabled_subscription: Option<Subscription>,
|
||||
inlay_hints_enabled_subscription: Option<Subscription>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ impl QuickActionBar {
|
|||
Self {
|
||||
buffer_search_bar,
|
||||
active_item: None,
|
||||
_inlay_hints_enabled_subscription: None,
|
||||
inlay_hints_enabled_subscription: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
|
@ -161,12 +161,12 @@ impl ToolbarItemView for QuickActionBar {
|
|||
match active_pane_item {
|
||||
Some(active_item) => {
|
||||
self.active_item = Some(active_item.boxed_clone());
|
||||
self._inlay_hints_enabled_subscription.take();
|
||||
self.inlay_hints_enabled_subscription.take();
|
||||
|
||||
if let Some(editor) = active_item.downcast::<Editor>() {
|
||||
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
|
||||
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
|
||||
self._inlay_hints_enabled_subscription =
|
||||
self.inlay_hints_enabled_subscription =
|
||||
Some(cx.observe(&editor, move |_, editor, cx| {
|
||||
let editor = editor.read(cx);
|
||||
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
|
||||
|
|
|
@ -9,7 +9,7 @@ path = "src/quick_action_bar.rs"
|
|||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
#assistant = { path = "../assistant" }
|
||||
assistant = { package = "assistant2", path = "../assistant2" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// use assistant::{assistant_panel::InlineAssist, AssistantPanel};
|
||||
use assistant::{AssistantPanel, InlineAssist};
|
||||
use editor::Editor;
|
||||
|
||||
use gpui::{
|
||||
Action, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Stateful,
|
||||
Styled, Subscription, View, ViewContext, WeakView,
|
||||
Action, ClickEvent, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render,
|
||||
Stateful, Styled, Subscription, View, ViewContext, WeakView,
|
||||
};
|
||||
use search::BufferSearchBar;
|
||||
use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip};
|
||||
|
@ -15,7 +15,6 @@ pub struct QuickActionBar {
|
|||
buffer_search_bar: View<BufferSearchBar>,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
_inlay_hints_enabled_subscription: Option<Subscription>,
|
||||
#[allow(unused)]
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
|
@ -29,7 +28,6 @@ impl QuickActionBar {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn active_editor(&self) -> Option<View<Editor>> {
|
||||
self.active_item
|
||||
.as_ref()
|
||||
|
@ -41,111 +39,72 @@ impl Render for QuickActionBar {
|
|||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let search_button = QuickActionBarButton::new(
|
||||
let Some(editor) = self.active_editor() else {
|
||||
return div().id("empty quick action bar");
|
||||
};
|
||||
|
||||
let inlay_hints_button = Some(QuickActionBarButton::new(
|
||||
"toggle inlay hints",
|
||||
Icon::InlayHint,
|
||||
editor.read(cx).inlay_hints_enabled(),
|
||||
Box::new(editor::ToggleInlayHints),
|
||||
"Toggle Inlay Hints",
|
||||
{
|
||||
let editor = editor.clone();
|
||||
move |_, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
.filter(|_| editor.read(cx).supports_inlay_hints(cx));
|
||||
|
||||
let search_button = Some(QuickActionBarButton::new(
|
||||
"toggle buffer search",
|
||||
Icon::MagnifyingGlass,
|
||||
!self.buffer_search_bar.read(cx).is_dismissed(),
|
||||
Box::new(search::buffer_search::Deploy { focus: false }),
|
||||
"Buffer Search",
|
||||
);
|
||||
{
|
||||
let buffer_search_bar = self.buffer_search_bar.clone();
|
||||
move |_, cx| {
|
||||
buffer_search_bar.update(cx, |search_bar, cx| search_bar.toggle(cx));
|
||||
}
|
||||
},
|
||||
))
|
||||
.filter(|_| editor.is_singleton(cx));
|
||||
|
||||
let assistant_button = QuickActionBarButton::new(
|
||||
"toggle inline assitant",
|
||||
"toggle inline assistant",
|
||||
Icon::MagicWand,
|
||||
false,
|
||||
Box::new(gpui::NoAction),
|
||||
"Inline assistant",
|
||||
Box::new(InlineAssist),
|
||||
"Inline Assist",
|
||||
{
|
||||
let workspace = self.workspace.clone();
|
||||
move |_, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
AssistantPanel::inline_assist(workspace, &InlineAssist, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
h_stack()
|
||||
.id("quick action bar")
|
||||
.p_1()
|
||||
.gap_2()
|
||||
.child(search_button)
|
||||
.child(
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(assistant_button),
|
||||
)
|
||||
.children(inlay_hints_button)
|
||||
.children(search_button)
|
||||
.child(assistant_button)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
|
||||
|
||||
// impl View for QuickActionBar {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "QuickActionsBar"
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
|
||||
// let Some(editor) = self.active_editor() else {
|
||||
// return div();
|
||||
// };
|
||||
|
||||
// let mut bar = Flex::row();
|
||||
// if editor.read(cx).supports_inlay_hints(cx) {
|
||||
// bar = bar.with_child(render_quick_action_bar_button(
|
||||
// 0,
|
||||
// "icons/inlay_hint.svg",
|
||||
// editor.read(cx).inlay_hints_enabled(),
|
||||
// (
|
||||
// "Toggle Inlay Hints".to_string(),
|
||||
// Some(Box::new(editor::ToggleInlayHints)),
|
||||
// ),
|
||||
// cx,
|
||||
// |this, cx| {
|
||||
// if let Some(editor) = this.active_editor() {
|
||||
// editor.update(cx, |editor, cx| {
|
||||
// editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// ));
|
||||
// }
|
||||
|
||||
// if editor.read(cx).buffer().read(cx).is_singleton() {
|
||||
// let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
|
||||
// let search_action = buffer_search::Deploy { focus: true };
|
||||
|
||||
// bar = bar.with_child(render_quick_action_bar_button(
|
||||
// 1,
|
||||
// "icons/magnifying_glass.svg",
|
||||
// search_bar_shown,
|
||||
// (
|
||||
// "Buffer Search".to_string(),
|
||||
// Some(Box::new(search_action.clone())),
|
||||
// ),
|
||||
// cx,
|
||||
// move |this, cx| {
|
||||
// this.buffer_search_bar.update(cx, |buffer_search_bar, cx| {
|
||||
// if search_bar_shown {
|
||||
// buffer_search_bar.dismiss(&buffer_search::Dismiss, cx);
|
||||
// } else {
|
||||
// buffer_search_bar.deploy(&search_action, cx);
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
// ));
|
||||
// }
|
||||
|
||||
// bar.add_child(render_quick_action_bar_button(
|
||||
// 2,
|
||||
// "icons/magic-wand.svg",
|
||||
// false,
|
||||
// ("Inline Assist".into(), Some(Box::new(InlineAssist))),
|
||||
// cx,
|
||||
// move |this, cx| {
|
||||
// if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
// workspace.update(cx, |workspace, cx| {
|
||||
// AssistantPanel::inline_assist(workspace, &Default::default(), cx);
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// ));
|
||||
|
||||
// bar.into_any()
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct QuickActionBarButton {
|
||||
id: ElementId,
|
||||
|
@ -153,7 +112,7 @@ struct QuickActionBarButton {
|
|||
toggled: bool,
|
||||
action: Box<dyn Action>,
|
||||
tooltip: SharedString,
|
||||
tooltip_meta: Option<SharedString>,
|
||||
on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
|
||||
}
|
||||
|
||||
impl QuickActionBarButton {
|
||||
|
@ -163,6 +122,7 @@ impl QuickActionBarButton {
|
|||
toggled: bool,
|
||||
action: Box<dyn Action>,
|
||||
tooltip: impl Into<SharedString>,
|
||||
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
|
@ -170,15 +130,9 @@ impl QuickActionBarButton {
|
|||
toggled,
|
||||
action,
|
||||
tooltip: tooltip.into(),
|
||||
tooltip_meta: None,
|
||||
on_click: Box::new(on_click),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn meta(mut self, meta: Option<impl Into<SharedString>>) -> Self {
|
||||
self.tooltip_meta = meta.map(|meta| meta.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for QuickActionBarButton {
|
||||
|
@ -187,66 +141,17 @@ impl RenderOnce for QuickActionBarButton {
|
|||
fn render(self, _: &mut WindowContext) -> Self::Rendered {
|
||||
let tooltip = self.tooltip.clone();
|
||||
let action = self.action.boxed_clone();
|
||||
let tooltip_meta = self.tooltip_meta.clone();
|
||||
|
||||
IconButton::new(self.id.clone(), self.icon)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.selected(self.toggled)
|
||||
.tooltip(move |cx| {
|
||||
if let Some(meta) = &tooltip_meta {
|
||||
Tooltip::with_meta(tooltip.clone(), Some(&*action), meta.clone(), cx)
|
||||
} else {
|
||||
Tooltip::for_action(tooltip.clone(), &*action, cx)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let action = self.action.boxed_clone();
|
||||
move |_, cx| cx.dispatch_action(action.boxed_clone())
|
||||
})
|
||||
.tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
|
||||
.on_click(move |event, cx| (self.on_click)(event, cx))
|
||||
}
|
||||
}
|
||||
|
||||
// fn render_quick_action_bar_button<
|
||||
// F: 'static + Fn(&mut QuickActionBar, &mut ViewContext<QuickActionBar>),
|
||||
// >(
|
||||
// index: usize,
|
||||
// icon: &'static str,
|
||||
// toggled: bool,
|
||||
// tooltip: (String, Option<Box<dyn Action>>),
|
||||
// cx: &mut ViewContext<QuickActionBar>,
|
||||
// on_click: F,
|
||||
// ) -> AnyElement<QuickActionBar> {
|
||||
// enum QuickActionBarButton {}
|
||||
|
||||
// let theme = theme::current(cx);
|
||||
// let (tooltip_text, action) = tooltip;
|
||||
|
||||
// MouseEventHandler::new::<QuickActionBarButton, _>(index, cx, |mouse_state, _| {
|
||||
// let style = theme
|
||||
// .workspace
|
||||
// .toolbar
|
||||
// .toggleable_tool
|
||||
// .in_state(toggled)
|
||||
// .style_for(mouse_state);
|
||||
// Svg::new(icon)
|
||||
// .with_color(style.color)
|
||||
// .constrained()
|
||||
// .with_width(style.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(style.button_width)
|
||||
// .with_height(style.button_width)
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
|
||||
// .with_tooltip::<QuickActionBarButton>(index, tooltip_text, action, theme.tooltip.clone(), cx)
|
||||
// .into_any_named("quick action bar button")
|
||||
// }
|
||||
|
||||
impl ToolbarItemView for QuickActionBar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
|
|
31
crates/recent_projects2/Cargo.toml
Normal file
31
crates/recent_projects2/Cargo.toml
Normal file
|
@ -0,0 +1,31 @@
|
|||
[package]
|
||||
name = "recent_projects2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/recent_projects.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
db = { path = "../db" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
picker = { package = "picker2", path = "../picker2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
text = { package = "text2", path = "../text2" }
|
||||
util = { path = "../util"}
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
||||
futures.workspace = true
|
||||
ordered-float.workspace = true
|
||||
postage.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
131
crates/recent_projects2/src/highlighted_workspace_location.rs
Normal file
131
crates/recent_projects2/src/highlighted_workspace_location.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
use std::path::Path;
|
||||
|
||||
use fuzzy::StringMatch;
|
||||
use ui::{prelude::*, HighlightedLabel};
|
||||
use util::paths::PathExt;
|
||||
use workspace::WorkspaceLocation;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct HighlightedText {
|
||||
pub text: String,
|
||||
pub highlight_positions: Vec<usize>,
|
||||
char_count: usize,
|
||||
}
|
||||
|
||||
impl HighlightedText {
|
||||
fn join(components: impl Iterator<Item = Self>, separator: &str) -> Self {
|
||||
let mut char_count = 0;
|
||||
let separator_char_count = separator.chars().count();
|
||||
let mut text = String::new();
|
||||
let mut highlight_positions = Vec::new();
|
||||
for component in components {
|
||||
if char_count != 0 {
|
||||
text.push_str(separator);
|
||||
char_count += separator_char_count;
|
||||
}
|
||||
|
||||
highlight_positions.extend(
|
||||
component
|
||||
.highlight_positions
|
||||
.iter()
|
||||
.map(|position| position + char_count),
|
||||
);
|
||||
text.push_str(&component.text);
|
||||
char_count += component.text.chars().count();
|
||||
}
|
||||
|
||||
Self {
|
||||
text,
|
||||
highlight_positions,
|
||||
char_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for HighlightedText {
|
||||
type Rendered = HighlightedLabel;
|
||||
|
||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||
HighlightedLabel::new(self.text, self.highlight_positions)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HighlightedWorkspaceLocation {
|
||||
pub names: HighlightedText,
|
||||
pub paths: Vec<HighlightedText>,
|
||||
}
|
||||
|
||||
impl HighlightedWorkspaceLocation {
|
||||
pub fn new(string_match: &StringMatch, location: &WorkspaceLocation) -> Self {
|
||||
let mut path_start_offset = 0;
|
||||
let (names, paths): (Vec<_>, Vec<_>) = location
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|path| {
|
||||
let path = path.compact();
|
||||
let highlighted_text = Self::highlights_for_path(
|
||||
path.as_ref(),
|
||||
&string_match.positions,
|
||||
path_start_offset,
|
||||
);
|
||||
|
||||
path_start_offset += highlighted_text.1.char_count;
|
||||
|
||||
highlighted_text
|
||||
})
|
||||
.unzip();
|
||||
|
||||
Self {
|
||||
names: HighlightedText::join(names.into_iter().filter_map(|name| name), ", "),
|
||||
paths,
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the highlighted text for the name and path
|
||||
fn highlights_for_path(
|
||||
path: &Path,
|
||||
match_positions: &Vec<usize>,
|
||||
path_start_offset: usize,
|
||||
) -> (Option<HighlightedText>, HighlightedText) {
|
||||
let path_string = path.to_string_lossy();
|
||||
let path_char_count = path_string.chars().count();
|
||||
// Get the subset of match highlight positions that line up with the given path.
|
||||
// Also adjusts them to start at the path start
|
||||
let path_positions = match_positions
|
||||
.iter()
|
||||
.copied()
|
||||
.skip_while(|position| *position < path_start_offset)
|
||||
.take_while(|position| *position < path_start_offset + path_char_count)
|
||||
.map(|position| position - path_start_offset)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Again subset the highlight positions to just those that line up with the file_name
|
||||
// again adjusted to the start of the file_name
|
||||
let file_name_text_and_positions = path.file_name().map(|file_name| {
|
||||
let text = file_name.to_string_lossy();
|
||||
let char_count = text.chars().count();
|
||||
let file_name_start = path_char_count - char_count;
|
||||
let highlight_positions = path_positions
|
||||
.iter()
|
||||
.copied()
|
||||
.skip_while(|position| *position < file_name_start)
|
||||
.take_while(|position| *position < file_name_start + char_count)
|
||||
.map(|position| position - file_name_start)
|
||||
.collect::<Vec<_>>();
|
||||
HighlightedText {
|
||||
text: text.to_string(),
|
||||
highlight_positions,
|
||||
char_count,
|
||||
}
|
||||
});
|
||||
|
||||
(
|
||||
file_name_text_and_positions,
|
||||
HighlightedText {
|
||||
text: path_string.to_string(),
|
||||
highlight_positions: path_positions,
|
||||
char_count: path_char_count,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
233
crates/recent_projects2/src/recent_projects.rs
Normal file
233
crates/recent_projects2/src/recent_projects.rs
Normal file
|
@ -0,0 +1,233 @@
|
|||
mod highlighted_workspace_location;
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Task,
|
||||
View, ViewContext, WeakView,
|
||||
};
|
||||
use highlighted_workspace_location::HighlightedWorkspaceLocation;
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, ListItem};
|
||||
use util::paths::PathExt;
|
||||
use workspace::{
|
||||
notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
|
||||
WORKSPACE_DB,
|
||||
};
|
||||
|
||||
actions!(OpenRecent);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(RecentProjects::register).detach();
|
||||
}
|
||||
|
||||
pub struct RecentProjects {
|
||||
picker: View<Picker<RecentProjectsDelegate>>,
|
||||
}
|
||||
|
||||
impl RecentProjects {
|
||||
fn new(delegate: RecentProjectsDelegate, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
picker: cx.build_view(|cx| Picker::new(delegate, cx)),
|
||||
}
|
||||
}
|
||||
|
||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|workspace, _: &OpenRecent, cx| {
|
||||
let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
|
||||
if let Some(handler) = Self::open(workspace, cx) {
|
||||
handler.detach_and_log_err(cx);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
recent_projects.update(cx, |recent_projects, cx| {
|
||||
recent_projects
|
||||
.picker
|
||||
.update(cx, |picker, cx| picker.cycle_selection(cx))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn open(_: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> {
|
||||
Some(cx.spawn(|workspace, mut cx| async move {
|
||||
let workspace_locations: Vec<_> = cx
|
||||
.background_executor()
|
||||
.spawn(async {
|
||||
WORKSPACE_DB
|
||||
.recent_workspaces_on_disk()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(_, location)| location)
|
||||
.collect()
|
||||
})
|
||||
.await;
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
if !workspace_locations.is_empty() {
|
||||
let weak_workspace = cx.view().downgrade();
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
let delegate =
|
||||
RecentProjectsDelegate::new(weak_workspace, workspace_locations, true);
|
||||
|
||||
RecentProjects::new(delegate, cx)
|
||||
});
|
||||
} else {
|
||||
workspace.show_notification(0, cx, |cx| {
|
||||
cx.build_view(|_| MessageNotification::new("No recent projects to open."))
|
||||
})
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for RecentProjects {}
|
||||
|
||||
impl FocusableView for RecentProjects {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RecentProjects {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
v_stack().w_96().child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RecentProjectsDelegate {
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace_locations: Vec<WorkspaceLocation>,
|
||||
selected_match_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
render_paths: bool,
|
||||
}
|
||||
|
||||
impl RecentProjectsDelegate {
|
||||
fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace_locations: Vec<WorkspaceLocation>,
|
||||
render_paths: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
workspace_locations,
|
||||
selected_match_index: 0,
|
||||
matches: Default::default(),
|
||||
render_paths,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for RecentProjectsDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Recent Projects...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_match_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_match_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let query = query.trim_start();
|
||||
let smart_case = query.chars().any(|c| c.is_uppercase());
|
||||
let candidates = self
|
||||
.workspace_locations
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, location)| {
|
||||
let combined_string = location
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|path| path.compact().to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
StringMatchCandidate::new(id, combined_string)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.matches = smol::block_on(fuzzy::match_strings(
|
||||
candidates.as_slice(),
|
||||
query,
|
||||
smart_case,
|
||||
100,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
self.matches.sort_unstable_by_key(|m| m.candidate_id);
|
||||
|
||||
self.selected_match_index = self
|
||||
.matches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.max_by_key(|(_, m)| OrderedFloat(m.score))
|
||||
.map(|(ix, _)| ix)
|
||||
.unwrap_or(0);
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some((selected_match, workspace)) = self
|
||||
.matches
|
||||
.get(self.selected_index())
|
||||
.zip(self.workspace.upgrade())
|
||||
{
|
||||
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
self.dismissed(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let Some(r#match) = self.matches.get(ix) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let highlighted_location = HighlightedWorkspaceLocation::new(
|
||||
&r#match,
|
||||
&self.workspace_locations[r#match.candidate_id],
|
||||
);
|
||||
|
||||
Some(
|
||||
ListItem::new(ix).inset(true).selected(selected).child(
|
||||
v_stack()
|
||||
.child(highlighted_location.names)
|
||||
.when(self.render_paths, |this| {
|
||||
this.children(highlighted_location.paths)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -10,15 +10,15 @@ use collections::HashMap;
|
|||
use editor::{Editor, EditorMode};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _,
|
||||
WeakView, WindowContext,
|
||||
actions, div, red, Action, AppContext, Div, EventEmitter, FocusableView,
|
||||
InteractiveElement as _, IntoElement, ParentElement as _, Render, Styled, Subscription, Task,
|
||||
View, ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
use project::search::SearchQuery;
|
||||
use serde::Deserialize;
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use ui::{h_stack, Icon, IconButton, IconElement};
|
||||
use ui::{h_stack, Clickable, Icon, IconButton, IconElement};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::ItemHandle,
|
||||
|
@ -161,16 +161,6 @@ impl Render for BufferSearchBar {
|
|||
|
||||
Some(ui::Label::new(message))
|
||||
});
|
||||
let nav_button_for_direction = |icon, direction| {
|
||||
render_nav_button(
|
||||
icon,
|
||||
self.active_match_index.is_some(),
|
||||
cx.listener(move |this, _, cx| match direction {
|
||||
Direction::Prev => this.select_prev_match(&Default::default(), cx),
|
||||
Direction::Next => this.select_next_match(&Default::default(), cx),
|
||||
}),
|
||||
)
|
||||
};
|
||||
let should_show_replace_input = self.replace_enabled && supported_options.replacement;
|
||||
let replace_all = should_show_replace_input
|
||||
.then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll));
|
||||
|
@ -237,20 +227,32 @@ impl Render for BufferSearchBar {
|
|||
h_stack()
|
||||
.gap_0p5()
|
||||
.flex_none()
|
||||
.child(self.render_action_button())
|
||||
.child(self.render_action_button(cx))
|
||||
.children(match_count)
|
||||
.child(nav_button_for_direction(
|
||||
.child(render_nav_button(
|
||||
ui::Icon::ChevronLeft,
|
||||
Direction::Prev,
|
||||
self.active_match_index.is_some(),
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.select_prev_match(&Default::default(), cx);
|
||||
}),
|
||||
))
|
||||
.child(nav_button_for_direction(
|
||||
.child(render_nav_button(
|
||||
ui::Icon::ChevronRight,
|
||||
Direction::Next,
|
||||
self.active_match_index.is_some(),
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.select_next_match(&Default::default(), cx);
|
||||
}),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for BufferSearchBar {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.query_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for BufferSearchBar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
|
@ -311,13 +313,7 @@ impl BufferSearchBar {
|
|||
pane.update(cx, |this, cx| {
|
||||
this.toolbar().update(cx, |this, cx| {
|
||||
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |this, cx| {
|
||||
if this.is_dismissed() {
|
||||
this.show(cx);
|
||||
} else {
|
||||
this.dismiss(&Dismiss, cx);
|
||||
}
|
||||
});
|
||||
search_bar.update(cx, |this, cx| this.toggle(cx));
|
||||
return;
|
||||
}
|
||||
let view = cx.build_view(|cx| BufferSearchBar::new(cx));
|
||||
|
@ -481,6 +477,14 @@ impl BufferSearchBar {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn toggle(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.is_dismissed() {
|
||||
self.show(cx);
|
||||
} else {
|
||||
self.dismiss(&Dismiss, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.active_searchable_item.is_none() {
|
||||
return false;
|
||||
|
@ -582,12 +586,14 @@ impl BufferSearchBar {
|
|||
self.update_matches(cx)
|
||||
}
|
||||
|
||||
fn render_action_button(&self) -> impl IntoElement {
|
||||
fn render_action_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// let tooltip_style = theme.tooltip.clone();
|
||||
|
||||
// let style = theme.search.action_button.clone();
|
||||
|
||||
IconButton::new(0, ui::Icon::SelectAll).action(Box::new(SelectAllMatches))
|
||||
IconButton::new("select-all", ui::Icon::SelectAll).on_click(cx.listener(|this, _, cx| {
|
||||
this.select_all_matches(&SelectAllMatches, cx);
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
|
||||
|
|
|
@ -124,6 +124,17 @@ pub fn update_settings_file<T: Settings>(
|
|||
|
||||
pub fn load_default_keymap(cx: &mut AppContext) {
|
||||
for path in ["keymaps/default.json", "keymaps/vim.json"] {
|
||||
// TODO: Remove this conditional when we're ready to add Vim support.
|
||||
// Right now we're avoiding loading the Vim keymap to silence the warnings
|
||||
// about invalid action bindings.
|
||||
if path.contains("vim") {
|
||||
let _: Option<()> = Err(format!(
|
||||
"TODO: Skipping {path} until we're ready to add Vim support"
|
||||
))
|
||||
.log_err();
|
||||
continue;
|
||||
}
|
||||
|
||||
KeymapFile::load_asset(path, cx).unwrap();
|
||||
}
|
||||
|
||||
|
@ -132,39 +143,3 @@ pub fn load_default_keymap(cx: &mut AppContext) {
|
|||
// KeymapFile::load_asset(asset_path, cx).unwrap();
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn handle_keymap_file_changes(
|
||||
mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
cx.spawn(move |cx| async move {
|
||||
// let mut settings_subscription = None;
|
||||
while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
|
||||
if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() {
|
||||
cx.update(|cx| reload_keymaps(cx, &keymap_content)).ok();
|
||||
|
||||
// todo!()
|
||||
// let mut old_base_keymap = cx.read(|cx| *settings::get::<BaseKeymap>(cx));
|
||||
// drop(settings_subscription);
|
||||
// settings_subscription = Some(cx.update(|cx| {
|
||||
// cx.observe_global::<SettingsStore, _>(move |cx| {
|
||||
// let new_base_keymap = *settings::get::<BaseKeymap>(cx);
|
||||
// if new_base_keymap != old_base_keymap {
|
||||
// old_base_keymap = new_base_keymap.clone();
|
||||
// reload_keymaps(cx, &keymap_content);
|
||||
// }
|
||||
// })
|
||||
// }));
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
|
||||
// todo!()
|
||||
// cx.clear_bindings();
|
||||
load_default_keymap(cx);
|
||||
keymap_content.clone().add_to_cx(cx).log_err();
|
||||
// cx.set_menus(menus::menus());
|
||||
}
|
||||
|
|
|
@ -1,118 +1,5 @@
|
|||
// todo!()
|
||||
use alacritty_terminal::term::color::Rgb as AlacRgb;
|
||||
// use gpui::color::Color;
|
||||
// use theme2::TerminalStyle;
|
||||
|
||||
///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
|
||||
// pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
|
||||
// match alac_color {
|
||||
// //Named and theme defined colors
|
||||
// alacritty_terminal::ansi::Color::Named(n) => match n {
|
||||
// alacritty_terminal::ansi::NamedColor::Black => style.black,
|
||||
// alacritty_terminal::ansi::NamedColor::Red => style.red,
|
||||
// alacritty_terminal::ansi::NamedColor::Green => style.green,
|
||||
// alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
|
||||
// alacritty_terminal::ansi::NamedColor::Blue => style.blue,
|
||||
// alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
|
||||
// alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
|
||||
// alacritty_terminal::ansi::NamedColor::White => style.white,
|
||||
// alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
|
||||
// alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
|
||||
// alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
|
||||
// alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
|
||||
// alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
|
||||
// alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
|
||||
// alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
|
||||
// alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
|
||||
// alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
|
||||
// alacritty_terminal::ansi::NamedColor::Background => style.background,
|
||||
// alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
|
||||
// alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
|
||||
// alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
|
||||
// alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
|
||||
// alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
|
||||
// alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
|
||||
// alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
|
||||
// alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
|
||||
// alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
|
||||
// alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
|
||||
// alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
|
||||
// },
|
||||
// //'True' colors
|
||||
// alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
|
||||
// //8 bit, indexed colors
|
||||
// alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), style),
|
||||
// }
|
||||
// }
|
||||
|
||||
/// TODO: Move this
|
||||
///Converts an 8 bit ANSI color to it's GPUI equivalent.
|
||||
///Accepts usize for compatibility with the alacritty::Colors interface,
|
||||
///Other than that use case, should only be called with values in the [0,255] range
|
||||
// pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color {
|
||||
// match index {
|
||||
// //0-15 are the same as the named colors above
|
||||
// 0 => style.black,
|
||||
// 1 => style.red,
|
||||
// 2 => style.green,
|
||||
// 3 => style.yellow,
|
||||
// 4 => style.blue,
|
||||
// 5 => style.magenta,
|
||||
// 6 => style.cyan,
|
||||
// 7 => style.white,
|
||||
// 8 => style.bright_black,
|
||||
// 9 => style.bright_red,
|
||||
// 10 => style.bright_green,
|
||||
// 11 => style.bright_yellow,
|
||||
// 12 => style.bright_blue,
|
||||
// 13 => style.bright_magenta,
|
||||
// 14 => style.bright_cyan,
|
||||
// 15 => style.bright_white,
|
||||
// //16-231 are mapped to their RGB colors on a 0-5 range per channel
|
||||
// 16..=231 => {
|
||||
// let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components
|
||||
// let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow
|
||||
// Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color
|
||||
// }
|
||||
// //232-255 are a 24 step grayscale from black to white
|
||||
// 232..=255 => {
|
||||
// let i = *index as u8 - 232; //Align index to 0..24
|
||||
// let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
|
||||
// Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
|
||||
// }
|
||||
// //For compatibility with the alacritty::Colors interface
|
||||
// 256 => style.foreground,
|
||||
// 257 => style.background,
|
||||
// 258 => style.cursor,
|
||||
// 259 => style.dim_black,
|
||||
// 260 => style.dim_red,
|
||||
// 261 => style.dim_green,
|
||||
// 262 => style.dim_yellow,
|
||||
// 263 => style.dim_blue,
|
||||
// 264 => style.dim_magenta,
|
||||
// 265 => style.dim_cyan,
|
||||
// 266 => style.dim_white,
|
||||
// 267 => style.bright_foreground,
|
||||
// 268 => style.black, //'Dim Background', non-standard color
|
||||
// _ => Color::new(0, 0, 0, 255),
|
||||
// }
|
||||
// }
|
||||
///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
|
||||
///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
|
||||
///
|
||||
///Wikipedia gives a formula for calculating the index for a given color:
|
||||
///
|
||||
///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
|
||||
///
|
||||
///This function does the reverse, calculating the r, g, and b components from a given index.
|
||||
// fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
|
||||
// debug_assert!((&16..=&231).contains(&i));
|
||||
// let i = i - 16;
|
||||
// let r = (i - (i % 36)) / 36;
|
||||
// let g = ((i % 36) - (i % 6)) / 6;
|
||||
// let b = (i % 36) % 6;
|
||||
// (r, g, b)
|
||||
// }
|
||||
use gpui::Rgba;
|
||||
|
||||
//Convenience method to convert from a GPUI color to an alacritty Rgb
|
||||
|
@ -123,15 +10,3 @@ pub fn to_alac_rgb(color: impl Into<Rgba>) -> AlacRgb {
|
|||
let b = ((color.b * color.a) * 255.) as u8;
|
||||
AlacRgb::new(r, g, b)
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// #[test]
|
||||
// fn test_rgb_for_index() {
|
||||
// //Test every possible value in the color cube
|
||||
// for i in 16..=231 {
|
||||
// let (r, g, b) = crate::mappings::colors::rgb_for_index(&(i as u8));
|
||||
// assert_eq!(i, 16 + 36 * r + 6 * g + b);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -186,9 +186,9 @@ pub fn mouse_side(
|
|||
}
|
||||
|
||||
pub fn grid_point(pos: Point<Pixels>, cur_size: TerminalSize, display_offset: usize) -> AlacPoint {
|
||||
let col = GridCol((cur_size.cell_width / pos.x) as usize);
|
||||
let col = GridCol((pos.x / cur_size.cell_width) as usize);
|
||||
let col = min(col, cur_size.last_column());
|
||||
let line = (cur_size.line_height / pos.y) as i32;
|
||||
let line = (pos.y / cur_size.line_height) as i32;
|
||||
let line = min(line, cur_size.bottommost_line().0);
|
||||
AlacPoint::new(GridLine(line - display_offset as i32), col)
|
||||
}
|
||||
|
|
|
@ -1103,7 +1103,12 @@ impl Terminal {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn mouse_drag(&mut self, e: MouseMoveEvent, origin: Point<Pixels>, region: Bounds<Pixels>) {
|
||||
pub fn mouse_drag(
|
||||
&mut self,
|
||||
e: &MouseMoveEvent,
|
||||
origin: Point<Pixels>,
|
||||
region: Bounds<Pixels>,
|
||||
) {
|
||||
let position = e.position - origin;
|
||||
self.last_mouse_position = Some(position);
|
||||
|
||||
|
@ -1129,7 +1134,7 @@ impl Terminal {
|
|||
}
|
||||
}
|
||||
|
||||
fn drag_line_delta(&mut self, e: MouseMoveEvent, region: Bounds<Pixels>) -> Option<Pixels> {
|
||||
fn drag_line_delta(&mut self, e: &MouseMoveEvent, region: Bounds<Pixels>) -> Option<Pixels> {
|
||||
//TODO: Why do these need to be doubled? Probably the same problem that the IME has
|
||||
let top = region.origin.y + (self.last_content.size.line_height * 2.);
|
||||
let bottom = region.lower_left().y - (self.last_content.size.line_height * 2.);
|
||||
|
@ -1229,7 +1234,7 @@ impl Terminal {
|
|||
}
|
||||
|
||||
///Scroll the terminal
|
||||
pub fn scroll_wheel(&mut self, e: ScrollWheelEvent, origin: Point<Pixels>) {
|
||||
pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Point<Pixels>) {
|
||||
let mouse_mode = self.mouse_mode(e.shift);
|
||||
|
||||
if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use gpui::{AppContext, FontFeatures, Pixels};
|
||||
use gpui::{px, AbsoluteLength, AppContext, FontFeatures, Pixels};
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
@ -114,12 +114,13 @@ pub enum TerminalLineHeight {
|
|||
}
|
||||
|
||||
impl TerminalLineHeight {
|
||||
pub fn value(&self) -> f32 {
|
||||
match self {
|
||||
pub fn value(&self) -> AbsoluteLength {
|
||||
let value = match self {
|
||||
TerminalLineHeight::Comfortable => 1.618,
|
||||
TerminalLineHeight::Standard => 1.3,
|
||||
TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
|
||||
}
|
||||
};
|
||||
px(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ workspace = { package = "workspace2", path = "../workspace2" }
|
|||
db = { package = "db2", path = "../db2" }
|
||||
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
|
||||
terminal = { package = "terminal2", path = "../terminal2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
mio-extras = "2.0.6"
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,8 +4,8 @@ use crate::TerminalView;
|
|||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
actions, div, serde_json, AppContext, AsyncWindowContext, Div, Entity, EventEmitter,
|
||||
FocusHandle, FocusableView, ParentElement, Render, Subscription, Task, View, ViewContext,
|
||||
VisualContext, WeakView, WindowContext,
|
||||
FocusHandle, FocusableView, ParentElement, Render, Styled, Subscription, Task, View,
|
||||
ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use project::Fs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -339,7 +339,7 @@ impl Render for TerminalPanel {
|
|||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().child(self.pane.clone())
|
||||
div().size_full().child(self.pane.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -415,10 +415,6 @@ impl Panel for TerminalPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool {
|
||||
self.pane.read(cx).has_focus(cx)
|
||||
}
|
||||
|
||||
fn persistent_name() -> &'static str {
|
||||
"TerminalPanel"
|
||||
}
|
||||
|
|
|
@ -9,10 +9,9 @@ pub mod terminal_panel;
|
|||
// use crate::terminal_element::TerminalElement;
|
||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, div, Action, AnyElement, AppContext, Div, Element, EventEmitter, FocusEvent,
|
||||
FocusHandle, Focusable, FocusableElement, FocusableView, InputHandler, InteractiveElement,
|
||||
KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Render,
|
||||
SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
actions, div, Action, AnyElement, AppContext, Div, EventEmitter, FocusEvent, FocusHandle,
|
||||
Focusable, FocusableElement, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model,
|
||||
MouseButton, MouseDownEvent, Pixels, Render, Subscription, Task, View, VisualContext, WeakView,
|
||||
};
|
||||
use language::Bias;
|
||||
use persistence::TERMINAL_DB;
|
||||
|
@ -25,13 +24,14 @@ use terminal::{
|
|||
terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
|
||||
Event, MaybeNavigationTarget, Terminal,
|
||||
};
|
||||
use terminal_element::TerminalElement;
|
||||
use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label};
|
||||
use util::{paths::PathLikeWithPosition, ResultExt};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent},
|
||||
notifications::NotifyResultExt,
|
||||
register_deserializable_item,
|
||||
searchable::{SearchEvent, SearchOptions, SearchableItem},
|
||||
ui::{ContextMenu, Icon, IconElement, Label},
|
||||
CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
|
@ -90,6 +90,7 @@ pub struct TerminalView {
|
|||
blink_epoch: usize,
|
||||
can_navigate_to_selected_word: bool,
|
||||
workspace_id: WorkspaceId,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for TerminalView {}
|
||||
|
@ -261,6 +262,20 @@ impl TerminalView {
|
|||
})
|
||||
.detach();
|
||||
|
||||
let focus = cx.focus_handle();
|
||||
// let focus_in = cx.on_focus_in(&focus, |this, cx| {
|
||||
// this.has_new_content = false;
|
||||
// this.terminal.read(cx).focus_in();
|
||||
// this.blink_cursors(this.blink_epoch, cx);
|
||||
// cx.notify();
|
||||
// });
|
||||
// let focus_out = cx.on_focus_out(&focus, |this, cx| {
|
||||
// this.terminal.update(cx, |terminal, _| {
|
||||
// terminal.focus_out();
|
||||
// });
|
||||
// cx.notify();
|
||||
// });
|
||||
|
||||
Self {
|
||||
terminal,
|
||||
has_new_content: true,
|
||||
|
@ -273,6 +288,7 @@ impl TerminalView {
|
|||
blink_epoch: 0,
|
||||
can_navigate_to_selected_word: false,
|
||||
workspace_id,
|
||||
_subscriptions: vec![/*focus_in, focus_out*/],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -302,8 +318,7 @@ impl TerminalView {
|
|||
menu.action("Clear", Box::new(Clear))
|
||||
.action("Close", Box::new(CloseActiveItem { save_intent: None }))
|
||||
}));
|
||||
dbg!(&position);
|
||||
// todo!()
|
||||
// todo!(context menus)
|
||||
// self.context_menu
|
||||
// .show(position, AnchorCorner::TopLeft, menu_entries, cx);
|
||||
// cx.notify();
|
||||
|
@ -448,6 +463,81 @@ impl TerminalView {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_context(&self, cx: &AppContext) -> KeyContext {
|
||||
let mut dispatch_context = KeyContext::default();
|
||||
dispatch_context.add("Terminal");
|
||||
|
||||
let mode = self.terminal.read(cx).last_content.mode;
|
||||
dispatch_context.set(
|
||||
"screen",
|
||||
if mode.contains(TermMode::ALT_SCREEN) {
|
||||
"alt"
|
||||
} else {
|
||||
"normal"
|
||||
},
|
||||
);
|
||||
|
||||
if mode.contains(TermMode::APP_CURSOR) {
|
||||
dispatch_context.add("DECCKM");
|
||||
}
|
||||
if mode.contains(TermMode::APP_KEYPAD) {
|
||||
dispatch_context.add("DECPAM");
|
||||
} else {
|
||||
dispatch_context.add("DECPNM");
|
||||
}
|
||||
if mode.contains(TermMode::SHOW_CURSOR) {
|
||||
dispatch_context.add("DECTCEM");
|
||||
}
|
||||
if mode.contains(TermMode::LINE_WRAP) {
|
||||
dispatch_context.add("DECAWM");
|
||||
}
|
||||
if mode.contains(TermMode::ORIGIN) {
|
||||
dispatch_context.add("DECOM");
|
||||
}
|
||||
if mode.contains(TermMode::INSERT) {
|
||||
dispatch_context.add("IRM");
|
||||
}
|
||||
//LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
|
||||
if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
|
||||
dispatch_context.add("LNM");
|
||||
}
|
||||
if mode.contains(TermMode::FOCUS_IN_OUT) {
|
||||
dispatch_context.add("report_focus");
|
||||
}
|
||||
if mode.contains(TermMode::ALTERNATE_SCROLL) {
|
||||
dispatch_context.add("alternate_scroll");
|
||||
}
|
||||
if mode.contains(TermMode::BRACKETED_PASTE) {
|
||||
dispatch_context.add("bracketed_paste");
|
||||
}
|
||||
if mode.intersects(TermMode::MOUSE_MODE) {
|
||||
dispatch_context.add("any_mouse_reporting");
|
||||
}
|
||||
{
|
||||
let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
|
||||
"click"
|
||||
} else if mode.contains(TermMode::MOUSE_DRAG) {
|
||||
"drag"
|
||||
} else if mode.contains(TermMode::MOUSE_MOTION) {
|
||||
"motion"
|
||||
} else {
|
||||
"off"
|
||||
};
|
||||
dispatch_context.set("mouse_reporting", mouse_reporting);
|
||||
}
|
||||
{
|
||||
let format = if mode.contains(TermMode::SGR_MOUSE) {
|
||||
"sgr"
|
||||
} else if mode.contains(TermMode::UTF8_MOUSE) {
|
||||
"utf8"
|
||||
} else {
|
||||
"normal"
|
||||
};
|
||||
dispatch_context.set("mouse_format", format);
|
||||
};
|
||||
dispatch_context
|
||||
}
|
||||
}
|
||||
|
||||
fn possible_open_targets(
|
||||
|
@ -532,17 +622,20 @@ impl Render for TerminalView {
|
|||
type Element = Focusable<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let terminal_handle = self.terminal.clone().downgrade();
|
||||
let terminal_handle = self.terminal.clone();
|
||||
let this_view = cx.view().clone();
|
||||
|
||||
let self_id = cx.entity_id();
|
||||
let focused = self.focus_handle.is_focused(cx);
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.z_index(0)
|
||||
.absolute()
|
||||
.size_full()
|
||||
.on_key_down(cx.listener(Self::key_down))
|
||||
.on_action(cx.listener(TerminalView::send_text))
|
||||
.on_action(cx.listener(TerminalView::send_keystroke))
|
||||
|
@ -551,15 +644,14 @@ impl Render for TerminalView {
|
|||
.on_action(cx.listener(TerminalView::clear))
|
||||
.on_action(cx.listener(TerminalView::show_character_palette))
|
||||
.on_action(cx.listener(TerminalView::select_all))
|
||||
// todo!()
|
||||
.child(
|
||||
"TERMINAL HERE", // TerminalElement::new(
|
||||
// terminal_handle,
|
||||
// focused,
|
||||
// self.should_show_cursor(focused, cx),
|
||||
// self.can_navigate_to_selected_word,
|
||||
// )
|
||||
)
|
||||
.child(TerminalElement::new(
|
||||
terminal_handle,
|
||||
this_view,
|
||||
self.focus_handle.clone(),
|
||||
focused,
|
||||
self.should_show_cursor(focused, cx),
|
||||
self.can_navigate_to_selected_word,
|
||||
))
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
cx.listener(|this, event: &MouseDownEvent, cx| {
|
||||
|
@ -579,162 +671,6 @@ impl Render for TerminalView {
|
|||
}
|
||||
}
|
||||
|
||||
// impl View for TerminalView {
|
||||
//todo!()
|
||||
// fn modifiers_changed(
|
||||
// &mut self,
|
||||
// event: &ModifiersChangedEvent,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> bool {
|
||||
// let handled = self
|
||||
// .terminal()
|
||||
// .update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
|
||||
// if handled {
|
||||
// cx.notify();
|
||||
// }
|
||||
// handled
|
||||
// }
|
||||
// }
|
||||
|
||||
// todo!()
|
||||
// fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &gpui::AppContext) {
|
||||
// Self::reset_to_default_keymap_context(keymap);
|
||||
|
||||
// let mode = self.terminal.read(cx).last_content.mode;
|
||||
// keymap.add_key(
|
||||
// "screen",
|
||||
// if mode.contains(TermMode::ALT_SCREEN) {
|
||||
// "alt"
|
||||
// } else {
|
||||
// "normal"
|
||||
// },
|
||||
// );
|
||||
|
||||
// if mode.contains(TermMode::APP_CURSOR) {
|
||||
// keymap.add_identifier("DECCKM");
|
||||
// }
|
||||
// if mode.contains(TermMode::APP_KEYPAD) {
|
||||
// keymap.add_identifier("DECPAM");
|
||||
// } else {
|
||||
// keymap.add_identifier("DECPNM");
|
||||
// }
|
||||
// if mode.contains(TermMode::SHOW_CURSOR) {
|
||||
// keymap.add_identifier("DECTCEM");
|
||||
// }
|
||||
// if mode.contains(TermMode::LINE_WRAP) {
|
||||
// keymap.add_identifier("DECAWM");
|
||||
// }
|
||||
// if mode.contains(TermMode::ORIGIN) {
|
||||
// keymap.add_identifier("DECOM");
|
||||
// }
|
||||
// if mode.contains(TermMode::INSERT) {
|
||||
// keymap.add_identifier("IRM");
|
||||
// }
|
||||
// //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
|
||||
// if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
|
||||
// keymap.add_identifier("LNM");
|
||||
// }
|
||||
// if mode.contains(TermMode::FOCUS_IN_OUT) {
|
||||
// keymap.add_identifier("report_focus");
|
||||
// }
|
||||
// if mode.contains(TermMode::ALTERNATE_SCROLL) {
|
||||
// keymap.add_identifier("alternate_scroll");
|
||||
// }
|
||||
// if mode.contains(TermMode::BRACKETED_PASTE) {
|
||||
// keymap.add_identifier("bracketed_paste");
|
||||
// }
|
||||
// if mode.intersects(TermMode::MOUSE_MODE) {
|
||||
// keymap.add_identifier("any_mouse_reporting");
|
||||
// }
|
||||
// {
|
||||
// let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
|
||||
// "click"
|
||||
// } else if mode.contains(TermMode::MOUSE_DRAG) {
|
||||
// "drag"
|
||||
// } else if mode.contains(TermMode::MOUSE_MOTION) {
|
||||
// "motion"
|
||||
// } else {
|
||||
// "off"
|
||||
// };
|
||||
// keymap.add_key("mouse_reporting", mouse_reporting);
|
||||
// }
|
||||
// {
|
||||
// let format = if mode.contains(TermMode::SGR_MOUSE) {
|
||||
// "sgr"
|
||||
// } else if mode.contains(TermMode::UTF8_MOUSE) {
|
||||
// "utf8"
|
||||
// } else {
|
||||
// "normal"
|
||||
// };
|
||||
// keymap.add_key("mouse_format", format);
|
||||
// }
|
||||
// }
|
||||
|
||||
impl InputHandler for TerminalView {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range: std::ops::Range<usize>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn selected_text_range(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<std::ops::Range<usize>> {
|
||||
if self
|
||||
.terminal
|
||||
.read(cx)
|
||||
.last_content
|
||||
.mode
|
||||
.contains(TermMode::ALT_SCREEN)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(0..0)
|
||||
}
|
||||
}
|
||||
|
||||
fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<std::ops::Range<usize>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
_: Option<std::ops::Range<usize>>,
|
||||
text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.terminal.update(cx, |terminal, _| {
|
||||
terminal.input(text.into());
|
||||
});
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range: Option<std::ops::Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range: Option<std::ops::Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
range_utf16: std::ops::Range<usize>,
|
||||
element_bounds: gpui::Bounds<Pixels>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<gpui::Bounds<Pixels>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for TerminalView {
|
||||
type Event = ItemEvent;
|
||||
|
||||
|
@ -745,7 +681,8 @@ impl Item for TerminalView {
|
|||
fn tab_content(&self, _detail: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let title = self.terminal().read(cx).title();
|
||||
|
||||
div()
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(IconElement::new(Icon::Terminal))
|
||||
.child(Label::new(title))
|
||||
.into_any()
|
||||
|
@ -778,7 +715,7 @@ impl Item for TerminalView {
|
|||
false
|
||||
}
|
||||
|
||||
// todo!()
|
||||
// todo!(search)
|
||||
// fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
// Some(Box::new(handle.clone()))
|
||||
// }
|
||||
|
@ -808,22 +745,23 @@ impl Item for TerminalView {
|
|||
let window = cx.window_handle();
|
||||
cx.spawn(|pane, mut cx| async move {
|
||||
let cwd = None;
|
||||
// todo!()
|
||||
// TERMINAL_DB
|
||||
// .get_working_directory(item_id, workspace_id)
|
||||
// .log_err()
|
||||
// .flatten()
|
||||
// .or_else(|| {
|
||||
// cx.read(|cx| {
|
||||
// let strategy = TerminalSettings::get_global(cx).working_directory.clone();
|
||||
// workspace
|
||||
// .upgrade()
|
||||
// .map(|workspace| {
|
||||
// get_working_directory(workspace.read(cx), cx, strategy)
|
||||
// })
|
||||
// .flatten()
|
||||
// })
|
||||
// });
|
||||
TERMINAL_DB
|
||||
.get_working_directory(item_id, workspace_id)
|
||||
.log_err()
|
||||
.flatten()
|
||||
.or_else(|| {
|
||||
cx.update(|_, cx| {
|
||||
let strategy = TerminalSettings::get_global(cx).working_directory.clone();
|
||||
workspace
|
||||
.upgrade()
|
||||
.map(|workspace| {
|
||||
get_working_directory(workspace.read(cx), cx, strategy)
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
|
||||
let terminal = project.update(&mut cx, |project, cx| {
|
||||
project.create_terminal(cwd, window, cx)
|
||||
|
@ -835,14 +773,13 @@ impl Item for TerminalView {
|
|||
}
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
// todo!()
|
||||
// cx.background()
|
||||
// .spawn(TERMINAL_DB.update_workspace_id(
|
||||
// workspace.database_id(),
|
||||
// self.workspace_id,
|
||||
// cx.view_id(),
|
||||
// ))
|
||||
// .detach();
|
||||
cx.background_executor()
|
||||
.spawn(TERMINAL_DB.update_workspace_id(
|
||||
workspace.database_id(),
|
||||
self.workspace_id,
|
||||
cx.entity_id().as_u64(),
|
||||
))
|
||||
.detach();
|
||||
self.workspace_id = workspace.database_id();
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,8 @@ impl ThemeColors {
|
|||
tab_bar_background: neutral().light().step_2(),
|
||||
tab_active_background: neutral().light().step_1(),
|
||||
tab_inactive_background: neutral().light().step_2(),
|
||||
search_match_background: neutral().light().step_2(), // todo!(this was inserted by Mikayla)
|
||||
|
||||
editor_background: neutral().light().step_1(),
|
||||
editor_gutter_background: neutral().light().step_1(), // todo!("pick the right colors")
|
||||
editor_subheader_background: neutral().light().step_2(),
|
||||
|
@ -121,6 +123,8 @@ impl ThemeColors {
|
|||
tab_bar_background: neutral().dark().step_2(),
|
||||
tab_active_background: neutral().dark().step_1(),
|
||||
tab_inactive_background: neutral().dark().step_2(),
|
||||
search_match_background: neutral().dark().step_2(), // todo!(this was inserted by Mikayla)
|
||||
|
||||
editor_background: neutral().dark().step_1(),
|
||||
editor_gutter_background: neutral().dark().step_1(),
|
||||
editor_subheader_background: neutral().dark().step_3(),
|
||||
|
|
|
@ -75,6 +75,8 @@ pub(crate) fn one_dark() -> Theme {
|
|||
tab_bar_background: bg,
|
||||
tab_inactive_background: bg,
|
||||
tab_active_background: editor,
|
||||
search_match_background: bg, // todo!(this was inserted by Mikayla)
|
||||
|
||||
editor_background: editor,
|
||||
editor_gutter_background: editor,
|
||||
editor_subheader_background: bg,
|
||||
|
@ -92,6 +94,7 @@ pub(crate) fn one_dark() -> Theme {
|
|||
0.2,
|
||||
),
|
||||
editor_document_highlight_write_background: gpui::red(),
|
||||
|
||||
terminal_background: bg,
|
||||
// todo!("Use one colors for terminal")
|
||||
terminal_ansi_black: crate::black().dark().step_12(),
|
||||
|
@ -191,8 +194,6 @@ pub(crate) fn one_dark() -> Theme {
|
|||
("variable.special".into(), red.into()),
|
||||
("variant".into(), HighlightStyle::default()),
|
||||
],
|
||||
inlay_style: HighlightStyle::default(),
|
||||
suggestion_style: HighlightStyle::default(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ pub struct ThemeSettings {
|
|||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AdjustedBufferFontSize(Option<Pixels>);
|
||||
pub struct AdjustedBufferFontSize(Pixels);
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ThemeSettingsContent {
|
||||
|
@ -69,12 +69,10 @@ impl BufferLineHeight {
|
|||
}
|
||||
|
||||
impl ThemeSettings {
|
||||
pub fn buffer_font_size(&self, cx: &mut AppContext) -> Pixels {
|
||||
let font_size = *cx
|
||||
.default_global::<AdjustedBufferFontSize>()
|
||||
.0
|
||||
.get_or_insert(self.buffer_font_size.into());
|
||||
font_size.max(MIN_FONT_SIZE)
|
||||
pub fn buffer_font_size(&self, cx: &AppContext) -> Pixels {
|
||||
cx.try_global::<AdjustedBufferFontSize>()
|
||||
.map_or(self.buffer_font_size, |size| size.0)
|
||||
.max(MIN_FONT_SIZE)
|
||||
}
|
||||
|
||||
pub fn line_height(&self) -> f32 {
|
||||
|
@ -83,9 +81,9 @@ impl ThemeSettings {
|
|||
}
|
||||
|
||||
pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
|
||||
if let Some(adjusted_size) = cx.default_global::<AdjustedBufferFontSize>().0 {
|
||||
if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::<AdjustedBufferFontSize>() {
|
||||
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
|
||||
let delta = adjusted_size - buffer_font_size;
|
||||
let delta = *adjusted_size - buffer_font_size;
|
||||
size + delta
|
||||
} else {
|
||||
size
|
||||
|
@ -95,18 +93,19 @@ pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
|
|||
|
||||
pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) {
|
||||
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
|
||||
let adjusted_size = cx
|
||||
.default_global::<AdjustedBufferFontSize>()
|
||||
.0
|
||||
.get_or_insert(buffer_font_size);
|
||||
f(adjusted_size);
|
||||
*adjusted_size = (*adjusted_size).max(MIN_FONT_SIZE - buffer_font_size);
|
||||
let mut adjusted_size = cx
|
||||
.try_global::<AdjustedBufferFontSize>()
|
||||
.map_or(buffer_font_size, |adjusted_size| adjusted_size.0);
|
||||
|
||||
f(&mut adjusted_size);
|
||||
adjusted_size = adjusted_size.max(MIN_FONT_SIZE);
|
||||
cx.set_global(AdjustedBufferFontSize(adjusted_size));
|
||||
cx.refresh();
|
||||
}
|
||||
|
||||
pub fn reset_font_size(cx: &mut AppContext) {
|
||||
if cx.has_global::<AdjustedBufferFontSize>() {
|
||||
cx.global_mut::<AdjustedBufferFontSize>().0 = None;
|
||||
cx.remove_global::<AdjustedBufferFontSize>();
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,6 +114,7 @@ pub struct ThemeColors {
|
|||
pub tab_bar_background: Hsla,
|
||||
pub tab_inactive_background: Hsla,
|
||||
pub tab_active_background: Hsla,
|
||||
pub search_match_background: Hsla,
|
||||
// pub panel_background: Hsla,
|
||||
// pub pane_focused_border: Hsla,
|
||||
// /// The color of the scrollbar thumb.
|
||||
|
|
|
@ -8,12 +8,6 @@ use crate::{
|
|||
#[derive(Clone, Default)]
|
||||
pub struct SyntaxTheme {
|
||||
pub highlights: Vec<(String, HighlightStyle)>,
|
||||
// todo!("Remove this in favor of StatusColor.hint")
|
||||
// If this should be overridable we should move it to ThemeColors
|
||||
pub inlay_style: HighlightStyle,
|
||||
// todo!("Remove this in favor of StatusColor.prediction")
|
||||
// If this should be overridable we should move it to ThemeColors
|
||||
pub suggestion_style: HighlightStyle,
|
||||
}
|
||||
|
||||
impl SyntaxTheme {
|
||||
|
@ -72,8 +66,6 @@ impl SyntaxTheme {
|
|||
("variable.special".into(), red().light().step_9().into()),
|
||||
("variant".into(), red().light().step_9().into()),
|
||||
],
|
||||
inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style")
|
||||
suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,8 +124,6 @@ impl SyntaxTheme {
|
|||
("variable.special".into(), red().dark().step_11().into()),
|
||||
("variant".into(), red().dark().step_11().into()),
|
||||
],
|
||||
inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style")
|
||||
suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,8 +142,6 @@ impl SyntaxTheme {
|
|||
)
|
||||
})
|
||||
.collect(),
|
||||
inlay_style: HighlightStyle::default(),
|
||||
suggestion_style: HighlightStyle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -359,11 +359,7 @@ impl RenderOnce for ButtonLike {
|
|||
},
|
||||
)
|
||||
.when_some(self.tooltip, |this, tooltip| {
|
||||
if !self.selected {
|
||||
this.tooltip(move |cx| tooltip(cx))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
this.tooltip(move |cx| tooltip(cx))
|
||||
})
|
||||
.children(self.children)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use gpui::{Action, AnyView, DefiniteLength};
|
||||
use gpui::{AnyView, DefiniteLength};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize};
|
||||
|
@ -39,10 +39,6 @@ impl IconButton {
|
|||
self.selected_icon = icon.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn action(self, action: Box<dyn Action>) -> Self {
|
||||
self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for IconButton {
|
||||
|
|
|
@ -92,6 +92,7 @@ pub enum Icon {
|
|||
Shift,
|
||||
Option,
|
||||
Return,
|
||||
Update,
|
||||
ZedXCopilot,
|
||||
}
|
||||
|
||||
|
@ -168,6 +169,7 @@ impl Icon {
|
|||
Icon::Shift => "icons/shift.svg",
|
||||
Icon::Option => "icons/option.svg",
|
||||
Icon::Return => "icons/return.svg",
|
||||
Icon::Update => "icons/update.svg",
|
||||
Icon::ZedXCopilot => "icons/zed_x_copilot.svg",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,183 +1,7 @@
|
|||
use std::ops::Range;
|
||||
mod highlighted_label;
|
||||
mod label;
|
||||
mod label_like;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::styled_ext::StyledExt;
|
||||
use gpui::{relative, Div, HighlightStyle, IntoElement, StyledText, WindowContext};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum LabelSize {
|
||||
#[default]
|
||||
Default,
|
||||
Small,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum LineHeightStyle {
|
||||
#[default]
|
||||
TextLabel,
|
||||
/// Sets the line height to 1
|
||||
UILabel,
|
||||
}
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct Label {
|
||||
label: SharedString,
|
||||
size: LabelSize,
|
||||
line_height_style: LineHeightStyle,
|
||||
color: Color,
|
||||
strikethrough: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for Label {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.when(self.strikethrough, |this| {
|
||||
this.relative().child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_1_2()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(Color::Hidden.color(cx)),
|
||||
)
|
||||
})
|
||||
.map(|this| match self.size {
|
||||
LabelSize::Default => this.text_ui(),
|
||||
LabelSize::Small => this.text_ui_sm(),
|
||||
})
|
||||
.when(self.line_height_style == LineHeightStyle::UILabel, |this| {
|
||||
this.line_height(relative(1.))
|
||||
})
|
||||
.text_color(self.color.color(cx))
|
||||
.child(self.label.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
size: LabelSize::Default,
|
||||
line_height_style: LineHeightStyle::default(),
|
||||
color: Color::Default,
|
||||
strikethrough: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: LabelSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.line_height_style = line_height_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct HighlightedLabel {
|
||||
label: SharedString,
|
||||
size: LabelSize,
|
||||
color: Color,
|
||||
highlight_indices: Vec<usize>,
|
||||
strikethrough: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for HighlightedLabel {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let highlight_color = cx.theme().colors().text_accent;
|
||||
|
||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
||||
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
|
||||
|
||||
while let Some(start_ix) = highlight_indices.next() {
|
||||
let mut end_ix = start_ix;
|
||||
|
||||
loop {
|
||||
end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
|
||||
if let Some(&next_ix) = highlight_indices.peek() {
|
||||
if next_ix == end_ix {
|
||||
end_ix = next_ix;
|
||||
highlight_indices.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
highlights.push((
|
||||
start_ix..end_ix,
|
||||
HighlightStyle {
|
||||
color: Some(highlight_color),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let mut text_style = cx.text_style().clone();
|
||||
text_style.color = self.color.color(cx);
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.when(self.strikethrough, |this| {
|
||||
this.relative().child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_px()
|
||||
.my_auto()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(Color::Hidden.color(cx)),
|
||||
)
|
||||
})
|
||||
.map(|this| match self.size {
|
||||
LabelSize::Default => this.text_ui(),
|
||||
LabelSize::Small => this.text_ui_sm(),
|
||||
})
|
||||
.child(StyledText::new(self.label).with_highlights(&text_style, highlights))
|
||||
}
|
||||
}
|
||||
|
||||
impl HighlightedLabel {
|
||||
/// shows a label with the given characters highlighted.
|
||||
/// characters are identified by utf8 byte position.
|
||||
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
size: LabelSize::Default,
|
||||
color: Color::Default,
|
||||
highlight_indices,
|
||||
strikethrough: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: LabelSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
}
|
||||
pub use highlighted_label::*;
|
||||
pub use label::*;
|
||||
pub use label_like::*;
|
||||
|
|
86
crates/ui2/src/components/label/highlighted_label.rs
Normal file
86
crates/ui2/src/components/label/highlighted_label.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use gpui::{HighlightStyle, StyledText};
|
||||
|
||||
use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct HighlightedLabel {
|
||||
base: LabelLike,
|
||||
label: SharedString,
|
||||
highlight_indices: Vec<usize>,
|
||||
}
|
||||
|
||||
impl HighlightedLabel {
|
||||
/// Constructs a label with the given characters highlighted.
|
||||
/// Characters are identified by UTF-8 byte position.
|
||||
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
|
||||
Self {
|
||||
base: LabelLike::new(),
|
||||
label: label.into(),
|
||||
highlight_indices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for HighlightedLabel {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.base = self.base.line_height_style(line_height_style);
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.base = self.base.color(color);
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.base = self.base.strikethrough(strikethrough);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for HighlightedLabel {
|
||||
type Rendered = LabelLike;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let highlight_color = cx.theme().colors().text_accent;
|
||||
|
||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
||||
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
|
||||
|
||||
while let Some(start_ix) = highlight_indices.next() {
|
||||
let mut end_ix = start_ix;
|
||||
|
||||
loop {
|
||||
end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
|
||||
if let Some(&next_ix) = highlight_indices.peek() {
|
||||
if next_ix == end_ix {
|
||||
end_ix = next_ix;
|
||||
highlight_indices.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
highlights.push((
|
||||
start_ix..end_ix,
|
||||
HighlightStyle {
|
||||
color: Some(highlight_color),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let mut text_style = cx.text_style().clone();
|
||||
text_style.color = self.base.color.color(cx);
|
||||
|
||||
LabelLike::new().child(StyledText::new(self.label).with_highlights(&text_style, highlights))
|
||||
}
|
||||
}
|
48
crates/ui2/src/components/label/label.rs
Normal file
48
crates/ui2/src/components/label/label.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use gpui::WindowContext;
|
||||
|
||||
use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Label {
|
||||
base: LabelLike,
|
||||
label: SharedString,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: LabelLike::new(),
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for Label {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.base = self.base.line_height_style(line_height_style);
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.base = self.base.color(color);
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.base = self.base.strikethrough(strikethrough);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Label {
|
||||
type Rendered = LabelLike;
|
||||
|
||||
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
|
||||
self.base.child(self.label)
|
||||
}
|
||||
}
|
102
crates/ui2/src/components/label/label_like.rs
Normal file
102
crates/ui2/src/components/label/label_like.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use gpui::{relative, AnyElement, Div, Styled};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
|
||||
pub enum LabelSize {
|
||||
#[default]
|
||||
Default,
|
||||
Small,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum LineHeightStyle {
|
||||
#[default]
|
||||
TextLabel,
|
||||
/// Sets the line height to 1
|
||||
UILabel,
|
||||
}
|
||||
|
||||
pub trait LabelCommon {
|
||||
fn size(self, size: LabelSize) -> Self;
|
||||
fn line_height_style(self, line_height_style: LineHeightStyle) -> Self;
|
||||
fn color(self, color: Color) -> Self;
|
||||
fn strikethrough(self, strikethrough: bool) -> Self;
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct LabelLike {
|
||||
size: LabelSize,
|
||||
line_height_style: LineHeightStyle,
|
||||
pub(crate) color: Color,
|
||||
strikethrough: bool,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl LabelLike {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
size: LabelSize::Default,
|
||||
line_height_style: LineHeightStyle::default(),
|
||||
color: Color::Default,
|
||||
strikethrough: false,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for LabelLike {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.line_height_style = line_height_style;
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for LabelLike {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for LabelLike {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
.when(self.strikethrough, |this| {
|
||||
this.relative().child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_1_2()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(Color::Hidden.color(cx)),
|
||||
)
|
||||
})
|
||||
.map(|this| match self.size {
|
||||
LabelSize::Default => this.text_ui(),
|
||||
LabelSize::Small => this.text_ui_sm(),
|
||||
})
|
||||
.when(self.line_height_style == LineHeightStyle::UILabel, |this| {
|
||||
this.line_height(relative(1.))
|
||||
})
|
||||
.text_color(self.color.color(cx))
|
||||
.children(self.children)
|
||||
}
|
||||
}
|
|
@ -51,5 +51,13 @@ impl Render for IconButtonStory {
|
|||
.tooltip(|cx| Tooltip::text("Open messages", cx)),
|
||||
),
|
||||
)
|
||||
.child(Story::label("Selected with `tooltip`"))
|
||||
.child(
|
||||
div().w_8().child(
|
||||
IconButton::new("selected_with_tooltip", Icon::InlayHint)
|
||||
.selected(true)
|
||||
.tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,6 +84,7 @@ impl Render for Tooltip {
|
|||
.px_2()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(self.title.clone())
|
||||
.when_some(self.key_binding.clone(), |this, key_binding| {
|
||||
this.justify_between().child(key_binding)
|
||||
|
|
|
@ -13,5 +13,5 @@ pub use crate::{h_stack, v_stack};
|
|||
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton};
|
||||
pub use crate::{ButtonCommon, Color, StyledExt};
|
||||
pub use crate::{Icon, IconElement, IconSize};
|
||||
pub use crate::{Label, LabelSize, LineHeightStyle};
|
||||
pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle};
|
||||
pub use theme::ActiveTheme;
|
||||
|
|
|
@ -70,8 +70,7 @@ pub trait StyledExt: Styled + Sized {
|
|||
/// or other places that text needs to match the user's buffer font size.
|
||||
fn text_buffer(self, cx: &mut WindowContext) -> Self {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
self.text_size(settings.buffer_font_size)
|
||||
self.text_size(settings.buffer_font_size(cx))
|
||||
}
|
||||
|
||||
/// The [`Surface`](ui2::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements
|
||||
|
|
|
@ -44,12 +44,18 @@ impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> From<Arc<T>> for ArcCow<'_, T> {
|
||||
impl<T: ?Sized> From<Arc<T>> for ArcCow<'_, T> {
|
||||
fn from(s: Arc<T>) -> Self {
|
||||
Self::Owned(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> From<&'_ Arc<T>> for ArcCow<'_, T> {
|
||||
fn from(s: &'_ Arc<T>) -> Self {
|
||||
Self::Owned(s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ArcCow<'_, str> {
|
||||
fn from(value: String) -> Self {
|
||||
Self::Owned(value.into())
|
||||
|
|
|
@ -26,6 +26,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
|
|||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
|
||||
fn size(&self, cx: &WindowContext) -> f32;
|
||||
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
|
||||
// todo!("We should have a icon tooltip method, rather than using persistant_name")
|
||||
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
|
||||
fn toggle_action(&self) -> Box<dyn Action>;
|
||||
fn icon_label(&self, _: &WindowContext) -> Option<String> {
|
||||
|
@ -36,7 +37,6 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
|
|||
}
|
||||
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
|
||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool;
|
||||
}
|
||||
|
||||
pub trait PanelHandle: Send + Sync {
|
||||
|
@ -53,7 +53,6 @@ pub trait PanelHandle: Send + Sync {
|
|||
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
|
||||
fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
|
||||
fn icon_label(&self, cx: &WindowContext) -> Option<String>;
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool;
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
||||
fn to_any(&self) -> AnyView;
|
||||
}
|
||||
|
@ -114,10 +113,6 @@ where
|
|||
self.read(cx).icon_label(cx)
|
||||
}
|
||||
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool {
|
||||
self.read(cx).has_focus(cx)
|
||||
}
|
||||
|
||||
fn to_any(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
|
@ -319,7 +314,7 @@ impl Dock {
|
|||
}
|
||||
PanelEvent::ZoomIn => {
|
||||
this.set_panel_zoomed(&panel.to_any(), true, cx);
|
||||
if !panel.has_focus(cx) {
|
||||
if !panel.focus_handle(cx).contains_focused(cx) {
|
||||
cx.focus_view(&panel);
|
||||
}
|
||||
workspace
|
||||
|
@ -729,7 +724,10 @@ impl Render for PanelButtons {
|
|||
.trigger(
|
||||
IconButton::new(name, icon)
|
||||
.selected(is_active_button)
|
||||
.action(action.boxed_clone())
|
||||
.on_click({
|
||||
let action = action.boxed_clone();
|
||||
move |_, cx| cx.dispatch_action(action.boxed_clone())
|
||||
})
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(tooltip.clone(), &*action, cx)
|
||||
}),
|
||||
|
@ -760,7 +758,7 @@ pub mod test {
|
|||
pub position: DockPosition,
|
||||
pub zoomed: bool,
|
||||
pub active: bool,
|
||||
pub has_focus: bool,
|
||||
pub focus_handle: FocusHandle,
|
||||
pub size: f32,
|
||||
}
|
||||
actions!(ToggleTestPanel);
|
||||
|
@ -768,12 +766,12 @@ pub mod test {
|
|||
impl EventEmitter<PanelEvent> for TestPanel {}
|
||||
|
||||
impl TestPanel {
|
||||
pub fn new(position: DockPosition) -> Self {
|
||||
pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self {
|
||||
Self {
|
||||
position,
|
||||
zoomed: false,
|
||||
active: false,
|
||||
has_focus: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
size: 300.,
|
||||
}
|
||||
}
|
||||
|
@ -832,15 +830,11 @@ pub mod test {
|
|||
fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
|
||||
self.active = active;
|
||||
}
|
||||
|
||||
fn has_focus(&self, _cx: &WindowContext) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for TestPanel {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
unimplemented!()
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,22 +62,13 @@ impl Render for StatusBar {
|
|||
)
|
||||
.child(
|
||||
// Right Dock
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
// Terminal
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(IconButton::new("status-assistant", Icon::Ai)),
|
||||
)
|
||||
.child(
|
||||
// Terminal
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(IconButton::new("status-chat", Icon::MessageBubbles)),
|
||||
),
|
||||
h_stack().gap_1().child(
|
||||
// Terminal
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(IconButton::new("status-chat", Icon::MessageBubbles)),
|
||||
),
|
||||
)
|
||||
.child(self.render_right_tools(cx)),
|
||||
)
|
||||
|
|
|
@ -45,9 +45,10 @@ use node_runtime::NodeRuntime;
|
|||
use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
|
||||
pub use pane::*;
|
||||
pub use pane_group::*;
|
||||
use persistence::DB;
|
||||
pub use persistence::{
|
||||
model::{ItemId, SerializedWorkspace, WorkspaceLocation},
|
||||
WorkspaceDb, DB,
|
||||
WorkspaceDb, DB as WORKSPACE_DB,
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||
|
@ -65,7 +66,7 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
pub use ui;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
|
@ -1542,7 +1543,7 @@ impl Workspace {
|
|||
|
||||
if let Some(active_panel) = dock.active_panel() {
|
||||
if was_visible {
|
||||
if active_panel.has_focus(cx) {
|
||||
if active_panel.focus_handle(cx).contains_focused(cx) {
|
||||
focus_center = true;
|
||||
}
|
||||
} else {
|
||||
|
@ -1589,7 +1590,9 @@ impl Workspace {
|
|||
/// Focus the panel of the given type if it isn't already focused. If it is
|
||||
/// already focused, then transfer focus back to the workspace center.
|
||||
pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
|
||||
self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
|
||||
!panel.focus_handle(cx).contains_focused(cx)
|
||||
});
|
||||
}
|
||||
|
||||
/// Focus or unfocus the given panel type, depending on the given callback.
|
||||
|
@ -1681,7 +1684,7 @@ impl Workspace {
|
|||
if Some(dock.position()) != dock_to_reveal {
|
||||
if let Some(panel) = dock.active_panel() {
|
||||
if panel.is_zoomed(cx) {
|
||||
focus_center |= panel.has_focus(cx);
|
||||
focus_center |= panel.focus_handle(cx).contains_focused(cx);
|
||||
dock.set_open(false, cx);
|
||||
}
|
||||
}
|
||||
|
@ -2075,6 +2078,7 @@ impl Workspace {
|
|||
}
|
||||
if &pane == self.active_pane() {
|
||||
self.active_item_path_changed(cx);
|
||||
self.update_active_view_for_followers(cx);
|
||||
}
|
||||
}
|
||||
pane::Event::ChangeItemTitle => {
|
||||
|
@ -2754,18 +2758,18 @@ impl Workspace {
|
|||
fn update_active_view_for_followers(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let mut is_project_item = true;
|
||||
let mut update = proto::UpdateActiveView::default();
|
||||
if self.active_pane.read(cx).has_focus(cx) {
|
||||
let item = self
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.to_followable_item_handle(cx));
|
||||
if let Some(item) = item {
|
||||
is_project_item = item.is_project_item(cx);
|
||||
update = proto::UpdateActiveView {
|
||||
id: item
|
||||
.remote_id(&self.app_state.client, cx)
|
||||
.map(|id| id.to_proto()),
|
||||
leader_id: self.leader_for_pane(&self.active_pane),
|
||||
};
|
||||
|
||||
if let Some(item) = self.active_item(cx) {
|
||||
if item.focus_handle(cx).contains_focused(cx) {
|
||||
if let Some(item) = item.to_followable_item_handle(cx) {
|
||||
is_project_item = item.is_project_item(cx);
|
||||
update = proto::UpdateActiveView {
|
||||
id: item
|
||||
.remote_id(&self.app_state.client, cx)
|
||||
.map(|id| id.to_proto()),
|
||||
leader_id: self.leader_for_pane(&self.active_pane),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.116.0"
|
||||
version = "0.117.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
@ -78,7 +78,7 @@ workspace = { path = "../workspace" }
|
|||
welcome = { path = "../welcome" }
|
||||
zed-actions = {path = "../zed-actions"}
|
||||
anyhow.workspace = true
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
async-recursion = "0.3"
|
||||
async-trait.workspace = true
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed2"
|
||||
version = "0.109.0"
|
||||
version = "2.0.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
@ -49,14 +49,14 @@ lsp = { package = "lsp2", path = "../lsp2" }
|
|||
menu = { package = "menu2", path = "../menu2" }
|
||||
# language_tools = { path = "../language_tools" }
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
# assistant = { path = "../assistant" }
|
||||
assistant = { package = "assistant2", path = "../assistant2" }
|
||||
outline = { package = "outline2", path = "../outline2" }
|
||||
# plugin_runtime = { path = "../plugin_runtime",optional = true }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
project_panel = { package = "project_panel2", path = "../project_panel2" }
|
||||
# project_symbols = { path = "../project_symbols" }
|
||||
quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" }
|
||||
# recent_projects = { path = "../recent_projects" }
|
||||
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
|
||||
rope = { package = "rope2", path = "../rope2"}
|
||||
rpc = { package = "rpc2", path = "../rpc2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
|
@ -68,13 +68,13 @@ terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
|
|||
theme = { package = "theme2", path = "../theme2" }
|
||||
theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
|
||||
util = { path = "../util" }
|
||||
# semantic_index = { path = "../semantic_index" }
|
||||
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
|
||||
# vim = { path = "../vim" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
welcome = { package = "welcome2", path = "../welcome2" }
|
||||
zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
|
||||
anyhow.workspace = true
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
async-recursion = "0.3"
|
||||
async-trait.workspace = true
|
||||
|
|
175
crates/zed2/src/app_menus.rs
Normal file
175
crates/zed2/src/app_menus.rs
Normal file
|
@ -0,0 +1,175 @@
|
|||
use gpui::{Menu, MenuItem, OsAction};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn app_menus() -> Vec<Menu<'static>> {
|
||||
vec![
|
||||
Menu {
|
||||
name: "Zed",
|
||||
items: vec![
|
||||
MenuItem::action("About Zed…", super::About),
|
||||
MenuItem::action("Check for Updates", auto_update::Check),
|
||||
MenuItem::separator(),
|
||||
MenuItem::submenu(Menu {
|
||||
name: "Preferences",
|
||||
items: vec![
|
||||
MenuItem::action("Open Settings", super::OpenSettings),
|
||||
MenuItem::action("Open Key Bindings", super::OpenKeymap),
|
||||
MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
|
||||
MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap),
|
||||
MenuItem::action("Open Local Settings", super::OpenLocalSettings),
|
||||
MenuItem::action("Select Theme", theme_selector::Toggle),
|
||||
],
|
||||
}),
|
||||
MenuItem::action("Install CLI", install_cli::Install),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Hide Zed", super::Hide),
|
||||
MenuItem::action("Hide Others", super::HideOthers),
|
||||
MenuItem::action("Show All", super::ShowAll),
|
||||
MenuItem::action("Quit", super::Quit),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "File",
|
||||
items: vec![
|
||||
MenuItem::action("New", workspace::NewFile),
|
||||
MenuItem::action("New Window", workspace::NewWindow),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Open…", workspace::Open),
|
||||
MenuItem::action("Open Recent...", recent_projects::OpenRecent),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject),
|
||||
MenuItem::action("Save", workspace::Save { save_intent: None }),
|
||||
MenuItem::action("Save As…", workspace::SaveAs),
|
||||
MenuItem::action("Save All", workspace::SaveAll { save_intent: None }),
|
||||
MenuItem::action(
|
||||
"Close Editor",
|
||||
workspace::CloseActiveItem { save_intent: None },
|
||||
),
|
||||
MenuItem::action("Close Window", workspace::CloseWindow),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Edit",
|
||||
items: vec![
|
||||
MenuItem::os_action("Undo", editor::Undo, OsAction::Undo),
|
||||
MenuItem::os_action("Redo", editor::Redo, OsAction::Redo),
|
||||
MenuItem::separator(),
|
||||
MenuItem::os_action("Cut", editor::Cut, OsAction::Cut),
|
||||
MenuItem::os_action("Copy", editor::Copy, OsAction::Copy),
|
||||
MenuItem::os_action("Paste", editor::Paste, OsAction::Paste),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Find", search::buffer_search::Deploy { focus: true }),
|
||||
MenuItem::action("Find In Project", workspace::NewSearch),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Toggle Line Comment", editor::ToggleComments::default()),
|
||||
MenuItem::action("Emoji & Symbols", editor::ShowCharacterPalette),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Selection",
|
||||
items: vec![
|
||||
MenuItem::os_action("Select All", editor::SelectAll, OsAction::SelectAll),
|
||||
MenuItem::action("Expand Selection", editor::SelectLargerSyntaxNode),
|
||||
MenuItem::action("Shrink Selection", editor::SelectSmallerSyntaxNode),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Add Cursor Above", editor::AddSelectionAbove),
|
||||
MenuItem::action("Add Cursor Below", editor::AddSelectionBelow),
|
||||
MenuItem::action(
|
||||
"Select Next Occurrence",
|
||||
editor::SelectNext {
|
||||
replace_newest: false,
|
||||
},
|
||||
),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Move Line Up", editor::MoveLineUp),
|
||||
MenuItem::action("Move Line Down", editor::MoveLineDown),
|
||||
MenuItem::action("Duplicate Selection", editor::DuplicateLine),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "View",
|
||||
items: vec![
|
||||
MenuItem::action("Zoom In", super::IncreaseBufferFontSize),
|
||||
MenuItem::action("Zoom Out", super::DecreaseBufferFontSize),
|
||||
MenuItem::action("Reset Zoom", super::ResetBufferFontSize),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock),
|
||||
MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock),
|
||||
MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock),
|
||||
MenuItem::action("Close All Docks", workspace::CloseAllDocks),
|
||||
MenuItem::submenu(Menu {
|
||||
name: "Editor Layout",
|
||||
items: vec![
|
||||
MenuItem::action("Split Up", workspace::SplitUp),
|
||||
MenuItem::action("Split Down", workspace::SplitDown),
|
||||
MenuItem::action("Split Left", workspace::SplitLeft),
|
||||
MenuItem::action("Split Right", workspace::SplitRight),
|
||||
],
|
||||
}),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Project Panel", project_panel::ToggleFocus),
|
||||
MenuItem::action("Command Palette", command_palette::Toggle),
|
||||
MenuItem::action("Diagnostics", diagnostics::Deploy),
|
||||
MenuItem::separator(),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Go",
|
||||
items: vec![
|
||||
MenuItem::action("Back", workspace::GoBack),
|
||||
MenuItem::action("Forward", workspace::GoForward),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Go to File", file_finder::Toggle),
|
||||
// MenuItem::action("Go to Symbol in Project", project_symbols::Toggle),
|
||||
MenuItem::action("Go to Symbol in Editor", outline::Toggle),
|
||||
MenuItem::action("Go to Definition", editor::GoToDefinition),
|
||||
MenuItem::action("Go to Type Definition", editor::GoToTypeDefinition),
|
||||
MenuItem::action("Find All References", editor::FindAllReferences),
|
||||
MenuItem::action("Go to Line/Column", go_to_line::Toggle),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Next Problem", editor::GoToDiagnostic),
|
||||
MenuItem::action("Previous Problem", editor::GoToPrevDiagnostic),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Window",
|
||||
items: vec![
|
||||
MenuItem::action("Minimize", super::Minimize),
|
||||
MenuItem::action("Zoom", super::Zoom),
|
||||
MenuItem::separator(),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Help",
|
||||
items: vec![
|
||||
MenuItem::action("Command Palette", command_palette::Toggle),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("View Telemetry", crate::OpenTelemetryLog),
|
||||
MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
|
||||
MenuItem::action("Show Welcome", workspace::Welcome),
|
||||
MenuItem::separator(),
|
||||
// todo!(): Needs `feedback2` crate.
|
||||
// MenuItem::action("Give us feedback", feedback::feedback_editor::GiveFeedback),
|
||||
// MenuItem::action(
|
||||
// "Copy System Specs Into Clipboard",
|
||||
// feedback::CopySystemSpecsIntoClipboard,
|
||||
// ),
|
||||
// MenuItem::action("File Bug Report", feedback::FileBugReport),
|
||||
// MenuItem::action("Request Feature", feedback::RequestFeature),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action(
|
||||
"Documentation",
|
||||
crate::OpenBrowser {
|
||||
url: "https://zed.dev/docs".into(),
|
||||
},
|
||||
),
|
||||
MenuItem::action(
|
||||
"Zed Twitter",
|
||||
crate::OpenBrowser {
|
||||
url: "https://twitter.com/zeddotdev".into(),
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
|
@ -22,8 +22,7 @@ use node_runtime::RealNodeRuntime;
|
|||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{
|
||||
default_settings, handle_keymap_file_changes, handle_settings_file_changes, watch_config_file,
|
||||
Settings, SettingsStore,
|
||||
default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore,
|
||||
};
|
||||
use simplelog::ConfigBuilder;
|
||||
use smol::process::Command;
|
||||
|
@ -51,8 +50,9 @@ use uuid::Uuid;
|
|||
use welcome::{show_welcome_experience, FIRST_OPEN};
|
||||
use workspace::{AppState, WorkspaceStore};
|
||||
use zed2::{
|
||||
build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
|
||||
languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
|
||||
app_menus, build_window_options, ensure_only_instance, handle_cli_connection,
|
||||
handle_keymap_file_changes, initialize_workspace, languages, Assets, IsOnlyInstance,
|
||||
OpenListener, OpenRequest,
|
||||
};
|
||||
|
||||
mod open_listener;
|
||||
|
@ -161,11 +161,11 @@ fn main() {
|
|||
node_runtime.clone(),
|
||||
cx,
|
||||
);
|
||||
// assistant::init(cx);
|
||||
assistant::init(cx);
|
||||
// component_test::init(cx);
|
||||
|
||||
// cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
|
||||
// .detach();
|
||||
cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
|
||||
.detach();
|
||||
watch_file_types(fs.clone(), cx);
|
||||
|
||||
languages.set_theme(cx.theme().clone());
|
||||
|
@ -186,10 +186,10 @@ fn main() {
|
|||
.report_app_event(telemetry_settings, event_operation);
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
languages,
|
||||
languages: languages.clone(),
|
||||
client: client.clone(),
|
||||
user_store: user_store.clone(),
|
||||
fs,
|
||||
fs: fs.clone(),
|
||||
build_window_options,
|
||||
workspace_store,
|
||||
node_runtime,
|
||||
|
@ -200,7 +200,7 @@ fn main() {
|
|||
auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
|
||||
|
||||
workspace::init(app_state.clone(), cx);
|
||||
// recent_projects::init(cx);
|
||||
recent_projects::init(cx);
|
||||
|
||||
go_to_line::init(cx);
|
||||
file_finder::init(cx);
|
||||
|
@ -210,7 +210,7 @@ fn main() {
|
|||
channel::init(&client, user_store.clone(), cx);
|
||||
// diagnostics::init(cx);
|
||||
search::init(cx);
|
||||
// semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
|
||||
semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
|
||||
// vim::init(cx);
|
||||
terminal_view::init(cx);
|
||||
|
||||
|
@ -224,7 +224,7 @@ fn main() {
|
|||
// feedback::init(cx);
|
||||
welcome::init(cx);
|
||||
|
||||
// cx.set_menus(menus::menus());
|
||||
cx.set_menus(app_menus());
|
||||
initialize_workspace(app_state.clone(), cx);
|
||||
|
||||
if stdout_is_a_pty() {
|
||||
|
|
|
@ -1,26 +1,30 @@
|
|||
#![allow(unused_variables, unused_mut)]
|
||||
//todo!()
|
||||
|
||||
mod app_menus;
|
||||
mod assets;
|
||||
pub mod languages;
|
||||
mod only_instance;
|
||||
mod open_listener;
|
||||
|
||||
pub use app_menus::*;
|
||||
pub use assets::*;
|
||||
use assistant::AssistantPanel;
|
||||
use breadcrumbs::Breadcrumbs;
|
||||
use collections::VecDeque;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions,
|
||||
actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions, View,
|
||||
ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions,
|
||||
};
|
||||
pub use only_instance::*;
|
||||
pub use open_listener::*;
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use project_panel::ProjectPanel;
|
||||
use quick_action_bar::QuickActionBar;
|
||||
use settings::{initial_local_settings_content, Settings};
|
||||
use settings::{initial_local_settings_content, load_default_keymap, KeymapFile, Settings};
|
||||
use std::{borrow::Cow, ops::Deref, sync::Arc};
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use util::{
|
||||
|
@ -30,6 +34,7 @@ use util::{
|
|||
ResultExt,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use workspace::Pane;
|
||||
use workspace::{
|
||||
create_and_open_local_file, dock::PanelHandle,
|
||||
notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile,
|
||||
|
@ -92,37 +97,12 @@ pub fn build_window_options(
|
|||
pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
cx.observe_new_views(move |workspace: &mut Workspace, cx| {
|
||||
let workspace_handle = cx.view().clone();
|
||||
let center_pane = workspace.active_pane().clone();
|
||||
initialize_pane(workspace, ¢er_pane, cx);
|
||||
cx.subscribe(&workspace_handle, {
|
||||
move |workspace, _, event, cx| {
|
||||
if let workspace::Event::PaneAdded(pane) = event {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let breadcrumbs = cx.build_view(|_| Breadcrumbs::new(workspace));
|
||||
toolbar.add_item(breadcrumbs, cx);
|
||||
let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
|
||||
toolbar.add_item(buffer_search_bar.clone(), cx);
|
||||
|
||||
let quick_action_bar = cx
|
||||
.build_view(|_| QuickActionBar::new(buffer_search_bar, workspace));
|
||||
toolbar.add_item(quick_action_bar, cx);
|
||||
let diagnostic_editor_controls =
|
||||
cx.build_view(|_| diagnostics::ToolbarControls::new());
|
||||
// toolbar.add_item(diagnostic_editor_controls, cx);
|
||||
// let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
// toolbar.add_item(project_search_bar, cx);
|
||||
// let submit_feedback_button =
|
||||
// cx.add_view(|_| SubmitFeedbackButton::new());
|
||||
// toolbar.add_item(submit_feedback_button, cx);
|
||||
// let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
|
||||
// toolbar.add_item(feedback_info_text, cx);
|
||||
// let lsp_log_item =
|
||||
// cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
|
||||
// toolbar.add_item(lsp_log_item, cx);
|
||||
// let syntax_tree_item = cx
|
||||
// .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
|
||||
// toolbar.add_item(syntax_tree_item, cx);
|
||||
})
|
||||
});
|
||||
initialize_pane(workspace, pane, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -177,7 +157,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
cx.spawn(|workspace_handle, mut cx| async move {
|
||||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
|
||||
// let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let channels_panel =
|
||||
collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
|
||||
// let chat_panel =
|
||||
|
@ -189,14 +169,14 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
let (
|
||||
project_panel,
|
||||
terminal_panel,
|
||||
// assistant_panel,
|
||||
assistant_panel,
|
||||
channels_panel,
|
||||
// chat_panel,
|
||||
// notification_panel,
|
||||
) = futures::try_join!(
|
||||
project_panel,
|
||||
terminal_panel,
|
||||
// assistant_panel,
|
||||
assistant_panel,
|
||||
channels_panel,
|
||||
// chat_panel,
|
||||
// notification_panel,
|
||||
|
@ -206,25 +186,25 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
let project_panel_position = project_panel.position(cx);
|
||||
workspace.add_panel(project_panel, cx);
|
||||
workspace.add_panel(terminal_panel, cx);
|
||||
// workspace.add_panel(assistant_panel, cx);
|
||||
workspace.add_panel(assistant_panel, cx);
|
||||
workspace.add_panel(channels_panel, cx);
|
||||
// workspace.add_panel(chat_panel, cx);
|
||||
// workspace.add_panel(notification_panel, cx);
|
||||
|
||||
// if !was_deserialized
|
||||
// && workspace
|
||||
// .project()
|
||||
// .read(cx)
|
||||
// .visible_worktrees(cx)
|
||||
// .any(|tree| {
|
||||
// tree.read(cx)
|
||||
// .root_entry()
|
||||
// .map_or(false, |entry| entry.is_dir())
|
||||
// })
|
||||
// {
|
||||
// workspace.toggle_dock(project_panel_position, cx);
|
||||
// }
|
||||
// cx.focus_self();
|
||||
// if !was_deserialized
|
||||
// && workspace
|
||||
// .project()
|
||||
// .read(cx)
|
||||
// .visible_worktrees(cx)
|
||||
// .any(|tree| {
|
||||
// tree.read(cx)
|
||||
// .root_entry()
|
||||
// .map_or(false, |entry| entry.is_dir())
|
||||
// })
|
||||
// {
|
||||
// workspace.toggle_dock(project_panel_position, cx);
|
||||
// }
|
||||
cx.focus_self();
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
@ -255,14 +235,13 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
.open_urls(&[action.url.clone()])
|
||||
})
|
||||
.register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url))
|
||||
//todo!(buffer font size)
|
||||
// cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
|
||||
// theme::adjust_font_size(cx, |size| *size += 1.0)
|
||||
// });
|
||||
// cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
|
||||
// theme::adjust_font_size(cx, |size| *size -= 1.0)
|
||||
// });
|
||||
// cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx));
|
||||
.register_action(move |_, _: &IncreaseBufferFontSize, cx| {
|
||||
theme::adjust_font_size(cx, |size| *size += px(1.0))
|
||||
})
|
||||
.register_action(move |_, _: &DecreaseBufferFontSize, cx| {
|
||||
theme::adjust_font_size(cx, |size| *size -= px(1.0))
|
||||
})
|
||||
.register_action(move |_, _: &ResetBufferFontSize, cx| theme::reset_font_size(cx))
|
||||
.register_action(|_, _: &install_cli::Install, cx| {
|
||||
cx.spawn(|_, cx| async move {
|
||||
install_cli::install_cli(cx.deref())
|
||||
|
@ -434,6 +413,36 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
.detach();
|
||||
}
|
||||
|
||||
fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let breadcrumbs = cx.build_view(|_| Breadcrumbs::new(workspace));
|
||||
toolbar.add_item(breadcrumbs, cx);
|
||||
let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
|
||||
toolbar.add_item(buffer_search_bar.clone(), cx);
|
||||
|
||||
let quick_action_bar =
|
||||
cx.build_view(|_| QuickActionBar::new(buffer_search_bar, workspace));
|
||||
toolbar.add_item(quick_action_bar, cx);
|
||||
let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new());
|
||||
// toolbar.add_item(diagnostic_editor_controls, cx);
|
||||
// let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
// toolbar.add_item(project_search_bar, cx);
|
||||
// let submit_feedback_button =
|
||||
// cx.add_view(|_| SubmitFeedbackButton::new());
|
||||
// toolbar.add_item(submit_feedback_button, cx);
|
||||
// let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
|
||||
// toolbar.add_item(feedback_info_text, cx);
|
||||
// let lsp_log_item =
|
||||
// cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
|
||||
// toolbar.add_item(lsp_log_item, cx);
|
||||
// let syntax_tree_item = cx
|
||||
// .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
|
||||
// toolbar.add_item(syntax_tree_item, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
|
@ -559,6 +568,42 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
|||
.detach();
|
||||
}
|
||||
|
||||
pub fn handle_keymap_file_changes(
|
||||
mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
cx.spawn(move |cx| async move {
|
||||
// let mut settings_subscription = None;
|
||||
while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
|
||||
if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() {
|
||||
cx.update(|cx| reload_keymaps(cx, &keymap_content)).ok();
|
||||
|
||||
// todo!()
|
||||
// let mut old_base_keymap = cx.read(|cx| *settings::get::<BaseKeymap>(cx));
|
||||
// drop(settings_subscription);
|
||||
// settings_subscription = Some(cx.update(|cx| {
|
||||
// cx.observe_global::<SettingsStore, _>(move |cx| {
|
||||
// let new_base_keymap = *settings::get::<BaseKeymap>(cx);
|
||||
// if new_base_keymap != old_base_keymap {
|
||||
// old_base_keymap = new_base_keymap.clone();
|
||||
// reload_keymaps(cx, &keymap_content);
|
||||
// }
|
||||
// })
|
||||
// }));
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
|
||||
// todo!()
|
||||
// cx.clear_bindings();
|
||||
load_default_keymap(cx);
|
||||
keymap_content.clone().add_to_cx(cx).log_err();
|
||||
cx.set_menus(app_menus());
|
||||
}
|
||||
|
||||
fn open_local_settings_file(
|
||||
workspace: &mut Workspace,
|
||||
_: &OpenLocalSettings,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue