Compare commits
123 Commits
v0.88.6
...
collab-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae7606ce21 | ||
|
|
38078b93cc | ||
|
|
6f4fd74f43 | ||
|
|
bf2016adf5 | ||
|
|
27ef0e2b52 | ||
|
|
45b42c512d | ||
|
|
705e36827c | ||
|
|
b875d4ed40 | ||
|
|
ed0b9acb0a | ||
|
|
ef80b539d1 | ||
|
|
62660f2766 | ||
|
|
193474a346 | ||
|
|
c48fed26bd | ||
|
|
3f6aa94a5f | ||
|
|
893615236d | ||
|
|
1f72f9e18b | ||
|
|
a10933c063 | ||
|
|
ac41564a8f | ||
|
|
c6672dbac2 | ||
|
|
76927b6d95 | ||
|
|
c9820fde61 | ||
|
|
584e5f7958 | ||
|
|
0122cd61c5 | ||
|
|
685e8d7007 | ||
|
|
9c707eff27 | ||
|
|
e80ab5f096 | ||
|
|
5e4a9abd09 | ||
|
|
5065804388 | ||
|
|
8f0fc918e9 | ||
|
|
9c9af5ed94 | ||
|
|
efcf78cbe9 | ||
|
|
e9ce85ebc0 | ||
|
|
501f9ab2c6 | ||
|
|
1a23fe91b4 | ||
|
|
84f98f13c4 | ||
|
|
31516b7863 | ||
|
|
51c7078f47 | ||
|
|
4568f80f2e | ||
|
|
14efc18eca | ||
|
|
05d1dd6b11 | ||
|
|
ac63043c27 | ||
|
|
0d62e76cd6 | ||
|
|
1fc9103b61 | ||
|
|
bd494037bb | ||
|
|
2269c19169 | ||
|
|
8a851ad7f8 | ||
|
|
063613bf4e | ||
|
|
88eb2b2163 | ||
|
|
6d3464fd1f | ||
|
|
edf8e276af | ||
|
|
d593377e45 | ||
|
|
cf75b090d3 | ||
|
|
986b02e217 | ||
|
|
fdad1adaf6 | ||
|
|
e4530471de | ||
|
|
1f42bfc1bd | ||
|
|
2db57b5139 | ||
|
|
02b95ef320 | ||
|
|
9cfe39f1aa | ||
|
|
5fda9e934d | ||
|
|
3a3c1c5a5b | ||
|
|
6628c4df28 | ||
|
|
59bfd40679 | ||
|
|
f890eefdef | ||
|
|
cf2bbfc85a | ||
|
|
508533ebb7 | ||
|
|
2fdc960704 | ||
|
|
b75c27da6f | ||
|
|
364631a155 | ||
|
|
128c19875d | ||
|
|
5501dd741c | ||
|
|
7900d2a20a | ||
|
|
5e39ba596e | ||
|
|
f40c498491 | ||
|
|
8d662edb6c | ||
|
|
cede296b04 | ||
|
|
54421b11f3 | ||
|
|
ae3bdd755e | ||
|
|
739d5ca373 | ||
|
|
24098c561d | ||
|
|
4185a178e7 | ||
|
|
f549ada54f | ||
|
|
aa8c0475b1 | ||
|
|
e2ff829f98 | ||
|
|
414b97adce | ||
|
|
dba7ec4a35 | ||
|
|
c2f3ac24a4 | ||
|
|
7e1ea8f274 | ||
|
|
aa58d0fd77 | ||
|
|
f812151840 | ||
|
|
eca6d2b597 | ||
|
|
58a56bdda2 | ||
|
|
d1f4b60fa1 | ||
|
|
b9dabb165e | ||
|
|
3327e8a6dd | ||
|
|
f6d7b3d2e8 | ||
|
|
793486b2e8 | ||
|
|
3f79b0c7cc | ||
|
|
cc3186cb4a | ||
|
|
0b7d095c0a | ||
|
|
37d35db3d7 | ||
|
|
26051de160 | ||
|
|
747322a02d | ||
|
|
d495c1b804 | ||
|
|
35f9996a4f | ||
|
|
ba79a8ba94 | ||
|
|
50e1e17d09 | ||
|
|
051c7566cc | ||
|
|
8abaf66602 | ||
|
|
6368ade1da | ||
|
|
637ed79603 | ||
|
|
65fc50745b | ||
|
|
d934da1905 | ||
|
|
7be41e19f7 | ||
|
|
073967c80b | ||
|
|
5362e7d346 | ||
|
|
394e87d17c | ||
|
|
0de5a444d3 | ||
|
|
234dbc3ca9 | ||
|
|
c4d88bc529 | ||
|
|
30de64845f | ||
|
|
7e6cccfa3d | ||
|
|
912fd23006 |
7
.github/pull_request_template.md
vendored
7
.github/pull_request_template.md
vendored
@@ -2,12 +2,11 @@
|
||||
|
||||
Release Notes:
|
||||
|
||||
Use `N/A` in this section if this item should be skipped in the release notes.
|
||||
- N/A
|
||||
|
||||
Add release note lines here:
|
||||
or
|
||||
|
||||
* (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
|
||||
* ...
|
||||
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
|
||||
|
||||
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
|
||||
These will be removed by the person making the release.
|
||||
|
||||
27
Cargo.lock
generated
27
Cargo.lock
generated
@@ -95,6 +95,22 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ai"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"collections",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"isahc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alacritty_config"
|
||||
version = "0.1.1-dev"
|
||||
@@ -1230,7 +1246,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.12.4"
|
||||
version = "0.12.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-tungstenite",
|
||||
@@ -2220,6 +2236,7 @@ dependencies = [
|
||||
"log",
|
||||
"postage",
|
||||
"project",
|
||||
"regex",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -3728,9 +3745,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.91.1"
|
||||
version = "0.94.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2368312c59425dd133cb9a327afee65be0a633a8ce471d248e2202a48f8f68ae"
|
||||
checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"serde",
|
||||
@@ -6793,6 +6810,7 @@ dependencies = [
|
||||
"procinfo",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"settings",
|
||||
@@ -8759,9 +8777,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.88.0"
|
||||
version = "0.90.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
"anyhow",
|
||||
"assets",
|
||||
"async-compression",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/ai",
|
||||
"crates/assets",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@@ -78,6 +79,8 @@ ctor = { version = "0.1" }
|
||||
env_logger = { version = "0.9" }
|
||||
futures = { version = "0.3" }
|
||||
globset = { version = "0.4" }
|
||||
indoc = "1"
|
||||
isahc = "1.7.2"
|
||||
lazy_static = { version = "1.4.0" }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
ordered-float = { version = "2.1.1" }
|
||||
|
||||
6
assets/contexts/system.zmd
Normal file
6
assets/contexts/system.zmd
Normal file
@@ -0,0 +1,6 @@
|
||||
User input begins on a line starting with /.
|
||||
Don't apologize ever.
|
||||
Never say "I apologize".
|
||||
Use simple language and don't flatter the users.
|
||||
Keep it short.
|
||||
Risk being rude.
|
||||
@@ -188,6 +188,12 @@
|
||||
"alt-[": "copilot::PreviousSuggestion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && extension == zmd",
|
||||
"bindings": {
|
||||
"cmd-enter": "ai::Assist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == auto_height",
|
||||
"bindings": {
|
||||
@@ -367,30 +373,9 @@
|
||||
"workspace::ActivatePane",
|
||||
8
|
||||
],
|
||||
"cmd-b": [
|
||||
"workspace::ToggleLeftDock",
|
||||
{ "focus": true }
|
||||
],
|
||||
"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-b": "workspace::ToggleLeftDock",
|
||||
"cmd-r": "workspace::ToggleRightDock",
|
||||
"cmd-j": "workspace::ToggleBottomDock",
|
||||
"cmd-shift-f": "workspace::NewSearch",
|
||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||
"cmd-k cmd-s": "zed::OpenKeymap",
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||
"use_on_type_format": true,
|
||||
// Controls whether copilot provides suggestion immediately
|
||||
// or waits for a `copilot::Toggle`
|
||||
"show_copilot_suggestions": true,
|
||||
|
||||
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" }
|
||||
util = { path = "../util" }
|
||||
anyhow.workspace = true
|
||||
isahc = "1.7"
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.12.4"
|
||||
version = "0.12.5"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
@@ -76,7 +76,7 @@ workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
indoc = "1.0.4"
|
||||
indoc.workspace = true
|
||||
util = { path = "../util" }
|
||||
lazy_static.workspace = true
|
||||
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
|
||||
|
||||
@@ -223,6 +223,7 @@ impl Server {
|
||||
.add_request_handler(forward_project_request::<proto::RenameProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::CopyProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
|
||||
.add_message_handler(create_buffer_for_peer)
|
||||
.add_request_handler(update_buffer)
|
||||
.add_message_handler(update_buffer_file)
|
||||
|
||||
@@ -5010,19 +5010,21 @@ async fn test_project_symbols(
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
|
||||
#[allow(deprecated)]
|
||||
Ok(Some(vec![lsp::SymbolInformation {
|
||||
name: "TWO".into(),
|
||||
location: lsp::Location {
|
||||
uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
|
||||
fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
|
||||
Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
|
||||
#[allow(deprecated)]
|
||||
lsp::SymbolInformation {
|
||||
name: "TWO".into(),
|
||||
location: lsp::Location {
|
||||
uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
|
||||
},
|
||||
kind: lsp::SymbolKind::CONSTANT,
|
||||
tags: None,
|
||||
container_name: None,
|
||||
deprecated: None,
|
||||
},
|
||||
kind: lsp::SymbolKind::CONSTANT,
|
||||
tags: None,
|
||||
container_name: None,
|
||||
deprecated: None,
|
||||
}]))
|
||||
])))
|
||||
});
|
||||
|
||||
// Request the definition of a symbol as the guest.
|
||||
@@ -6606,7 +6608,7 @@ async fn test_basic_following(
|
||||
// When client A navigates back and forth, client B does so as well.
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace::Pane::go_back(workspace, None, cx)
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -6617,7 +6619,7 @@ async fn test_basic_following(
|
||||
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace::Pane::go_back(workspace, None, cx)
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -6628,7 +6630,7 @@ async fn test_basic_following(
|
||||
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace::Pane::go_forward(workspace, None, cx)
|
||||
workspace.go_forward(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -7377,6 +7379,265 @@ async fn test_peers_simultaneously_following_each_other(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_on_input_format_from_host_to_guest(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
|
||||
first_trigger_character: ":".to_string(),
|
||||
more_trigger_character: Some(vec![">".to_string()]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": "fn main() { a }",
|
||||
"other.rs": "// Test file",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open a file in an editor as the host.
|
||||
let buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let (window_a, _) = cx_a.add_window(|_| EmptyView);
|
||||
let editor_a = cx_a.add_view(window_a, |cx| {
|
||||
Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
|
||||
});
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
cx_b.foreground().run_until_parked();
|
||||
|
||||
// Receive an OnTypeFormatting request as the host's language server.
|
||||
// Return some formattings from the host's language server.
|
||||
fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
|
||||
|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
lsp::Url::from_file_path("/a/main.rs").unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
lsp::Position::new(0, 14),
|
||||
);
|
||||
|
||||
Ok(Some(vec![lsp::TextEdit {
|
||||
new_text: "~<".to_string(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
|
||||
}]))
|
||||
},
|
||||
);
|
||||
|
||||
// Open the buffer on the guest and see that the formattings worked
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Type a on type formatting trigger character as the guest.
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
cx.focus(&editor_a);
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(">", cx);
|
||||
});
|
||||
|
||||
cx_b.foreground().run_until_parked();
|
||||
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "fn main() { a>~< }")
|
||||
});
|
||||
|
||||
// Undo should remove LSP edits first
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a>~< }");
|
||||
editor.undo(&Undo, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a> }");
|
||||
});
|
||||
cx_b.foreground().run_until_parked();
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "fn main() { a> }")
|
||||
});
|
||||
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a> }");
|
||||
editor.undo(&Undo, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a }");
|
||||
});
|
||||
cx_b.foreground().run_until_parked();
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "fn main() { a }")
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_on_input_format_from_guest_to_host(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
|
||||
first_trigger_character: ":".to_string(),
|
||||
more_trigger_character: Some(vec![">".to_string()]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": "fn main() { a }",
|
||||
"other.rs": "// Test file",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open a file in an editor as the guest.
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let (window_b, _) = cx_b.add_window(|_| EmptyView);
|
||||
let editor_b = cx_b.add_view(window_b, |cx| {
|
||||
Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
|
||||
});
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
cx_a.foreground().run_until_parked();
|
||||
// Type a on type formatting trigger character as the guest.
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(":", cx);
|
||||
cx.focus(&editor_b);
|
||||
});
|
||||
|
||||
// Receive an OnTypeFormatting request as the host's language server.
|
||||
// Return some formattings from the host's language server.
|
||||
cx_a.foreground().start_waiting();
|
||||
fake_language_server
|
||||
.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
lsp::Url::from_file_path("/a/main.rs").unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
lsp::Position::new(0, 14),
|
||||
);
|
||||
|
||||
Ok(Some(vec![lsp::TextEdit {
|
||||
new_text: "~:".to_string(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
|
||||
}]))
|
||||
})
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.foreground().finish_waiting();
|
||||
|
||||
// Open the buffer on the host and see that the formattings worked
|
||||
let buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.foreground().run_until_parked();
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "fn main() { a:~: }")
|
||||
});
|
||||
|
||||
// Undo should remove LSP edits first
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a:~: }");
|
||||
editor.undo(&Undo, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a: }");
|
||||
});
|
||||
cx_a.foreground().run_until_parked();
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "fn main() { a: }")
|
||||
});
|
||||
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a: }");
|
||||
editor.undo(&Undo, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a }");
|
||||
});
|
||||
cx_a.foreground().run_until_parked();
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "fn main() { a }")
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct RoomParticipants {
|
||||
remote: Vec<String>,
|
||||
|
||||
@@ -18,7 +18,7 @@ sqlez = { path = "../sqlez" }
|
||||
sqlez_macros = { path = "../sqlez_macros" }
|
||||
util = { path = "../util" }
|
||||
anyhow.workspace = true
|
||||
indoc = "1.0.4"
|
||||
indoc.workspace = true
|
||||
async-trait.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -609,15 +609,6 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn git_diff_recalc(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
Editor::to_item_events(event)
|
||||
}
|
||||
@@ -1508,6 +1499,7 @@ mod tests {
|
||||
language::init(cx);
|
||||
client::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2122,6 +2122,15 @@ impl Editor {
|
||||
let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
|
||||
// When buffer contents is updated and caret is moved, try triggering on type formatting.
|
||||
if settings::get::<EditorSettings>(cx).use_on_type_format {
|
||||
if let Some(on_type_format_task) =
|
||||
this.trigger_on_type_formatting(text.to_string(), cx)
|
||||
{
|
||||
on_type_format_task.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
if had_active_copilot_suggestion {
|
||||
this.refresh_copilot_suggestions(true, cx);
|
||||
if !this.has_active_copilot_suggestion(cx) {
|
||||
@@ -2500,6 +2509,52 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn trigger_on_type_formatting(
|
||||
&self,
|
||||
input: String,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if input.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let project = self.project.as_ref()?;
|
||||
let position = self.selections.newest_anchor().head();
|
||||
let (buffer, buffer_position) = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.text_anchor_for_position(position.clone(), cx)?;
|
||||
|
||||
// OnTypeFormatting retuns a list of edits, no need to pass them between Zed instances,
|
||||
// hence we do LSP request & edit on host side only — add formats to host's history.
|
||||
let push_to_lsp_host_history = true;
|
||||
// If this is not the host, append its history with new edits.
|
||||
let push_to_client_history = project.read(cx).is_remote();
|
||||
|
||||
let on_type_formatting = project.update(cx, |project, cx| {
|
||||
project.on_type_format(
|
||||
buffer.clone(),
|
||||
buffer_position,
|
||||
input,
|
||||
push_to_lsp_host_history,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
Some(cx.spawn(|editor, mut cx| async move {
|
||||
if let Some(transaction) = on_type_formatting.await? {
|
||||
if push_to_client_history {
|
||||
buffer.update(&mut cx, |buffer, _| {
|
||||
buffer.push_transaction(transaction, Instant::now());
|
||||
});
|
||||
}
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.refresh_document_highlights(cx);
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext<Self>) {
|
||||
if self.pending_rename.is_some() {
|
||||
return;
|
||||
@@ -4876,12 +4931,12 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn push_to_nav_history(
|
||||
&self,
|
||||
&mut self,
|
||||
cursor_anchor: Anchor,
|
||||
new_position: Option<Point>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(nav_history) = &self.nav_history {
|
||||
if let Some(nav_history) = self.nav_history.as_mut() {
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
let cursor_position = cursor_anchor.to_point(&buffer);
|
||||
let scroll_state = self.scroll_manager.anchor();
|
||||
@@ -6818,6 +6873,7 @@ impl Editor {
|
||||
multi_buffer::Event::Saved => cx.emit(Event::Saved),
|
||||
multi_buffer::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
|
||||
multi_buffer::Event::Reloaded => cx.emit(Event::TitleChanged),
|
||||
multi_buffer::Event::DiffBaseChanged => cx.emit(Event::DiffBaseChanged),
|
||||
multi_buffer::Event::Closed => cx.emit(Event::Closed),
|
||||
multi_buffer::Event::DiagnosticsUpdated => {
|
||||
self.refresh_active_diagnostics(cx);
|
||||
@@ -7206,6 +7262,7 @@ pub enum Event {
|
||||
DirtyChanged,
|
||||
Saved,
|
||||
TitleChanged,
|
||||
DiffBaseChanged,
|
||||
SelectionsChanged {
|
||||
local: bool,
|
||||
},
|
||||
@@ -7330,7 +7387,7 @@ impl View for Editor {
|
||||
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);
|
||||
let mode = match self.mode {
|
||||
EditorMode::SingleLine => "single_line",
|
||||
@@ -7346,10 +7403,18 @@ impl View for Editor {
|
||||
Some(ContextMenu::CodeActions(_)) => keymap.add_identifier("showing_code_actions"),
|
||||
None => {}
|
||||
}
|
||||
|
||||
for layer in self.keymap_context_layers.values() {
|
||||
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> {
|
||||
|
||||
@@ -7,6 +7,7 @@ pub struct EditorSettings {
|
||||
pub cursor_blink: bool,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub show_completions_on_input: bool,
|
||||
pub use_on_type_format: bool,
|
||||
pub scrollbar: Scrollbar,
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ pub struct EditorSettingsContent {
|
||||
pub cursor_blink: Option<bool>,
|
||||
pub hover_popover_enabled: Option<bool>,
|
||||
pub show_completions_on_input: Option<bool>,
|
||||
pub use_on_type_format: Option<bool>,
|
||||
pub scrollbar: Option<ScrollbarContent>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1246,7 +1246,7 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
|
||||
cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
|
||||
@@ -1358,7 +1358,7 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
|
||||
#[gpui::test]
|
||||
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
|
||||
cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
|
||||
@@ -1473,7 +1473,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state("one «two threeˇ» four");
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
|
||||
@@ -1637,7 +1637,7 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) {
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
cx.set_state(indoc! {"
|
||||
const a: ˇA = (
|
||||
@@ -1685,7 +1685,7 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
cx.set_state(indoc! {"
|
||||
const a: ˇA = (
|
||||
@@ -1751,7 +1751,7 @@ async fn test_tab(cx: &mut gpui::TestAppContext) {
|
||||
settings.defaults.tab_size = NonZeroU32::new(3)
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state(indoc! {"
|
||||
ˇabˇc
|
||||
ˇ🏀ˇ🏀ˇefg
|
||||
@@ -1779,7 +1779,7 @@ async fn test_tab(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig::default(),
|
||||
@@ -1850,7 +1850,7 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
@@ -1876,7 +1876,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
||||
settings.defaults.tab_size = NonZeroU32::new(4);
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
«oneˇ» «twoˇ»
|
||||
@@ -1949,7 +1949,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
|
||||
settings.defaults.hard_tabs = Some(true);
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
// select two ranges on one line
|
||||
cx.set_state(indoc! {"
|
||||
@@ -2156,7 +2156,7 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
|
||||
async fn test_backspace(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
// Basic backspace
|
||||
cx.set_state(indoc! {"
|
||||
@@ -2205,7 +2205,7 @@ async fn test_backspace(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_delete(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state(indoc! {"
|
||||
onˇe two three
|
||||
fou«rˇ» five six
|
||||
@@ -2559,7 +2559,7 @@ fn test_transpose(cx: &mut TestAppContext) {
|
||||
async fn test_clipboard(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
|
||||
cx.update_editor(|e, cx| e.cut(&Cut, cx));
|
||||
@@ -2641,7 +2641,7 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig::default(),
|
||||
Some(tree_sitter_rust::language()),
|
||||
@@ -3085,7 +3085,7 @@ fn test_add_selection_above_below(cx: &mut TestAppContext) {
|
||||
async fn test_select_next(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
|
||||
@@ -3314,7 +3314,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
@@ -3485,7 +3485,7 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let html_language = Arc::new(
|
||||
Language::new(
|
||||
@@ -3721,7 +3721,7 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let rust_language = Arc::new(
|
||||
Language::new(
|
||||
@@ -4938,7 +4938,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
registry.add(language.clone());
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language_registry(registry);
|
||||
buffer.set_language(Some(language), cx);
|
||||
@@ -5060,7 +5060,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
|
||||
async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let html_language = Arc::new(
|
||||
Language::new(
|
||||
@@ -5985,7 +5985,7 @@ fn test_combine_syntax_and_fuzzy_match_highlights() {
|
||||
async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let diff_base = r#"
|
||||
use some::mod;
|
||||
|
||||
@@ -40,7 +40,10 @@ use language::{
|
||||
language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16,
|
||||
Selection,
|
||||
};
|
||||
use project::ProjectPath;
|
||||
use project::{
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
ProjectPath,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
@@ -51,7 +54,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use text::Point;
|
||||
use workspace::{item::Item, GitGutterSetting, WorkspaceSettings};
|
||||
use workspace::item::Item;
|
||||
|
||||
enum FoldMarkers {}
|
||||
|
||||
@@ -551,11 +554,8 @@ impl EditorElement {
|
||||
let scroll_top = scroll_position.y() * line_height;
|
||||
|
||||
let show_gutter = matches!(
|
||||
settings::get::<WorkspaceSettings>(cx)
|
||||
.git
|
||||
.git_gutter
|
||||
.unwrap_or_default(),
|
||||
GitGutterSetting::TrackedFiles
|
||||
settings::get::<ProjectSettings>(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
);
|
||||
|
||||
if show_gutter {
|
||||
|
||||
@@ -720,17 +720,6 @@ impl Item for Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn git_diff_recalc(
|
||||
&mut self,
|
||||
_project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.buffer().update(cx, |multibuffer, cx| {
|
||||
multibuffer.git_diff_recalc(cx);
|
||||
});
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
let mut result = SmallVec::new();
|
||||
match event {
|
||||
|
||||
@@ -66,6 +66,7 @@ pub enum Event {
|
||||
},
|
||||
Edited,
|
||||
Reloaded,
|
||||
DiffBaseChanged,
|
||||
LanguageChanged,
|
||||
Reparsed,
|
||||
Saved,
|
||||
@@ -343,17 +344,6 @@ impl MultiBuffer {
|
||||
self.read(cx).symbols_containing(offset, theme)
|
||||
}
|
||||
|
||||
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let buffers = self.buffers.borrow();
|
||||
for buffer_state in buffers.values() {
|
||||
if buffer_state.buffer.read(cx).needs_git_diff_recalc() {
|
||||
buffer_state
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| buffer.git_diff_recalc(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit<I, S, T>(
|
||||
&mut self,
|
||||
edits: I,
|
||||
@@ -1312,6 +1302,7 @@ impl MultiBuffer {
|
||||
language::Event::Saved => Event::Saved,
|
||||
language::Event::FileHandleChanged => Event::FileHandleChanged,
|
||||
language::Event::Reloaded => Event::Reloaded,
|
||||
language::Event::DiffBaseChanged => Event::DiffBaseChanged,
|
||||
language::Event::LanguageChanged => Event::LanguageChanged,
|
||||
language::Event::Reparsed => Event::Reparsed,
|
||||
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
|
||||
@@ -1550,6 +1541,13 @@ impl MultiBuffer {
|
||||
cx.add_model(|cx| Self::singleton(buffer, cx))
|
||||
}
|
||||
|
||||
pub fn build_from_buffer(
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> ModelHandle<Self> {
|
||||
cx.add_model(|cx| Self::singleton(buffer, cx))
|
||||
}
|
||||
|
||||
pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> ModelHandle<Self> {
|
||||
cx.add_model(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(0);
|
||||
@@ -3870,10 +3868,13 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::editor_tests::init_test;
|
||||
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use language::{Buffer, Rope};
|
||||
use project::{FakeFs, Project};
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use std::{env, rc::Rc};
|
||||
@@ -4564,73 +4565,85 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
|
||||
use git::diff::DiffHunkStatus;
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
|
||||
// buffer has two modified hunks with two rows each
|
||||
let buffer_1 = cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(
|
||||
0,
|
||||
"
|
||||
1.zero
|
||||
1.ONE
|
||||
1.TWO
|
||||
1.three
|
||||
1.FOUR
|
||||
1.FIVE
|
||||
1.six
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
let buffer_1 = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_buffer(
|
||||
"
|
||||
1.zero
|
||||
1.ONE
|
||||
1.TWO
|
||||
1.three
|
||||
1.FOUR
|
||||
1.FIVE
|
||||
1.six
|
||||
"
|
||||
.unindent()
|
||||
.as_str(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
buffer_1.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(
|
||||
Some(
|
||||
"
|
||||
1.zero
|
||||
1.one
|
||||
1.two
|
||||
1.three
|
||||
1.four
|
||||
1.five
|
||||
1.six
|
||||
"
|
||||
1.zero
|
||||
1.one
|
||||
1.two
|
||||
1.three
|
||||
1.four
|
||||
1.five
|
||||
1.six
|
||||
"
|
||||
.unindent(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
buffer
|
||||
});
|
||||
|
||||
// buffer has a deletion hunk and an insertion hunk
|
||||
let buffer_2 = cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(
|
||||
0,
|
||||
"
|
||||
2.zero
|
||||
2.one
|
||||
2.two
|
||||
2.three
|
||||
2.four
|
||||
2.five
|
||||
2.six
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
let buffer_2 = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_buffer(
|
||||
"
|
||||
2.zero
|
||||
2.one
|
||||
2.two
|
||||
2.three
|
||||
2.four
|
||||
2.five
|
||||
2.six
|
||||
"
|
||||
.unindent()
|
||||
.as_str(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
buffer_2.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(
|
||||
Some(
|
||||
"
|
||||
2.zero
|
||||
2.one
|
||||
2.one-and-a-half
|
||||
2.two
|
||||
2.three
|
||||
2.four
|
||||
2.six
|
||||
"
|
||||
2.zero
|
||||
2.one
|
||||
2.one-and-a-half
|
||||
2.two
|
||||
2.three
|
||||
2.four
|
||||
2.six
|
||||
"
|
||||
.unindent(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
buffer
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
|
||||
@@ -7,6 +7,7 @@ use gpui::{
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{Buffer, BufferSnapshot};
|
||||
use project::{FakeFs, Project};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
@@ -25,11 +26,16 @@ pub struct EditorTestContext<'a> {
|
||||
}
|
||||
|
||||
impl<'a> EditorTestContext<'a> {
|
||||
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
||||
pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.create_buffer("", None, cx))
|
||||
.unwrap();
|
||||
let (window_id, editor) = cx.update(|cx| {
|
||||
cx.add_window(Default::default(), |cx| {
|
||||
cx.focus_self();
|
||||
build_editor(MultiBuffer::build_simple("", cx), cx)
|
||||
build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx)
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ editor = { path = "../editor" }
|
||||
language = { path = "../language" }
|
||||
gpui = { path = "../gpui" }
|
||||
project = { path = "../project" }
|
||||
regex.workspace = true
|
||||
search = { path = "../search" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
@@ -27,7 +28,7 @@ futures.workspace = true
|
||||
anyhow.workspace = true
|
||||
smallvec.workspace = true
|
||||
human_bytes = "0.4.1"
|
||||
isahc = "1.7"
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -14,6 +14,7 @@ use isahc::Request;
|
||||
use language::Buffer;
|
||||
use postage::prelude::Stream;
|
||||
use project::Project;
|
||||
use regex::Regex;
|
||||
use serde::Serialize;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
@@ -46,6 +47,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
#[derive(Serialize)]
|
||||
struct FeedbackRequestBody<'a> {
|
||||
feedback_text: &'a str,
|
||||
email: Option<String>,
|
||||
metrics_id: Option<Arc<str>>,
|
||||
installation_id: Option<Arc<str>>,
|
||||
system_specs: SystemSpecs,
|
||||
@@ -157,8 +159,18 @@ impl FeedbackEditor {
|
||||
let is_staff = telemetry.is_staff();
|
||||
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 {
|
||||
feedback_text: &feedback_text,
|
||||
email,
|
||||
metrics_id,
|
||||
installation_id,
|
||||
system_specs,
|
||||
|
||||
@@ -34,7 +34,7 @@ impl View for FeedbackInfoText {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
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(),
|
||||
)
|
||||
.with_soft_wrap(false)
|
||||
@@ -60,7 +60,7 @@ impl View for FeedbackInfoText {
|
||||
}),
|
||||
)
|
||||
.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)
|
||||
.aligned(),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ use gpui::{
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
use std::{
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{self, AtomicBool},
|
||||
Arc,
|
||||
@@ -25,11 +25,57 @@ pub struct FileFinderDelegate {
|
||||
latest_search_id: usize,
|
||||
latest_search_did_cancel: bool,
|
||||
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
|
||||
currently_opened_path: Option<ProjectPath>,
|
||||
matches: Vec<PathMatch>,
|
||||
selected: Option<(usize, Arc<Path>)>,
|
||||
currently_opened_path: Option<FoundPath>,
|
||||
matches: Matches,
|
||||
selected_index: Option<usize>,
|
||||
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]);
|
||||
@@ -43,10 +89,41 @@ const MAX_RECENT_SELECTIONS: usize = 20;
|
||||
|
||||
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
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
|
||||
.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 workspace = cx.handle().downgrade();
|
||||
@@ -87,37 +164,11 @@ impl FileSearchQuery {
|
||||
}
|
||||
|
||||
impl FileFinderDelegate {
|
||||
fn labels_for_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)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
fn new(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
project: ModelHandle<Project>,
|
||||
currently_opened_path: Option<ProjectPath>,
|
||||
history_items: Vec<ProjectPath>,
|
||||
currently_opened_path: Option<FoundPath>,
|
||||
history_items: Vec<FoundPath>,
|
||||
cx: &mut ViewContext<FileFinder>,
|
||||
) -> Self {
|
||||
cx.observe(&project, |picker, _, cx| {
|
||||
@@ -132,8 +183,8 @@ impl FileFinderDelegate {
|
||||
latest_search_did_cancel: false,
|
||||
latest_search_query: None,
|
||||
currently_opened_path,
|
||||
matches: Vec::new(),
|
||||
selected: None,
|
||||
matches: Matches::default(),
|
||||
selected_index: None,
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
history_items,
|
||||
}
|
||||
@@ -147,7 +198,7 @@ impl FileFinderDelegate {
|
||||
let relative_to = self
|
||||
.currently_opened_path
|
||||
.as_ref()
|
||||
.map(|project_path| Arc::clone(&project_path.path));
|
||||
.map(|found_path| Arc::clone(&found_path.project.path));
|
||||
let worktrees = self
|
||||
.project
|
||||
.read(cx)
|
||||
@@ -188,13 +239,13 @@ impl FileFinderDelegate {
|
||||
.update(&mut cx, |picker, cx| {
|
||||
picker
|
||||
.delegate_mut()
|
||||
.set_matches(search_id, did_cancel, query, matches, cx)
|
||||
.set_search_matches(search_id, did_cancel, query, matches, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn set_matches(
|
||||
fn set_search_matches(
|
||||
&mut self,
|
||||
search_id: usize,
|
||||
did_cancel: bool,
|
||||
@@ -211,15 +262,126 @@ impl FileFinderDelegate {
|
||||
.as_ref()
|
||||
.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 {
|
||||
self.matches = matches;
|
||||
self.matches = Matches::Search(matches);
|
||||
}
|
||||
self.latest_search_query = Some(query);
|
||||
self.latest_search_did_cancel = did_cancel;
|
||||
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 {
|
||||
@@ -232,45 +394,35 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
if let Some(selected) = self.selected.as_ref() {
|
||||
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
|
||||
self.selected_index.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) {
|
||||
let mat = &self.matches[ix];
|
||||
self.selected = Some((mat.worktree_id, mat.path.clone()));
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
|
||||
if raw_query.is_empty() {
|
||||
let project = self.project.read(cx);
|
||||
self.latest_search_id = post_inc(&mut self.search_count);
|
||||
self.matches.clear();
|
||||
|
||||
self.matches = self
|
||||
.currently_opened_path
|
||||
.iter() // if exists, bubble the currently opened path to the top
|
||||
.chain(self.history_items.iter().filter(|history_item| {
|
||||
Some(*history_item) != self.currently_opened_path.as_ref()
|
||||
}))
|
||||
.enumerate()
|
||||
.map(|(i, history_item)| PathMatch {
|
||||
score: i as f64,
|
||||
positions: Vec::new(),
|
||||
worktree_id: history_item.worktree_id.to_usize(),
|
||||
path: Arc::clone(&history_item.path),
|
||||
path_prefix: "".into(),
|
||||
distance_to_relative_ancestor: usize::MAX,
|
||||
})
|
||||
.collect();
|
||||
self.matches = Matches::History(
|
||||
self.history_items
|
||||
.iter()
|
||||
.filter(|history_item| {
|
||||
project
|
||||
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||
.is_some()
|
||||
|| (project.is_local()
|
||||
&& history_item
|
||||
.absolute
|
||||
.as_ref()
|
||||
.filter(|abs_path| abs_path.exists())
|
||||
.is_some())
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
);
|
||||
cx.notify();
|
||||
Task::ready(())
|
||||
} else {
|
||||
@@ -293,16 +445,52 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
|
||||
if let Some(m) = self.matches.get(self.selected_index()) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(m.worktree_id),
|
||||
path: m.path.clone(),
|
||||
};
|
||||
let open_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path.clone(), None, true, cx)
|
||||
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),
|
||||
path: m.path.clone(),
|
||||
},
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
),
|
||||
});
|
||||
|
||||
let workspace = workspace.downgrade();
|
||||
|
||||
let row = self
|
||||
.latest_search_query
|
||||
.as_ref()
|
||||
@@ -333,6 +521,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
}
|
||||
}
|
||||
workspace
|
||||
.downgrade()
|
||||
.update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
|
||||
.log_err();
|
||||
|
||||
@@ -352,11 +541,14 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
selected: bool,
|
||||
cx: &AppContext,
|
||||
) -> 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 style = theme.picker.item.style_for(mouse_state, selected);
|
||||
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()
|
||||
.with_child(
|
||||
Label::new(file_name, style.label.clone()).with_highlights(file_name_positions),
|
||||
@@ -373,7 +565,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{assert_eq, collections::HashMap, time::Duration};
|
||||
use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
|
||||
|
||||
use super::*;
|
||||
use editor::Editor;
|
||||
@@ -649,12 +841,16 @@ mod tests {
|
||||
|
||||
finder.update(cx, |finder, cx| {
|
||||
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,
|
||||
// returning only a subset of the matches that would have been found.
|
||||
drop(delegate.spawn_search(query.clone(), cx));
|
||||
delegate.set_matches(
|
||||
delegate.set_search_matches(
|
||||
delegate.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
@@ -664,7 +860,7 @@ mod tests {
|
||||
|
||||
// Simulate another cancellation.
|
||||
drop(delegate.spawn_search(query.clone(), cx));
|
||||
delegate.set_matches(
|
||||
delegate.set_search_matches(
|
||||
delegate.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
@@ -672,7 +868,12 @@ mod tests {
|
||||
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| {
|
||||
let finder = finder.read(cx);
|
||||
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) =
|
||||
delegate.labels_for_match(&delegate.matches[0]);
|
||||
delegate.labels_for_path_match(&matches[0]);
|
||||
assert_eq!(file_name, "the-file");
|
||||
assert_eq!(file_name_positions, &[0, 1, 4]);
|
||||
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
|
||||
// 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
|
||||
let b_path = Some(ProjectPath {
|
||||
let b_path = Some(dummy_found_path(ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("/root/dir2/b.txt")),
|
||||
});
|
||||
}));
|
||||
let (_, finder) = cx.add_window(|cx| {
|
||||
Picker::new(
|
||||
FileFinderDelegate::new(
|
||||
@@ -901,8 +1106,12 @@ mod tests {
|
||||
|
||||
finder.read_with(cx, |f, _| {
|
||||
let delegate = f.delegate();
|
||||
assert_eq!(delegate.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
|
||||
assert_eq!(delegate.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
|
||||
let matches = match &delegate.matches {
|
||||
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;
|
||||
assert_eq!(
|
||||
history_after_first,
|
||||
vec![ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
}],
|
||||
vec![FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
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"
|
||||
);
|
||||
|
||||
@@ -1032,14 +1244,20 @@ mod tests {
|
||||
assert_eq!(
|
||||
history_after_second,
|
||||
vec![
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/second.rs")),
|
||||
},
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
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,
|
||||
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. \
|
||||
2nd item should be the first in the history, as the last opened."
|
||||
@@ -1058,18 +1276,27 @@ mod tests {
|
||||
assert_eq!(
|
||||
history_after_third,
|
||||
vec![
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/third.rs")),
|
||||
},
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/second.rs")),
|
||||
},
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/third.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/third.rs"))
|
||||
),
|
||||
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,
|
||||
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. \
|
||||
3rd item should be the first in the history, as the last opened."
|
||||
@@ -1088,24 +1315,162 @@ mod tests {
|
||||
assert_eq!(
|
||||
history_after_second_again,
|
||||
vec![
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/second.rs")),
|
||||
},
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/third.rs")),
|
||||
},
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("test/first.rs")),
|
||||
},
|
||||
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,
|
||||
path: Arc::from(Path::new("test/third.rs")),
|
||||
},
|
||||
Some(PathBuf::from("/src/test/third.rs"))
|
||||
),
|
||||
FoundPath::new(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
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. \
|
||||
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(
|
||||
input: &str,
|
||||
expected_matches: usize,
|
||||
@@ -1114,7 +1479,7 @@ mod tests {
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
deterministic: &gpui::executor::Deterministic,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Vec<ProjectPath> {
|
||||
) -> Vec<FoundPath> {
|
||||
cx.dispatch_action(window_id, Toggle);
|
||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||
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();
|
||||
cx.read(|cx| {
|
||||
for pane in workspace.read(cx).panes() {
|
||||
@@ -1161,6 +1536,8 @@ mod tests {
|
||||
assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
|
||||
}
|
||||
});
|
||||
|
||||
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
|
||||
active_pane
|
||||
.update(cx, |pane, cx| {
|
||||
pane.close_active_item(&workspace::CloseActiveItem, cx)
|
||||
@@ -1185,8 +1562,10 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
history_items
|
||||
assert!(
|
||||
original_items.len() <= 1,
|
||||
"At most one panel should got closed"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
@@ -1198,6 +1577,7 @@ mod tests {
|
||||
super::init(cx);
|
||||
editor::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
state
|
||||
})
|
||||
}
|
||||
@@ -1215,4 +1595,11 @@ mod tests {
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
|
||||
FoundPath {
|
||||
project: project_path,
|
||||
absolute: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,13 +161,6 @@ impl BufferDiff {
|
||||
self.tree = SumTree::new();
|
||||
}
|
||||
|
||||
pub fn needs_update(&self, buffer: &text::BufferSnapshot) -> bool {
|
||||
match &self.last_buffer_version {
|
||||
Some(last) => buffer.version().changed_since(last),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) {
|
||||
let mut tree = SumTree::new();
|
||||
|
||||
|
||||
@@ -434,7 +434,9 @@ impl<T: Entity> ModelHandle<T> {
|
||||
Duration::from_secs(1)
|
||||
};
|
||||
|
||||
let executor = cx.background().clone();
|
||||
async move {
|
||||
executor.start_waiting();
|
||||
let notification = crate::util::timeout(duration, rx.next())
|
||||
.await
|
||||
.expect("next notification timed out");
|
||||
|
||||
@@ -876,6 +876,14 @@ impl Background {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn start_waiting(&self) {
|
||||
match self {
|
||||
Self::Deterministic { executor, .. } => executor.start_waiting(),
|
||||
_ => panic!("this method can only be called on a deterministic executor"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Background {
|
||||
@@ -960,7 +968,7 @@ impl<T: 'static, E: 'static + Display> Task<Result<T, E>> {
|
||||
pub fn detach_and_log_err(self, cx: &mut AppContext) {
|
||||
cx.spawn(|_| async move {
|
||||
if let Err(err) = self.await {
|
||||
log::error!("{}", err);
|
||||
log::error!("{:#}", err);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -25,8 +25,9 @@ struct Family {
|
||||
pub struct FontCache(RwLock<FontCacheState>);
|
||||
|
||||
pub struct FontCacheState {
|
||||
fonts: Arc<dyn platform::FontSystem>,
|
||||
font_system: Arc<dyn platform::FontSystem>,
|
||||
families: Vec<Family>,
|
||||
default_family: Option<FamilyId>,
|
||||
font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>,
|
||||
metrics: HashMap<FontId, Metrics>,
|
||||
wrapper_pool: HashMap<(FontId, OrderedFloat<f32>), Vec<LineWrapper>>,
|
||||
@@ -42,8 +43,9 @@ unsafe impl Send for FontCache {}
|
||||
impl FontCache {
|
||||
pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
|
||||
Self(RwLock::new(FontCacheState {
|
||||
fonts,
|
||||
font_system: fonts,
|
||||
families: Default::default(),
|
||||
default_family: None,
|
||||
font_selections: Default::default(),
|
||||
metrics: Default::default(),
|
||||
wrapper_pool: Default::default(),
|
||||
@@ -73,14 +75,14 @@ impl FontCache {
|
||||
|
||||
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() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let family_id = FamilyId(state.families.len());
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
self.select_font(family_id, &Properties::default()).unwrap()
|
||||
}
|
||||
@@ -115,7 +142,7 @@ impl FontCache {
|
||||
let mut inner = RwLockUpgradableReadGuard::upgrade(inner);
|
||||
let family = &inner.families[family_id.0];
|
||||
let font_id = inner
|
||||
.fonts
|
||||
.font_system
|
||||
.select_font(&family.font_ids, properties)
|
||||
.unwrap_or(family.font_ids[0]);
|
||||
|
||||
@@ -137,7 +164,7 @@ impl FontCache {
|
||||
if let Some(metrics) = state.metrics.get(&font_id) {
|
||||
f(metrics)
|
||||
} else {
|
||||
let metrics = state.fonts.font_metrics(font_id);
|
||||
let metrics = state.font_system.font_metrics(font_id);
|
||||
let metric = f(&metrics);
|
||||
let mut state = RwLockUpgradableReadGuard::upgrade(state);
|
||||
state.metrics.insert(font_id, metrics);
|
||||
@@ -157,8 +184,11 @@ impl FontCache {
|
||||
let bounds;
|
||||
{
|
||||
let state = self.0.read();
|
||||
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
|
||||
bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap();
|
||||
glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
|
||||
bounds = state
|
||||
.font_system
|
||||
.typographic_bounds(font_id, glyph_id)
|
||||
.unwrap();
|
||||
}
|
||||
bounds.width() * self.em_scale(font_id, font_size)
|
||||
}
|
||||
@@ -168,8 +198,8 @@ impl FontCache {
|
||||
let advance;
|
||||
{
|
||||
let state = self.0.read();
|
||||
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
|
||||
advance = state.fonts.advance(font_id, glyph_id).unwrap();
|
||||
glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
|
||||
advance = state.font_system.advance(font_id, glyph_id).unwrap();
|
||||
}
|
||||
advance.x() * self.em_scale(font_id, font_size)
|
||||
}
|
||||
@@ -214,7 +244,7 @@ impl FontCache {
|
||||
.or_default();
|
||||
let wrapper = wrappers
|
||||
.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 {
|
||||
wrapper: Some(wrapper),
|
||||
font_cache: self.clone(),
|
||||
|
||||
@@ -295,13 +295,14 @@ impl Default for TextStyle {
|
||||
.as_ref()
|
||||
.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
|
||||
.load_family(&[&font_family_name], &Default::default())
|
||||
.unwrap();
|
||||
let font_family_id = font_cache.known_existing_family();
|
||||
let font_id = font_cache
|
||||
.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 {
|
||||
color: Default::default(),
|
||||
font_family_name,
|
||||
|
||||
@@ -343,6 +343,7 @@ pub enum RasterizationOptions {
|
||||
|
||||
pub trait FontSystem: Send + Sync {
|
||||
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 select_font(
|
||||
&self,
|
||||
|
||||
@@ -66,6 +66,14 @@ impl platform::FontSystem for FontSystem {
|
||||
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>> {
|
||||
self.0.write().load_family(name, features)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ settings = { path = "../settings", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
indoc = "1.0.4"
|
||||
indoc.workspace = true
|
||||
rand.workspace = true
|
||||
tree-sitter-embedded-template = "*"
|
||||
tree-sitter-html = "*"
|
||||
|
||||
@@ -50,16 +50,10 @@ pub use {tree_sitter_rust, tree_sitter_typescript};
|
||||
|
||||
pub use lsp::DiagnosticSeverity;
|
||||
|
||||
struct GitDiffStatus {
|
||||
diff: git::diff::BufferDiff,
|
||||
update_in_progress: bool,
|
||||
update_requested: bool,
|
||||
}
|
||||
|
||||
pub struct Buffer {
|
||||
text: TextBuffer,
|
||||
diff_base: Option<String>,
|
||||
git_diff_status: GitDiffStatus,
|
||||
git_diff: git::diff::BufferDiff,
|
||||
file: Option<Arc<dyn File>>,
|
||||
saved_version: clock::Global,
|
||||
saved_version_fingerprint: RopeFingerprint,
|
||||
@@ -195,6 +189,7 @@ pub enum Event {
|
||||
Saved,
|
||||
FileHandleChanged,
|
||||
Reloaded,
|
||||
DiffBaseChanged,
|
||||
LanguageChanged,
|
||||
Reparsed,
|
||||
DiagnosticsUpdated,
|
||||
@@ -466,11 +461,7 @@ impl Buffer {
|
||||
was_dirty_before_starting_transaction: None,
|
||||
text: buffer,
|
||||
diff_base,
|
||||
git_diff_status: GitDiffStatus {
|
||||
diff: git::diff::BufferDiff::new(),
|
||||
update_in_progress: false,
|
||||
update_requested: false,
|
||||
},
|
||||
git_diff: git::diff::BufferDiff::new(),
|
||||
file,
|
||||
syntax_map: Mutex::new(SyntaxMap::new()),
|
||||
parsing_in_background: false,
|
||||
@@ -501,7 +492,7 @@ impl Buffer {
|
||||
BufferSnapshot {
|
||||
text,
|
||||
syntax,
|
||||
git_diff: self.git_diff_status.diff.clone(),
|
||||
git_diff: self.git_diff.clone(),
|
||||
file: self.file.clone(),
|
||||
remote_selections: self.remote_selections.clone(),
|
||||
diagnostics: self.diagnostics.clone(),
|
||||
@@ -620,7 +611,6 @@ impl Buffer {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
self.git_diff_recalc(cx);
|
||||
cx.emit(Event::Reloaded);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -676,50 +666,29 @@ impl Buffer {
|
||||
pub fn set_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
|
||||
self.diff_base = diff_base;
|
||||
self.git_diff_recalc(cx);
|
||||
cx.emit(Event::DiffBaseChanged);
|
||||
}
|
||||
|
||||
pub fn needs_git_diff_recalc(&self) -> bool {
|
||||
self.git_diff_status.diff.needs_update(self)
|
||||
}
|
||||
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<()>> {
|
||||
let diff_base = self.diff_base.clone()?; // TODO: Make this an Arc
|
||||
let snapshot = self.snapshot();
|
||||
|
||||
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if self.git_diff_status.update_in_progress {
|
||||
self.git_diff_status.update_requested = true;
|
||||
return;
|
||||
}
|
||||
let mut diff = self.git_diff.clone();
|
||||
let diff = cx.background().spawn(async move {
|
||||
diff.update(&diff_base, &snapshot).await;
|
||||
diff
|
||||
});
|
||||
|
||||
if let Some(diff_base) = &self.diff_base {
|
||||
let snapshot = self.snapshot();
|
||||
let diff_base = diff_base.clone();
|
||||
|
||||
let mut diff = self.git_diff_status.diff.clone();
|
||||
let diff = cx.background().spawn(async move {
|
||||
diff.update(&diff_base, &snapshot).await;
|
||||
diff
|
||||
});
|
||||
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
let buffer_diff = diff.await;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.git_diff_status.diff = buffer_diff;
|
||||
this.git_diff_update_count += 1;
|
||||
cx.notify();
|
||||
|
||||
this.git_diff_status.update_in_progress = false;
|
||||
if this.git_diff_status.update_requested {
|
||||
this.git_diff_recalc(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
} else {
|
||||
let snapshot = self.snapshot();
|
||||
self.git_diff_status.diff.clear(&snapshot);
|
||||
self.git_diff_update_count += 1;
|
||||
cx.notify();
|
||||
}
|
||||
let handle = cx.weak_handle();
|
||||
Some(cx.spawn_weak(|_, mut cx| async move {
|
||||
let buffer_diff = diff.await;
|
||||
if let Some(this) = handle.upgrade(&mut cx) {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.git_diff = buffer_diff;
|
||||
this.git_diff_update_count += 1;
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn close(&mut self, cx: &mut ModelContext<Self>) {
|
||||
|
||||
@@ -796,6 +796,12 @@ impl LanguageRegistry {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<PendingLanguageServer> {
|
||||
let server_id = self.state.write().next_language_server_id();
|
||||
log::info!(
|
||||
"starting language server name:{}, path:{root_path:?}, id:{server_id}",
|
||||
adapter.name.0
|
||||
);
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if language.fake_adapter.is_some() {
|
||||
let task = cx.spawn(|cx| async move {
|
||||
@@ -825,7 +831,6 @@ impl LanguageRegistry {
|
||||
Ok(server)
|
||||
});
|
||||
|
||||
let server_id = self.state.write().next_language_server_id();
|
||||
return Some(PendingLanguageServer { server_id, task });
|
||||
}
|
||||
|
||||
@@ -834,7 +839,6 @@ impl LanguageRegistry {
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("language server download directory has not been assigned"))
|
||||
.log_err()?;
|
||||
|
||||
let this = self.clone();
|
||||
let language = language.clone();
|
||||
let http_client = http_client.clone();
|
||||
@@ -843,7 +847,6 @@ impl LanguageRegistry {
|
||||
let adapter = adapter.clone();
|
||||
let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
|
||||
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
|
||||
let server_id = self.state.write().next_language_server_id();
|
||||
|
||||
let task = cx.spawn(|cx| async move {
|
||||
login_shell_env_loaded.await;
|
||||
|
||||
@@ -46,6 +46,7 @@ collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
live_kit_server = { path = "../live_kit_server" }
|
||||
media = { path = "../media" }
|
||||
nanoid = "0.4"
|
||||
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
|
||||
@@ -20,7 +20,7 @@ anyhow.workspace = true
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
lsp-types = "0.91"
|
||||
lsp-types = "0.94"
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -361,13 +361,18 @@ impl LanguageServer {
|
||||
capabilities: ClientCapabilities {
|
||||
workspace: Some(WorkspaceClientCapabilities {
|
||||
configuration: Some(true),
|
||||
did_change_watched_files: Some(DynamicRegistrationClientCapabilities {
|
||||
did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities {
|
||||
dynamic_registration: Some(true),
|
||||
relative_pattern_support: Some(true),
|
||||
}),
|
||||
did_change_configuration: Some(DynamicRegistrationClientCapabilities {
|
||||
dynamic_registration: Some(true),
|
||||
}),
|
||||
workspace_folders: Some(true),
|
||||
symbol: Some(WorkspaceSymbolClientCapabilities {
|
||||
resolve_support: None,
|
||||
..WorkspaceSymbolClientCapabilities::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
text_document: Some(TextDocumentClientCapabilities {
|
||||
@@ -849,10 +854,12 @@ impl FakeLanguageServer {
|
||||
T: request::Request,
|
||||
T::Result: 'static + Send,
|
||||
{
|
||||
self.server.executor.start_waiting();
|
||||
self.server.request::<T>(params).await
|
||||
}
|
||||
|
||||
pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
|
||||
self.server.executor.start_waiting();
|
||||
self.try_receive_notification::<T>().await.unwrap()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,24 @@ use client::proto::{self, PeerId};
|
||||
use fs::LineEnding;
|
||||
use gpui::{AppContext, AsyncAppContext, ModelHandle};
|
||||
use language::{
|
||||
language_settings::language_settings,
|
||||
point_from_lsp, point_to_lsp,
|
||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
|
||||
Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Unclipped,
|
||||
Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
};
|
||||
use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities};
|
||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||
|
||||
pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
|
||||
lsp::FormattingOptions {
|
||||
tab_size,
|
||||
insert_spaces: true,
|
||||
insert_final_newline: Some(true),
|
||||
..lsp::FormattingOptions::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub(crate) trait LspCommand: 'static + Sized {
|
||||
type Response: 'static + Default + Send;
|
||||
@@ -109,6 +119,25 @@ pub(crate) struct GetCodeActions {
|
||||
pub range: Range<Anchor>,
|
||||
}
|
||||
|
||||
pub(crate) struct OnTypeFormatting {
|
||||
pub position: PointUtf16,
|
||||
pub trigger: String,
|
||||
pub options: FormattingOptions,
|
||||
pub push_to_history: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct FormattingOptions {
|
||||
tab_size: u32,
|
||||
}
|
||||
|
||||
impl From<lsp::FormattingOptions> for FormattingOptions {
|
||||
fn from(value: lsp::FormattingOptions) -> Self {
|
||||
Self {
|
||||
tab_size: value.tab_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for PrepareRename {
|
||||
type Response = Option<Range<Anchor>>;
|
||||
@@ -1495,6 +1524,7 @@ impl LspCommand for GetCodeActions {
|
||||
context: lsp::CodeActionContext {
|
||||
diagnostics: relevant_diagnostics,
|
||||
only: language_server.code_action_kinds(),
|
||||
..lsp::CodeActionContext::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1596,3 +1626,134 @@ impl LspCommand for GetCodeActions {
|
||||
message.buffer_id
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for OnTypeFormatting {
|
||||
type Response = Option<Transaction>;
|
||||
type LspRequest = lsp::request::OnTypeFormatting;
|
||||
type ProtoRequest = proto::OnTypeFormatting;
|
||||
|
||||
fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool {
|
||||
let Some(on_type_formatting_options) = &server_capabilities.document_on_type_formatting_provider else { return false };
|
||||
on_type_formatting_options
|
||||
.first_trigger_character
|
||||
.contains(&self.trigger)
|
||||
|| on_type_formatting_options
|
||||
.more_trigger_character
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|chars| chars.contains(&self.trigger))
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
_: &Buffer,
|
||||
_: &Arc<LanguageServer>,
|
||||
_: &AppContext,
|
||||
) -> lsp::DocumentOnTypeFormattingParams {
|
||||
lsp::DocumentOnTypeFormattingParams {
|
||||
text_document_position: lsp::TextDocumentPositionParams::new(
|
||||
lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path).unwrap()),
|
||||
point_to_lsp(self.position),
|
||||
),
|
||||
ch: self.trigger.clone(),
|
||||
options: lsp_formatting_options(self.options.tab_size),
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
message: Option<Vec<lsp::TextEdit>>,
|
||||
project: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
server_id: LanguageServerId,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Option<Transaction>> {
|
||||
if let Some(edits) = message {
|
||||
let (lsp_adapter, lsp_server) =
|
||||
language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
|
||||
Project::deserialize_edits(
|
||||
project,
|
||||
buffer,
|
||||
edits,
|
||||
self.push_to_history,
|
||||
lsp_adapter,
|
||||
lsp_server,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::OnTypeFormatting {
|
||||
proto::OnTypeFormatting {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id(),
|
||||
position: Some(language::proto::serialize_anchor(
|
||||
&buffer.anchor_before(self.position),
|
||||
)),
|
||||
trigger: self.trigger.clone(),
|
||||
version: serialize_version(&buffer.version()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_proto(
|
||||
message: proto::OnTypeFormatting,
|
||||
_: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let position = message
|
||||
.position
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid position"))?;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_version(deserialize_version(&message.version))
|
||||
})
|
||||
.await?;
|
||||
|
||||
let tab_size = buffer.read_with(&cx, |buffer, cx| {
|
||||
let language_name = buffer.language().map(|language| language.name());
|
||||
language_settings(language_name.as_deref(), cx).tab_size
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
|
||||
trigger: message.trigger.clone(),
|
||||
options: lsp_formatting_options(tab_size.get()).into(),
|
||||
push_to_history: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn response_to_proto(
|
||||
response: Option<Transaction>,
|
||||
_: &mut Project,
|
||||
_: PeerId,
|
||||
_: &clock::Global,
|
||||
_: &mut AppContext,
|
||||
) -> proto::OnTypeFormattingResponse {
|
||||
proto::OnTypeFormattingResponse {
|
||||
transaction: response
|
||||
.map(|transaction| language::proto::serialize_transaction(&transaction)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_proto(
|
||||
self,
|
||||
message: proto::OnTypeFormattingResponse,
|
||||
_: ModelHandle<Project>,
|
||||
_: ModelHandle<Buffer>,
|
||||
_: AsyncAppContext,
|
||||
) -> Result<Option<Transaction>> {
|
||||
let Some(transaction) = message.transaction else { return Ok(None) };
|
||||
Ok(Some(language::proto::deserialize_transaction(transaction)?))
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &proto::OnTypeFormatting) -> u64 {
|
||||
message.buffer_id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod ignore;
|
||||
mod lsp_command;
|
||||
mod project_settings;
|
||||
pub mod project_settings;
|
||||
pub mod search;
|
||||
pub mod terminals;
|
||||
pub mod worktree;
|
||||
@@ -14,7 +14,10 @@ use clock::ReplicaId;
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||
use copilot::Copilot;
|
||||
use futures::{
|
||||
channel::mpsc::{self, UnboundedReceiver},
|
||||
channel::{
|
||||
mpsc::{self, UnboundedReceiver},
|
||||
oneshot,
|
||||
},
|
||||
future::{try_join_all, Shared},
|
||||
stream::FuturesUnordered,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||
@@ -37,6 +40,7 @@ use language::{
|
||||
PendingLanguageServer, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16,
|
||||
Transaction, Unclipped,
|
||||
};
|
||||
use log::error;
|
||||
use lsp::{
|
||||
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
|
||||
DocumentHighlightKind, LanguageServer, LanguageServerId,
|
||||
@@ -130,6 +134,8 @@ pub struct Project {
|
||||
incomplete_remote_buffers: HashMap<u64, Option<ModelHandle<Buffer>>>,
|
||||
buffer_snapshots: HashMap<u64, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
|
||||
buffers_being_formatted: HashSet<u64>,
|
||||
buffers_needing_diff: HashSet<WeakModelHandle<Buffer>>,
|
||||
git_diff_debouncer: DelayedDebounced,
|
||||
nonce: u128,
|
||||
_maintain_buffer_languages: Task<()>,
|
||||
_maintain_workspace_config: Task<()>,
|
||||
@@ -137,6 +143,49 @@ pub struct Project {
|
||||
copilot_enabled: bool,
|
||||
}
|
||||
|
||||
struct DelayedDebounced {
|
||||
task: Option<Task<()>>,
|
||||
cancel_channel: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl DelayedDebounced {
|
||||
fn new() -> DelayedDebounced {
|
||||
DelayedDebounced {
|
||||
task: None,
|
||||
cancel_channel: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn fire_new<F>(&mut self, delay: Duration, cx: &mut ModelContext<Project>, func: F)
|
||||
where
|
||||
F: 'static + FnOnce(&mut Project, &mut ModelContext<Project>) -> Task<()>,
|
||||
{
|
||||
if let Some(channel) = self.cancel_channel.take() {
|
||||
_ = channel.send(());
|
||||
}
|
||||
|
||||
let (sender, mut receiver) = oneshot::channel::<()>();
|
||||
self.cancel_channel = Some(sender);
|
||||
|
||||
let previous_task = self.task.take();
|
||||
self.task = Some(cx.spawn(|workspace, mut cx| async move {
|
||||
let mut timer = cx.background().timer(delay).fuse();
|
||||
if let Some(previous_task) = previous_task {
|
||||
previous_task.await;
|
||||
}
|
||||
|
||||
futures::select_biased! {
|
||||
_ = receiver => return,
|
||||
_ = timer => {}
|
||||
}
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| (func)(workspace, cx))
|
||||
.await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
struct LspBufferSnapshot {
|
||||
version: i32,
|
||||
snapshot: TextBufferSnapshot,
|
||||
@@ -417,6 +466,7 @@ impl Project {
|
||||
client.add_model_request_handler(Self::handle_delete_project_entry);
|
||||
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
|
||||
client.add_model_request_handler(Self::handle_apply_code_action);
|
||||
client.add_model_request_handler(Self::handle_on_type_formatting);
|
||||
client.add_model_request_handler(Self::handle_reload_buffers);
|
||||
client.add_model_request_handler(Self::handle_synchronize_buffers);
|
||||
client.add_model_request_handler(Self::handle_format_buffers);
|
||||
@@ -483,6 +533,8 @@ impl Project {
|
||||
language_server_statuses: Default::default(),
|
||||
last_workspace_edits_by_language_server: Default::default(),
|
||||
buffers_being_formatted: Default::default(),
|
||||
buffers_needing_diff: Default::default(),
|
||||
git_diff_debouncer: DelayedDebounced::new(),
|
||||
nonce: StdRng::from_entropy().gen(),
|
||||
terminals: Terminals {
|
||||
local_handles: Vec::new(),
|
||||
@@ -572,6 +624,8 @@ impl Project {
|
||||
last_workspace_edits_by_language_server: Default::default(),
|
||||
opened_buffers: Default::default(),
|
||||
buffers_being_formatted: Default::default(),
|
||||
buffers_needing_diff: Default::default(),
|
||||
git_diff_debouncer: DelayedDebounced::new(),
|
||||
buffer_snapshots: Default::default(),
|
||||
nonce: StdRng::from_entropy().gen(),
|
||||
terminals: Terminals {
|
||||
@@ -1406,7 +1460,7 @@ impl Project {
|
||||
};
|
||||
|
||||
cx.foreground().spawn(async move {
|
||||
pump_loading_buffer_reciever(loading_watch)
|
||||
wait_for_loading_buffer(loading_watch)
|
||||
.await
|
||||
.map_err(|error| anyhow!("{}", error))
|
||||
})
|
||||
@@ -1606,6 +1660,7 @@ impl Project {
|
||||
buffer: &ModelHandle<Buffer>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
self.request_buffer_diff_recalculation(buffer, cx);
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.set_language_registry(self.languages.clone())
|
||||
});
|
||||
@@ -1923,6 +1978,13 @@ impl Project {
|
||||
event: &BufferEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<()> {
|
||||
if matches!(
|
||||
event,
|
||||
BufferEvent::Edited { .. } | BufferEvent::Reloaded | BufferEvent::DiffBaseChanged
|
||||
) {
|
||||
self.request_buffer_diff_recalculation(&buffer, cx);
|
||||
}
|
||||
|
||||
match event {
|
||||
BufferEvent::Operation(operation) => {
|
||||
self.buffer_ordered_messages_tx
|
||||
@@ -2062,6 +2124,74 @@ impl Project {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_buffer_diff_recalculation(
|
||||
&mut self,
|
||||
buffer: &ModelHandle<Buffer>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.buffers_needing_diff.insert(buffer.downgrade());
|
||||
let first_insertion = self.buffers_needing_diff.len() == 1;
|
||||
|
||||
let settings = settings::get::<ProjectSettings>(cx);
|
||||
let delay = if let Some(delay) = settings.git.gutter_debounce {
|
||||
delay
|
||||
} else {
|
||||
if first_insertion {
|
||||
let this = cx.weak_handle();
|
||||
cx.defer(move |cx| {
|
||||
if let Some(this) = this.upgrade(cx) {
|
||||
this.update(cx, |this, cx| {
|
||||
this.recalculate_buffer_diffs(cx).detach();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const MIN_DELAY: u64 = 50;
|
||||
let delay = delay.max(MIN_DELAY);
|
||||
let duration = Duration::from_millis(delay);
|
||||
|
||||
self.git_diff_debouncer
|
||||
.fire_new(duration, cx, move |this, cx| {
|
||||
this.recalculate_buffer_diffs(cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn recalculate_buffer_diffs(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let buffers: Vec<_> = this.update(&mut cx, |this, _| {
|
||||
this.buffers_needing_diff.drain().collect()
|
||||
});
|
||||
|
||||
let tasks: Vec<_> = this.update(&mut cx, |_, cx| {
|
||||
buffers
|
||||
.iter()
|
||||
.filter_map(|buffer| {
|
||||
let buffer = buffer.upgrade(cx)?;
|
||||
buffer.update(cx, |buffer, cx| buffer.git_diff_recalc(cx))
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
futures::future::join_all(tasks).await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if !this.buffers_needing_diff.is_empty() {
|
||||
this.recalculate_buffer_diffs(cx).detach();
|
||||
} else {
|
||||
// TODO: Would a `ModelContext<Project>.notify()` suffice here?
|
||||
for buffer in buffers {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
buffer.update(cx, |_, cx| cx.notify());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn language_servers_for_worktree(
|
||||
&self,
|
||||
worktree_id: WorktreeId,
|
||||
@@ -2888,10 +3018,12 @@ impl Project {
|
||||
if let Some(worktree) = worktree.upgrade(cx) {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Some(abs_path) = worktree.abs_path().to_str() {
|
||||
if let Some(suffix) = watcher
|
||||
.glob_pattern
|
||||
.strip_prefix(abs_path)
|
||||
.and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR))
|
||||
if let Some(suffix) = match &watcher.glob_pattern {
|
||||
lsp::GlobPattern::String(s) => s,
|
||||
lsp::GlobPattern::Relative(rp) => &rp.pattern,
|
||||
}
|
||||
.strip_prefix(abs_path)
|
||||
.and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR))
|
||||
{
|
||||
if let Some(glob) = Glob::new(suffix).log_err() {
|
||||
builders
|
||||
@@ -3089,7 +3221,7 @@ impl Project {
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let (worktree, relative_path) = self
|
||||
.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 {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
@@ -3476,12 +3608,7 @@ impl Project {
|
||||
language_server
|
||||
.request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
|
||||
text_document,
|
||||
options: lsp::FormattingOptions {
|
||||
tab_size: tab_size.into(),
|
||||
insert_spaces: true,
|
||||
insert_final_newline: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
options: lsp_command::lsp_formatting_options(tab_size.get()),
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await?
|
||||
@@ -3497,12 +3624,7 @@ impl Project {
|
||||
.request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
|
||||
text_document,
|
||||
range: lsp::Range::new(buffer_start, buffer_end),
|
||||
options: lsp::FormattingOptions {
|
||||
tab_size: tab_size.into(),
|
||||
insert_spaces: true,
|
||||
insert_final_newline: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
options: lsp_command::lsp_formatting_options(tab_size.get()),
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await?
|
||||
@@ -3640,7 +3762,7 @@ impl Project {
|
||||
let worktree_abs_path = worktree.abs_path().clone();
|
||||
requests.push(
|
||||
server
|
||||
.request::<lsp::request::WorkspaceSymbol>(
|
||||
.request::<lsp::request::WorkspaceSymbolRequest>(
|
||||
lsp::WorkspaceSymbolParams {
|
||||
query: query.to_string(),
|
||||
..Default::default()
|
||||
@@ -3648,12 +3770,32 @@ impl Project {
|
||||
)
|
||||
.log_err()
|
||||
.map(move |response| {
|
||||
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
|
||||
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
|
||||
flat_responses.into_iter().map(|lsp_symbol| {
|
||||
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
|
||||
nested_responses.into_iter().filter_map(|lsp_symbol| {
|
||||
let location = match lsp_symbol.location {
|
||||
lsp::OneOf::Left(location) => location,
|
||||
lsp::OneOf::Right(_) => {
|
||||
error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
|
||||
return None
|
||||
}
|
||||
};
|
||||
Some((lsp_symbol.name, lsp_symbol.kind, location))
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
}).unwrap_or_default();
|
||||
|
||||
(
|
||||
adapter,
|
||||
language,
|
||||
worktree_id,
|
||||
worktree_abs_path,
|
||||
response.unwrap_or_default(),
|
||||
lsp_symbols,
|
||||
)
|
||||
}),
|
||||
);
|
||||
@@ -3675,53 +3817,54 @@ impl Project {
|
||||
adapter_language,
|
||||
source_worktree_id,
|
||||
worktree_abs_path,
|
||||
response,
|
||||
lsp_symbols,
|
||||
) in responses
|
||||
{
|
||||
symbols.extend(response.into_iter().flatten().filter_map(|lsp_symbol| {
|
||||
let abs_path = lsp_symbol.location.uri.to_file_path().ok()?;
|
||||
let mut worktree_id = source_worktree_id;
|
||||
let path;
|
||||
if let Some((worktree, rel_path)) =
|
||||
this.find_local_worktree(&abs_path, cx)
|
||||
{
|
||||
worktree_id = worktree.read(cx).id();
|
||||
path = rel_path;
|
||||
} else {
|
||||
path = relativize_path(&worktree_abs_path, &abs_path);
|
||||
}
|
||||
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path.into(),
|
||||
};
|
||||
let signature = this.symbol_signature(&project_path);
|
||||
let adapter_language = adapter_language.clone();
|
||||
let language = this
|
||||
.languages
|
||||
.language_for_file(&project_path.path, None)
|
||||
.unwrap_or_else(move |_| adapter_language);
|
||||
let language_server_name = adapter.name.clone();
|
||||
Some(async move {
|
||||
let language = language.await;
|
||||
let label = language
|
||||
.label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
|
||||
.await;
|
||||
|
||||
Symbol {
|
||||
language_server_name,
|
||||
source_worktree_id,
|
||||
path: project_path,
|
||||
label: label.unwrap_or_else(|| {
|
||||
CodeLabel::plain(lsp_symbol.name.clone(), None)
|
||||
}),
|
||||
kind: lsp_symbol.kind,
|
||||
name: lsp_symbol.name,
|
||||
range: range_from_lsp(lsp_symbol.location.range),
|
||||
signature,
|
||||
symbols.extend(lsp_symbols.into_iter().filter_map(
|
||||
|(symbol_name, symbol_kind, symbol_location)| {
|
||||
let abs_path = symbol_location.uri.to_file_path().ok()?;
|
||||
let mut worktree_id = source_worktree_id;
|
||||
let path;
|
||||
if let Some((worktree, rel_path)) =
|
||||
this.find_local_worktree(&abs_path, cx)
|
||||
{
|
||||
worktree_id = worktree.read(cx).id();
|
||||
path = rel_path;
|
||||
} else {
|
||||
path = relativize_path(&worktree_abs_path, &abs_path);
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path.into(),
|
||||
};
|
||||
let signature = this.symbol_signature(&project_path);
|
||||
let adapter_language = adapter_language.clone();
|
||||
let language = this
|
||||
.languages
|
||||
.language_for_file(&project_path.path, None)
|
||||
.unwrap_or_else(move |_| adapter_language);
|
||||
let language_server_name = adapter.name.clone();
|
||||
Some(async move {
|
||||
let language = language.await;
|
||||
let label =
|
||||
language.label_for_symbol(&symbol_name, symbol_kind).await;
|
||||
|
||||
Symbol {
|
||||
language_server_name,
|
||||
source_worktree_id,
|
||||
path: project_path,
|
||||
label: label.unwrap_or_else(|| {
|
||||
CodeLabel::plain(symbol_name.clone(), None)
|
||||
}),
|
||||
kind: symbol_kind,
|
||||
name: symbol_name,
|
||||
range: range_from_lsp(symbol_location.range),
|
||||
signature,
|
||||
}
|
||||
})
|
||||
},
|
||||
));
|
||||
}
|
||||
symbols
|
||||
});
|
||||
@@ -4044,6 +4187,109 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_on_type_formatting(
|
||||
&self,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
position: Anchor,
|
||||
trigger: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Transaction>>> {
|
||||
if self.is_local() {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
// Do not allow multiple concurrent formatting requests for the
|
||||
// same buffer.
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.buffers_being_formatted
|
||||
.insert(buffer.read(cx).remote_id())
|
||||
});
|
||||
|
||||
let _cleanup = defer({
|
||||
let this = this.clone();
|
||||
let mut cx = cx.clone();
|
||||
let closure_buffer = buffer.clone();
|
||||
move || {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.buffers_being_formatted
|
||||
.remove(&closure_buffer.read(cx).remote_id());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_edits(Some(position.timestamp))
|
||||
})
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
this.on_type_format(buffer, position, trigger, false, cx)
|
||||
})
|
||||
.await
|
||||
})
|
||||
} else if let Some(project_id) = self.remote_id() {
|
||||
let client = self.client.clone();
|
||||
let request = proto::OnTypeFormatting {
|
||||
project_id,
|
||||
buffer_id: buffer.read(cx).remote_id(),
|
||||
position: Some(serialize_anchor(&position)),
|
||||
trigger,
|
||||
version: serialize_version(&buffer.read(cx).version()),
|
||||
};
|
||||
cx.spawn(|_, _| async move {
|
||||
client
|
||||
.request(request)
|
||||
.await?
|
||||
.transaction
|
||||
.map(language::proto::deserialize_transaction)
|
||||
.transpose()
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("project does not have a remote id")))
|
||||
}
|
||||
}
|
||||
|
||||
async fn deserialize_edits(
|
||||
this: ModelHandle<Self>,
|
||||
buffer_to_edit: ModelHandle<Buffer>,
|
||||
edits: Vec<lsp::TextEdit>,
|
||||
push_to_history: bool,
|
||||
_: Arc<CachedLspAdapter>,
|
||||
language_server: Arc<LanguageServer>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Option<Transaction>> {
|
||||
let edits = this
|
||||
.update(cx, |this, cx| {
|
||||
this.edits_from_lsp(
|
||||
&buffer_to_edit,
|
||||
edits,
|
||||
language_server.server_id(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let transaction = buffer_to_edit.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.start_transaction();
|
||||
for (range, text) in edits {
|
||||
buffer.edit([(range, text)], None, cx);
|
||||
}
|
||||
|
||||
if buffer.end_transaction(cx).is_some() {
|
||||
let transaction = buffer.finalize_last_transaction().unwrap().clone();
|
||||
if !push_to_history {
|
||||
buffer.forget_transaction(transaction.id);
|
||||
}
|
||||
Some(transaction)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
async fn deserialize_workspace_edit(
|
||||
this: ModelHandle<Self>,
|
||||
edit: lsp::WorkspaceEdit,
|
||||
@@ -4209,6 +4455,31 @@ impl Project {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn on_type_format<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
position: T,
|
||||
trigger: String,
|
||||
push_to_history: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Transaction>>> {
|
||||
let tab_size = buffer.read_with(cx, |buffer, cx| {
|
||||
let language_name = buffer.language().map(|language| language.name());
|
||||
language_settings(language_name.as_deref(), cx).tab_size
|
||||
});
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
OnTypeFormatting {
|
||||
position,
|
||||
trigger,
|
||||
options: lsp_command::lsp_formatting_options(tab_size.get()).into(),
|
||||
push_to_history,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn search(
|
||||
&self,
|
||||
@@ -4600,7 +4871,7 @@ impl Project {
|
||||
if worktree.read(cx).is_local() {
|
||||
cx.subscribe(worktree, |this, worktree, event, cx| match event {
|
||||
worktree::Event::UpdatedEntries(changes) => {
|
||||
this.update_local_worktree_buffers(&worktree, &changes, cx);
|
||||
this.update_local_worktree_buffers(&worktree, changes, cx);
|
||||
this.update_local_worktree_language_servers(&worktree, changes, cx);
|
||||
}
|
||||
worktree::Event::UpdatedGitRepositories(updated_repos) => {
|
||||
@@ -4634,13 +4905,13 @@ impl Project {
|
||||
fn update_local_worktree_buffers(
|
||||
&mut self,
|
||||
worktree_handle: &ModelHandle<Worktree>,
|
||||
changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
|
||||
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let snapshot = worktree_handle.read(cx).snapshot();
|
||||
|
||||
let mut renamed_buffers = Vec::new();
|
||||
for (path, entry_id) in changes.keys() {
|
||||
for (path, entry_id, _) in changes {
|
||||
let worktree_id = worktree_handle.read(cx).id();
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
@@ -4746,7 +5017,7 @@ impl Project {
|
||||
fn update_local_worktree_language_servers(
|
||||
&mut self,
|
||||
worktree_handle: &ModelHandle<Worktree>,
|
||||
changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
|
||||
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if changes.is_empty() {
|
||||
@@ -4777,23 +5048,21 @@ impl Project {
|
||||
let params = lsp::DidChangeWatchedFilesParams {
|
||||
changes: changes
|
||||
.iter()
|
||||
.filter_map(|((path, _), change)| {
|
||||
if watched_paths.is_match(&path) {
|
||||
Some(lsp::FileEvent {
|
||||
uri: lsp::Url::from_file_path(abs_path.join(path))
|
||||
.unwrap(),
|
||||
typ: match change {
|
||||
PathChange::Added => lsp::FileChangeType::CREATED,
|
||||
PathChange::Removed => lsp::FileChangeType::DELETED,
|
||||
PathChange::Updated
|
||||
| PathChange::AddedOrUpdated => {
|
||||
lsp::FileChangeType::CHANGED
|
||||
}
|
||||
},
|
||||
})
|
||||
} else {
|
||||
None
|
||||
.filter_map(|(path, _, change)| {
|
||||
if !watched_paths.is_match(&path) {
|
||||
return None;
|
||||
}
|
||||
let typ = match change {
|
||||
PathChange::Loaded => return None,
|
||||
PathChange::Added => lsp::FileChangeType::CREATED,
|
||||
PathChange::Removed => lsp::FileChangeType::DELETED,
|
||||
PathChange::Updated => lsp::FileChangeType::CHANGED,
|
||||
PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED,
|
||||
};
|
||||
Some(lsp::FileEvent {
|
||||
uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(),
|
||||
typ,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
@@ -4812,98 +5081,102 @@ impl Project {
|
||||
fn update_local_worktree_buffers_git_repos(
|
||||
&mut self,
|
||||
worktree_handle: ModelHandle<Worktree>,
|
||||
repos: &HashMap<Arc<Path>, LocalRepositoryEntry>,
|
||||
changed_repos: &UpdatedGitRepositoriesSet,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
debug_assert!(worktree_handle.read(cx).is_local());
|
||||
|
||||
// Setup the pending buffers
|
||||
// Identify the loading buffers whose containing repository that has changed.
|
||||
let future_buffers = self
|
||||
.loading_buffers_by_path
|
||||
.iter()
|
||||
.filter_map(|(path, receiver)| {
|
||||
let path = &path.path;
|
||||
let (work_directory, repo) = repos
|
||||
.iter()
|
||||
.find(|(work_directory, _)| path.starts_with(work_directory))?;
|
||||
|
||||
let repo_relative_path = path.strip_prefix(work_directory).log_err()?;
|
||||
|
||||
.filter_map(|(project_path, receiver)| {
|
||||
if project_path.worktree_id != worktree_handle.read(cx).id() {
|
||||
return None;
|
||||
}
|
||||
let path = &project_path.path;
|
||||
changed_repos.iter().find(|(work_dir, change)| {
|
||||
path.starts_with(work_dir) && change.git_dir_changed
|
||||
})?;
|
||||
let receiver = receiver.clone();
|
||||
let repo_ptr = repo.repo_ptr.clone();
|
||||
let repo_relative_path = repo_relative_path.to_owned();
|
||||
let path = path.clone();
|
||||
Some(async move {
|
||||
pump_loading_buffer_reciever(receiver)
|
||||
wait_for_loading_buffer(receiver)
|
||||
.await
|
||||
.ok()
|
||||
.map(|buffer| (buffer, repo_relative_path, repo_ptr))
|
||||
.map(|buffer| (buffer, path))
|
||||
})
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.filter_map(|result| async move {
|
||||
let (buffer_handle, repo_relative_path, repo_ptr) = result?;
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
let lock = repo_ptr.lock();
|
||||
lock.load_index_text(&repo_relative_path)
|
||||
.map(|diff_base| (diff_base, buffer_handle))
|
||||
});
|
||||
// Identify the current buffers whose containing repository has changed.
|
||||
let current_buffers = self
|
||||
.opened_buffers
|
||||
.values()
|
||||
.filter_map(|buffer| {
|
||||
let buffer = buffer.upgrade(cx)?;
|
||||
let file = File::from_dyn(buffer.read(cx).file())?;
|
||||
if file.worktree != worktree_handle {
|
||||
return None;
|
||||
}
|
||||
let path = file.path();
|
||||
changed_repos.iter().find(|(work_dir, change)| {
|
||||
path.starts_with(work_dir) && change.git_dir_changed
|
||||
})?;
|
||||
Some((buffer, path.clone()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let update_diff_base_fn = update_diff_base(self);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let diff_base_tasks = cx
|
||||
if future_buffers.len() + current_buffers.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let remote_id = self.remote_id();
|
||||
let client = self.client.clone();
|
||||
cx.spawn_weak(move |_, mut cx| async move {
|
||||
// Wait for all of the buffers to load.
|
||||
let future_buffers = future_buffers.collect::<Vec<_>>().await;
|
||||
|
||||
// Reload the diff base for every buffer whose containing git repository has changed.
|
||||
let snapshot =
|
||||
worktree_handle.read_with(&cx, |tree, _| tree.as_local().unwrap().snapshot());
|
||||
let diff_bases_by_buffer = cx
|
||||
.background()
|
||||
.spawn(future_buffers.collect::<Vec<_>>())
|
||||
.spawn(async move {
|
||||
future_buffers
|
||||
.into_iter()
|
||||
.filter_map(|e| e)
|
||||
.chain(current_buffers)
|
||||
.filter_map(|(buffer, path)| {
|
||||
let (work_directory, repo) =
|
||||
snapshot.repository_and_work_directory_for_path(&path)?;
|
||||
let repo = snapshot.get_local_repo(&repo)?;
|
||||
let relative_path = path.strip_prefix(&work_directory).ok()?;
|
||||
let base_text = repo.repo_ptr.lock().load_index_text(&relative_path);
|
||||
Some((buffer, base_text))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.await;
|
||||
|
||||
for (diff_base, buffer) in diff_base_tasks.into_iter() {
|
||||
update_diff_base_fn(Some(diff_base), buffer, &mut cx);
|
||||
// Assign the new diff bases on all of the buffers.
|
||||
for (buffer, diff_base) in diff_bases_by_buffer {
|
||||
let buffer_id = buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_diff_base(diff_base.clone(), cx);
|
||||
buffer.remote_id()
|
||||
});
|
||||
if let Some(project_id) = remote_id {
|
||||
client
|
||||
.send(proto::UpdateDiffBase {
|
||||
project_id,
|
||||
buffer_id,
|
||||
diff_base,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// And the current buffers
|
||||
for (_, buffer) in &self.opened_buffers {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
let file = match File::from_dyn(buffer.read(cx).file()) {
|
||||
Some(file) => file,
|
||||
None => continue,
|
||||
};
|
||||
if file.worktree != worktree_handle {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = file.path().clone();
|
||||
|
||||
let worktree = worktree_handle.read(cx);
|
||||
|
||||
let (work_directory, repo) = match repos
|
||||
.iter()
|
||||
.find(|(work_directory, _)| path.starts_with(work_directory))
|
||||
{
|
||||
Some(repo) => repo.clone(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let relative_repo = match path.strip_prefix(work_directory).log_err() {
|
||||
Some(relative_repo) => relative_repo.to_owned(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
drop(worktree);
|
||||
|
||||
let update_diff_base_fn = update_diff_base(self);
|
||||
let git_ptr = repo.repo_ptr.clone();
|
||||
let diff_base_task = cx
|
||||
.background()
|
||||
.spawn(async move { git_ptr.lock().load_index_text(&relative_repo) });
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let diff_base = diff_base_task.await;
|
||||
update_diff_base_fn(diff_base, buffer, &mut cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||
@@ -4991,6 +5264,20 @@ impl Project {
|
||||
Some(ProjectPath { worktree_id, path })
|
||||
}
|
||||
|
||||
pub fn absolute_path(&self, project_path: &ProjectPath, cx: &AppContext) -> Option<PathBuf> {
|
||||
let workspace_root = self
|
||||
.worktree_for_id(project_path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.abs_path();
|
||||
let project_path = project_path.path.as_ref();
|
||||
|
||||
Some(if project_path == Path::new("") {
|
||||
workspace_root.to_path_buf()
|
||||
} else {
|
||||
workspace_root.join(project_path)
|
||||
})
|
||||
}
|
||||
|
||||
// RPC message handlers
|
||||
|
||||
async fn handle_unshare_project(
|
||||
@@ -5601,7 +5888,7 @@ impl Project {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let Some(guest_id) = envelope.original_sender_id else {
|
||||
log::error!("missing original_sender_id on SynchronizeBuffers request");
|
||||
error!("missing original_sender_id on SynchronizeBuffers request");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -5779,6 +6066,38 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_on_type_formatting(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::OnTypeFormatting>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::OnTypeFormattingResponse> {
|
||||
let on_type_formatting = this.update(&mut cx, |this, cx| {
|
||||
let buffer = this
|
||||
.opened_buffers
|
||||
.get(&envelope.payload.buffer_id)
|
||||
.and_then(|buffer| buffer.upgrade(cx))
|
||||
.ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
|
||||
let position = envelope
|
||||
.payload
|
||||
.position
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid position"))?;
|
||||
Ok::<_, anyhow::Error>(this.apply_on_type_formatting(
|
||||
buffer,
|
||||
position,
|
||||
envelope.payload.trigger.clone(),
|
||||
cx,
|
||||
))
|
||||
})?;
|
||||
|
||||
let transaction = on_type_formatting
|
||||
.await?
|
||||
.as_ref()
|
||||
.map(language::proto::serialize_transaction);
|
||||
Ok(proto::OnTypeFormattingResponse { transaction })
|
||||
}
|
||||
|
||||
async fn handle_lsp_command<T: LspCommand>(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<T::ProtoRequest>,
|
||||
@@ -6038,11 +6357,13 @@ impl Project {
|
||||
let Some(this) = this.upgrade(&cx) else {
|
||||
return Err(anyhow!("project dropped"));
|
||||
};
|
||||
|
||||
let buffer = this.read_with(&cx, |this, cx| {
|
||||
this.opened_buffers
|
||||
.get(&id)
|
||||
.and_then(|buffer| buffer.upgrade(cx))
|
||||
});
|
||||
|
||||
if let Some(buffer) = buffer {
|
||||
break buffer;
|
||||
} else if this.read_with(&cx, |this, _| this.is_read_only()) {
|
||||
@@ -6053,12 +6374,13 @@ impl Project {
|
||||
this.incomplete_remote_buffers.entry(id).or_default();
|
||||
});
|
||||
drop(this);
|
||||
|
||||
opened_buffer_rx
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
|
||||
};
|
||||
buffer.update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx));
|
||||
|
||||
Ok(buffer)
|
||||
})
|
||||
}
|
||||
@@ -6790,7 +7112,7 @@ impl Item for Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
async fn pump_loading_buffer_reciever(
|
||||
async fn wait_for_loading_buffer(
|
||||
mut receiver: postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
|
||||
) -> Result<ModelHandle<Buffer>, Arc<anyhow::Error>> {
|
||||
loop {
|
||||
@@ -6803,26 +7125,3 @@ async fn pump_loading_buffer_reciever(
|
||||
receiver.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_diff_base(
|
||||
project: &Project,
|
||||
) -> impl Fn(Option<String>, ModelHandle<Buffer>, &mut AsyncAppContext) {
|
||||
let remote_id = project.remote_id();
|
||||
let client = project.client().clone();
|
||||
move |diff_base, buffer, cx| {
|
||||
let buffer_id = buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(diff_base.clone(), cx);
|
||||
buffer.remote_id()
|
||||
});
|
||||
|
||||
if let Some(project_id) = remote_id {
|
||||
client
|
||||
.send(proto::UpdateDiffBase {
|
||||
project_id,
|
||||
buffer_id: buffer_id as u64,
|
||||
diff_base,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,22 @@ use std::sync::Arc;
|
||||
pub struct ProjectSettings {
|
||||
#[serde(default)]
|
||||
pub lsp: HashMap<Arc<str>, LspSettings>,
|
||||
#[serde(default)]
|
||||
pub git: GitSettings,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GitSettings {
|
||||
pub git_gutter: Option<GitGutterSetting>,
|
||||
pub gutter_debounce: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitGutterSetting {
|
||||
#[default]
|
||||
TrackedFiles,
|
||||
Hide,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
|
||||
@@ -506,7 +506,9 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
register_options: serde_json::to_value(
|
||||
lsp::DidChangeWatchedFilesRegistrationOptions {
|
||||
watchers: vec![lsp::FileSystemWatcher {
|
||||
glob_pattern: "/the-root/*.{rs,c}".to_string(),
|
||||
glob_pattern: lsp::GlobPattern::String(
|
||||
"/the-root/*.{rs,c}".to_string(),
|
||||
),
|
||||
kind: None,
|
||||
}],
|
||||
},
|
||||
@@ -1193,7 +1195,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 3)]
|
||||
async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -1273,6 +1275,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
// The diagnostics have moved down since they were created.
|
||||
buffer.next_notification(cx).await;
|
||||
cx.foreground().run_until_parked();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer
|
||||
@@ -1351,6 +1354,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
|
||||
buffer.next_notification(cx).await;
|
||||
cx.foreground().run_until_parked();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer
|
||||
@@ -1443,6 +1447,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
|
||||
buffer.next_notification(cx).await;
|
||||
cx.foreground().run_until_parked();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer
|
||||
@@ -2523,29 +2528,21 @@ async fn test_rescan_and_remote_updates(
|
||||
|
||||
// Create a remote copy of this worktree.
|
||||
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
|
||||
let remote = cx.update(|cx| {
|
||||
Worktree::remote(
|
||||
1,
|
||||
1,
|
||||
proto::WorktreeMetadata {
|
||||
id: initial_snapshot.id().to_proto(),
|
||||
root_name: initial_snapshot.root_name().into(),
|
||||
abs_path: initial_snapshot
|
||||
.abs_path()
|
||||
.as_os_str()
|
||||
.to_string_lossy()
|
||||
.into(),
|
||||
visible: true,
|
||||
},
|
||||
rpc.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
remote.update(cx, |remote, _| {
|
||||
let update = initial_snapshot.build_initial_update(1);
|
||||
remote.as_remote_mut().unwrap().update_from_remote(update);
|
||||
|
||||
let metadata = tree.read_with(cx, |tree, _| tree.as_local().unwrap().metadata_proto());
|
||||
|
||||
let updates = Arc::new(Mutex::new(Vec::new()));
|
||||
tree.update(cx, |tree, cx| {
|
||||
let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
|
||||
let updates = updates.clone();
|
||||
move |update| {
|
||||
updates.lock().push(update);
|
||||
async { true }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let remote = cx.update(|cx| Worktree::remote(1, 1, metadata, rpc.clone(), cx));
|
||||
deterministic.run_until_parked();
|
||||
|
||||
cx.read(|cx| {
|
||||
@@ -2611,14 +2608,11 @@ async fn test_rescan_and_remote_updates(
|
||||
|
||||
// Update the remote worktree. Check that it becomes consistent with the
|
||||
// local worktree.
|
||||
remote.update(cx, |remote, cx| {
|
||||
let update = tree.read(cx).as_local().unwrap().snapshot().build_update(
|
||||
&initial_snapshot,
|
||||
1,
|
||||
1,
|
||||
true,
|
||||
);
|
||||
remote.as_remote_mut().unwrap().update_from_remote(update);
|
||||
deterministic.run_until_parked();
|
||||
remote.update(cx, |remote, _| {
|
||||
for update in updates.lock().drain(..) {
|
||||
remote.as_remote_mut().unwrap().update_from_remote(update);
|
||||
}
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
remote.read_with(cx, |remote, _| {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -527,18 +527,22 @@ impl ProjectPanel {
|
||||
let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
|
||||
let filename = self.filename_editor.read(cx).text(cx);
|
||||
|
||||
let path_already_exists = |path| worktree.read(cx).entry_for_path(path).is_some();
|
||||
let edit_task;
|
||||
let edited_entry_id;
|
||||
|
||||
if is_new_entry {
|
||||
self.selection = Some(Selection {
|
||||
worktree_id,
|
||||
entry_id: NEW_ENTRY_ID,
|
||||
});
|
||||
let new_path = entry.path.join(&filename);
|
||||
if path_already_exists(new_path.as_path()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
edited_entry_id = NEW_ENTRY_ID;
|
||||
edit_task = self.project.update(cx, |project, cx| {
|
||||
project.create_entry((worktree_id, new_path), is_dir, cx)
|
||||
project.create_entry((worktree_id, &new_path), is_dir, cx)
|
||||
})?;
|
||||
} else {
|
||||
let new_path = if let Some(parent) = entry.path.clone().parent() {
|
||||
@@ -546,9 +550,13 @@ impl ProjectPanel {
|
||||
} else {
|
||||
filename.clone().into()
|
||||
};
|
||||
if path_already_exists(new_path.as_path()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
edited_entry_id = entry.id;
|
||||
edit_task = self.project.update(cx, |project, cx| {
|
||||
project.rename_entry(entry.id, new_path, cx)
|
||||
project.rename_entry(entry.id, new_path.as_path(), cx)
|
||||
})?;
|
||||
};
|
||||
|
||||
@@ -2126,6 +2134,152 @@ mod tests {
|
||||
ensure_no_open_items_and_panes(window_id, &workspace, cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
"first.rs": "// First Rust file",
|
||||
"second.rs": "// Second Rust file",
|
||||
"third.rs": "// Third Rust file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
|
||||
|
||||
select_path(&panel, "src/", cx);
|
||||
panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v src <== selected", " > test"]
|
||||
);
|
||||
panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
|
||||
cx.read_window(window_id, |cx| {
|
||||
let panel = panel.read(cx);
|
||||
assert!(panel.filename_editor.is_focused(cx));
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v src", " > [EDITOR: ''] <== selected", " > test"]
|
||||
);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.filename_editor
|
||||
.update(cx, |editor, cx| editor.set_text("test", cx));
|
||||
assert!(
|
||||
panel.confirm(&Confirm, cx).is_none(),
|
||||
"Should not allow to confirm on conflicting new directory name"
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v src", " > test"],
|
||||
"File list should be unchanged after failed folder create confirmation"
|
||||
);
|
||||
|
||||
select_path(&panel, "src/test/", cx);
|
||||
panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v src", " > test <== selected"]
|
||||
);
|
||||
panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
|
||||
cx.read_window(window_id, |cx| {
|
||||
let panel = panel.read(cx);
|
||||
assert!(panel.filename_editor.is_focused(cx));
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" [EDITOR: ''] <== selected",
|
||||
" first.rs",
|
||||
" second.rs",
|
||||
" third.rs"
|
||||
]
|
||||
);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.filename_editor
|
||||
.update(cx, |editor, cx| editor.set_text("first.rs", cx));
|
||||
assert!(
|
||||
panel.confirm(&Confirm, cx).is_none(),
|
||||
"Should not allow to confirm on conflicting new file name"
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" first.rs",
|
||||
" second.rs",
|
||||
" third.rs"
|
||||
],
|
||||
"File list should be unchanged after failed file create confirmation"
|
||||
);
|
||||
|
||||
select_path(&panel, "src/test/first.rs", cx);
|
||||
panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" first.rs <== selected",
|
||||
" second.rs",
|
||||
" third.rs"
|
||||
],
|
||||
);
|
||||
panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
|
||||
cx.read_window(window_id, |cx| {
|
||||
let panel = panel.read(cx);
|
||||
assert!(panel.filename_editor.is_focused(cx));
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" [EDITOR: 'first.rs'] <== selected",
|
||||
" second.rs",
|
||||
" third.rs"
|
||||
]
|
||||
);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.filename_editor
|
||||
.update(cx, |editor, cx| editor.set_text("second.rs", cx));
|
||||
assert!(
|
||||
panel.confirm(&Confirm, cx).is_none(),
|
||||
"Should not allow to confirm on conflicting file rename"
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" first.rs <== selected",
|
||||
" second.rs",
|
||||
" third.rs"
|
||||
],
|
||||
"File list should be unchanged after failed rename confirmation"
|
||||
);
|
||||
}
|
||||
|
||||
fn toggle_expand_dir(
|
||||
panel: &ViewHandle<ProjectPanel>,
|
||||
path: impl AsRef<Path>,
|
||||
@@ -2229,6 +2383,7 @@ mod tests {
|
||||
editor::init_settings(cx);
|
||||
crate::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2243,6 +2398,7 @@ mod tests {
|
||||
pane::init(cx);
|
||||
crate::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -284,7 +284,7 @@ mod tests {
|
||||
symbol("uno", "/dir/test.rs"),
|
||||
];
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
fake_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(
|
||||
fake_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(
|
||||
move |params: lsp::WorkspaceSymbolParams, cx| {
|
||||
let executor = cx.background();
|
||||
let fake_symbols = fake_symbols.clone();
|
||||
@@ -308,12 +308,12 @@ mod tests {
|
||||
.await
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
Ok(Some(lsp::WorkspaceSymbolResponse::Flat(
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| fake_symbols[mat.candidate_id].clone())
|
||||
.collect(),
|
||||
))
|
||||
)))
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -129,6 +129,9 @@ message Envelope {
|
||||
GetPrivateUserInfo get_private_user_info = 105;
|
||||
GetPrivateUserInfoResponse get_private_user_info_response = 106;
|
||||
UpdateDiffBase update_diff_base = 107;
|
||||
|
||||
OnTypeFormatting on_type_formatting = 111;
|
||||
OnTypeFormattingResponse on_type_formatting_response = 112;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,6 +673,18 @@ message PerformRename {
|
||||
repeated VectorClockEntry version = 5;
|
||||
}
|
||||
|
||||
message OnTypeFormatting {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Anchor position = 3;
|
||||
string trigger = 4;
|
||||
repeated VectorClockEntry version = 5;
|
||||
}
|
||||
|
||||
message OnTypeFormattingResponse {
|
||||
Transaction transaction = 1;
|
||||
}
|
||||
|
||||
message PerformRenameResponse {
|
||||
ProjectTransaction transaction = 2;
|
||||
}
|
||||
|
||||
@@ -195,6 +195,8 @@ messages!(
|
||||
(OpenBufferResponse, Background),
|
||||
(PerformRename, Background),
|
||||
(PerformRenameResponse, Background),
|
||||
(OnTypeFormatting, Background),
|
||||
(OnTypeFormattingResponse, Background),
|
||||
(Ping, Foreground),
|
||||
(PrepareRename, Background),
|
||||
(PrepareRenameResponse, Background),
|
||||
@@ -279,6 +281,7 @@ request_messages!(
|
||||
(Ping, Ack),
|
||||
(PerformRename, PerformRenameResponse),
|
||||
(PrepareRename, PrepareRenameResponse),
|
||||
(OnTypeFormatting, OnTypeFormattingResponse),
|
||||
(ReloadBuffers, ReloadBuffersResponse),
|
||||
(RequestContact, Ack),
|
||||
(RemoveContact, Ack),
|
||||
@@ -323,6 +326,7 @@ entity_messages!(
|
||||
OpenBufferByPath,
|
||||
OpenBufferForSymbol,
|
||||
PerformRename,
|
||||
OnTypeFormatting,
|
||||
PrepareRename,
|
||||
ReloadBuffers,
|
||||
RemoveProjectCollaborator,
|
||||
|
||||
@@ -6,4 +6,4 @@ pub use conn::Connection;
|
||||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 55;
|
||||
pub const PROTOCOL_VERSION: u32 = 56;
|
||||
|
||||
@@ -44,11 +44,11 @@ struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSe
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(ActiveSearches::default());
|
||||
cx.add_action(ProjectSearchView::deploy);
|
||||
cx.add_action(ProjectSearchView::move_focus_to_results);
|
||||
cx.add_action(ProjectSearchBar::search);
|
||||
cx.add_action(ProjectSearchBar::search_in_new);
|
||||
cx.add_action(ProjectSearchBar::select_next_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_previous);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
||||
@@ -360,15 +360,6 @@ impl Item for ProjectSearchView {
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
}
|
||||
|
||||
fn git_diff_recalc(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.results_editor
|
||||
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
match event {
|
||||
ViewEvent::UpdateTab => {
|
||||
@@ -717,6 +708,23 @@ impl ProjectSearchView {
|
||||
pub fn has_matches(&self) -> bool {
|
||||
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 {
|
||||
@@ -794,23 +802,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>) {
|
||||
self.cycle_field(Direction::Next, cx);
|
||||
}
|
||||
@@ -1257,7 +1248,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) {
|
||||
cx.foreground().forbid_parking();
|
||||
let fonts = cx.font_cache();
|
||||
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
|
||||
theme.search.match_background = Color::red();
|
||||
@@ -1275,8 +1441,10 @@ pub mod tests {
|
||||
|
||||
language::init(cx);
|
||||
client::init_settings(cx);
|
||||
editor::init_settings(cx);
|
||||
editor::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
super::init(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
indoc = "1.0.7"
|
||||
indoc.workspace = true
|
||||
libsqlite3-sys = { version = "0.24", features = ["bundled"] }
|
||||
smol.workspace = true
|
||||
thread_local = "1.1.4"
|
||||
|
||||
@@ -14,6 +14,7 @@ editor = { path = "../editor" }
|
||||
language = { path = "../language" }
|
||||
gpui = { path = "../gpui" }
|
||||
project = { path = "../project" }
|
||||
search = { path = "../search" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
|
||||
@@ -22,7 +22,7 @@ const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
|
||||
actions!(terminal_panel, [ToggleFocus]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(TerminalPanel::add_terminal);
|
||||
cx.add_action(TerminalPanel::new_terminal);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
@@ -50,11 +50,13 @@ impl TerminalPanel {
|
||||
let window_id = cx.window_id();
|
||||
let mut pane = Pane::new(
|
||||
workspace.weak_handle(),
|
||||
workspace.project().clone(),
|
||||
workspace.app_state().background_actions,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(false, cx);
|
||||
pane.set_can_navigate(false, cx);
|
||||
pane.on_can_drop(move |drag_and_drop, cx| {
|
||||
drag_and_drop
|
||||
.currently_dragged::<DraggedItem>(window_id)
|
||||
@@ -68,6 +70,7 @@ impl TerminalPanel {
|
||||
.with_child(Pane::render_tab_bar_button(
|
||||
0,
|
||||
"icons/plus_12.svg",
|
||||
false,
|
||||
Some((
|
||||
"New Terminal".into(),
|
||||
Some(Box::new(workspace::NewTerminal)),
|
||||
@@ -78,7 +81,7 @@ impl TerminalPanel {
|
||||
cx.window_context().defer(move |cx| {
|
||||
if let Some(this) = this.upgrade(cx) {
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_terminal(&Default::default(), cx);
|
||||
this.add_terminal(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -92,6 +95,7 @@ impl TerminalPanel {
|
||||
} else {
|
||||
"icons/maximize_8.svg"
|
||||
},
|
||||
pane.is_zoomed(),
|
||||
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
|
||||
cx,
|
||||
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|
||||
@@ -99,6 +103,9 @@ impl TerminalPanel {
|
||||
))
|
||||
.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
|
||||
});
|
||||
let subscriptions = vec![
|
||||
@@ -172,8 +179,9 @@ impl TerminalPanel {
|
||||
(panel, pane, items)
|
||||
})?;
|
||||
|
||||
let pane = pane.downgrade();
|
||||
let items = futures::future::join_all(items).await;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let active_item_id = serialized_panel
|
||||
.as_ref()
|
||||
.and_then(|panel| panel.active_item_id);
|
||||
@@ -181,17 +189,15 @@ impl TerminalPanel {
|
||||
for item in items {
|
||||
if let Some(item) = item.log_err() {
|
||||
let item_id = item.id();
|
||||
Pane::add_item(workspace, &pane, Box::new(item), false, false, None, cx);
|
||||
pane.add_item(Box::new(item), false, false, None, cx);
|
||||
if Some(item_id) == active_item_id {
|
||||
active_ix = Some(pane.read(cx).items_len() - 1);
|
||||
active_ix = Some(pane.items_len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(active_ix) = active_ix {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(active_ix, false, false, cx)
|
||||
});
|
||||
pane.activate_item(active_ix, false, false, cx)
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -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();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
|
||||
@@ -236,8 +254,10 @@ impl TerminalPanel {
|
||||
Box::new(cx.add_view(|cx| {
|
||||
TerminalView::new(terminal, workspace.database_id(), cx)
|
||||
}));
|
||||
let focus = pane.read(cx).has_focus();
|
||||
Pane::add_item(workspace, &pane, terminal, true, focus, None, cx);
|
||||
pane.update(cx, |pane, cx| {
|
||||
let focus = pane.has_focus();
|
||||
pane.add_item(terminal, true, focus, None, cx);
|
||||
});
|
||||
}
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.serialize(cx))?;
|
||||
@@ -357,7 +377,7 @@ impl Panel for TerminalPanel {
|
||||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
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,
|
||||
pane, register_deserializable_item,
|
||||
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
||||
Pane, ToolbarItemLocation, Workspace, WorkspaceId,
|
||||
NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
pub use terminal::TerminalSettings;
|
||||
@@ -66,10 +66,10 @@ pub fn init(cx: &mut AppContext) {
|
||||
terminal_panel::init(cx);
|
||||
terminal::init(cx);
|
||||
|
||||
cx.add_action(TerminalView::deploy);
|
||||
|
||||
register_deserializable_item::<TerminalView>(cx);
|
||||
|
||||
cx.add_action(TerminalView::deploy);
|
||||
|
||||
//Useful terminal views
|
||||
cx.add_action(TerminalView::send_text);
|
||||
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
|
||||
pub fn deploy(
|
||||
workspace: &mut Workspace,
|
||||
_: &workspace::NewTerminal,
|
||||
_: &NewCenterTerminal,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let strategy = settings::get::<TerminalSettings>(cx);
|
||||
|
||||
@@ -89,7 +89,8 @@ pub struct Workspace {
|
||||
pub breadcrumbs: Interactive<ContainedText>,
|
||||
pub disconnected_overlay: ContainedText,
|
||||
pub modal: ContainerStyle,
|
||||
pub zoomed_foreground: ContainerStyle,
|
||||
pub zoomed_panel_foreground: ContainerStyle,
|
||||
pub zoomed_pane_foreground: ContainerStyle,
|
||||
pub zoomed_background: ContainerStyle,
|
||||
pub notification: ContainerStyle,
|
||||
pub notifications: Notifications,
|
||||
|
||||
@@ -17,7 +17,7 @@ backtrace = "0.3"
|
||||
log.workspace = true
|
||||
lazy_static.workspace = true
|
||||
futures.workspace = true
|
||||
isahc = "1.7"
|
||||
isahc.workspace = true
|
||||
smol.workspace = true
|
||||
url = "2.2"
|
||||
rand.workspace = true
|
||||
|
||||
@@ -35,7 +35,7 @@ settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc = "1.0.4"
|
||||
indoc.workspace = true
|
||||
parking_lot.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) {
|
||||
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));
|
||||
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
|
||||
cx.focus(&welcome_page);
|
||||
|
||||
@@ -62,5 +62,5 @@ settings = { path = "../settings", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
||||
db = { path = "../db", features = ["test-support"] }
|
||||
|
||||
indoc = "1.0.4"
|
||||
indoc.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 {
|
||||
self.is_open
|
||||
}
|
||||
|
||||
pub fn has_focus(&self, cx: &WindowContext) -> bool {
|
||||
self.active_panel()
|
||||
self.visible_panel()
|
||||
.map_or(false, |panel| panel.has_focus(cx))
|
||||
}
|
||||
|
||||
@@ -201,7 +205,7 @@ impl Dock {
|
||||
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 {
|
||||
self.is_open = open;
|
||||
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(
|
||||
&mut self,
|
||||
panel: &AnyViewHandle,
|
||||
@@ -259,7 +258,7 @@ impl Dock {
|
||||
cx.focus(&panel);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
@@ -315,12 +314,16 @@ impl Dock {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
|
||||
let entry = self.active_entry()?;
|
||||
pub fn visible_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
|
||||
let entry = self.visible_entry()?;
|
||||
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 {
|
||||
self.panel_entries.get(self.active_panel_index)
|
||||
} else {
|
||||
@@ -329,7 +332,7 @@ impl Dock {
|
||||
}
|
||||
|
||||
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) {
|
||||
Some(entry.panel.clone())
|
||||
} else {
|
||||
@@ -362,7 +365,7 @@ impl Dock {
|
||||
}
|
||||
|
||||
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()
|
||||
.into_any()
|
||||
.contained()
|
||||
@@ -399,7 +402,7 @@ impl View for Dock {
|
||||
}
|
||||
|
||||
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);
|
||||
ChildView::new(active_entry.panel.as_any(), cx)
|
||||
.contained()
|
||||
@@ -417,7 +420,7 @@ impl View for Dock {
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
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());
|
||||
} else {
|
||||
cx.focus_parent();
|
||||
@@ -473,11 +476,22 @@ impl View for PanelButtons {
|
||||
Flex::row()
|
||||
.with_children(panels.into_iter().enumerate().map(
|
||||
|(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()
|
||||
.with_child(
|
||||
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);
|
||||
Flex::row()
|
||||
.with_child(
|
||||
@@ -504,13 +518,22 @@ impl View for PanelButtons {
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, {
|
||||
let tooltip_action =
|
||||
tooltip_action.as_ref().map(|action| action.boxed_clone());
|
||||
move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_panel(dock_position, panel_ix, cx)
|
||||
});
|
||||
});
|
||||
if let Some(tooltip_action) = &tooltip_action {
|
||||
let window_id = cx.window_id();
|
||||
let view_id = this.workspace.id();
|
||||
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,9 +1,8 @@
|
||||
use crate::{
|
||||
pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction,
|
||||
FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace,
|
||||
WorkspaceId,
|
||||
pane, persistence::model::ItemId, searchable::SearchableItemHandle, FollowableItemBuilders,
|
||||
ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
use crate::{AutosaveSetting, WorkspaceSettings};
|
||||
use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
|
||||
use anyhow::Result;
|
||||
use client::{proto, Client};
|
||||
use gpui::{
|
||||
@@ -102,13 +101,6 @@ pub trait Item: View {
|
||||
) -> Task<Result<()>> {
|
||||
unimplemented!("reload() must be implemented if can_save() returns true")
|
||||
}
|
||||
fn git_diff_recalc(
|
||||
&mut self,
|
||||
_project: ModelHandle<Project>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
fn to_item_events(_event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
@@ -221,11 +213,6 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<()>>;
|
||||
fn reload(&self, project: ModelHandle<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
|
||||
fn git_diff_recalc(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<()>>;
|
||||
fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle>;
|
||||
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
||||
fn on_release(
|
||||
@@ -381,7 +368,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
.is_none()
|
||||
{
|
||||
let mut pending_autosave = DelayedDebouncedEditAction::new();
|
||||
let mut pending_git_update = DelayedDebouncedEditAction::new();
|
||||
let pending_update = Rc::new(RefCell::new(None));
|
||||
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
|
||||
|
||||
@@ -450,48 +436,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
}
|
||||
|
||||
ItemEvent::Edit => {
|
||||
let settings = settings::get::<WorkspaceSettings>(cx);
|
||||
let debounce_delay = settings.git.gutter_debounce;
|
||||
|
||||
if let AutosaveSetting::AfterDelay { milliseconds } =
|
||||
settings.autosave
|
||||
{
|
||||
let autosave = settings::get::<WorkspaceSettings>(cx).autosave;
|
||||
if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
|
||||
let delay = Duration::from_millis(milliseconds);
|
||||
let item = item.clone();
|
||||
pending_autosave.fire_new(delay, cx, move |workspace, cx| {
|
||||
Pane::autosave_item(&item, workspace.project().clone(), cx)
|
||||
});
|
||||
}
|
||||
|
||||
let item = item.clone();
|
||||
|
||||
if let Some(delay) = debounce_delay {
|
||||
const MIN_GIT_DELAY: u64 = 50;
|
||||
|
||||
let delay = delay.max(MIN_GIT_DELAY);
|
||||
let duration = Duration::from_millis(delay);
|
||||
|
||||
pending_git_update.fire_new(
|
||||
duration,
|
||||
cx,
|
||||
move |workspace, cx| {
|
||||
item.git_diff_recalc(workspace.project().clone(), cx)
|
||||
},
|
||||
);
|
||||
} else {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
item.git_diff_recalc(
|
||||
workspace.project().clone(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
@@ -576,14 +528,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
self.update(cx, |item, cx| item.reload(project, cx))
|
||||
}
|
||||
|
||||
fn git_diff_recalc(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<()>> {
|
||||
self.update(cx, |item, cx| item.git_diff_recalc(project, cx))
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle> {
|
||||
self.read(cx).act_as_type(type_id, self, cx)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{Toast, Workspace};
|
||||
use collections::HashSet;
|
||||
use collections::HashMap;
|
||||
use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
|
||||
use std::{any::TypeId, ops::DerefMut};
|
||||
|
||||
@@ -33,12 +33,12 @@ impl From<&dyn NotificationHandle> for AnyViewHandle {
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationTracker {
|
||||
notifications_sent: HashSet<TypeId>,
|
||||
pub(crate) struct NotificationTracker {
|
||||
notifications_sent: HashMap<TypeId, Vec<usize>>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for NotificationTracker {
|
||||
type Target = HashSet<TypeId>;
|
||||
type Target = HashMap<TypeId, Vec<usize>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.notifications_sent
|
||||
@@ -54,24 +54,33 @@ impl DerefMut for NotificationTracker {
|
||||
impl NotificationTracker {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
notifications_sent: HashSet::default(),
|
||||
notifications_sent: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>(
|
||||
&mut self,
|
||||
id: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
|
||||
) {
|
||||
if !cx
|
||||
.global::<NotificationTracker>()
|
||||
.contains(&TypeId::of::<V>())
|
||||
{
|
||||
if !self.has_shown_notification_once::<V>(id, cx) {
|
||||
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)
|
||||
@@ -154,9 +163,10 @@ pub mod simple_message_notification {
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
|
||||
fonts::TextStyle,
|
||||
impl_actions,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Element, Entity, View, ViewContext,
|
||||
AnyElement, AppContext, Element, Entity, View, ViewContext,
|
||||
};
|
||||
use menu::Cancel;
|
||||
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 {
|
||||
message: Cow<'static, str>,
|
||||
message: NotificationMessage,
|
||||
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
|
||||
click_message: Option<Cow<'static, str>>,
|
||||
}
|
||||
@@ -204,7 +219,17 @@ pub mod simple_message_notification {
|
||||
S: Into<Cow<'static, str>>,
|
||||
{
|
||||
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,
|
||||
click_message: None,
|
||||
}
|
||||
@@ -243,84 +268,90 @@ pub mod simple_message_notification {
|
||||
enum MessageNotificationTag {}
|
||||
|
||||
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 has_click_action = on_click.is_some();
|
||||
|
||||
MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Text::new(message, theme.message.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.message.container)
|
||||
.aligned()
|
||||
.top()
|
||||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
})
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.dismiss(&Default::default(), cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(
|
||||
cx.font_cache().line_height(theme.message.text.font_size),
|
||||
)
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
message
|
||||
.contained()
|
||||
.with_style(theme.message.container)
|
||||
.aligned()
|
||||
.top()
|
||||
.flex_float(),
|
||||
),
|
||||
)
|
||||
.with_children({
|
||||
let style = theme.action_message.style_for(state, false);
|
||||
if let Some(click_message) = click_message {
|
||||
Some(
|
||||
Flex::row().with_child(
|
||||
Text::new(click_message, style.text.clone())
|
||||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
})
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.dismiss(&Default::default(), cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(cx.font_cache().line_height(theme.message.text.font_size))
|
||||
.aligned()
|
||||
.top()
|
||||
.flex_float(),
|
||||
),
|
||||
)
|
||||
.with_children({
|
||||
click_message
|
||||
.map(|click_message| {
|
||||
MouseEventHandler::<MessageNotificationTag, _>::new(
|
||||
0,
|
||||
cx,
|
||||
|state, _| {
|
||||
let style = theme.action_message.style_for(state, false);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Text::new(click_message, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.container),
|
||||
),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(on_click) = on_click.as_ref() {
|
||||
on_click(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 {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::Arrow
|
||||
})
|
||||
})
|
||||
.into_iter()
|
||||
})
|
||||
.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| {
|
||||
if let Some(on_click) = on_click.as_ref() {
|
||||
on_click(cx);
|
||||
this.dismiss(&Default::default(), cx);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(if has_click_action {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::Arrow
|
||||
})
|
||||
.into_any()
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -183,7 +183,7 @@ pub fn handle_dropped_item<V: View>(
|
||||
.zip(pane.upgrade(cx))
|
||||
{
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::move_item(workspace, from, to, item_id, index, cx);
|
||||
workspace.move_item(from, to, item_id, index, cx);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use db::sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
@@ -230,7 +230,7 @@ impl SerializedPane {
|
||||
pub async fn deserialize_to(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
pane_handle: &WeakViewHandle<Pane>,
|
||||
pane: &WeakViewHandle<Pane>,
|
||||
workspace_id: WorkspaceId,
|
||||
workspace: &WeakViewHandle<Workspace>,
|
||||
cx: &mut AsyncAppContext,
|
||||
@@ -239,7 +239,7 @@ impl SerializedPane {
|
||||
let mut active_item_index = None;
|
||||
for (index, item) in self.children.iter().enumerate() {
|
||||
let project = project.clone();
|
||||
let item_handle = pane_handle
|
||||
let item_handle = pane
|
||||
.update(cx, |_, cx| {
|
||||
if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
|
||||
deserializer(project, workspace.clone(), workspace_id, item.item_id, cx)
|
||||
@@ -256,13 +256,9 @@ impl SerializedPane {
|
||||
items.push(item_handle.clone());
|
||||
|
||||
if let Some(item_handle) = item_handle {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let pane_handle = pane_handle
|
||||
.upgrade(cx)
|
||||
.ok_or_else(|| anyhow!("pane was dropped"))?;
|
||||
Pane::add_item(workspace, &pane_handle, item_handle, true, true, None, cx);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.add_item(item_handle.clone(), true, true, None, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
if item.active {
|
||||
@@ -271,7 +267,7 @@ impl SerializedPane {
|
||||
}
|
||||
|
||||
if let Some(active_item_index) = active_item_index {
|
||||
pane_handle.update(cx, |pane, cx| {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(active_item_index, false, false, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ impl Item for SharedScreen {
|
||||
Some(format!("{}'s screen", self.user.github_login).into())
|
||||
}
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(nav_history) = self.nav_history.as_ref() {
|
||||
if let Some(nav_history) = self.nav_history.as_mut() {
|
||||
nav_history.push::<()>(None, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ pub enum ToolbarItemLocation {
|
||||
pub struct Toolbar {
|
||||
active_pane_item: Option<Box<dyn ItemHandle>>,
|
||||
hidden: bool,
|
||||
can_navigate: bool,
|
||||
pane: WeakViewHandle<Pane>,
|
||||
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
||||
}
|
||||
@@ -132,76 +133,86 @@ impl View for Toolbar {
|
||||
let button_style = theme.nav_button;
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(nav_button(
|
||||
"icons/arrow_left_16.svg",
|
||||
button_style,
|
||||
nav_button_height,
|
||||
tooltip_style.clone(),
|
||||
enable_go_backward,
|
||||
spacing,
|
||||
let mut primary_items = Flex::row();
|
||||
if self.can_navigate {
|
||||
primary_items.add_child(nav_button(
|
||||
"icons/arrow_left_16.svg",
|
||||
button_style,
|
||||
nav_button_height,
|
||||
tooltip_style.clone(),
|
||||
enable_go_backward,
|
||||
spacing,
|
||||
{
|
||||
let pane = pane.clone();
|
||||
move |toolbar, cx| {
|
||||
if let Some(workspace) = toolbar
|
||||
.pane
|
||||
.upgrade(cx)
|
||||
.and_then(|pane| pane.read(cx).workspace().upgrade(cx))
|
||||
{
|
||||
let pane = pane.clone();
|
||||
move |toolbar, cx| {
|
||||
if let Some(workspace) = toolbar
|
||||
.pane
|
||||
.upgrade(cx)
|
||||
.and_then(|pane| pane.read(cx).workspace().upgrade(cx))
|
||||
{
|
||||
let pane = pane.clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::go_back(workspace, Some(pane.clone()), cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoBack { pane: None },
|
||||
"Go Back",
|
||||
cx,
|
||||
))
|
||||
.with_child(nav_button(
|
||||
"icons/arrow_right_16.svg",
|
||||
button_style,
|
||||
nav_button_height,
|
||||
tooltip_style,
|
||||
enable_go_forward,
|
||||
spacing,
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.go_back(pane.clone(), cx).detach_and_log_err(cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoBack,
|
||||
"Go Back",
|
||||
cx,
|
||||
));
|
||||
primary_items.add_child(nav_button(
|
||||
"icons/arrow_right_16.svg",
|
||||
button_style,
|
||||
nav_button_height,
|
||||
tooltip_style,
|
||||
enable_go_forward,
|
||||
spacing,
|
||||
{
|
||||
let pane = pane.clone();
|
||||
move |toolbar, cx| {
|
||||
if let Some(workspace) = toolbar
|
||||
.pane
|
||||
.upgrade(cx)
|
||||
.and_then(|pane| pane.read(cx).workspace().upgrade(cx))
|
||||
{
|
||||
let pane = pane.clone();
|
||||
move |toolbar, cx| {
|
||||
if let Some(workspace) = toolbar
|
||||
.pane
|
||||
.upgrade(cx)
|
||||
.and_then(|pane| pane.read(cx).workspace().upgrade(cx))
|
||||
{
|
||||
let pane = pane.clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::go_forward(workspace, Some(pane.clone()), cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoForward { pane: None },
|
||||
"Go Forward",
|
||||
cx,
|
||||
))
|
||||
.with_children(primary_left_items)
|
||||
.with_children(primary_right_items)
|
||||
.constrained()
|
||||
.with_height(height),
|
||||
)
|
||||
.with_children(secondary_item)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.into_any_named("toolbar")
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.go_forward(pane.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoForward,
|
||||
"Go Forward",
|
||||
cx,
|
||||
));
|
||||
}
|
||||
primary_items.extend(primary_left_items);
|
||||
primary_items.extend(primary_right_items);
|
||||
|
||||
let mut toolbar = Flex::column();
|
||||
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()
|
||||
.with_style(container_style)
|
||||
.into_any_named("toolbar")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,9 +275,15 @@ impl Toolbar {
|
||||
pane,
|
||||
items: Default::default(),
|
||||
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>)
|
||||
where
|
||||
T: 'static + ToolbarItemView,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ pub struct WorkspaceSettings {
|
||||
pub confirm_quit: bool,
|
||||
pub show_call_status_icon: bool,
|
||||
pub autosave: AutosaveSetting,
|
||||
pub git: GitSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -17,7 +16,6 @@ pub struct WorkspaceSettingsContent {
|
||||
pub confirm_quit: Option<bool>,
|
||||
pub show_call_status_icon: Option<bool>,
|
||||
pub autosave: Option<AutosaveSetting>,
|
||||
pub git: Option<GitSettings>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.88.0"
|
||||
version = "0.90.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
@@ -48,6 +48,7 @@ language_selector = { path = "../language_selector" }
|
||||
lsp = { path = "../lsp" }
|
||||
lsp_log = { path = "../lsp_log" }
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
ai = { path = "../ai" }
|
||||
outline = { path = "../outline" }
|
||||
plugin_runtime = { path = "../plugin_runtime" }
|
||||
project = { path = "../project" }
|
||||
@@ -81,7 +82,7 @@ futures.workspace = true
|
||||
ignore = "0.4"
|
||||
image = "0.23"
|
||||
indexmap = "1.6.2"
|
||||
isahc = "1.7"
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
libc = "0.2"
|
||||
log.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name = "Markdown"
|
||||
path_suffixes = ["md", "mdx"]
|
||||
path_suffixes = ["md", "mdx", "zmd"]
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
|
||||
@@ -161,6 +161,7 @@ fn main() {
|
||||
terminal_view::init(cx);
|
||||
theme_testbench::init(cx);
|
||||
copilot::init(http.clone(), node_runtime, cx);
|
||||
ai::init(cx);
|
||||
|
||||
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("Reset Zoom", super::ResetBufferFontSize),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action(
|
||||
"Toggle Left Dock",
|
||||
workspace::ToggleLeftDock { focus: false },
|
||||
),
|
||||
MenuItem::action(
|
||||
"Toggle Right Dock",
|
||||
workspace::ToggleRightDock { focus: false },
|
||||
),
|
||||
MenuItem::action(
|
||||
"Toggle Bottom Dock",
|
||||
workspace::ToggleBottomDock { focus: false },
|
||||
),
|
||||
MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock),
|
||||
MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock),
|
||||
MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock),
|
||||
MenuItem::submenu(Menu {
|
||||
name: "Editor Layout",
|
||||
items: vec![
|
||||
@@ -120,8 +111,8 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||
Menu {
|
||||
name: "Go",
|
||||
items: vec![
|
||||
MenuItem::action("Back", workspace::GoBack { pane: None }),
|
||||
MenuItem::action("Forward", workspace::GoForward { pane: None }),
|
||||
MenuItem::action("Back", workspace::GoBack),
|
||||
MenuItem::action("Forward", workspace::GoForward),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Go to File", file_finder::Toggle),
|
||||
MenuItem::action("Go to Symbol in Project", project_symbols::Toggle),
|
||||
|
||||
@@ -354,7 +354,7 @@ pub fn initialize_workspace(
|
||||
.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)
|
||||
@@ -663,7 +663,7 @@ mod tests {
|
||||
use util::http::FakeHttpClient;
|
||||
use workspace::{
|
||||
item::{Item, ItemHandle},
|
||||
open_new, open_paths, pane, NewFile, Pane, SplitDirection, WorkspaceHandle,
|
||||
open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1488,7 +1488,7 @@ mod tests {
|
||||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1497,7 +1497,7 @@ mod tests {
|
||||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1506,7 +1506,7 @@ mod tests {
|
||||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1515,7 +1515,7 @@ mod tests {
|
||||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1525,7 +1525,7 @@ mod tests {
|
||||
|
||||
// Go back one more time and ensure we don't navigate past the first item in the history.
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1534,7 +1534,7 @@ mod tests {
|
||||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1543,7 +1543,7 @@ mod tests {
|
||||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1561,7 +1561,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1570,7 +1570,7 @@ mod tests {
|
||||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1579,7 +1579,7 @@ mod tests {
|
||||
);
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1601,7 +1601,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1609,7 +1609,7 @@ mod tests {
|
||||
(file1.clone(), DisplayPoint::new(10, 0), 0.)
|
||||
);
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1653,7 +1653,7 @@ mod tests {
|
||||
})
|
||||
});
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1661,7 +1661,7 @@ mod tests {
|
||||
(file1.clone(), DisplayPoint::new(2, 0), 0.)
|
||||
);
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_back(w, None, cx))
|
||||
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1766,81 +1766,97 @@ mod tests {
|
||||
// Reopen all the closed items, ensuring they are reopened in the same order
|
||||
// in which they were closed.
|
||||
workspace
|
||||
.update(cx, Pane::reopen_closed_item)
|
||||
.update(cx, Workspace::reopen_closed_item)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, Pane::reopen_closed_item)
|
||||
.update(cx, Workspace::reopen_closed_item)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, Pane::reopen_closed_item)
|
||||
.update(cx, Workspace::reopen_closed_item)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, Pane::reopen_closed_item)
|
||||
.update(cx, Workspace::reopen_closed_item)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
|
||||
|
||||
// Reopening past the last closed item is a no-op.
|
||||
workspace
|
||||
.update(cx, Pane::reopen_closed_item)
|
||||
.update(cx, Workspace::reopen_closed_item)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
|
||||
|
||||
// Reopening closed items doesn't interfere with navigation history.
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.go_back(workspace.active_pane().downgrade(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
|
||||
@@ -2085,6 +2101,7 @@ mod tests {
|
||||
theme::init((), cx);
|
||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
project_panel::init_settings(cx);
|
||||
|
||||
63
script/get-changes-since
Executable file
63
script/get-changes-since
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node --redirect-warnings=/dev/null
|
||||
|
||||
const { execFileSync } = require("child_process");
|
||||
const { GITHUB_ACCESS_TOKEN } = process.env;
|
||||
const PR_REGEX = /#\d+/ // Ex: matches on #4241
|
||||
const FIXES_REGEX = /(fixes|closes|completes) (.+[/#]\d+.*)$/im;
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
// Use form of: YYYY-MM-DD - 2023-01-09
|
||||
const startDate = new Date(process.argv[2]);
|
||||
const today = new Date()
|
||||
|
||||
console.log(`Changes from ${startDate} to ${today}\n`);
|
||||
|
||||
let pullRequestNumbers = getPullRequestNumbers(startDate, today);
|
||||
|
||||
// Fetch the pull requests from the GitHub API.
|
||||
console.log("Merged Pull requests:");
|
||||
for (const pullRequestNumber of pullRequestNumbers) {
|
||||
const webURL = `https://github.com/zed-industries/zed/pull/${pullRequestNumber}`;
|
||||
const apiURL = `https://api.github.com/repos/zed-industries/zed/pulls/${pullRequestNumber}`;
|
||||
|
||||
const response = await fetch(apiURL, {
|
||||
headers: {
|
||||
Authorization: `token ${GITHUB_ACCESS_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
const pullRequest = await response.json();
|
||||
console.log("*", pullRequest.title);
|
||||
console.log(" PR URL: ", webURL);
|
||||
console.log(" Merged: ", pullRequest.merged_at);
|
||||
console.log()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getPullRequestNumbers(startDate, endDate) {
|
||||
const sinceDate = startDate.toISOString();
|
||||
const untilDate = endDate.toISOString();
|
||||
|
||||
const pullRequestNumbers = execFileSync(
|
||||
"git",
|
||||
[
|
||||
"log",
|
||||
`--since=${sinceDate}`,
|
||||
`--until=${untilDate}`,
|
||||
"--oneline"
|
||||
],
|
||||
{ encoding: "utf8" }
|
||||
)
|
||||
.split("\n")
|
||||
.filter(line => line.length > 0)
|
||||
.map(line => {
|
||||
const match = line.match(/#(\d+)/);
|
||||
return match ? match[1] : null;
|
||||
})
|
||||
.filter(line => line);
|
||||
|
||||
return pullRequestNumbers;
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import * as fs from "fs"
|
||||
import toml from "toml"
|
||||
import { schemeMeta } from "./colorSchemes"
|
||||
import { Meta, Verification } from "./themes/common/colorScheme"
|
||||
import https from "https"
|
||||
import crypto from "crypto"
|
||||
import { MetaAndLicense } from "./themes/common/colorScheme"
|
||||
|
||||
const accepted_licenses_file = `${__dirname}/../../script/licenses/zed-licenses.toml`
|
||||
const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml`
|
||||
|
||||
// Use the cargo-about configuration file as the source of truth for supported licenses.
|
||||
function parseAcceptedToml(file: string): string[] {
|
||||
@@ -20,8 +18,11 @@ function parseAcceptedToml(file: string): string[] {
|
||||
return obj.accepted
|
||||
}
|
||||
|
||||
function checkLicenses(schemeMeta: Meta[], licenses: string[]) {
|
||||
for (let meta of schemeMeta) {
|
||||
function checkLicenses(
|
||||
schemeMetaWithLicense: MetaAndLicense[],
|
||||
licenses: string[]
|
||||
) {
|
||||
for (const { meta } of schemeMetaWithLicense) {
|
||||
// FIXME: Add support for conjuctions and conditions
|
||||
if (licenses.indexOf(meta.license.SPDX) < 0) {
|
||||
throw Error(
|
||||
@@ -31,62 +32,23 @@ function checkLicenses(schemeMeta: Meta[], licenses: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function getLicenseText(
|
||||
schemeMeta: Meta[],
|
||||
callback: (meta: Meta, license_text: string) => void
|
||||
) {
|
||||
for (let meta of schemeMeta) {
|
||||
if (typeof meta.license.license_text == "string") {
|
||||
callback(meta, meta.license.license_text)
|
||||
} else {
|
||||
let license_text_obj: Verification = meta.license.license_text
|
||||
// The following copied from the example code on nodejs.org:
|
||||
// https://nodejs.org/api/http.html#httpgetoptions-callback
|
||||
https
|
||||
.get(license_text_obj.https_url, (res) => {
|
||||
const { statusCode } = res
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
throw new Error(
|
||||
`Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}`
|
||||
)
|
||||
}
|
||||
|
||||
res.setEncoding("utf8")
|
||||
let rawData = ""
|
||||
res.on("data", (chunk) => {
|
||||
rawData += chunk
|
||||
})
|
||||
res.on("end", () => {
|
||||
const hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(rawData)
|
||||
.digest("hex")
|
||||
if (license_text_obj.license_checksum == hash) {
|
||||
callback(meta, rawData)
|
||||
} else {
|
||||
throw Error(
|
||||
`Checksum for ${meta.name} did not match file downloaded from ${license_text_obj.https_url}`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
.on("error", (e) => {
|
||||
throw e
|
||||
})
|
||||
}
|
||||
function generateLicenseFile(schemeMetaWithLicense: MetaAndLicense[]) {
|
||||
for (const { meta, licenseFile } of schemeMetaWithLicense) {
|
||||
const licenseText = fs.readFileSync(licenseFile).toString()
|
||||
writeLicense(meta.name, meta.url, licenseText)
|
||||
}
|
||||
}
|
||||
|
||||
function writeLicense(schemeMeta: Meta, text: String) {
|
||||
function writeLicense(
|
||||
themeName: string,
|
||||
themeUrl: string,
|
||||
licenseText: String
|
||||
) {
|
||||
process.stdout.write(
|
||||
`## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n`
|
||||
`## [${themeName}](${themeUrl})\n\n${licenseText}\n********************************************************************************\n\n`
|
||||
)
|
||||
}
|
||||
|
||||
const accepted_licenses = parseAcceptedToml(accepted_licenses_file)
|
||||
checkLicenses(schemeMeta, accepted_licenses)
|
||||
|
||||
getLicenseText(schemeMeta, (meta, text) => {
|
||||
writeLicense(meta, text)
|
||||
})
|
||||
const acceptedLicenses = parseAcceptedToml(ACCEPTED_LICENSES_FILE)
|
||||
checkLicenses(schemeMeta, acceptedLicenses)
|
||||
generateLicenseFile(schemeMeta)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from "fs"
|
||||
import { tmpdir } from "os"
|
||||
import * as path from "path"
|
||||
import colorSchemes, { staffColorSchemes } from "./colorSchemes"
|
||||
import { colorSchemes, staffColorSchemes } from "./colorSchemes"
|
||||
import app from "./styleTree/app"
|
||||
import { ColorScheme } from "./themes/common/colorScheme"
|
||||
import snakeCase from "./utils/snakeCase"
|
||||
|
||||
@@ -1,54 +1,79 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { ColorScheme, Meta } from "./themes/common/colorScheme"
|
||||
import { ColorScheme, MetaAndLicense } from "./themes/common/colorScheme"
|
||||
|
||||
const colorSchemes: ColorScheme[] = []
|
||||
export default colorSchemes
|
||||
const THEMES_DIRECTORY = path.resolve(`${__dirname}/themes`)
|
||||
const STAFF_DIRECTORY = path.resolve(`${__dirname}/themes/staff`)
|
||||
const IGNORE_ITEMS = ["staff", "common", "common.ts"]
|
||||
const ACCEPT_EXTENSION = ".ts"
|
||||
const LICENSE_FILE_NAME = "LICENSE"
|
||||
|
||||
const schemeMeta: Meta[] = []
|
||||
export { schemeMeta }
|
||||
function getAllTsFiles(directoryPath: string) {
|
||||
const files = fs.readdirSync(directoryPath)
|
||||
const fileList: string[] = []
|
||||
|
||||
const staffColorSchemes: ColorScheme[] = []
|
||||
export { staffColorSchemes }
|
||||
for (const file of files) {
|
||||
if (!IGNORE_ITEMS.includes(file)) {
|
||||
const filePath = path.join(directoryPath, file)
|
||||
|
||||
const experimentalColorSchemes: ColorScheme[] = []
|
||||
export { experimentalColorSchemes }
|
||||
|
||||
const themes_directory = path.resolve(`${__dirname}/themes`)
|
||||
|
||||
function for_all_color_schemes_in(
|
||||
themesPath: string,
|
||||
callback: (module: any, path: string) => void
|
||||
) {
|
||||
for (const fileName of fs.readdirSync(themesPath)) {
|
||||
if (fileName == "template.ts") continue
|
||||
const filePath = path.join(themesPath, fileName)
|
||||
|
||||
if (fs.statSync(filePath).isFile()) {
|
||||
const colorScheme = require(filePath)
|
||||
callback(colorScheme, path.basename(filePath))
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
fileList.push(...getAllTsFiles(filePath))
|
||||
} else if (path.extname(file) === ACCEPT_EXTENSION) {
|
||||
fileList.push(filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fileList
|
||||
}
|
||||
|
||||
function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) {
|
||||
for_all_color_schemes_in(themesPath, (colorScheme, _path) => {
|
||||
function getAllColorSchemes(directoryPath: string) {
|
||||
const files = getAllTsFiles(directoryPath)
|
||||
return files.map((filePath) => ({
|
||||
colorScheme: require(filePath),
|
||||
filePath,
|
||||
fileName: path.basename(filePath),
|
||||
licenseFile: `${path.dirname(filePath)}/${LICENSE_FILE_NAME}`,
|
||||
}))
|
||||
}
|
||||
|
||||
function getColorSchemes(directoryPath: string) {
|
||||
const colorSchemes: ColorScheme[] = []
|
||||
|
||||
for (const { colorScheme } of getAllColorSchemes(directoryPath)) {
|
||||
if (colorScheme.dark) colorSchemes.push(colorScheme.dark)
|
||||
if (colorScheme.light) colorSchemes.push(colorScheme.light)
|
||||
})
|
||||
else if (colorScheme.light) colorSchemes.push(colorScheme.light)
|
||||
}
|
||||
|
||||
return colorSchemes
|
||||
}
|
||||
|
||||
fillColorSchemes(themes_directory, colorSchemes)
|
||||
fillColorSchemes(path.resolve(`${themes_directory}/staff`), staffColorSchemes)
|
||||
function getMetaAndLicense(directoryPath: string) {
|
||||
const meta: MetaAndLicense[] = []
|
||||
|
||||
function fillMeta(themesPath: string, meta: Meta[]) {
|
||||
for_all_color_schemes_in(themesPath, (colorScheme, path) => {
|
||||
if (colorScheme.meta) {
|
||||
meta.push(colorScheme.meta)
|
||||
} else {
|
||||
throw Error(`Public theme ${path} must have a meta field`)
|
||||
for (const { colorScheme, filePath, licenseFile } of getAllColorSchemes(
|
||||
directoryPath
|
||||
)) {
|
||||
const licenseExists = fs.existsSync(licenseFile)
|
||||
if (!licenseExists) {
|
||||
throw Error(
|
||||
`Public theme should have a LICENSE file ${licenseFile}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (!colorScheme.meta) {
|
||||
throw Error(`Public theme ${filePath} must have a meta field`)
|
||||
}
|
||||
|
||||
meta.push({
|
||||
meta: colorScheme.meta,
|
||||
licenseFile,
|
||||
})
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
fillMeta(themes_directory, schemeMeta)
|
||||
export const colorSchemes = getColorSchemes(THEMES_DIRECTORY)
|
||||
export const staffColorSchemes = getColorSchemes(STAFF_DIRECTORY)
|
||||
export const schemeMeta = getMetaAndLicense(THEMES_DIRECTORY)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ColorScheme, Layer, StyleSets } from "../themes/common/colorScheme"
|
||||
import { background, border, borderColor, foreground, text } from "./components"
|
||||
import hoverPopover from "./hoverPopover"
|
||||
|
||||
import { SyntaxHighlightStyle, buildSyntax } from "../themes/common/syntax"
|
||||
import { buildSyntax } from "../themes/common/syntax"
|
||||
|
||||
export default function editor(colorScheme: ColorScheme) {
|
||||
const { isLight } = colorScheme
|
||||
@@ -103,7 +103,7 @@ export default function editor(colorScheme: ColorScheme) {
|
||||
? colorScheme.ramps.red(0.5).hex()
|
||||
: colorScheme.ramps.red(0.4).hex(),
|
||||
modified: isLight
|
||||
? colorScheme.ramps.yellow(0.3).hex()
|
||||
? colorScheme.ramps.yellow(0.5).hex()
|
||||
: colorScheme.ramps.yellow(0.5).hex(),
|
||||
inserted: isLight
|
||||
? colorScheme.ramps.green(0.4).hex()
|
||||
@@ -244,12 +244,12 @@ export default function editor(colorScheme: ColorScheme) {
|
||||
thumb: {
|
||||
background: withOpacity(background(layer, "inverted"), 0.3),
|
||||
border: {
|
||||
width: 1,
|
||||
color: borderColor(layer, "variant"),
|
||||
top: false,
|
||||
right: true,
|
||||
left: true,
|
||||
bottom: false,
|
||||
width: 1,
|
||||
color: borderColor(layer, "variant"),
|
||||
top: false,
|
||||
right: true,
|
||||
left: true,
|
||||
bottom: false,
|
||||
}
|
||||
},
|
||||
git: {
|
||||
|
||||
@@ -94,6 +94,9 @@ export default function tabBar(colorScheme: ColorScheme) {
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
active: {
|
||||
color: foreground(layer, "accent"),
|
||||
}
|
||||
},
|
||||
paneButtonContainer: {
|
||||
background: tab.background,
|
||||
|
||||
@@ -13,6 +13,7 @@ import tabBar from "./tabBar"
|
||||
|
||||
export default function workspace(colorScheme: ColorScheme) {
|
||||
const layer = colorScheme.lowest
|
||||
const isLight = colorScheme.isLight
|
||||
const itemSpacing = 8
|
||||
const titlebarButton = {
|
||||
cornerRadius: 6,
|
||||
@@ -119,13 +120,19 @@ export default function workspace(colorScheme: ColorScheme) {
|
||||
cursor: "Arrow",
|
||||
},
|
||||
zoomedBackground: {
|
||||
padding: 10,
|
||||
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,
|
||||
border: border(colorScheme.highest, { overlay: true }),
|
||||
border: border(colorScheme.lowest, { overlay: true }),
|
||||
},
|
||||
zoomedPanelForeground: {
|
||||
margin: 16,
|
||||
border: border(colorScheme.lowest, { overlay: true }),
|
||||
},
|
||||
dock: {
|
||||
left: {
|
||||
|
||||
21
styles/src/themes/andromeda/LICENSE
Normal file
21
styles/src/themes/andromeda/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 <eliverlara@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,6 +1,6 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
|
||||
const name = "Andromeda"
|
||||
|
||||
@@ -34,12 +34,6 @@ export const meta: Meta = {
|
||||
author: "EliverLara",
|
||||
license: {
|
||||
SPDX: "MIT",
|
||||
license_text: {
|
||||
https_url:
|
||||
"https://raw.githubusercontent.com/EliverLara/Andromeda/master/LICENSE.md",
|
||||
license_checksum:
|
||||
"2f7886f1a05cefc2c26f5e49de1a39fa4466413c1ccb06fc80960e73f5ed4b89",
|
||||
},
|
||||
},
|
||||
url: "https://github.com/EliverLara/Andromeda",
|
||||
}
|
||||
21
styles/src/themes/atelier/LICENSE
Normal file
21
styles/src/themes/atelier/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2023 Bram de Haan, http://atelierbramdehaan.nl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import chroma from "chroma-js"
|
||||
import { Meta } from "./common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "./common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
|
||||
import { Meta } from "../common/colorScheme"
|
||||
import { colorRamp, createColorScheme } from "../common/ramps"
|
||||
import { metaCommon, name, buildSyntax, Variant } from "./common"
|
||||
|
||||
const variant: Variant = {
|
||||
meta: {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user