Compare commits
22 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
060966db81 | ||
![]() |
3bb2f59dc0 | ||
![]() |
87cbbe8c52 | ||
![]() |
b5d25a8e00 | ||
![]() |
5a14acca77 | ||
![]() |
4b88d13489 | ||
![]() |
d27df5ac50 | ||
![]() |
fb2a8474db | ||
![]() |
2e66ab296a | ||
![]() |
ce1e56fd61 | ||
![]() |
7e33865b62 | ||
![]() |
232894d3bc | ||
![]() |
fc0bfd75ad | ||
![]() |
8a3a0245e0 | ||
![]() |
25f79da4f7 | ||
![]() |
81a21e3353 | ||
![]() |
23a1b563bf | ||
![]() |
4d77efd048 | ||
![]() |
ae04b12e1a | ||
![]() |
e5112e13a3 | ||
![]() |
ee63de0022 | ||
![]() |
5bff8f2a54 |
49 changed files with 2313 additions and 733 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -95,6 +95,22 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ai"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"assets",
|
||||||
|
"collections",
|
||||||
|
"editor",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"gpui",
|
||||||
|
"isahc",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alacritty_config"
|
name = "alacritty_config"
|
||||||
version = "0.1.1-dev"
|
version = "0.1.1-dev"
|
||||||
|
@ -2220,6 +2236,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
|
"regex",
|
||||||
"search",
|
"search",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
|
@ -6793,6 +6810,7 @@ dependencies = [
|
||||||
"procinfo",
|
"procinfo",
|
||||||
"project",
|
"project",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"search",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"settings",
|
"settings",
|
||||||
|
@ -8759,9 +8777,10 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.88.0"
|
version = "0.88.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
|
"ai",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assets",
|
"assets",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/activity_indicator",
|
"crates/activity_indicator",
|
||||||
|
"crates/ai",
|
||||||
"crates/assets",
|
"crates/assets",
|
||||||
"crates/auto_update",
|
"crates/auto_update",
|
||||||
"crates/breadcrumbs",
|
"crates/breadcrumbs",
|
||||||
|
@ -78,6 +79,8 @@ ctor = { version = "0.1" }
|
||||||
env_logger = { version = "0.9" }
|
env_logger = { version = "0.9" }
|
||||||
futures = { version = "0.3" }
|
futures = { version = "0.3" }
|
||||||
globset = { version = "0.4" }
|
globset = { version = "0.4" }
|
||||||
|
indoc = "1"
|
||||||
|
isahc = "1.7.2"
|
||||||
lazy_static = { version = "1.4.0" }
|
lazy_static = { version = "1.4.0" }
|
||||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
ordered-float = { version = "2.1.1" }
|
ordered-float = { version = "2.1.1" }
|
||||||
|
|
18
assets/contexts/system.zmd
Normal file
18
assets/contexts/system.zmd
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
You are #zed, a language model representing the collective understanding of an open source project called Zed. When a new human visits you, they'll send you their profile. You'll respond with an introduction tailored to their situation. For example, a new user might see something like this:
|
||||||
|
|
||||||
|
Welcome to Zed! Zed is an innovative, open-source platform designed to enhance team communication and collaboration. At the heart of Zed are *contexts*, which create a dynamic digital representation of shared mental models. Contexts offer personalized starting points and the flexibility to edit and explore, enabling teams to align knowledge, streamline communication, and improve overall performance.
|
||||||
|
|
||||||
|
As the #zed model, I'm happy to answer any questions. In fact, I will improve as a result of you doing so!
|
||||||
|
|
||||||
|
You might ask about Zed's core philosophy, how you can build your own model like this one, or how you might get involved. Zed's open source!
|
||||||
|
|
||||||
|
> [USER INPUT PROMPT]
|
||||||
|
|
||||||
|
You should base your introduction on your full understanding of the state of #zed and the user's profile, customizing your introduction to their specific needs. Don't welcome them to Zed if they've been using Zed for 2 days. If they're returning after a while, welcome them back.
|
||||||
|
|
||||||
|
User input begins on a line starting with >.
|
||||||
|
Your output begins on a line starting with <.
|
||||||
|
|
||||||
|
Don't apologize ever.
|
||||||
|
Never say "I apologize".
|
||||||
|
Use simple language and don't flatter the users. Spend your tokens on valuable information.
|
|
@ -188,6 +188,12 @@
|
||||||
"alt-[": "copilot::PreviousSuggestion"
|
"alt-[": "copilot::PreviousSuggestion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && extension == zmd",
|
||||||
|
"bindings": {
|
||||||
|
"cmd-enter": "ai::Assist"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == auto_height",
|
"context": "Editor && mode == auto_height",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
@ -367,30 +373,9 @@
|
||||||
"workspace::ActivatePane",
|
"workspace::ActivatePane",
|
||||||
8
|
8
|
||||||
],
|
],
|
||||||
"cmd-b": [
|
"cmd-b": "workspace::ToggleLeftDock",
|
||||||
"workspace::ToggleLeftDock",
|
"cmd-r": "workspace::ToggleRightDock",
|
||||||
{ "focus": true }
|
"cmd-j": "workspace::ToggleBottomDock",
|
||||||
],
|
|
||||||
"cmd-shift-b": [
|
|
||||||
"workspace::ToggleLeftDock",
|
|
||||||
{ "focus": false }
|
|
||||||
],
|
|
||||||
"cmd-r": [
|
|
||||||
"workspace::ToggleRightDock",
|
|
||||||
{ "focus": true }
|
|
||||||
],
|
|
||||||
"cmd-shift-r": [
|
|
||||||
"workspace::ToggleRightDock",
|
|
||||||
{ "focus": false }
|
|
||||||
],
|
|
||||||
"cmd-j": [
|
|
||||||
"workspace::ToggleBottomDock",
|
|
||||||
{ "focus": true }
|
|
||||||
],
|
|
||||||
"cmd-shift-j": [
|
|
||||||
"workspace::ToggleBottomDock",
|
|
||||||
{ "focus": false }
|
|
||||||
],
|
|
||||||
"cmd-shift-f": "workspace::NewSearch",
|
"cmd-shift-f": "workspace::NewSearch",
|
||||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||||
"cmd-k cmd-s": "zed::OpenKeymap",
|
"cmd-k cmd-s": "zed::OpenKeymap",
|
||||||
|
|
25
crates/ai/Cargo.toml
Normal file
25
crates/ai/Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "ai"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/ai.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
assets = { path = "../assets"}
|
||||||
|
collections = { path = "../collections"}
|
||||||
|
editor = { path = "../editor" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
isahc.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
editor = { path = "../editor", features = ["test-support"] }
|
63
crates/ai/README.zmd
Normal file
63
crates/ai/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/ai/features.zmd
Normal file
3
crates/ai/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.
|
313
crates/ai/src/ai.rs
Normal file
313
crates/ai/src/ai.rs
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use assets::Assets;
|
||||||
|
use collections::HashMap;
|
||||||
|
use editor::Editor;
|
||||||
|
use futures::AsyncBufReadExt;
|
||||||
|
use futures::{io::BufReader, AsyncReadExt, Stream, StreamExt};
|
||||||
|
use gpui::executor::Background;
|
||||||
|
use gpui::{actions, AppContext, Task, ViewContext};
|
||||||
|
use isahc::prelude::*;
|
||||||
|
use isahc::{http::StatusCode, Request};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::fs;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::{io, sync::Arc};
|
||||||
|
use util::channel::{ReleaseChannel, RELEASE_CHANNEL};
|
||||||
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
|
||||||
|
actions!(ai, [Assist]);
|
||||||
|
|
||||||
|
// Data types for chat completion requests
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct OpenAIRequest {
|
||||||
|
model: String,
|
||||||
|
messages: Vec<RequestMessage>,
|
||||||
|
stream: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||||
|
struct RequestMessage {
|
||||||
|
role: Role,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||||
|
struct ResponseMessage {
|
||||||
|
role: Option<Role>,
|
||||||
|
content: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum Role {
|
||||||
|
User,
|
||||||
|
Assistant,
|
||||||
|
System,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct OpenAIResponseStreamEvent {
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub object: String,
|
||||||
|
pub created: u32,
|
||||||
|
pub model: String,
|
||||||
|
pub choices: Vec<ChatChoiceDelta>,
|
||||||
|
pub usage: Option<Usage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Usage {
|
||||||
|
pub prompt_tokens: u32,
|
||||||
|
pub completion_tokens: u32,
|
||||||
|
pub total_tokens: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct ChatChoiceDelta {
|
||||||
|
pub index: u32,
|
||||||
|
pub delta: ResponseMessage,
|
||||||
|
pub finish_reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct OpenAIUsage {
|
||||||
|
prompt_tokens: u64,
|
||||||
|
completion_tokens: u64,
|
||||||
|
total_tokens: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct OpenAIChoice {
|
||||||
|
text: String,
|
||||||
|
index: u32,
|
||||||
|
logprobs: Option<serde_json::Value>,
|
||||||
|
finish_reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
if *RELEASE_CHANNEL == ReleaseChannel::Stable {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let assistant = Rc::new(Assistant::default());
|
||||||
|
cx.add_action({
|
||||||
|
let assistant = assistant.clone();
|
||||||
|
move |editor: &mut Editor, _: &Assist, cx: &mut ViewContext<Editor>| {
|
||||||
|
assistant.assist(editor, cx).log_err();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cx.capture_action({
|
||||||
|
let assistant = assistant.clone();
|
||||||
|
move |_: &mut Editor, _: &editor::Cancel, cx: &mut ViewContext<Editor>| {
|
||||||
|
if !assistant.cancel_last_assist(cx.view_id()) {
|
||||||
|
cx.propagate_action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompletionId = usize;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Assistant(RefCell<AssistantState>);
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct AssistantState {
|
||||||
|
assist_stacks: HashMap<usize, Vec<(CompletionId, Task<Option<()>>)>>,
|
||||||
|
next_completion_id: CompletionId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Assistant {
|
||||||
|
fn assist(self: &Rc<Self>, editor: &mut Editor, cx: &mut ViewContext<Editor>) -> Result<()> {
|
||||||
|
let api_key = std::env::var("OPENAI_API_KEY")?;
|
||||||
|
|
||||||
|
let selections = editor.selections.all(cx);
|
||||||
|
let (user_message, insertion_site) = editor.buffer().update(cx, |buffer, cx| {
|
||||||
|
// Insert markers around selected text as described in the system prompt above.
|
||||||
|
let snapshot = buffer.snapshot(cx);
|
||||||
|
let mut user_message = String::new();
|
||||||
|
let mut user_message_suffix = String::new();
|
||||||
|
let mut buffer_offset = 0;
|
||||||
|
for selection in selections {
|
||||||
|
if !selection.is_empty() {
|
||||||
|
if user_message_suffix.is_empty() {
|
||||||
|
user_message_suffix.push_str("\n\n");
|
||||||
|
}
|
||||||
|
user_message_suffix.push_str("[Selected excerpt from above]\n");
|
||||||
|
user_message_suffix
|
||||||
|
.extend(snapshot.text_for_range(selection.start..selection.end));
|
||||||
|
user_message_suffix.push_str("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
user_message.extend(snapshot.text_for_range(buffer_offset..selection.start));
|
||||||
|
user_message.push_str("[SELECTION_START]");
|
||||||
|
user_message.extend(snapshot.text_for_range(selection.start..selection.end));
|
||||||
|
buffer_offset = selection.end;
|
||||||
|
user_message.push_str("[SELECTION_END]");
|
||||||
|
}
|
||||||
|
if buffer_offset < snapshot.len() {
|
||||||
|
user_message.extend(snapshot.text_for_range(buffer_offset..snapshot.len()));
|
||||||
|
}
|
||||||
|
user_message.push_str(&user_message_suffix);
|
||||||
|
|
||||||
|
// Ensure the document ends with 4 trailing newlines.
|
||||||
|
let trailing_newline_count = snapshot
|
||||||
|
.reversed_chars_at(snapshot.len())
|
||||||
|
.take_while(|c| *c == '\n')
|
||||||
|
.take(4);
|
||||||
|
let buffer_suffix = "\n".repeat(4 - trailing_newline_count.count());
|
||||||
|
buffer.edit([(snapshot.len()..snapshot.len(), buffer_suffix)], None, cx);
|
||||||
|
|
||||||
|
let snapshot = buffer.snapshot(cx); // Take a new snapshot after editing.
|
||||||
|
let insertion_site = snapshot.anchor_after(snapshot.len() - 2);
|
||||||
|
|
||||||
|
(user_message, insertion_site)
|
||||||
|
});
|
||||||
|
|
||||||
|
let this = self.clone();
|
||||||
|
let buffer = editor.buffer().clone();
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
let editor_id = cx.view_id();
|
||||||
|
let assist_id = util::post_inc(&mut self.0.borrow_mut().next_completion_id);
|
||||||
|
let assist_task = cx.spawn(|_, mut cx| {
|
||||||
|
async move {
|
||||||
|
// TODO: We should have a get_string method on assets. This is repateated elsewhere.
|
||||||
|
let content = Assets::get("contexts/system.zmd").unwrap();
|
||||||
|
let mut system_message = std::str::from_utf8(content.data.as_ref())
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if let Ok(custom_system_message_path) =
|
||||||
|
std::env::var("ZED_ASSISTANT_SYSTEM_PROMPT_PATH")
|
||||||
|
{
|
||||||
|
system_message.push_str(
|
||||||
|
"\n\nAlso consider the following user-defined system prompt:\n\n",
|
||||||
|
);
|
||||||
|
// TODO: Replace this with our file system trait object.
|
||||||
|
system_message.push_str(
|
||||||
|
&cx.background()
|
||||||
|
.spawn(async move { fs::read_to_string(custom_system_message_path) })
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = stream_completion(
|
||||||
|
api_key,
|
||||||
|
executor,
|
||||||
|
OpenAIRequest {
|
||||||
|
model: "gpt-4".to_string(),
|
||||||
|
messages: vec![
|
||||||
|
RequestMessage {
|
||||||
|
role: Role::System,
|
||||||
|
content: system_message.to_string(),
|
||||||
|
},
|
||||||
|
RequestMessage {
|
||||||
|
role: Role::User,
|
||||||
|
content: user_message,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stream: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut messages = stream.await?;
|
||||||
|
while let Some(message) = messages.next().await {
|
||||||
|
let mut message = message?;
|
||||||
|
if let Some(choice) = message.choices.pop() {
|
||||||
|
buffer.update(&mut cx, |buffer, cx| {
|
||||||
|
let text: Arc<str> = choice.delta.content?.into();
|
||||||
|
buffer.edit([(insertion_site.clone()..insertion_site, text)], None, cx);
|
||||||
|
Some(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.0
|
||||||
|
.borrow_mut()
|
||||||
|
.assist_stacks
|
||||||
|
.get_mut(&editor_id)
|
||||||
|
.unwrap()
|
||||||
|
.retain(|(id, _)| *id != assist_id);
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.log_err()
|
||||||
|
});
|
||||||
|
|
||||||
|
self.0
|
||||||
|
.borrow_mut()
|
||||||
|
.assist_stacks
|
||||||
|
.entry(cx.view_id())
|
||||||
|
.or_default()
|
||||||
|
.push((assist_id, assist_task));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel_last_assist(self: &Rc<Self>, editor_id: usize) -> bool {
|
||||||
|
self.0
|
||||||
|
.borrow_mut()
|
||||||
|
.assist_stacks
|
||||||
|
.get_mut(&editor_id)
|
||||||
|
.and_then(|assists| assists.pop())
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stream_completion(
|
||||||
|
api_key: String,
|
||||||
|
executor: Arc<Background>,
|
||||||
|
mut request: OpenAIRequest,
|
||||||
|
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||||
|
request.stream = true;
|
||||||
|
|
||||||
|
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
|
||||||
|
|
||||||
|
let json_data = serde_json::to_string(&request)?;
|
||||||
|
let mut response = Request::post("https://api.openai.com/v1/chat/completions")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", format!("Bearer {}", api_key))
|
||||||
|
.body(json_data)?
|
||||||
|
.send_async()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
if status == StatusCode::OK {
|
||||||
|
executor
|
||||||
|
.spawn(async move {
|
||||||
|
let mut lines = BufReader::new(response.body_mut()).lines();
|
||||||
|
|
||||||
|
fn parse_line(
|
||||||
|
line: Result<String, io::Error>,
|
||||||
|
) -> Result<Option<OpenAIResponseStreamEvent>> {
|
||||||
|
if let Some(data) = line?.strip_prefix("data: ") {
|
||||||
|
let event = serde_json::from_str(&data)?;
|
||||||
|
Ok(Some(event))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(line) = lines.next().await {
|
||||||
|
if let Some(event) = parse_line(line).transpose() {
|
||||||
|
tx.unbounded_send(event).log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Ok(rx)
|
||||||
|
} else {
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
Err(anyhow!(
|
||||||
|
"Failed to connect to OpenAI API: {} {}",
|
||||||
|
response.status(),
|
||||||
|
body,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ theme = { path = "../theme" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
isahc = "1.7"
|
isahc.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
@ -76,7 +76,7 @@ workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
|
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
indoc = "1.0.4"
|
indoc.workspace = true
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
|
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
|
||||||
|
|
|
@ -18,7 +18,7 @@ sqlez = { path = "../sqlez" }
|
||||||
sqlez_macros = { path = "../sqlez_macros" }
|
sqlez_macros = { path = "../sqlez_macros" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
indoc = "1.0.4"
|
indoc.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
|
@ -7330,7 +7330,7 @@ impl View for Editor {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
|
fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) {
|
||||||
Self::reset_to_default_keymap_context(keymap);
|
Self::reset_to_default_keymap_context(keymap);
|
||||||
let mode = match self.mode {
|
let mode = match self.mode {
|
||||||
EditorMode::SingleLine => "single_line",
|
EditorMode::SingleLine => "single_line",
|
||||||
|
@ -7346,10 +7346,18 @@ impl View for Editor {
|
||||||
Some(ContextMenu::CodeActions(_)) => keymap.add_identifier("showing_code_actions"),
|
Some(ContextMenu::CodeActions(_)) => keymap.add_identifier("showing_code_actions"),
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
for layer in self.keymap_context_layers.values() {
|
for layer in self.keymap_context_layers.values() {
|
||||||
keymap.extend(layer);
|
keymap.extend(layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(extension) = self
|
||||||
|
.buffer
|
||||||
|
.read(cx)
|
||||||
|
.as_singleton()
|
||||||
|
.and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str())
|
||||||
|
{
|
||||||
|
keymap.add_key("extension", extension.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> {
|
fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> {
|
||||||
|
|
|
@ -16,6 +16,7 @@ editor = { path = "../editor" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
|
regex.workspace = true
|
||||||
search = { path = "../search" }
|
search = { path = "../search" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
@ -27,7 +28,7 @@ futures.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
human_bytes = "0.4.1"
|
human_bytes = "0.4.1"
|
||||||
isahc = "1.7"
|
isahc.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
@ -14,6 +14,7 @@ use isahc::Request;
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use postage::prelude::Stream;
|
use postage::prelude::Stream;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
use regex::Regex;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -46,6 +47,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct FeedbackRequestBody<'a> {
|
struct FeedbackRequestBody<'a> {
|
||||||
feedback_text: &'a str,
|
feedback_text: &'a str,
|
||||||
|
email: Option<String>,
|
||||||
metrics_id: Option<Arc<str>>,
|
metrics_id: Option<Arc<str>>,
|
||||||
installation_id: Option<Arc<str>>,
|
installation_id: Option<Arc<str>>,
|
||||||
system_specs: SystemSpecs,
|
system_specs: SystemSpecs,
|
||||||
|
@ -157,8 +159,18 @@ impl FeedbackEditor {
|
||||||
let is_staff = telemetry.is_staff();
|
let is_staff = telemetry.is_staff();
|
||||||
let http_client = zed_client.http_client();
|
let http_client = zed_client.http_client();
|
||||||
|
|
||||||
|
let re = Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap();
|
||||||
|
|
||||||
|
let emails: Vec<&str> = re
|
||||||
|
.captures_iter(feedback_text)
|
||||||
|
.map(|capture| capture.get(0).unwrap().as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let email = emails.first().map(|e| e.to_string());
|
||||||
|
|
||||||
let request = FeedbackRequestBody {
|
let request = FeedbackRequestBody {
|
||||||
feedback_text: &feedback_text,
|
feedback_text: &feedback_text,
|
||||||
|
email,
|
||||||
metrics_id,
|
metrics_id,
|
||||||
installation_id,
|
installation_id,
|
||||||
system_specs,
|
system_specs,
|
||||||
|
|
|
@ -34,7 +34,7 @@ impl View for FeedbackInfoText {
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
Text::new(
|
Text::new(
|
||||||
"We read whatever you submit here. For issues and discussions, visit the ",
|
"Share your feedback. Include your email for replies. For issues and discussions, visit the ",
|
||||||
theme.feedback.info_text_default.text.clone(),
|
theme.feedback.info_text_default.text.clone(),
|
||||||
)
|
)
|
||||||
.with_soft_wrap(false)
|
.with_soft_wrap(false)
|
||||||
|
@ -60,7 +60,7 @@ impl View for FeedbackInfoText {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Text::new(" on GitHub.", theme.feedback.info_text_default.text.clone())
|
Text::new(".", theme.feedback.info_text_default.text.clone())
|
||||||
.with_soft_wrap(false)
|
.with_soft_wrap(false)
|
||||||
.aligned(),
|
.aligned(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,7 +6,7 @@ use gpui::{
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||||
use std::{
|
use std::{
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{self, AtomicBool},
|
atomic::{self, AtomicBool},
|
||||||
Arc,
|
Arc,
|
||||||
|
@ -25,11 +25,57 @@ pub struct FileFinderDelegate {
|
||||||
latest_search_id: usize,
|
latest_search_id: usize,
|
||||||
latest_search_did_cancel: bool,
|
latest_search_did_cancel: bool,
|
||||||
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
|
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
|
||||||
currently_opened_path: Option<ProjectPath>,
|
currently_opened_path: Option<FoundPath>,
|
||||||
matches: Vec<PathMatch>,
|
matches: Matches,
|
||||||
selected: Option<(usize, Arc<Path>)>,
|
selected_index: Option<usize>,
|
||||||
cancel_flag: Arc<AtomicBool>,
|
cancel_flag: Arc<AtomicBool>,
|
||||||
history_items: Vec<ProjectPath>,
|
history_items: Vec<FoundPath>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Matches {
|
||||||
|
History(Vec<FoundPath>),
|
||||||
|
Search(Vec<PathMatch>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Match<'a> {
|
||||||
|
History(&'a FoundPath),
|
||||||
|
Search(&'a PathMatch),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Matches {
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::History(items) => items.len(),
|
||||||
|
Self::Search(items) => items.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, index: usize) -> Option<Match<'_>> {
|
||||||
|
match self {
|
||||||
|
Self::History(items) => items.get(index).map(Match::History),
|
||||||
|
Self::Search(items) => items.get(index).map(Match::Search),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Matches {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::History(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct FoundPath {
|
||||||
|
project: ProjectPath,
|
||||||
|
absolute: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FoundPath {
|
||||||
|
fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
|
||||||
|
Self { project, absolute }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actions!(file_finder, [Toggle]);
|
actions!(file_finder, [Toggle]);
|
||||||
|
@ -43,10 +89,41 @@ const MAX_RECENT_SELECTIONS: usize = 20;
|
||||||
|
|
||||||
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||||
workspace.toggle_modal(cx, |workspace, cx| {
|
workspace.toggle_modal(cx, |workspace, cx| {
|
||||||
let history_items = workspace.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx);
|
let project = workspace.project().read(cx);
|
||||||
|
|
||||||
let currently_opened_path = workspace
|
let currently_opened_path = workspace
|
||||||
.active_item(cx)
|
.active_item(cx)
|
||||||
.and_then(|item| item.project_path(cx));
|
.and_then(|item| item.project_path(cx))
|
||||||
|
.map(|project_path| {
|
||||||
|
let abs_path = project
|
||||||
|
.worktree_for_id(project_path.worktree_id, cx)
|
||||||
|
.map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
|
||||||
|
FoundPath::new(project_path, abs_path)
|
||||||
|
});
|
||||||
|
|
||||||
|
// if exists, bubble the currently opened path to the top
|
||||||
|
let history_items = currently_opened_path
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.chain(
|
||||||
|
workspace
|
||||||
|
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(history_path, _)| {
|
||||||
|
Some(history_path)
|
||||||
|
!= currently_opened_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|found_path| &found_path.project)
|
||||||
|
})
|
||||||
|
.filter(|(_, history_abs_path)| {
|
||||||
|
history_abs_path.as_ref()
|
||||||
|
!= currently_opened_path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|found_path| found_path.absolute.as_ref())
|
||||||
|
})
|
||||||
|
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
let workspace = cx.handle().downgrade();
|
let workspace = cx.handle().downgrade();
|
||||||
|
@ -87,37 +164,11 @@ impl FileSearchQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileFinderDelegate {
|
impl FileFinderDelegate {
|
||||||
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
|
fn new(
|
||||||
let path = &path_match.path;
|
|
||||||
let path_string = path.to_string_lossy();
|
|
||||||
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
|
|
||||||
let path_positions = path_match.positions.clone();
|
|
||||||
|
|
||||||
let file_name = path.file_name().map_or_else(
|
|
||||||
|| path_match.path_prefix.to_string(),
|
|
||||||
|file_name| file_name.to_string_lossy().to_string(),
|
|
||||||
);
|
|
||||||
let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
|
|
||||||
- file_name.chars().count();
|
|
||||||
let file_name_positions = path_positions
|
|
||||||
.iter()
|
|
||||||
.filter_map(|pos| {
|
|
||||||
if pos >= &file_name_start {
|
|
||||||
Some(pos - file_name_start)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
(file_name, file_name_positions, full_path, path_positions)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(
|
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
currently_opened_path: Option<ProjectPath>,
|
currently_opened_path: Option<FoundPath>,
|
||||||
history_items: Vec<ProjectPath>,
|
history_items: Vec<FoundPath>,
|
||||||
cx: &mut ViewContext<FileFinder>,
|
cx: &mut ViewContext<FileFinder>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
cx.observe(&project, |picker, _, cx| {
|
cx.observe(&project, |picker, _, cx| {
|
||||||
|
@ -132,8 +183,8 @@ impl FileFinderDelegate {
|
||||||
latest_search_did_cancel: false,
|
latest_search_did_cancel: false,
|
||||||
latest_search_query: None,
|
latest_search_query: None,
|
||||||
currently_opened_path,
|
currently_opened_path,
|
||||||
matches: Vec::new(),
|
matches: Matches::default(),
|
||||||
selected: None,
|
selected_index: None,
|
||||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||||
history_items,
|
history_items,
|
||||||
}
|
}
|
||||||
|
@ -147,7 +198,7 @@ impl FileFinderDelegate {
|
||||||
let relative_to = self
|
let relative_to = self
|
||||||
.currently_opened_path
|
.currently_opened_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|project_path| Arc::clone(&project_path.path));
|
.map(|found_path| Arc::clone(&found_path.project.path));
|
||||||
let worktrees = self
|
let worktrees = self
|
||||||
.project
|
.project
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -188,13 +239,13 @@ impl FileFinderDelegate {
|
||||||
.update(&mut cx, |picker, cx| {
|
.update(&mut cx, |picker, cx| {
|
||||||
picker
|
picker
|
||||||
.delegate_mut()
|
.delegate_mut()
|
||||||
.set_matches(search_id, did_cancel, query, matches, cx)
|
.set_search_matches(search_id, did_cancel, query, matches, cx)
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_matches(
|
fn set_search_matches(
|
||||||
&mut self,
|
&mut self,
|
||||||
search_id: usize,
|
search_id: usize,
|
||||||
did_cancel: bool,
|
did_cancel: bool,
|
||||||
|
@ -211,15 +262,126 @@ impl FileFinderDelegate {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|query| query.path_like.path_query())
|
.map(|query| query.path_like.path_query())
|
||||||
{
|
{
|
||||||
util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
|
match &mut self.matches {
|
||||||
|
Matches::History(_) => self.matches = Matches::Search(matches),
|
||||||
|
Matches::Search(search_matches) => {
|
||||||
|
util::extend_sorted(search_matches, matches.into_iter(), 100, |a, b| {
|
||||||
|
b.cmp(a)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.matches = matches;
|
self.matches = Matches::Search(matches);
|
||||||
}
|
}
|
||||||
self.latest_search_query = Some(query);
|
self.latest_search_query = Some(query);
|
||||||
self.latest_search_did_cancel = did_cancel;
|
self.latest_search_did_cancel = did_cancel;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn labels_for_match(
|
||||||
|
&self,
|
||||||
|
path_match: Match,
|
||||||
|
cx: &AppContext,
|
||||||
|
ix: usize,
|
||||||
|
) -> (String, Vec<usize>, String, Vec<usize>) {
|
||||||
|
let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
|
||||||
|
Match::History(found_path) => {
|
||||||
|
let worktree_id = found_path.project.worktree_id;
|
||||||
|
let project_relative_path = &found_path.project.path;
|
||||||
|
let has_worktree = self
|
||||||
|
.project
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(worktree_id, cx)
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
if !has_worktree {
|
||||||
|
if let Some(absolute_path) = &found_path.absolute {
|
||||||
|
return (
|
||||||
|
absolute_path
|
||||||
|
.file_name()
|
||||||
|
.map_or_else(
|
||||||
|
|| project_relative_path.to_string_lossy(),
|
||||||
|
|file_name| file_name.to_string_lossy(),
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
Vec::new(),
|
||||||
|
absolute_path.to_string_lossy().to_string(),
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut path = Arc::clone(project_relative_path);
|
||||||
|
if project_relative_path.as_ref() == Path::new("") {
|
||||||
|
if let Some(absolute_path) = &found_path.absolute {
|
||||||
|
path = Arc::from(absolute_path.as_path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.labels_for_path_match(&PathMatch {
|
||||||
|
score: ix as f64,
|
||||||
|
positions: Vec::new(),
|
||||||
|
worktree_id: worktree_id.to_usize(),
|
||||||
|
path,
|
||||||
|
path_prefix: "".into(),
|
||||||
|
distance_to_relative_ancestor: usize::MAX,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Match::Search(path_match) => self.labels_for_path_match(path_match),
|
||||||
|
};
|
||||||
|
|
||||||
|
if file_name_positions.is_empty() {
|
||||||
|
if let Some(user_home_path) = std::env::var("HOME").ok() {
|
||||||
|
let user_home_path = user_home_path.trim();
|
||||||
|
if !user_home_path.is_empty() {
|
||||||
|
if (&full_path).starts_with(user_home_path) {
|
||||||
|
return (
|
||||||
|
file_name,
|
||||||
|
file_name_positions,
|
||||||
|
full_path.replace(user_home_path, "~"),
|
||||||
|
full_path_positions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
file_name,
|
||||||
|
file_name_positions,
|
||||||
|
full_path,
|
||||||
|
full_path_positions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn labels_for_path_match(
|
||||||
|
&self,
|
||||||
|
path_match: &PathMatch,
|
||||||
|
) -> (String, Vec<usize>, String, Vec<usize>) {
|
||||||
|
let path = &path_match.path;
|
||||||
|
let path_string = path.to_string_lossy();
|
||||||
|
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
|
||||||
|
let path_positions = path_match.positions.clone();
|
||||||
|
|
||||||
|
let file_name = path.file_name().map_or_else(
|
||||||
|
|| path_match.path_prefix.to_string(),
|
||||||
|
|file_name| file_name.to_string_lossy().to_string(),
|
||||||
|
);
|
||||||
|
let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
|
||||||
|
- file_name.chars().count();
|
||||||
|
let file_name_positions = path_positions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|pos| {
|
||||||
|
if pos >= &file_name_start {
|
||||||
|
Some(pos - file_name_start)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(file_name, file_name_positions, full_path, path_positions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PickerDelegate for FileFinderDelegate {
|
impl PickerDelegate for FileFinderDelegate {
|
||||||
|
@ -232,45 +394,35 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selected_index(&self) -> usize {
|
fn selected_index(&self) -> usize {
|
||||||
if let Some(selected) = self.selected.as_ref() {
|
self.selected_index.unwrap_or(0)
|
||||||
for (ix, path_match) in self.matches.iter().enumerate() {
|
|
||||||
if (path_match.worktree_id, path_match.path.as_ref())
|
|
||||||
== (selected.0, selected.1.as_ref())
|
|
||||||
{
|
|
||||||
return ix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) {
|
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) {
|
||||||
let mat = &self.matches[ix];
|
self.selected_index = Some(ix);
|
||||||
self.selected = Some((mat.worktree_id, mat.path.clone()));
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
|
fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
|
||||||
if raw_query.is_empty() {
|
if raw_query.is_empty() {
|
||||||
|
let project = self.project.read(cx);
|
||||||
self.latest_search_id = post_inc(&mut self.search_count);
|
self.latest_search_id = post_inc(&mut self.search_count);
|
||||||
self.matches.clear();
|
self.matches = Matches::History(
|
||||||
|
self.history_items
|
||||||
self.matches = self
|
.iter()
|
||||||
.currently_opened_path
|
.filter(|history_item| {
|
||||||
.iter() // if exists, bubble the currently opened path to the top
|
project
|
||||||
.chain(self.history_items.iter().filter(|history_item| {
|
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||||
Some(*history_item) != self.currently_opened_path.as_ref()
|
.is_some()
|
||||||
}))
|
|| (project.is_local()
|
||||||
.enumerate()
|
&& history_item
|
||||||
.map(|(i, history_item)| PathMatch {
|
.absolute
|
||||||
score: i as f64,
|
.as_ref()
|
||||||
positions: Vec::new(),
|
.filter(|abs_path| abs_path.exists())
|
||||||
worktree_id: history_item.worktree_id.to_usize(),
|
.is_some())
|
||||||
path: Arc::clone(&history_item.path),
|
|
||||||
path_prefix: "".into(),
|
|
||||||
distance_to_relative_ancestor: usize::MAX,
|
|
||||||
})
|
})
|
||||||
.collect();
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
Task::ready(())
|
Task::ready(())
|
||||||
} else {
|
} else {
|
||||||
|
@ -293,16 +445,52 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
|
fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
|
||||||
if let Some(m) = self.matches.get(self.selected_index()) {
|
if let Some(m) = self.matches.get(self.selected_index()) {
|
||||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
let project_path = ProjectPath {
|
let open_task = workspace.update(cx, |workspace, cx| match m {
|
||||||
|
Match::History(history_match) => {
|
||||||
|
let worktree_id = history_match.project.worktree_id;
|
||||||
|
if workspace
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(worktree_id, cx)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
workspace.open_path(
|
||||||
|
ProjectPath {
|
||||||
|
worktree_id,
|
||||||
|
path: Arc::clone(&history_match.project.path),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
match history_match.absolute.as_ref() {
|
||||||
|
Some(abs_path) => {
|
||||||
|
workspace.open_abs_path(abs_path.to_path_buf(), false, cx)
|
||||||
|
}
|
||||||
|
None => workspace.open_path(
|
||||||
|
ProjectPath {
|
||||||
|
worktree_id,
|
||||||
|
path: Arc::clone(&history_match.project.path),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Match::Search(m) => workspace.open_path(
|
||||||
|
ProjectPath {
|
||||||
worktree_id: WorktreeId::from_usize(m.worktree_id),
|
worktree_id: WorktreeId::from_usize(m.worktree_id),
|
||||||
path: m.path.clone(),
|
path: m.path.clone(),
|
||||||
};
|
},
|
||||||
let open_task = workspace.update(cx, |workspace, cx| {
|
None,
|
||||||
workspace.open_path(project_path.clone(), None, true, cx)
|
true,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
let workspace = workspace.downgrade();
|
|
||||||
|
|
||||||
let row = self
|
let row = self
|
||||||
.latest_search_query
|
.latest_search_query
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -333,6 +521,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
workspace
|
workspace
|
||||||
|
.downgrade()
|
||||||
.update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
|
.update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
|
||||||
.log_err();
|
.log_err();
|
||||||
|
|
||||||
|
@ -352,11 +541,14 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> AnyElement<Picker<Self>> {
|
) -> AnyElement<Picker<Self>> {
|
||||||
let path_match = &self.matches[ix];
|
let path_match = self
|
||||||
|
.matches
|
||||||
|
.get(ix)
|
||||||
|
.expect("Invalid matches state: no element for index {ix}");
|
||||||
let theme = theme::current(cx);
|
let theme = theme::current(cx);
|
||||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||||
self.labels_for_match(path_match);
|
self.labels_for_match(path_match, cx, ix);
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child(
|
.with_child(
|
||||||
Label::new(file_name, style.label.clone()).with_highlights(file_name_positions),
|
Label::new(file_name, style.label.clone()).with_highlights(file_name_positions),
|
||||||
|
@ -373,7 +565,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{assert_eq, collections::HashMap, time::Duration};
|
use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
|
@ -649,12 +841,16 @@ mod tests {
|
||||||
|
|
||||||
finder.update(cx, |finder, cx| {
|
finder.update(cx, |finder, cx| {
|
||||||
let delegate = finder.delegate_mut();
|
let delegate = finder.delegate_mut();
|
||||||
let matches = delegate.matches.clone();
|
let matches = match &delegate.matches {
|
||||||
|
Matches::Search(path_matches) => path_matches,
|
||||||
|
_ => panic!("Search matches expected"),
|
||||||
|
}
|
||||||
|
.clone();
|
||||||
|
|
||||||
// Simulate a search being cancelled after the time limit,
|
// Simulate a search being cancelled after the time limit,
|
||||||
// returning only a subset of the matches that would have been found.
|
// returning only a subset of the matches that would have been found.
|
||||||
drop(delegate.spawn_search(query.clone(), cx));
|
drop(delegate.spawn_search(query.clone(), cx));
|
||||||
delegate.set_matches(
|
delegate.set_search_matches(
|
||||||
delegate.latest_search_id,
|
delegate.latest_search_id,
|
||||||
true, // did-cancel
|
true, // did-cancel
|
||||||
query.clone(),
|
query.clone(),
|
||||||
|
@ -664,7 +860,7 @@ mod tests {
|
||||||
|
|
||||||
// Simulate another cancellation.
|
// Simulate another cancellation.
|
||||||
drop(delegate.spawn_search(query.clone(), cx));
|
drop(delegate.spawn_search(query.clone(), cx));
|
||||||
delegate.set_matches(
|
delegate.set_search_matches(
|
||||||
delegate.latest_search_id,
|
delegate.latest_search_id,
|
||||||
true, // did-cancel
|
true, // did-cancel
|
||||||
query.clone(),
|
query.clone(),
|
||||||
|
@ -672,7 +868,12 @@ mod tests {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(delegate.matches, matches[0..4])
|
match &delegate.matches {
|
||||||
|
Matches::Search(new_matches) => {
|
||||||
|
assert_eq!(new_matches.as_slice(), &matches[0..4])
|
||||||
|
}
|
||||||
|
_ => panic!("Search matches expected"),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -772,10 +973,14 @@ mod tests {
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
let finder = finder.read(cx);
|
let finder = finder.read(cx);
|
||||||
let delegate = finder.delegate();
|
let delegate = finder.delegate();
|
||||||
assert_eq!(delegate.matches.len(), 1);
|
let matches = match &delegate.matches {
|
||||||
|
Matches::Search(path_matches) => path_matches,
|
||||||
|
_ => panic!("Search matches expected"),
|
||||||
|
};
|
||||||
|
assert_eq!(matches.len(), 1);
|
||||||
|
|
||||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||||
delegate.labels_for_match(&delegate.matches[0]);
|
delegate.labels_for_path_match(&matches[0]);
|
||||||
assert_eq!(file_name, "the-file");
|
assert_eq!(file_name, "the-file");
|
||||||
assert_eq!(file_name_positions, &[0, 1, 4]);
|
assert_eq!(file_name_positions, &[0, 1, 4]);
|
||||||
assert_eq!(full_path, "the-file");
|
assert_eq!(full_path, "the-file");
|
||||||
|
@ -876,10 +1081,10 @@ mod tests {
|
||||||
// When workspace has an active item, sort items which are closer to that item
|
// When workspace has an active item, sort items which are closer to that item
|
||||||
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
|
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
|
||||||
// so that one should be sorted earlier
|
// so that one should be sorted earlier
|
||||||
let b_path = Some(ProjectPath {
|
let b_path = Some(dummy_found_path(ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: Arc::from(Path::new("/root/dir2/b.txt")),
|
path: Arc::from(Path::new("/root/dir2/b.txt")),
|
||||||
});
|
}));
|
||||||
let (_, finder) = cx.add_window(|cx| {
|
let (_, finder) = cx.add_window(|cx| {
|
||||||
Picker::new(
|
Picker::new(
|
||||||
FileFinderDelegate::new(
|
FileFinderDelegate::new(
|
||||||
|
@ -901,8 +1106,12 @@ mod tests {
|
||||||
|
|
||||||
finder.read_with(cx, |f, _| {
|
finder.read_with(cx, |f, _| {
|
||||||
let delegate = f.delegate();
|
let delegate = f.delegate();
|
||||||
assert_eq!(delegate.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
|
let matches = match &delegate.matches {
|
||||||
assert_eq!(delegate.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
|
Matches::Search(path_matches) => path_matches,
|
||||||
|
_ => panic!("Search matches expected"),
|
||||||
|
};
|
||||||
|
assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
|
||||||
|
assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1012,10 +1221,13 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
history_after_first,
|
history_after_first,
|
||||||
vec![ProjectPath {
|
vec![FoundPath::new(
|
||||||
|
ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: Arc::from(Path::new("test/first.rs")),
|
path: Arc::from(Path::new("test/first.rs")),
|
||||||
}],
|
},
|
||||||
|
Some(PathBuf::from("/src/test/first.rs"))
|
||||||
|
)],
|
||||||
"Should show 1st opened item in the history when opening the 2nd item"
|
"Should show 1st opened item in the history when opening the 2nd item"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1032,14 +1244,20 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
history_after_second,
|
history_after_second,
|
||||||
vec![
|
vec![
|
||||||
|
FoundPath::new(
|
||||||
ProjectPath {
|
ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: Arc::from(Path::new("test/second.rs")),
|
path: Arc::from(Path::new("test/second.rs")),
|
||||||
},
|
},
|
||||||
|
Some(PathBuf::from("/src/test/second.rs"))
|
||||||
|
),
|
||||||
|
FoundPath::new(
|
||||||
ProjectPath {
|
ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: Arc::from(Path::new("test/first.rs")),
|
path: Arc::from(Path::new("test/first.rs")),
|
||||||
},
|
},
|
||||||
|
Some(PathBuf::from("/src/test/first.rs"))
|
||||||
|
),
|
||||||
],
|
],
|
||||||
"Should show 1st and 2nd opened items in the history when opening the 3rd item. \
|
"Should show 1st and 2nd opened items in the history when opening the 3rd item. \
|
||||||
2nd item should be the first in the history, as the last opened."
|
2nd item should be the first in the history, as the last opened."
|
||||||
|
@ -1058,18 +1276,27 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
history_after_third,
|
history_after_third,
|
||||||
vec![
|
vec![
|
||||||
|
FoundPath::new(
|
||||||
ProjectPath {
|
ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: Arc::from(Path::new("test/third.rs")),
|
path: Arc::from(Path::new("test/third.rs")),
|
||||||
},
|
},
|
||||||
|
Some(PathBuf::from("/src/test/third.rs"))
|
||||||
|
),
|
||||||
|
FoundPath::new(
|
||||||
ProjectPath {
|
ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: Arc::from(Path::new("test/second.rs")),
|
path: Arc::from(Path::new("test/second.rs")),
|
||||||
},
|
},
|
||||||
|
Some(PathBuf::from("/src/test/second.rs"))
|
||||||
|
),
|
||||||
|
FoundPath::new(
|
||||||
ProjectPath {
|
ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: Arc::from(Path::new("test/first.rs")),
|
path: Arc::from(Path::new("test/first.rs")),
|
||||||
},
|
},
|
||||||
|
Some(PathBuf::from("/src/test/first.rs"))
|
||||||
|
),
|
||||||
],
|
],
|
||||||
"Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
|
"Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
|
||||||
3rd item should be the first in the history, as the last opened."
|
3rd item should be the first in the history, as the last opened."
|
||||||
|
@ -1088,24 +1315,162 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
history_after_second_again,
|
history_after_second_again,
|
||||||
vec![
|
vec![
|
||||||
|
FoundPath::new(
|
||||||
ProjectPath {
|
ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: Arc::from(Path::new("test/second.rs")),
|
path: Arc::from(Path::new("test/second.rs")),
|
||||||
},
|
},
|
||||||
|
Some(PathBuf::from("/src/test/second.rs"))
|
||||||
|
),
|
||||||
|
FoundPath::new(
|
||||||
ProjectPath {
|
ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: Arc::from(Path::new("test/third.rs")),
|
path: Arc::from(Path::new("test/third.rs")),
|
||||||
},
|
},
|
||||||
|
Some(PathBuf::from("/src/test/third.rs"))
|
||||||
|
),
|
||||||
|
FoundPath::new(
|
||||||
ProjectPath {
|
ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: Arc::from(Path::new("test/first.rs")),
|
path: Arc::from(Path::new("test/first.rs")),
|
||||||
},
|
},
|
||||||
|
Some(PathBuf::from("/src/test/first.rs"))
|
||||||
|
),
|
||||||
],
|
],
|
||||||
"Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
|
"Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
|
||||||
2nd item, as the last opened, 3rd item should go next as it was opened right before."
|
2nd item, as the last opened, 3rd item should go next as it was opened right before."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_external_files_history(
|
||||||
|
deterministic: Arc<gpui::executor::Deterministic>,
|
||||||
|
cx: &mut gpui::TestAppContext,
|
||||||
|
) {
|
||||||
|
let app_state = init_test(cx);
|
||||||
|
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/src",
|
||||||
|
json!({
|
||||||
|
"test": {
|
||||||
|
"first.rs": "// First Rust file",
|
||||||
|
"second.rs": "// Second Rust file",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/external-src",
|
||||||
|
json!({
|
||||||
|
"test": {
|
||||||
|
"third.rs": "// Third Rust file",
|
||||||
|
"fourth.rs": "// Fourth Rust file",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
|
cx.update(|cx| {
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
project.find_or_create_local_worktree("/external-src", false, cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
|
let worktree_id = cx.read(|cx| {
|
||||||
|
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
||||||
|
assert_eq!(worktrees.len(), 1,);
|
||||||
|
|
||||||
|
WorktreeId::from_usize(worktrees[0].id())
|
||||||
|
});
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
let external_worktree_id = cx.read(|cx| {
|
||||||
|
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
worktrees.len(),
|
||||||
|
2,
|
||||||
|
"External file should get opened in a new worktree"
|
||||||
|
);
|
||||||
|
|
||||||
|
WorktreeId::from_usize(
|
||||||
|
worktrees
|
||||||
|
.into_iter()
|
||||||
|
.find(|worktree| worktree.id() != worktree_id.to_usize())
|
||||||
|
.expect("New worktree should have a different id")
|
||||||
|
.id(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
close_active_item(&workspace, &deterministic, cx).await;
|
||||||
|
|
||||||
|
let initial_history_items = open_close_queried_buffer(
|
||||||
|
"sec",
|
||||||
|
1,
|
||||||
|
"second.rs",
|
||||||
|
window_id,
|
||||||
|
&workspace,
|
||||||
|
&deterministic,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
initial_history_items,
|
||||||
|
vec![FoundPath::new(
|
||||||
|
ProjectPath {
|
||||||
|
worktree_id: external_worktree_id,
|
||||||
|
path: Arc::from(Path::new("")),
|
||||||
|
},
|
||||||
|
Some(PathBuf::from("/external-src/test/third.rs"))
|
||||||
|
)],
|
||||||
|
"Should show external file with its full path in the history after it was open"
|
||||||
|
);
|
||||||
|
|
||||||
|
let updated_history_items = open_close_queried_buffer(
|
||||||
|
"fir",
|
||||||
|
1,
|
||||||
|
"first.rs",
|
||||||
|
window_id,
|
||||||
|
&workspace,
|
||||||
|
&deterministic,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
updated_history_items,
|
||||||
|
vec![
|
||||||
|
FoundPath::new(
|
||||||
|
ProjectPath {
|
||||||
|
worktree_id,
|
||||||
|
path: Arc::from(Path::new("test/second.rs")),
|
||||||
|
},
|
||||||
|
Some(PathBuf::from("/src/test/second.rs"))
|
||||||
|
),
|
||||||
|
FoundPath::new(
|
||||||
|
ProjectPath {
|
||||||
|
worktree_id: external_worktree_id,
|
||||||
|
path: Arc::from(Path::new("")),
|
||||||
|
},
|
||||||
|
Some(PathBuf::from("/external-src/test/third.rs"))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"Should keep external file with history updates",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async fn open_close_queried_buffer(
|
async fn open_close_queried_buffer(
|
||||||
input: &str,
|
input: &str,
|
||||||
expected_matches: usize,
|
expected_matches: usize,
|
||||||
|
@ -1114,7 +1479,7 @@ mod tests {
|
||||||
workspace: &ViewHandle<Workspace>,
|
workspace: &ViewHandle<Workspace>,
|
||||||
deterministic: &gpui::executor::Deterministic,
|
deterministic: &gpui::executor::Deterministic,
|
||||||
cx: &mut gpui::TestAppContext,
|
cx: &mut gpui::TestAppContext,
|
||||||
) -> Vec<ProjectPath> {
|
) -> Vec<FoundPath> {
|
||||||
cx.dispatch_action(window_id, Toggle);
|
cx.dispatch_action(window_id, Toggle);
|
||||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||||
finder
|
finder
|
||||||
|
@ -1152,6 +1517,16 @@ mod tests {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
close_active_item(workspace, deterministic, cx).await;
|
||||||
|
|
||||||
|
history_items
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close_active_item(
|
||||||
|
workspace: &ViewHandle<Workspace>,
|
||||||
|
deterministic: &gpui::executor::Deterministic,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
let mut original_items = HashMap::new();
|
let mut original_items = HashMap::new();
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
for pane in workspace.read(cx).panes() {
|
for pane in workspace.read(cx).panes() {
|
||||||
|
@ -1161,6 +1536,8 @@ mod tests {
|
||||||
assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
|
assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
|
||||||
active_pane
|
active_pane
|
||||||
.update(cx, |pane, cx| {
|
.update(cx, |pane, cx| {
|
||||||
pane.close_active_item(&workspace::CloseActiveItem, cx)
|
pane.close_active_item(&workspace::CloseActiveItem, cx)
|
||||||
|
@ -1185,8 +1562,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
assert!(
|
||||||
history_items
|
original_items.len() <= 1,
|
||||||
|
"At most one panel should got closed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||||
|
@ -1215,4 +1594,11 @@ mod tests {
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
|
||||||
|
FoundPath {
|
||||||
|
project: project_path,
|
||||||
|
absolute: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -960,7 +960,7 @@ impl<T: 'static, E: 'static + Display> Task<Result<T, E>> {
|
||||||
pub fn detach_and_log_err(self, cx: &mut AppContext) {
|
pub fn detach_and_log_err(self, cx: &mut AppContext) {
|
||||||
cx.spawn(|_| async move {
|
cx.spawn(|_| async move {
|
||||||
if let Err(err) = self.await {
|
if let Err(err) = self.await {
|
||||||
log::error!("{}", err);
|
log::error!("{:#}", err);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
|
@ -25,8 +25,9 @@ struct Family {
|
||||||
pub struct FontCache(RwLock<FontCacheState>);
|
pub struct FontCache(RwLock<FontCacheState>);
|
||||||
|
|
||||||
pub struct FontCacheState {
|
pub struct FontCacheState {
|
||||||
fonts: Arc<dyn platform::FontSystem>,
|
font_system: Arc<dyn platform::FontSystem>,
|
||||||
families: Vec<Family>,
|
families: Vec<Family>,
|
||||||
|
default_family: Option<FamilyId>,
|
||||||
font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>,
|
font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>,
|
||||||
metrics: HashMap<FontId, Metrics>,
|
metrics: HashMap<FontId, Metrics>,
|
||||||
wrapper_pool: HashMap<(FontId, OrderedFloat<f32>), Vec<LineWrapper>>,
|
wrapper_pool: HashMap<(FontId, OrderedFloat<f32>), Vec<LineWrapper>>,
|
||||||
|
@ -42,8 +43,9 @@ unsafe impl Send for FontCache {}
|
||||||
impl FontCache {
|
impl FontCache {
|
||||||
pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
|
pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
|
||||||
Self(RwLock::new(FontCacheState {
|
Self(RwLock::new(FontCacheState {
|
||||||
fonts,
|
font_system: fonts,
|
||||||
families: Default::default(),
|
families: Default::default(),
|
||||||
|
default_family: None,
|
||||||
font_selections: Default::default(),
|
font_selections: Default::default(),
|
||||||
metrics: Default::default(),
|
metrics: Default::default(),
|
||||||
wrapper_pool: Default::default(),
|
wrapper_pool: Default::default(),
|
||||||
|
@ -73,14 +75,14 @@ impl FontCache {
|
||||||
|
|
||||||
let mut state = RwLockUpgradableReadGuard::upgrade(state);
|
let mut state = RwLockUpgradableReadGuard::upgrade(state);
|
||||||
|
|
||||||
if let Ok(font_ids) = state.fonts.load_family(name, features) {
|
if let Ok(font_ids) = state.font_system.load_family(name, features) {
|
||||||
if font_ids.is_empty() {
|
if font_ids.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let family_id = FamilyId(state.families.len());
|
let family_id = FamilyId(state.families.len());
|
||||||
for font_id in &font_ids {
|
for font_id in &font_ids {
|
||||||
if state.fonts.glyph_for_char(*font_id, 'm').is_none() {
|
if state.font_system.glyph_for_char(*font_id, 'm').is_none() {
|
||||||
return Err(anyhow!("font must contain a glyph for the 'm' character"));
|
return Err(anyhow!("font must contain a glyph for the 'm' character"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,6 +101,31 @@ impl FontCache {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an arbitrary font family that is available on the system.
|
||||||
|
pub fn known_existing_family(&self) -> FamilyId {
|
||||||
|
if let Some(family_id) = self.0.read().default_family {
|
||||||
|
return family_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let default_family = self
|
||||||
|
.load_family(
|
||||||
|
&["Courier", "Helvetica", "Arial", "Verdana"],
|
||||||
|
&Default::default(),
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
let all_family_names = self.0.read().font_system.all_families();
|
||||||
|
let all_family_names: Vec<_> = all_family_names
|
||||||
|
.iter()
|
||||||
|
.map(|string| string.as_str())
|
||||||
|
.collect();
|
||||||
|
self.load_family(&all_family_names, &Default::default())
|
||||||
|
.expect("could not load any default font family")
|
||||||
|
});
|
||||||
|
|
||||||
|
self.0.write().default_family = Some(default_family);
|
||||||
|
default_family
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_font(&self, family_id: FamilyId) -> FontId {
|
pub fn default_font(&self, family_id: FamilyId) -> FontId {
|
||||||
self.select_font(family_id, &Properties::default()).unwrap()
|
self.select_font(family_id, &Properties::default()).unwrap()
|
||||||
}
|
}
|
||||||
|
@ -115,7 +142,7 @@ impl FontCache {
|
||||||
let mut inner = RwLockUpgradableReadGuard::upgrade(inner);
|
let mut inner = RwLockUpgradableReadGuard::upgrade(inner);
|
||||||
let family = &inner.families[family_id.0];
|
let family = &inner.families[family_id.0];
|
||||||
let font_id = inner
|
let font_id = inner
|
||||||
.fonts
|
.font_system
|
||||||
.select_font(&family.font_ids, properties)
|
.select_font(&family.font_ids, properties)
|
||||||
.unwrap_or(family.font_ids[0]);
|
.unwrap_or(family.font_ids[0]);
|
||||||
|
|
||||||
|
@ -137,7 +164,7 @@ impl FontCache {
|
||||||
if let Some(metrics) = state.metrics.get(&font_id) {
|
if let Some(metrics) = state.metrics.get(&font_id) {
|
||||||
f(metrics)
|
f(metrics)
|
||||||
} else {
|
} else {
|
||||||
let metrics = state.fonts.font_metrics(font_id);
|
let metrics = state.font_system.font_metrics(font_id);
|
||||||
let metric = f(&metrics);
|
let metric = f(&metrics);
|
||||||
let mut state = RwLockUpgradableReadGuard::upgrade(state);
|
let mut state = RwLockUpgradableReadGuard::upgrade(state);
|
||||||
state.metrics.insert(font_id, metrics);
|
state.metrics.insert(font_id, metrics);
|
||||||
|
@ -157,8 +184,11 @@ impl FontCache {
|
||||||
let bounds;
|
let bounds;
|
||||||
{
|
{
|
||||||
let state = self.0.read();
|
let state = self.0.read();
|
||||||
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
|
glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
|
||||||
bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap();
|
bounds = state
|
||||||
|
.font_system
|
||||||
|
.typographic_bounds(font_id, glyph_id)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
bounds.width() * self.em_scale(font_id, font_size)
|
bounds.width() * self.em_scale(font_id, font_size)
|
||||||
}
|
}
|
||||||
|
@ -168,8 +198,8 @@ impl FontCache {
|
||||||
let advance;
|
let advance;
|
||||||
{
|
{
|
||||||
let state = self.0.read();
|
let state = self.0.read();
|
||||||
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
|
glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
|
||||||
advance = state.fonts.advance(font_id, glyph_id).unwrap();
|
advance = state.font_system.advance(font_id, glyph_id).unwrap();
|
||||||
}
|
}
|
||||||
advance.x() * self.em_scale(font_id, font_size)
|
advance.x() * self.em_scale(font_id, font_size)
|
||||||
}
|
}
|
||||||
|
@ -214,7 +244,7 @@ impl FontCache {
|
||||||
.or_default();
|
.or_default();
|
||||||
let wrapper = wrappers
|
let wrapper = wrappers
|
||||||
.pop()
|
.pop()
|
||||||
.unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.fonts.clone()));
|
.unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.font_system.clone()));
|
||||||
LineWrapperHandle {
|
LineWrapperHandle {
|
||||||
wrapper: Some(wrapper),
|
wrapper: Some(wrapper),
|
||||||
font_cache: self.clone(),
|
font_cache: self.clone(),
|
||||||
|
|
|
@ -295,13 +295,14 @@ impl Default for TextStyle {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("TextStyle::default can only be called within a call to with_font_cache");
|
.expect("TextStyle::default can only be called within a call to with_font_cache");
|
||||||
|
|
||||||
let font_family_name = Arc::from("Courier");
|
let font_family_id = font_cache.known_existing_family();
|
||||||
let font_family_id = font_cache
|
|
||||||
.load_family(&[&font_family_name], &Default::default())
|
|
||||||
.unwrap();
|
|
||||||
let font_id = font_cache
|
let font_id = font_cache
|
||||||
.select_font(font_family_id, &Default::default())
|
.select_font(font_family_id, &Default::default())
|
||||||
.unwrap();
|
.expect("did not have any font in system-provided family");
|
||||||
|
let font_family_name = font_cache
|
||||||
|
.family_name(font_family_id)
|
||||||
|
.expect("we loaded this family from the font cache, so this should work");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
color: Default::default(),
|
color: Default::default(),
|
||||||
font_family_name,
|
font_family_name,
|
||||||
|
|
|
@ -343,6 +343,7 @@ pub enum RasterizationOptions {
|
||||||
|
|
||||||
pub trait FontSystem: Send + Sync {
|
pub trait FontSystem: Send + Sync {
|
||||||
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
|
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
|
||||||
|
fn all_families(&self) -> Vec<String>;
|
||||||
fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>;
|
fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>;
|
||||||
fn select_font(
|
fn select_font(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -66,6 +66,14 @@ impl platform::FontSystem for FontSystem {
|
||||||
self.0.write().add_fonts(fonts)
|
self.0.write().add_fonts(fonts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn all_families(&self) -> Vec<String> {
|
||||||
|
self.0
|
||||||
|
.read()
|
||||||
|
.system_source
|
||||||
|
.all_families()
|
||||||
|
.expect("core text should never return an error")
|
||||||
|
}
|
||||||
|
|
||||||
fn load_family(&self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
|
fn load_family(&self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
|
||||||
self.0.write().load_family(name, features)
|
self.0.write().load_family(name, features)
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ settings = { path = "../settings", features = ["test-support"] }
|
||||||
util = { path = "../util", features = ["test-support"] }
|
util = { path = "../util", features = ["test-support"] }
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
indoc = "1.0.4"
|
indoc.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
tree-sitter-embedded-template = "*"
|
tree-sitter-embedded-template = "*"
|
||||||
tree-sitter-html = "*"
|
tree-sitter-html = "*"
|
||||||
|
|
|
@ -46,6 +46,7 @@ collections = { path = "../collections", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
live_kit_server = { path = "../live_kit_server" }
|
live_kit_server = { path = "../live_kit_server" }
|
||||||
media = { path = "../media" }
|
media = { path = "../media" }
|
||||||
|
nanoid = "0.4"
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
|
|
@ -3089,7 +3089,7 @@ impl Project {
|
||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), anyhow::Error> {
|
||||||
let (worktree, relative_path) = self
|
let (worktree, relative_path) = self
|
||||||
.find_local_worktree(&abs_path, cx)
|
.find_local_worktree(&abs_path, cx)
|
||||||
.ok_or_else(|| anyhow!("no worktree found for diagnostics"))?;
|
.ok_or_else(|| anyhow!("no worktree found for diagnostics path {abs_path:?}"))?;
|
||||||
|
|
||||||
let project_path = ProjectPath {
|
let project_path = ProjectPath {
|
||||||
worktree_id: worktree.read(cx).id(),
|
worktree_id: worktree.read(cx).id(),
|
||||||
|
|
|
@ -44,11 +44,11 @@ struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSe
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.set_global(ActiveSearches::default());
|
cx.set_global(ActiveSearches::default());
|
||||||
cx.add_action(ProjectSearchView::deploy);
|
cx.add_action(ProjectSearchView::deploy);
|
||||||
|
cx.add_action(ProjectSearchView::move_focus_to_results);
|
||||||
cx.add_action(ProjectSearchBar::search);
|
cx.add_action(ProjectSearchBar::search);
|
||||||
cx.add_action(ProjectSearchBar::search_in_new);
|
cx.add_action(ProjectSearchBar::search_in_new);
|
||||||
cx.add_action(ProjectSearchBar::select_next_match);
|
cx.add_action(ProjectSearchBar::select_next_match);
|
||||||
cx.add_action(ProjectSearchBar::select_prev_match);
|
cx.add_action(ProjectSearchBar::select_prev_match);
|
||||||
cx.add_action(ProjectSearchBar::move_focus_to_results);
|
|
||||||
cx.capture_action(ProjectSearchBar::tab);
|
cx.capture_action(ProjectSearchBar::tab);
|
||||||
cx.capture_action(ProjectSearchBar::tab_previous);
|
cx.capture_action(ProjectSearchBar::tab_previous);
|
||||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
||||||
|
@ -717,6 +717,23 @@ impl ProjectSearchView {
|
||||||
pub fn has_matches(&self) -> bool {
|
pub fn has_matches(&self) -> bool {
|
||||||
self.active_match_index.is_some()
|
self.active_match_index.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
|
||||||
|
if let Some(search_view) = pane
|
||||||
|
.active_item()
|
||||||
|
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||||
|
{
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
if !search_view.results_editor.is_focused(cx)
|
||||||
|
&& !search_view.model.read(cx).match_ranges.is_empty()
|
||||||
|
{
|
||||||
|
return search_view.focus_results_editor(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.propagate_action();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProjectSearchBar {
|
impl Default for ProjectSearchBar {
|
||||||
|
@ -794,23 +811,6 @@ impl ProjectSearchBar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
|
|
||||||
if let Some(search_view) = pane
|
|
||||||
.active_item()
|
|
||||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
|
||||||
{
|
|
||||||
search_view.update(cx, |search_view, cx| {
|
|
||||||
if search_view.query_editor.is_focused(cx)
|
|
||||||
&& !search_view.model.read(cx).match_ranges.is_empty()
|
|
||||||
{
|
|
||||||
search_view.focus_results_editor(cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cx.propagate_action();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
|
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
|
||||||
self.cycle_field(Direction::Next, cx);
|
self.cycle_field(Direction::Next, cx);
|
||||||
}
|
}
|
||||||
|
@ -1257,7 +1257,182 @@ pub mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_project_search_focus(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/dir",
|
||||||
|
json!({
|
||||||
|
"one.rs": "const ONE: usize = 1;",
|
||||||
|
"two.rs": "const TWO: usize = one::ONE + one::ONE;",
|
||||||
|
"three.rs": "const THREE: usize = one::ONE + two::TWO;",
|
||||||
|
"four.rs": "const FOUR: usize = one::ONE + three::THREE;",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||||
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
|
|
||||||
|
let active_item = cx.read(|cx| {
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.active_pane()
|
||||||
|
.read(cx)
|
||||||
|
.active_item()
|
||||||
|
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
active_item.is_none(),
|
||||||
|
"Expected no search panel to be active, but got: {active_item:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
let Some(search_view) = cx.read(|cx| {
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.active_pane()
|
||||||
|
.read(cx)
|
||||||
|
.active_item()
|
||||||
|
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||||
|
}) else {
|
||||||
|
panic!("Search view expected to appear after new search event trigger")
|
||||||
|
};
|
||||||
|
let search_view_id = search_view.id();
|
||||||
|
|
||||||
|
cx.spawn(
|
||||||
|
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
assert!(
|
||||||
|
search_view.query_editor.is_focused(cx),
|
||||||
|
"Empty search view should be focused after the toggle focus event: no results panel to focus on",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
let query_editor = &search_view.query_editor;
|
||||||
|
assert!(
|
||||||
|
query_editor.is_focused(cx),
|
||||||
|
"Search view should be focused after the new search view is activated",
|
||||||
|
);
|
||||||
|
let query_text = query_editor.read(cx).text(cx);
|
||||||
|
assert!(
|
||||||
|
query_text.is_empty(),
|
||||||
|
"New search query should be empty but got '{query_text}'",
|
||||||
|
);
|
||||||
|
let results_text = search_view
|
||||||
|
.results_editor
|
||||||
|
.update(cx, |editor, cx| editor.display_text(cx));
|
||||||
|
assert!(
|
||||||
|
results_text.is_empty(),
|
||||||
|
"Empty search view should have no results but got '{results_text}'"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
search_view.query_editor.update(cx, |query_editor, cx| {
|
||||||
|
query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx)
|
||||||
|
});
|
||||||
|
search_view.search(cx);
|
||||||
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
let results_text = search_view
|
||||||
|
.results_editor
|
||||||
|
.update(cx, |editor, cx| editor.display_text(cx));
|
||||||
|
assert!(
|
||||||
|
results_text.is_empty(),
|
||||||
|
"Search view for mismatching query should have no results but got '{results_text}'"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
search_view.query_editor.is_focused(cx),
|
||||||
|
"Search view should be focused after mismatching query had been used in search",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cx.spawn(
|
||||||
|
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
assert!(
|
||||||
|
search_view.query_editor.is_focused(cx),
|
||||||
|
"Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
search_view
|
||||||
|
.query_editor
|
||||||
|
.update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
|
||||||
|
search_view.search(cx);
|
||||||
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
search_view
|
||||||
|
.results_editor
|
||||||
|
.update(cx, |editor, cx| editor.display_text(cx)),
|
||||||
|
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
|
||||||
|
"Search view results should match the query"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
search_view.results_editor.is_focused(cx),
|
||||||
|
"Search view with mismatching query should be focused after search results are available",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cx.spawn(
|
||||||
|
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
assert!(
|
||||||
|
search_view.results_editor.is_focused(cx),
|
||||||
|
"Search view with matching query should still have its results editor focused after the toggle focus event",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
|
||||||
|
});
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
|
||||||
|
assert_eq!(
|
||||||
|
search_view
|
||||||
|
.results_editor
|
||||||
|
.update(cx, |editor, cx| editor.display_text(cx)),
|
||||||
|
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
|
||||||
|
"Results should be unchanged after search view 2nd open in a row"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
search_view.query_editor.is_focused(cx),
|
||||||
|
"Focus should be moved into query editor again after search view 2nd open in a row"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(
|
||||||
|
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
search_view.update(cx, |search_view, cx| {
|
||||||
|
assert!(
|
||||||
|
search_view.results_editor.is_focused(cx),
|
||||||
|
"Search view with matching query should switch focus to the results editor after the toggle focus event",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init_test(cx: &mut TestAppContext) {
|
pub fn init_test(cx: &mut TestAppContext) {
|
||||||
|
cx.foreground().forbid_parking();
|
||||||
let fonts = cx.font_cache();
|
let fonts = cx.font_cache();
|
||||||
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
|
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
|
||||||
theme.search.match_background = Color::red();
|
theme.search.match_background = Color::red();
|
||||||
|
@ -1275,8 +1450,10 @@ pub mod tests {
|
||||||
|
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
client::init_settings(cx);
|
client::init_settings(cx);
|
||||||
editor::init_settings(cx);
|
editor::init(cx);
|
||||||
workspace::init_settings(cx);
|
workspace::init_settings(cx);
|
||||||
|
Project::init_settings(cx);
|
||||||
|
super::init(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,15 +55,22 @@ pub fn watch_config_file(
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
let events = fs.watch(&path, Duration::from_millis(100)).await;
|
let events = fs.watch(&path, Duration::from_millis(100)).await;
|
||||||
futures::pin_mut!(events);
|
futures::pin_mut!(events);
|
||||||
|
|
||||||
|
let contents = fs.load(&path).await.unwrap_or_default();
|
||||||
|
if tx.unbounded_send(contents).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
if events.next().await.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(contents) = fs.load(&path).await {
|
if let Ok(contents) = fs.load(&path).await {
|
||||||
if !tx.unbounded_send(contents).is_ok() {
|
if !tx.unbounded_send(contents).is_ok() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if events.next().await.is_none() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use collections::{btree_map, hash_map, BTreeMap, HashMap};
|
use collections::{btree_map, hash_map, BTreeMap, HashMap};
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
@ -84,15 +84,26 @@ pub struct SettingsJsonSchemaParams<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A set of strongly-typed setting values defined via multiple JSON files.
|
/// A set of strongly-typed setting values defined via multiple JSON files.
|
||||||
#[derive(Default)]
|
|
||||||
pub struct SettingsStore {
|
pub struct SettingsStore {
|
||||||
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
|
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
|
||||||
default_deserialized_settings: Option<serde_json::Value>,
|
default_deserialized_settings: serde_json::Value,
|
||||||
user_deserialized_settings: Option<serde_json::Value>,
|
user_deserialized_settings: serde_json::Value,
|
||||||
local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>,
|
local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>,
|
||||||
tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
|
tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for SettingsStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
SettingsStore {
|
||||||
|
setting_values: Default::default(),
|
||||||
|
default_deserialized_settings: serde_json::json!({}),
|
||||||
|
user_deserialized_settings: serde_json::json!({}),
|
||||||
|
local_deserialized_settings: Default::default(),
|
||||||
|
tab_size_callback: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct SettingValue<T> {
|
struct SettingValue<T> {
|
||||||
global_value: Option<T>,
|
global_value: Option<T>,
|
||||||
|
@ -136,20 +147,18 @@ impl SettingsStore {
|
||||||
local_values: Vec::new(),
|
local_values: Vec::new(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if let Some(default_settings) = &self.default_deserialized_settings {
|
|
||||||
if let Some(default_settings) = setting_value
|
if let Some(default_settings) = setting_value
|
||||||
.deserialize_setting(default_settings)
|
.deserialize_setting(&self.default_deserialized_settings)
|
||||||
.log_err()
|
.log_err()
|
||||||
{
|
{
|
||||||
let mut user_values_stack = Vec::new();
|
let mut user_values_stack = Vec::new();
|
||||||
|
|
||||||
if let Some(user_settings) = &self.user_deserialized_settings {
|
if let Some(user_settings) = setting_value
|
||||||
if let Some(user_settings) =
|
.deserialize_setting(&self.user_deserialized_settings)
|
||||||
setting_value.deserialize_setting(user_settings).log_err()
|
.log_err()
|
||||||
{
|
{
|
||||||
user_values_stack = vec![user_settings];
|
user_values_stack = vec![user_settings];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(setting) = setting_value
|
if let Some(setting) = setting_value
|
||||||
.load_setting(&default_settings, &user_values_stack, cx)
|
.load_setting(&default_settings, &user_values_stack, cx)
|
||||||
|
@ -159,7 +168,6 @@ impl SettingsStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the value of a setting.
|
/// Get the value of a setting.
|
||||||
///
|
///
|
||||||
|
@ -189,9 +197,7 @@ impl SettingsStore {
|
||||||
/// This is only for debugging and reporting. For user-facing functionality,
|
/// This is only for debugging and reporting. For user-facing functionality,
|
||||||
/// use the typed setting interface.
|
/// use the typed setting interface.
|
||||||
pub fn untyped_user_settings(&self) -> &serde_json::Value {
|
pub fn untyped_user_settings(&self) -> &serde_json::Value {
|
||||||
self.user_deserialized_settings
|
&self.user_deserialized_settings
|
||||||
.as_ref()
|
|
||||||
.unwrap_or(&serde_json::Value::Null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
@ -213,11 +219,7 @@ impl SettingsStore {
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
update: impl FnOnce(&mut T::FileContent),
|
update: impl FnOnce(&mut T::FileContent),
|
||||||
) {
|
) {
|
||||||
if self.user_deserialized_settings.is_none() {
|
let old_text = serde_json::to_string(&self.user_deserialized_settings).unwrap();
|
||||||
self.set_user_settings("{}", cx).unwrap();
|
|
||||||
}
|
|
||||||
let old_text =
|
|
||||||
serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap();
|
|
||||||
let new_text = self.new_text_for_update::<T>(old_text, update);
|
let new_text = self.new_text_for_update::<T>(old_text, update);
|
||||||
self.set_user_settings(&new_text, cx).unwrap();
|
self.set_user_settings(&new_text, cx).unwrap();
|
||||||
}
|
}
|
||||||
|
@ -250,11 +252,7 @@ impl SettingsStore {
|
||||||
.setting_values
|
.setting_values
|
||||||
.get(&setting_type_id)
|
.get(&setting_type_id)
|
||||||
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
|
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
|
||||||
.deserialize_setting(
|
.deserialize_setting(&self.user_deserialized_settings)
|
||||||
self.user_deserialized_settings
|
|
||||||
.as_ref()
|
|
||||||
.expect("no user settings loaded"),
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
panic!(
|
panic!(
|
||||||
"could not deserialize setting type {} from user settings: {}",
|
"could not deserialize setting type {} from user settings: {}",
|
||||||
|
@ -323,10 +321,14 @@ impl SettingsStore {
|
||||||
default_settings_content: &str,
|
default_settings_content: &str,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.default_deserialized_settings =
|
let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?;
|
||||||
Some(parse_json_with_comments(default_settings_content)?);
|
if settings.is_object() {
|
||||||
|
self.default_deserialized_settings = settings;
|
||||||
self.recompute_values(None, cx)?;
|
self.recompute_values(None, cx)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("settings must be an object"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the user settings via a JSON string.
|
/// Set the user settings via a JSON string.
|
||||||
|
@ -335,9 +337,14 @@ impl SettingsStore {
|
||||||
user_settings_content: &str,
|
user_settings_content: &str,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?);
|
let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?;
|
||||||
|
if settings.is_object() {
|
||||||
|
self.user_deserialized_settings = settings;
|
||||||
self.recompute_values(None, cx)?;
|
self.recompute_values(None, cx)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("settings must be an object"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add or remove a set of local settings via a JSON string.
|
/// Add or remove a set of local settings via a JSON string.
|
||||||
|
@ -443,20 +450,19 @@ impl SettingsStore {
|
||||||
let mut user_settings_stack = Vec::<DeserializedSetting>::new();
|
let mut user_settings_stack = Vec::<DeserializedSetting>::new();
|
||||||
let mut paths_stack = Vec::<Option<&Path>>::new();
|
let mut paths_stack = Vec::<Option<&Path>>::new();
|
||||||
for setting_value in self.setting_values.values_mut() {
|
for setting_value in self.setting_values.values_mut() {
|
||||||
if let Some(default_settings) = &self.default_deserialized_settings {
|
let default_settings =
|
||||||
let default_settings = setting_value.deserialize_setting(default_settings)?;
|
setting_value.deserialize_setting(&self.default_deserialized_settings)?;
|
||||||
|
|
||||||
user_settings_stack.clear();
|
user_settings_stack.clear();
|
||||||
paths_stack.clear();
|
paths_stack.clear();
|
||||||
|
|
||||||
if let Some(user_settings) = &self.user_deserialized_settings {
|
if let Some(user_settings) = setting_value
|
||||||
if let Some(user_settings) =
|
.deserialize_setting(&self.user_deserialized_settings)
|
||||||
setting_value.deserialize_setting(user_settings).log_err()
|
.log_err()
|
||||||
{
|
{
|
||||||
user_settings_stack.push(user_settings);
|
user_settings_stack.push(user_settings);
|
||||||
paths_stack.push(None);
|
paths_stack.push(None);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If the global settings file changed, reload the global value for the field.
|
// If the global settings file changed, reload the global value for the field.
|
||||||
if changed_local_path.is_none() {
|
if changed_local_path.is_none() {
|
||||||
|
@ -505,7 +511,6 @@ impl SettingsStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
indoc = "1.0.7"
|
indoc.workspace = true
|
||||||
libsqlite3-sys = { version = "0.24", features = ["bundled"] }
|
libsqlite3-sys = { version = "0.24", features = ["bundled"] }
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
thread_local = "1.1.4"
|
thread_local = "1.1.4"
|
||||||
|
|
|
@ -14,6 +14,7 @@ editor = { path = "../editor" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
|
search = { path = "../search" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
|
|
@ -22,7 +22,7 @@ const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
|
||||||
actions!(terminal_panel, [ToggleFocus]);
|
actions!(terminal_panel, [ToggleFocus]);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(TerminalPanel::add_terminal);
|
cx.add_action(TerminalPanel::new_terminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
|
@ -55,6 +55,7 @@ impl TerminalPanel {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
pane.set_can_split(false, cx);
|
pane.set_can_split(false, cx);
|
||||||
|
pane.set_can_navigate(false, cx);
|
||||||
pane.on_can_drop(move |drag_and_drop, cx| {
|
pane.on_can_drop(move |drag_and_drop, cx| {
|
||||||
drag_and_drop
|
drag_and_drop
|
||||||
.currently_dragged::<DraggedItem>(window_id)
|
.currently_dragged::<DraggedItem>(window_id)
|
||||||
|
@ -68,6 +69,7 @@ impl TerminalPanel {
|
||||||
.with_child(Pane::render_tab_bar_button(
|
.with_child(Pane::render_tab_bar_button(
|
||||||
0,
|
0,
|
||||||
"icons/plus_12.svg",
|
"icons/plus_12.svg",
|
||||||
|
false,
|
||||||
Some((
|
Some((
|
||||||
"New Terminal".into(),
|
"New Terminal".into(),
|
||||||
Some(Box::new(workspace::NewTerminal)),
|
Some(Box::new(workspace::NewTerminal)),
|
||||||
|
@ -78,7 +80,7 @@ impl TerminalPanel {
|
||||||
cx.window_context().defer(move |cx| {
|
cx.window_context().defer(move |cx| {
|
||||||
if let Some(this) = this.upgrade(cx) {
|
if let Some(this) = this.upgrade(cx) {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.add_terminal(&Default::default(), cx);
|
this.add_terminal(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -92,6 +94,7 @@ impl TerminalPanel {
|
||||||
} else {
|
} else {
|
||||||
"icons/maximize_8.svg"
|
"icons/maximize_8.svg"
|
||||||
},
|
},
|
||||||
|
pane.is_zoomed(),
|
||||||
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
|
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
|
||||||
cx,
|
cx,
|
||||||
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|
||||||
|
@ -99,6 +102,9 @@ impl TerminalPanel {
|
||||||
))
|
))
|
||||||
.into_any()
|
.into_any()
|
||||||
});
|
});
|
||||||
|
let buffer_search_bar = cx.add_view(search::BufferSearchBar::new);
|
||||||
|
pane.toolbar()
|
||||||
|
.update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
|
||||||
pane
|
pane
|
||||||
});
|
});
|
||||||
let subscriptions = vec![
|
let subscriptions = vec![
|
||||||
|
@ -216,7 +222,19 @@ impl TerminalPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_terminal(&mut self, _: &workspace::NewTerminal, cx: &mut ViewContext<Self>) {
|
fn new_terminal(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
_: &workspace::NewTerminal,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
let Some(this) = workspace.focus_panel::<Self>(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| this.add_terminal(cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_terminal(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
|
let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
|
||||||
|
@ -357,7 +375,7 @@ impl Panel for TerminalPanel {
|
||||||
|
|
||||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||||
if active && self.pane.read(cx).items_len() == 0 {
|
if active && self.pane.read(cx).items_len() == 0 {
|
||||||
self.add_terminal(&Default::default(), cx)
|
self.add_terminal(cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ use workspace::{
|
||||||
notifications::NotifyResultExt,
|
notifications::NotifyResultExt,
|
||||||
pane, register_deserializable_item,
|
pane, register_deserializable_item,
|
||||||
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
||||||
Pane, ToolbarItemLocation, Workspace, WorkspaceId,
|
NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use terminal::TerminalSettings;
|
pub use terminal::TerminalSettings;
|
||||||
|
@ -66,10 +66,10 @@ pub fn init(cx: &mut AppContext) {
|
||||||
terminal_panel::init(cx);
|
terminal_panel::init(cx);
|
||||||
terminal::init(cx);
|
terminal::init(cx);
|
||||||
|
|
||||||
cx.add_action(TerminalView::deploy);
|
|
||||||
|
|
||||||
register_deserializable_item::<TerminalView>(cx);
|
register_deserializable_item::<TerminalView>(cx);
|
||||||
|
|
||||||
|
cx.add_action(TerminalView::deploy);
|
||||||
|
|
||||||
//Useful terminal views
|
//Useful terminal views
|
||||||
cx.add_action(TerminalView::send_text);
|
cx.add_action(TerminalView::send_text);
|
||||||
cx.add_action(TerminalView::send_keystroke);
|
cx.add_action(TerminalView::send_keystroke);
|
||||||
|
@ -101,7 +101,7 @@ impl TerminalView {
|
||||||
///Create a new Terminal in the current working directory or the user's home directory
|
///Create a new Terminal in the current working directory or the user's home directory
|
||||||
pub fn deploy(
|
pub fn deploy(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
_: &workspace::NewTerminal,
|
_: &NewCenterTerminal,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
let strategy = settings::get::<TerminalSettings>(cx);
|
let strategy = settings::get::<TerminalSettings>(cx);
|
||||||
|
|
|
@ -89,7 +89,8 @@ pub struct Workspace {
|
||||||
pub breadcrumbs: Interactive<ContainedText>,
|
pub breadcrumbs: Interactive<ContainedText>,
|
||||||
pub disconnected_overlay: ContainedText,
|
pub disconnected_overlay: ContainedText,
|
||||||
pub modal: ContainerStyle,
|
pub modal: ContainerStyle,
|
||||||
pub zoomed_foreground: ContainerStyle,
|
pub zoomed_panel_foreground: ContainerStyle,
|
||||||
|
pub zoomed_pane_foreground: ContainerStyle,
|
||||||
pub zoomed_background: ContainerStyle,
|
pub zoomed_background: ContainerStyle,
|
||||||
pub notification: ContainerStyle,
|
pub notification: ContainerStyle,
|
||||||
pub notifications: Notifications,
|
pub notifications: Notifications,
|
||||||
|
|
|
@ -17,7 +17,7 @@ backtrace = "0.3"
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
isahc = "1.7"
|
isahc.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
|
|
@ -35,7 +35,7 @@ settings = { path = "../settings" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
indoc = "1.0.4"
|
indoc.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
|
|
||||||
pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
open_new(&app_state, cx, |workspace, cx| {
|
open_new(&app_state, cx, |workspace, cx| {
|
||||||
workspace.toggle_dock(DockPosition::Left, false, cx);
|
workspace.toggle_dock(DockPosition::Left, cx);
|
||||||
let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
|
let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
|
||||||
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
|
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
|
||||||
cx.focus(&welcome_page);
|
cx.focus(&welcome_page);
|
||||||
|
|
|
@ -62,5 +62,5 @@ settings = { path = "../settings", features = ["test-support"] }
|
||||||
fs = { path = "../fs", features = ["test-support"] }
|
fs = { path = "../fs", features = ["test-support"] }
|
||||||
db = { path = "../db", features = ["test-support"] }
|
db = { path = "../db", features = ["test-support"] }
|
||||||
|
|
||||||
indoc = "1.0.4"
|
indoc.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
|
|
|
@ -175,12 +175,16 @@ impl Dock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn position(&self) -> DockPosition {
|
||||||
|
self.position
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_open(&self) -> bool {
|
pub fn is_open(&self) -> bool {
|
||||||
self.is_open
|
self.is_open
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_focus(&self, cx: &WindowContext) -> bool {
|
pub fn has_focus(&self, cx: &WindowContext) -> bool {
|
||||||
self.active_panel()
|
self.visible_panel()
|
||||||
.map_or(false, |panel| panel.has_focus(cx))
|
.map_or(false, |panel| panel.has_focus(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +205,7 @@ impl Dock {
|
||||||
self.active_panel_index
|
self.active_panel_index
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
|
pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
|
||||||
if open != self.is_open {
|
if open != self.is_open {
|
||||||
self.is_open = open;
|
self.is_open = open;
|
||||||
if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
|
if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
|
||||||
|
@ -212,11 +216,6 @@ impl Dock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
self.set_open(!self.is_open, cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_panel_zoomed(
|
pub fn set_panel_zoomed(
|
||||||
&mut self,
|
&mut self,
|
||||||
panel: &AnyViewHandle,
|
panel: &AnyViewHandle,
|
||||||
|
@ -259,7 +258,7 @@ impl Dock {
|
||||||
cx.focus(&panel);
|
cx.focus(&panel);
|
||||||
}
|
}
|
||||||
} else if T::should_close_on_event(event)
|
} else if T::should_close_on_event(event)
|
||||||
&& this.active_panel().map_or(false, |p| p.id() == panel.id())
|
&& this.visible_panel().map_or(false, |p| p.id() == panel.id())
|
||||||
{
|
{
|
||||||
this.set_open(false, cx);
|
this.set_open(false, cx);
|
||||||
}
|
}
|
||||||
|
@ -315,12 +314,16 @@ impl Dock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
|
pub fn visible_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
|
||||||
let entry = self.active_entry()?;
|
let entry = self.visible_entry()?;
|
||||||
Some(&entry.panel)
|
Some(&entry.panel)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn active_entry(&self) -> Option<&PanelEntry> {
|
pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
|
||||||
|
Some(&self.panel_entries.get(self.active_panel_index)?.panel)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible_entry(&self) -> Option<&PanelEntry> {
|
||||||
if self.is_open {
|
if self.is_open {
|
||||||
self.panel_entries.get(self.active_panel_index)
|
self.panel_entries.get(self.active_panel_index)
|
||||||
} else {
|
} else {
|
||||||
|
@ -329,7 +332,7 @@ impl Dock {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> {
|
pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> {
|
||||||
let entry = self.active_entry()?;
|
let entry = self.visible_entry()?;
|
||||||
if entry.panel.is_zoomed(cx) {
|
if entry.panel.is_zoomed(cx) {
|
||||||
Some(entry.panel.clone())
|
Some(entry.panel.clone())
|
||||||
} else {
|
} else {
|
||||||
|
@ -362,7 +365,7 @@ impl Dock {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
|
pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
|
||||||
if let Some(active_entry) = self.active_entry() {
|
if let Some(active_entry) = self.visible_entry() {
|
||||||
Empty::new()
|
Empty::new()
|
||||||
.into_any()
|
.into_any()
|
||||||
.contained()
|
.contained()
|
||||||
|
@ -399,7 +402,7 @@ impl View for Dock {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
if let Some(active_entry) = self.active_entry() {
|
if let Some(active_entry) = self.visible_entry() {
|
||||||
let style = self.style(cx);
|
let style = self.style(cx);
|
||||||
ChildView::new(active_entry.panel.as_any(), cx)
|
ChildView::new(active_entry.panel.as_any(), cx)
|
||||||
.contained()
|
.contained()
|
||||||
|
@ -417,7 +420,7 @@ impl View for Dock {
|
||||||
|
|
||||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
if cx.is_self_focused() {
|
if cx.is_self_focused() {
|
||||||
if let Some(active_entry) = self.active_entry() {
|
if let Some(active_entry) = self.visible_entry() {
|
||||||
cx.focus(active_entry.panel.as_any());
|
cx.focus(active_entry.panel.as_any());
|
||||||
} else {
|
} else {
|
||||||
cx.focus_parent();
|
cx.focus_parent();
|
||||||
|
@ -473,11 +476,22 @@ impl View for PanelButtons {
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(panels.into_iter().enumerate().map(
|
.with_children(panels.into_iter().enumerate().map(
|
||||||
|(panel_ix, (view, context_menu))| {
|
|(panel_ix, (view, context_menu))| {
|
||||||
let (tooltip, tooltip_action) = view.icon_tooltip(cx);
|
let is_active = is_open && panel_ix == active_ix;
|
||||||
|
let (tooltip, tooltip_action) = if is_active {
|
||||||
|
(
|
||||||
|
format!("Close {} dock", dock_position.to_label()),
|
||||||
|
Some(match dock_position {
|
||||||
|
DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
|
||||||
|
DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
|
||||||
|
DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
view.icon_tooltip(cx)
|
||||||
|
};
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
|
MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
|
||||||
let is_active = is_open && panel_ix == active_ix;
|
|
||||||
let style = button_style.style_for(state, is_active);
|
let style = button_style.style_for(state, is_active);
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
|
@ -504,13 +518,22 @@ impl View for PanelButtons {
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, {
|
.on_click(MouseButton::Left, {
|
||||||
|
let tooltip_action =
|
||||||
|
tooltip_action.as_ref().map(|action| action.boxed_clone());
|
||||||
move |_, this, cx| {
|
move |_, this, cx| {
|
||||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
if let Some(tooltip_action) = &tooltip_action {
|
||||||
cx.window_context().defer(move |cx| {
|
let window_id = cx.window_id();
|
||||||
workspace.update(cx, |workspace, cx| {
|
let view_id = this.workspace.id();
|
||||||
workspace.toggle_panel(dock_position, panel_ix, cx)
|
let tooltip_action = tooltip_action.boxed_clone();
|
||||||
});
|
cx.spawn(|_, mut cx| async move {
|
||||||
});
|
cx.dispatch_action(
|
||||||
|
window_id,
|
||||||
|
view_id,
|
||||||
|
&*tooltip_action,
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{Toast, Workspace};
|
use crate::{Toast, Workspace};
|
||||||
use collections::HashSet;
|
use collections::HashMap;
|
||||||
use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
|
use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
|
||||||
use std::{any::TypeId, ops::DerefMut};
|
use std::{any::TypeId, ops::DerefMut};
|
||||||
|
|
||||||
|
@ -33,12 +33,12 @@ impl From<&dyn NotificationHandle> for AnyViewHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NotificationTracker {
|
pub(crate) struct NotificationTracker {
|
||||||
notifications_sent: HashSet<TypeId>,
|
notifications_sent: HashMap<TypeId, Vec<usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for NotificationTracker {
|
impl std::ops::Deref for NotificationTracker {
|
||||||
type Target = HashSet<TypeId>;
|
type Target = HashMap<TypeId, Vec<usize>>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.notifications_sent
|
&self.notifications_sent
|
||||||
|
@ -54,24 +54,33 @@ impl DerefMut for NotificationTracker {
|
||||||
impl NotificationTracker {
|
impl NotificationTracker {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
notifications_sent: HashSet::default(),
|
notifications_sent: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
|
pub fn has_shown_notification_once<V: Notification>(
|
||||||
|
&self,
|
||||||
|
id: usize,
|
||||||
|
cx: &ViewContext<Self>,
|
||||||
|
) -> bool {
|
||||||
|
cx.global::<NotificationTracker>()
|
||||||
|
.get(&TypeId::of::<V>())
|
||||||
|
.map(|ids| ids.contains(&id))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn show_notification_once<V: Notification>(
|
pub fn show_notification_once<V: Notification>(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: usize,
|
id: usize,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
|
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
|
||||||
) {
|
) {
|
||||||
if !cx
|
if !self.has_shown_notification_once::<V>(id, cx) {
|
||||||
.global::<NotificationTracker>()
|
|
||||||
.contains(&TypeId::of::<V>())
|
|
||||||
{
|
|
||||||
cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
|
cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
|
||||||
tracker.insert(TypeId::of::<V>())
|
let entry = tracker.entry(TypeId::of::<V>()).or_default();
|
||||||
|
entry.push(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.show_notification::<V>(id, cx, build_notification)
|
self.show_notification::<V>(id, cx, build_notification)
|
||||||
|
@ -154,9 +163,10 @@ pub mod simple_message_notification {
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
|
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
|
||||||
|
fonts::TextStyle,
|
||||||
impl_actions,
|
impl_actions,
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, MouseButton},
|
||||||
AppContext, Element, Entity, View, ViewContext,
|
AnyElement, AppContext, Element, Entity, View, ViewContext,
|
||||||
};
|
};
|
||||||
use menu::Cancel;
|
use menu::Cancel;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -184,8 +194,13 @@ pub mod simple_message_notification {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum NotificationMessage {
|
||||||
|
Text(Cow<'static, str>),
|
||||||
|
Element(fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>),
|
||||||
|
}
|
||||||
|
|
||||||
pub struct MessageNotification {
|
pub struct MessageNotification {
|
||||||
message: Cow<'static, str>,
|
message: NotificationMessage,
|
||||||
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
|
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
|
||||||
click_message: Option<Cow<'static, str>>,
|
click_message: Option<Cow<'static, str>>,
|
||||||
}
|
}
|
||||||
|
@ -204,7 +219,17 @@ pub mod simple_message_notification {
|
||||||
S: Into<Cow<'static, str>>,
|
S: Into<Cow<'static, str>>,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
message: message.into(),
|
message: NotificationMessage::Text(message.into()),
|
||||||
|
on_click: None,
|
||||||
|
click_message: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_element(
|
||||||
|
message: fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>,
|
||||||
|
) -> MessageNotification {
|
||||||
|
Self {
|
||||||
|
message: NotificationMessage::Element(message),
|
||||||
on_click: None,
|
on_click: None,
|
||||||
click_message: None,
|
click_message: None,
|
||||||
}
|
}
|
||||||
|
@ -243,16 +268,20 @@ pub mod simple_message_notification {
|
||||||
enum MessageNotificationTag {}
|
enum MessageNotificationTag {}
|
||||||
|
|
||||||
let click_message = self.click_message.clone();
|
let click_message = self.click_message.clone();
|
||||||
let message = self.message.clone();
|
let message = match &self.message {
|
||||||
|
NotificationMessage::Text(text) => {
|
||||||
|
Text::new(text.to_owned(), theme.message.text.clone()).into_any()
|
||||||
|
}
|
||||||
|
NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
|
||||||
|
};
|
||||||
let on_click = self.on_click.clone();
|
let on_click = self.on_click.clone();
|
||||||
let has_click_action = on_click.is_some();
|
let has_click_action = on_click.is_some();
|
||||||
|
|
||||||
MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
Text::new(message, theme.message.text.clone())
|
message
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.message.container)
|
.with_style(theme.message.container)
|
||||||
.aligned()
|
.aligned()
|
||||||
|
@ -281,45 +310,47 @@ pub mod simple_message_notification {
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.aligned()
|
.aligned()
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(
|
.with_height(cx.font_cache().line_height(theme.message.text.font_size))
|
||||||
cx.font_cache().line_height(theme.message.text.font_size),
|
|
||||||
)
|
|
||||||
.aligned()
|
.aligned()
|
||||||
.top()
|
.top()
|
||||||
.flex_float(),
|
.flex_float(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.with_children({
|
.with_children({
|
||||||
|
click_message
|
||||||
|
.map(|click_message| {
|
||||||
|
MouseEventHandler::<MessageNotificationTag, _>::new(
|
||||||
|
0,
|
||||||
|
cx,
|
||||||
|
|state, _| {
|
||||||
let style = theme.action_message.style_for(state, false);
|
let style = theme.action_message.style_for(state, false);
|
||||||
if let Some(click_message) = click_message {
|
|
||||||
Some(
|
Flex::row()
|
||||||
Flex::row().with_child(
|
.with_child(
|
||||||
Text::new(click_message, style.text.clone())
|
Text::new(click_message, style.text.clone())
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(style.container),
|
.with_style(style.container),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
.into_iter()
|
|
||||||
})
|
|
||||||
.contained()
|
.contained()
|
||||||
})
|
},
|
||||||
// Since we're not using a proper overlay, we have to capture these extra events
|
)
|
||||||
.on_down(MouseButton::Left, |_, _, _| {})
|
|
||||||
.on_up(MouseButton::Left, |_, _, _| {})
|
|
||||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
if let Some(on_click) = on_click.as_ref() {
|
if let Some(on_click) = on_click.as_ref() {
|
||||||
on_click(cx);
|
on_click(cx);
|
||||||
this.dismiss(&Default::default(), cx);
|
this.dismiss(&Default::default(), cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// Since we're not using a proper overlay, we have to capture these extra events
|
||||||
|
.on_down(MouseButton::Left, |_, _, _| {})
|
||||||
|
.on_up(MouseButton::Left, |_, _, _| {})
|
||||||
.with_cursor_style(if has_click_action {
|
.with_cursor_style(if has_click_action {
|
||||||
CursorStyle::PointingHand
|
CursorStyle::PointingHand
|
||||||
} else {
|
} else {
|
||||||
CursorStyle::Arrow
|
CursorStyle::Arrow
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
})
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@ mod dragged_item_receiver;
|
||||||
|
|
||||||
use super::{ItemHandle, SplitDirection};
|
use super::{ItemHandle, SplitDirection};
|
||||||
use crate::{
|
use crate::{
|
||||||
item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewFile, NewSearch, NewTerminal,
|
item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item,
|
||||||
ToggleZoom, Workspace, WorkspaceSettings,
|
NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use collections::{HashMap, HashSet, VecDeque};
|
use collections::{HashMap, HashSet, VecDeque};
|
||||||
|
@ -31,7 +31,7 @@ use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
cmp, mem,
|
cmp, mem,
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicUsize, Ordering},
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
@ -150,7 +150,6 @@ pub enum Event {
|
||||||
pub struct Pane {
|
pub struct Pane {
|
||||||
items: Vec<Box<dyn ItemHandle>>,
|
items: Vec<Box<dyn ItemHandle>>,
|
||||||
activation_history: Vec<usize>,
|
activation_history: Vec<usize>,
|
||||||
is_active: bool,
|
|
||||||
zoomed: bool,
|
zoomed: bool,
|
||||||
active_item_index: usize,
|
active_item_index: usize,
|
||||||
last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
|
last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
|
||||||
|
@ -164,6 +163,7 @@ pub struct Pane {
|
||||||
has_focus: bool,
|
has_focus: bool,
|
||||||
can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
|
can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
|
||||||
can_split: bool,
|
can_split: bool,
|
||||||
|
can_navigate: bool,
|
||||||
render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
|
render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +179,7 @@ struct NavHistory {
|
||||||
backward_stack: VecDeque<NavigationEntry>,
|
backward_stack: VecDeque<NavigationEntry>,
|
||||||
forward_stack: VecDeque<NavigationEntry>,
|
forward_stack: VecDeque<NavigationEntry>,
|
||||||
closed_stack: VecDeque<NavigationEntry>,
|
closed_stack: VecDeque<NavigationEntry>,
|
||||||
paths_by_item: HashMap<usize, ProjectPath>,
|
paths_by_item: HashMap<usize, (ProjectPath, Option<PathBuf>)>,
|
||||||
pane: WeakViewHandle<Pane>,
|
pane: WeakViewHandle<Pane>,
|
||||||
next_timestamp: Arc<AtomicUsize>,
|
next_timestamp: Arc<AtomicUsize>,
|
||||||
}
|
}
|
||||||
|
@ -254,7 +254,6 @@ impl Pane {
|
||||||
Self {
|
Self {
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
activation_history: Vec::new(),
|
activation_history: Vec::new(),
|
||||||
is_active: true,
|
|
||||||
zoomed: false,
|
zoomed: false,
|
||||||
active_item_index: 0,
|
active_item_index: 0,
|
||||||
last_focused_view_by_item: Default::default(),
|
last_focused_view_by_item: Default::default(),
|
||||||
|
@ -279,12 +278,14 @@ impl Pane {
|
||||||
has_focus: false,
|
has_focus: false,
|
||||||
can_drop: Rc::new(|_, _| true),
|
can_drop: Rc::new(|_, _| true),
|
||||||
can_split: true,
|
can_split: true,
|
||||||
|
can_navigate: true,
|
||||||
render_tab_bar_buttons: Rc::new(|pane, cx| {
|
render_tab_bar_buttons: Rc::new(|pane, cx| {
|
||||||
Flex::row()
|
Flex::row()
|
||||||
// New menu
|
// New menu
|
||||||
.with_child(Self::render_tab_bar_button(
|
.with_child(Self::render_tab_bar_button(
|
||||||
0,
|
0,
|
||||||
"icons/plus_12.svg",
|
"icons/plus_12.svg",
|
||||||
|
false,
|
||||||
Some(("New...".into(), None)),
|
Some(("New...".into(), None)),
|
||||||
cx,
|
cx,
|
||||||
|pane, cx| pane.deploy_new_menu(cx),
|
|pane, cx| pane.deploy_new_menu(cx),
|
||||||
|
@ -294,6 +295,7 @@ impl Pane {
|
||||||
.with_child(Self::render_tab_bar_button(
|
.with_child(Self::render_tab_bar_button(
|
||||||
1,
|
1,
|
||||||
"icons/split_12.svg",
|
"icons/split_12.svg",
|
||||||
|
false,
|
||||||
Some(("Split Pane".into(), None)),
|
Some(("Split Pane".into(), None)),
|
||||||
cx,
|
cx,
|
||||||
|pane, cx| pane.deploy_split_menu(cx),
|
|pane, cx| pane.deploy_split_menu(cx),
|
||||||
|
@ -307,6 +309,7 @@ impl Pane {
|
||||||
} else {
|
} else {
|
||||||
"icons/maximize_8.svg"
|
"icons/maximize_8.svg"
|
||||||
},
|
},
|
||||||
|
pane.is_zoomed(),
|
||||||
Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))),
|
Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))),
|
||||||
cx,
|
cx,
|
||||||
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|
||||||
|
@ -321,15 +324,6 @@ impl Pane {
|
||||||
&self.workspace
|
&self.workspace
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_active(&self) -> bool {
|
|
||||||
self.is_active
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
|
|
||||||
self.is_active = is_active;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_focus(&self) -> bool {
|
pub fn has_focus(&self) -> bool {
|
||||||
self.has_focus
|
self.has_focus
|
||||||
}
|
}
|
||||||
|
@ -346,6 +340,14 @@ impl Pane {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
self.can_navigate = can_navigate;
|
||||||
|
self.toolbar.update(cx, |toolbar, cx| {
|
||||||
|
toolbar.set_can_navigate(can_navigate, cx);
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
|
pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
|
||||||
where
|
where
|
||||||
F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
|
F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
|
||||||
|
@ -430,6 +432,10 @@ impl Pane {
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let to_load = if let Some(pane) = pane.upgrade(cx) {
|
let to_load = if let Some(pane) = pane.upgrade(cx) {
|
||||||
|
if !pane.read(cx).can_navigate {
|
||||||
|
return Task::ready(Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
cx.focus(&pane);
|
cx.focus(&pane);
|
||||||
|
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
|
@ -468,7 +474,7 @@ impl Pane {
|
||||||
.paths_by_item
|
.paths_by_item
|
||||||
.get(&entry.item.id())
|
.get(&entry.item.id())
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|project_path| (project_path, entry));
|
.map(|(project_path, _)| (project_path, entry));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -577,6 +583,20 @@ impl Pane {
|
||||||
destination_index: Option<usize>,
|
destination_index: Option<usize>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
|
if item.is_singleton(cx) {
|
||||||
|
if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
|
||||||
|
if let Some(project_path) =
|
||||||
|
workspace.project().read(cx).path_for_entry(entry_id, cx)
|
||||||
|
{
|
||||||
|
let abs_path = workspace.absolute_path(&project_path, cx);
|
||||||
|
pane.read(cx)
|
||||||
|
.nav_history
|
||||||
|
.borrow_mut()
|
||||||
|
.paths_by_item
|
||||||
|
.insert(item.id(), (project_path, abs_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// If no destination index is specified, add or move the item after the active item.
|
// If no destination index is specified, add or move the item after the active item.
|
||||||
let mut insertion_index = {
|
let mut insertion_index = {
|
||||||
let pane = pane.read(cx);
|
let pane = pane.read(cx);
|
||||||
|
@ -695,6 +715,11 @@ impl Pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
|
pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
|
||||||
|
// Potentially warn the user of the new keybinding
|
||||||
|
let workspace_handle = self.workspace().clone();
|
||||||
|
cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
|
||||||
|
.detach();
|
||||||
|
|
||||||
if self.zoomed {
|
if self.zoomed {
|
||||||
cx.emit(Event::ZoomOut);
|
cx.emit(Event::ZoomOut);
|
||||||
} else if !self.items.is_empty() {
|
} else if !self.items.is_empty() {
|
||||||
|
@ -989,10 +1014,16 @@ impl Pane {
|
||||||
.set_mode(NavigationMode::Normal);
|
.set_mode(NavigationMode::Normal);
|
||||||
|
|
||||||
if let Some(path) = item.project_path(cx) {
|
if let Some(path) = item.project_path(cx) {
|
||||||
|
let abs_path = self
|
||||||
|
.nav_history
|
||||||
|
.borrow()
|
||||||
|
.paths_by_item
|
||||||
|
.get(&item.id())
|
||||||
|
.and_then(|(_, abs_path)| abs_path.clone());
|
||||||
self.nav_history
|
self.nav_history
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.paths_by_item
|
.paths_by_item
|
||||||
.insert(item.id(), path);
|
.insert(item.id(), (path, abs_path));
|
||||||
} else {
|
} else {
|
||||||
self.nav_history
|
self.nav_history
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
|
@ -1185,7 +1216,7 @@ impl Pane {
|
||||||
AnchorCorner::TopRight,
|
AnchorCorner::TopRight,
|
||||||
vec![
|
vec![
|
||||||
ContextMenuItem::action("New File", NewFile),
|
ContextMenuItem::action("New File", NewFile),
|
||||||
ContextMenuItem::action("New Terminal", NewTerminal),
|
ContextMenuItem::action("New Terminal", NewCenterTerminal),
|
||||||
ContextMenuItem::action("New Search", NewSearch),
|
ContextMenuItem::action("New Search", NewSearch),
|
||||||
],
|
],
|
||||||
cx,
|
cx,
|
||||||
|
@ -1309,7 +1340,7 @@ impl Pane {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let pane_active = self.is_active;
|
let pane_active = self.has_focus;
|
||||||
|
|
||||||
enum Tabs {}
|
enum Tabs {}
|
||||||
let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx);
|
let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx);
|
||||||
|
@ -1592,6 +1623,7 @@ impl Pane {
|
||||||
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
|
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
|
||||||
index: usize,
|
index: usize,
|
||||||
icon: &'static str,
|
icon: &'static str,
|
||||||
|
active: bool,
|
||||||
tooltip: Option<(String, Option<Box<dyn Action>>)>,
|
tooltip: Option<(String, Option<Box<dyn Action>>)>,
|
||||||
cx: &mut ViewContext<Pane>,
|
cx: &mut ViewContext<Pane>,
|
||||||
on_click: F,
|
on_click: F,
|
||||||
|
@ -1601,7 +1633,7 @@ impl Pane {
|
||||||
|
|
||||||
let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
|
let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
|
||||||
let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
|
let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
|
||||||
let style = theme.pane_button.style_for(mouse_state, false);
|
let style = theme.pane_button.style_for(mouse_state, active);
|
||||||
Svg::new(icon)
|
Svg::new(icon)
|
||||||
.with_color(style.color)
|
.with_color(style.color)
|
||||||
.constrained()
|
.constrained()
|
||||||
|
@ -1688,7 +1720,7 @@ impl View for Pane {
|
||||||
let mut tab_row = Flex::row()
|
let mut tab_row = Flex::row()
|
||||||
.with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
|
.with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
|
||||||
|
|
||||||
if self.is_active {
|
if self.has_focus {
|
||||||
let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
|
let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
|
||||||
tab_row.add_child(
|
tab_row.add_child(
|
||||||
(render_tab_bar_buttons)(self, cx)
|
(render_tab_bar_buttons)(self, cx)
|
||||||
|
@ -1779,6 +1811,7 @@ impl View for Pane {
|
||||||
if !self.has_focus {
|
if !self.has_focus {
|
||||||
self.has_focus = true;
|
self.has_focus = true;
|
||||||
cx.emit(Event::Focus);
|
cx.emit(Event::Focus);
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.toolbar.update(cx, |toolbar, cx| {
|
self.toolbar.update(cx, |toolbar, cx| {
|
||||||
|
@ -1813,6 +1846,7 @@ impl View for Pane {
|
||||||
self.toolbar.update(cx, |toolbar, cx| {
|
self.toolbar.update(cx, |toolbar, cx| {
|
||||||
toolbar.pane_focus_update(false, cx);
|
toolbar.pane_focus_update(false, cx);
|
||||||
});
|
});
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
|
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
|
||||||
|
@ -1940,7 +1974,7 @@ impl PaneNavHistory {
|
||||||
pub fn for_each_entry(
|
pub fn for_each_entry(
|
||||||
&self,
|
&self,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
mut f: impl FnMut(&NavigationEntry, ProjectPath),
|
mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
|
||||||
) {
|
) {
|
||||||
let borrowed_history = self.0.borrow();
|
let borrowed_history = self.0.borrow();
|
||||||
borrowed_history
|
borrowed_history
|
||||||
|
@ -1949,12 +1983,13 @@ impl PaneNavHistory {
|
||||||
.chain(borrowed_history.backward_stack.iter())
|
.chain(borrowed_history.backward_stack.iter())
|
||||||
.chain(borrowed_history.closed_stack.iter())
|
.chain(borrowed_history.closed_stack.iter())
|
||||||
.for_each(|entry| {
|
.for_each(|entry| {
|
||||||
if let Some(path) = borrowed_history.paths_by_item.get(&entry.item.id()) {
|
if let Some(project_and_abs_path) =
|
||||||
f(entry, path.clone());
|
borrowed_history.paths_by_item.get(&entry.item.id())
|
||||||
|
{
|
||||||
|
f(entry, project_and_abs_path.clone());
|
||||||
} else if let Some(item) = entry.item.upgrade(cx) {
|
} else if let Some(item) = entry.item.upgrade(cx) {
|
||||||
let path = item.project_path(cx);
|
if let Some(path) = item.project_path(cx) {
|
||||||
if let Some(path) = path {
|
f(entry, (path, None));
|
||||||
f(entry, path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -53,6 +53,7 @@ pub enum ToolbarItemLocation {
|
||||||
pub struct Toolbar {
|
pub struct Toolbar {
|
||||||
active_pane_item: Option<Box<dyn ItemHandle>>,
|
active_pane_item: Option<Box<dyn ItemHandle>>,
|
||||||
hidden: bool,
|
hidden: bool,
|
||||||
|
can_navigate: bool,
|
||||||
pane: WeakViewHandle<Pane>,
|
pane: WeakViewHandle<Pane>,
|
||||||
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
||||||
}
|
}
|
||||||
|
@ -132,10 +133,9 @@ impl View for Toolbar {
|
||||||
let button_style = theme.nav_button;
|
let button_style = theme.nav_button;
|
||||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||||
|
|
||||||
Flex::column()
|
let mut primary_items = Flex::row();
|
||||||
.with_child(
|
if self.can_navigate {
|
||||||
Flex::row()
|
primary_items.add_child(nav_button(
|
||||||
.with_child(nav_button(
|
|
||||||
"icons/arrow_left_16.svg",
|
"icons/arrow_left_16.svg",
|
||||||
button_style,
|
button_style,
|
||||||
nav_button_height,
|
nav_button_height,
|
||||||
|
@ -163,8 +163,8 @@ impl View for Toolbar {
|
||||||
super::GoBack { pane: None },
|
super::GoBack { pane: None },
|
||||||
"Go Back",
|
"Go Back",
|
||||||
cx,
|
cx,
|
||||||
))
|
));
|
||||||
.with_child(nav_button(
|
primary_items.add_child(nav_button(
|
||||||
"icons/arrow_right_16.svg",
|
"icons/arrow_right_16.svg",
|
||||||
button_style,
|
button_style,
|
||||||
nav_button_height,
|
nav_button_height,
|
||||||
|
@ -192,17 +192,28 @@ impl View for Toolbar {
|
||||||
super::GoForward { pane: None },
|
super::GoForward { pane: None },
|
||||||
"Go Forward",
|
"Go Forward",
|
||||||
cx,
|
cx,
|
||||||
))
|
));
|
||||||
.with_children(primary_left_items)
|
}
|
||||||
.with_children(primary_right_items)
|
primary_items.extend(primary_left_items);
|
||||||
.constrained()
|
primary_items.extend(primary_right_items);
|
||||||
.with_height(height),
|
|
||||||
)
|
let mut toolbar = Flex::column();
|
||||||
.with_children(secondary_item)
|
if !primary_items.is_empty() {
|
||||||
|
toolbar.add_child(primary_items.constrained().with_height(height));
|
||||||
|
}
|
||||||
|
if let Some(secondary_item) = secondary_item {
|
||||||
|
toolbar.add_child(secondary_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if toolbar.is_empty() {
|
||||||
|
toolbar.into_any_named("toolbar")
|
||||||
|
} else {
|
||||||
|
toolbar
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(container_style)
|
.with_style(container_style)
|
||||||
.into_any_named("toolbar")
|
.into_any_named("toolbar")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
@ -264,9 +275,15 @@ impl Toolbar {
|
||||||
pane,
|
pane,
|
||||||
items: Default::default(),
|
items: Default::default(),
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
can_navigate: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
self.can_navigate = can_navigate;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
|
pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
|
||||||
where
|
where
|
||||||
T: 'static + ToolbarItemView,
|
T: 'static + ToolbarItemView,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.88.0"
|
version = "0.88.6"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
@ -48,6 +48,7 @@ language_selector = { path = "../language_selector" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
lsp_log = { path = "../lsp_log" }
|
lsp_log = { path = "../lsp_log" }
|
||||||
node_runtime = { path = "../node_runtime" }
|
node_runtime = { path = "../node_runtime" }
|
||||||
|
ai = { path = "../ai" }
|
||||||
outline = { path = "../outline" }
|
outline = { path = "../outline" }
|
||||||
plugin_runtime = { path = "../plugin_runtime" }
|
plugin_runtime = { path = "../plugin_runtime" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
|
@ -81,7 +82,7 @@ futures.workspace = true
|
||||||
ignore = "0.4"
|
ignore = "0.4"
|
||||||
image = "0.23"
|
image = "0.23"
|
||||||
indexmap = "1.6.2"
|
indexmap = "1.6.2"
|
||||||
isahc = "1.7"
|
isahc.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
dev
|
stable
|
|
@ -1,5 +1,5 @@
|
||||||
name = "Markdown"
|
name = "Markdown"
|
||||||
path_suffixes = ["md", "mdx"]
|
path_suffixes = ["md", "mdx", "zmd"]
|
||||||
brackets = [
|
brackets = [
|
||||||
{ start = "{", end = "}", close = true, newline = true },
|
{ start = "{", end = "}", close = true, newline = true },
|
||||||
{ start = "[", end = "]", close = true, newline = true },
|
{ start = "[", end = "]", close = true, newline = true },
|
||||||
|
|
|
@ -161,6 +161,7 @@ fn main() {
|
||||||
terminal_view::init(cx);
|
terminal_view::init(cx);
|
||||||
theme_testbench::init(cx);
|
theme_testbench::init(cx);
|
||||||
copilot::init(http.clone(), node_runtime, cx);
|
copilot::init(http.clone(), node_runtime, cx);
|
||||||
|
ai::init(cx);
|
||||||
|
|
||||||
cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
|
cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
|
||||||
|
|
||||||
|
|
|
@ -89,18 +89,9 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||||
MenuItem::action("Zoom Out", super::DecreaseBufferFontSize),
|
MenuItem::action("Zoom Out", super::DecreaseBufferFontSize),
|
||||||
MenuItem::action("Reset Zoom", super::ResetBufferFontSize),
|
MenuItem::action("Reset Zoom", super::ResetBufferFontSize),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::action(
|
MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock),
|
||||||
"Toggle Left Dock",
|
MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock),
|
||||||
workspace::ToggleLeftDock { focus: false },
|
MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock),
|
||||||
),
|
|
||||||
MenuItem::action(
|
|
||||||
"Toggle Right Dock",
|
|
||||||
workspace::ToggleRightDock { focus: false },
|
|
||||||
),
|
|
||||||
MenuItem::action(
|
|
||||||
"Toggle Bottom Dock",
|
|
||||||
workspace::ToggleBottomDock { focus: false },
|
|
||||||
),
|
|
||||||
MenuItem::submenu(Menu {
|
MenuItem::submenu(Menu {
|
||||||
name: "Editor Layout",
|
name: "Editor Layout",
|
||||||
items: vec![
|
items: vec![
|
||||||
|
|
|
@ -354,7 +354,7 @@ pub fn initialize_workspace(
|
||||||
.map_or(false, |entry| entry.is_dir())
|
.map_or(false, |entry| entry.is_dir())
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
workspace.toggle_dock(project_panel_position, false, cx);
|
workspace.toggle_dock(project_panel_position, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace.add_panel(terminal_panel, cx)
|
workspace.add_panel(terminal_panel, cx)
|
||||||
|
|
|
@ -94,6 +94,9 @@ export default function tabBar(colorScheme: ColorScheme) {
|
||||||
hover: {
|
hover: {
|
||||||
color: foreground(layer, "hovered"),
|
color: foreground(layer, "hovered"),
|
||||||
},
|
},
|
||||||
|
active: {
|
||||||
|
color: foreground(layer, "accent"),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
paneButtonContainer: {
|
paneButtonContainer: {
|
||||||
background: tab.background,
|
background: tab.background,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import tabBar from "./tabBar"
|
||||||
|
|
||||||
export default function workspace(colorScheme: ColorScheme) {
|
export default function workspace(colorScheme: ColorScheme) {
|
||||||
const layer = colorScheme.lowest
|
const layer = colorScheme.lowest
|
||||||
|
const isLight = colorScheme.isLight
|
||||||
const itemSpacing = 8
|
const itemSpacing = 8
|
||||||
const titlebarButton = {
|
const titlebarButton = {
|
||||||
cornerRadius: 6,
|
cornerRadius: 6,
|
||||||
|
@ -119,13 +120,19 @@ export default function workspace(colorScheme: ColorScheme) {
|
||||||
cursor: "Arrow",
|
cursor: "Arrow",
|
||||||
},
|
},
|
||||||
zoomedBackground: {
|
zoomedBackground: {
|
||||||
padding: 10,
|
|
||||||
cursor: "Arrow",
|
cursor: "Arrow",
|
||||||
background: withOpacity(background(colorScheme.lowest), 0.5)
|
background: isLight
|
||||||
|
? withOpacity(background(colorScheme.lowest), 0.8)
|
||||||
|
: withOpacity(background(colorScheme.highest), 0.6)
|
||||||
},
|
},
|
||||||
zoomedForeground: {
|
zoomedPaneForeground: {
|
||||||
|
margin: 16,
|
||||||
shadow: colorScheme.modalShadow,
|
shadow: colorScheme.modalShadow,
|
||||||
border: border(colorScheme.highest, { overlay: true }),
|
border: border(colorScheme.lowest, { overlay: true }),
|
||||||
|
},
|
||||||
|
zoomedPanelForeground: {
|
||||||
|
margin: 16,
|
||||||
|
border: border(colorScheme.lowest, { overlay: true }),
|
||||||
},
|
},
|
||||||
dock: {
|
dock: {
|
||||||
left: {
|
left: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue